feat: upgrade ai-sdk-provider-claude-code to v2.2.0 for native structured outputs (#1436)

This commit is contained in:
Ben Vargas 2025-11-24 08:38:50 -07:00 committed by GitHub
parent 522d9af580
commit c1df63d722
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 474 additions and 106 deletions

View File

@ -0,0 +1,5 @@
---
"task-master-ai": patch
---
Upgrade ai-sdk-provider-claude-code to v2.2.0 for native structured outputs support.

View File

@ -194,6 +194,68 @@ Task Master's Claude Code integration uses the official `ai-sdk-provider-claude-
- **Full AI SDK Compatibility**: Works with generateText, streamText, and other AI SDK functions
- **Automatic Error Handling**: Graceful degradation when Claude Code is unavailable
- **Type Safety**: Full TypeScript support with proper type definitions
- **Native Structured Outputs (v2.2.0+)**: Guaranteed schema-compliant JSON responses
## Native Structured Outputs (v2.2.0+)
As of `ai-sdk-provider-claude-code` v2.2.0, Claude Code supports **native structured outputs** via the Claude Agent SDK's `outputFormat` option. This provides significant benefits for Task Master's structured data generation:
### Key Benefits
- **Guaranteed Schema Compliance**: The SDK uses constrained decoding to ensure responses always match your Zod schema
- **No JSON Parsing Errors**: Schema validation is handled internally by the SDK
- **No Prompt Engineering Required**: No need for "output valid JSON" instructions - the SDK enforces schema natively
- **Better Performance**: No retry/extraction logic needed for valid JSON output
### How It Works
When Task Master calls `generateObject()` or `streamObject()` with a Zod schema, the Claude Code provider:
1. Sets `mode: 'json'` (via `needsExplicitJsonSchema = true`)
2. Passes the schema to the SDK
3. The SDK converts the Zod schema to JSON Schema and uses `outputFormat: { type: 'json_schema', schema: ... }`
4. Claude Agent SDK returns `structured_output` with guaranteed schema compliance
### Example
```javascript
import { z } from 'zod';
import { generateObjectService } from './scripts/modules/ai-services-unified.js';
// Define your schema
const taskSchema = z.object({
title: z.string().min(1),
description: z.string().min(1),
priority: z.enum(['high', 'medium', 'low']),
dependencies: z.array(z.number().int())
});
// Generate structured output - guaranteed to match schema
const result = await generateObjectService({
role: 'main', // Uses Claude Code if configured as main provider
schema: taskSchema,
objectName: 'task',
prompt: 'Create a task for implementing user authentication',
systemPrompt: 'You are a task planning assistant.',
commandName: 'add-task',
outputType: 'cli'
});
// result.mainResult is guaranteed to match taskSchema
console.log(result.mainResult);
// { title: "...", description: "...", priority: "high", dependencies: [] }
```
### Supported Commands
All Task Master commands that generate structured data benefit from native schema support:
- `parse-prd` - Parsing PRD documents into tasks
- `add-task` - Creating new tasks
- `expand-task` - Expanding tasks into subtasks
- `update-tasks` - Batch updating tasks
- `update-task-by-id` - Updating individual tasks
- `analyze-complexity` - Analyzing task complexity
### Example AI SDK Usage

145
package-lock.json generated
View File

@ -33,7 +33,7 @@
"@streamparser/json": "^0.0.22",
"@supabase/supabase-js": "^2.57.4",
"ai": "^5.0.51",
"ai-sdk-provider-claude-code": "^2.1.0",
"ai-sdk-provider-claude-code": "^2.2.0",
"ai-sdk-provider-codex-cli": "^0.3.0",
"ai-sdk-provider-gemini-cli": "^1.1.1",
"ajv": "^8.17.1",
@ -1563,6 +1563,7 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@ -2797,6 +2798,7 @@
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2",
@ -6031,6 +6033,7 @@
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"jiti": "bin/jiti.js"
}
@ -6162,7 +6165,6 @@
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
}
@ -6205,21 +6207,6 @@
"node": ">=14.0.0"
}
},
"node_modules/@mintlify/common/node_modules/yaml": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
}
},
"node_modules/@mintlify/link-rot": {
"version": "3.0.753",
"resolved": "https://registry.npmjs.org/@mintlify/link-rot/-/link-rot-3.0.753.tgz",
@ -6883,7 +6870,6 @@
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
}
@ -7341,6 +7327,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=8.0.0"
}
@ -7374,6 +7361,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz",
"integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/semantic-conventions": "^1.29.0"
},
@ -8338,6 +8326,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
"integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/core": "2.2.0",
"@opentelemetry/semantic-conventions": "^1.29.0"
@ -8687,8 +8676,7 @@
"integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true
"optional": true
},
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
@ -9277,7 +9265,6 @@
"integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-layout-effect": "1.1.1"
@ -9303,7 +9290,6 @@
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
@ -12318,6 +12304,7 @@
"integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@ -12328,6 +12315,7 @@
"integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==",
"dev": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^19.0.0"
}
@ -12593,30 +12581,6 @@
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/ui": {
"version": "4.0.12",
"resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.0.12.tgz",
"integrity": "sha512-RCqeApCnbwd5IFvxk6OeKMXTvzHU/cVqY8HAW0gWk0yAO6wXwQJMKhDfDtk2ss7JCy9u7RNC3kyazwiaDhBA/g==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@vitest/utils": "4.0.12",
"fflate": "^0.8.2",
"flatted": "^3.3.3",
"pathe": "^2.0.3",
"sirv": "^3.0.2",
"tinyglobby": "^0.2.15",
"tinyrainbow": "^3.0.3"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"vitest": "4.0.12"
}
},
"node_modules/@vitest/utils": {
"version": "4.0.12",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.12.tgz",
@ -12999,6 +12963,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@ -13076,6 +13041,7 @@
"resolved": "https://registry.npmjs.org/ai/-/ai-5.0.97.tgz",
"integrity": "sha512-8zBx0b/owis4eJI2tAlV8a1Rv0BANmLxontcAelkLNwEHhgfgXeKpDkhNB6OgV+BJSwboIUDkgd9312DdJnCOQ==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@ai-sdk/gateway": "2.0.12",
"@ai-sdk/provider": "2.0.0",
@ -13090,15 +13056,14 @@
}
},
"node_modules/ai-sdk-provider-claude-code": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ai-sdk-provider-claude-code/-/ai-sdk-provider-claude-code-2.1.0.tgz",
"integrity": "sha512-g+Nb+TKQs7RMP8PEn7NBjEHwVyIqHthXKSUqADO37oGPwOeMATuWCcuYAFmbeSdpIirV6iKRXRhdFipv2yTzqQ==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/ai-sdk-provider-claude-code/-/ai-sdk-provider-claude-code-2.2.0.tgz",
"integrity": "sha512-MN/l+GZ76tICIN+92Mhdi/XMiimPqcnx8HBARxfph2nnvt630ZPEBHAnqfa/BKGYbGtfxfnoiLLahfKfaYdiEg==",
"license": "MIT",
"dependencies": {
"@ai-sdk/provider": "2.0.0",
"@ai-sdk/provider-utils": "3.0.9",
"@anthropic-ai/claude-agent-sdk": "^0.1.20",
"jsonc-parser": "^3.3.1"
"@anthropic-ai/claude-agent-sdk": "^0.1.50"
},
"engines": {
"node": ">=18"
@ -13125,9 +13090,9 @@
}
},
"node_modules/ai-sdk-provider-claude-code/node_modules/@anthropic-ai/claude-agent-sdk": {
"version": "0.1.47",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.1.47.tgz",
"integrity": "sha512-0LAXuqp2AsvJcvrpVrJKANbmkqp3ZMpEfm03vRL6DrLc8JIQ5n25aCagIBDMwIVJscDwjd9cn4uAHvdI2PMLvw==",
"version": "0.1.50",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.1.50.tgz",
"integrity": "sha512-vHOLohUeiVadWl4eTAbw12ACIG1wZ/NN4ScLe8P/yrsldT1QkYwn6ndkoilaFBB2gIHECEx7wRAtSfCLefge4Q==",
"license": "SEE LICENSE IN README.md",
"engines": {
"node": ">=18.0.0"
@ -13303,6 +13268,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@ -14509,6 +14475,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.25",
"caniuse-lite": "^1.0.30001754",
@ -16735,7 +16702,8 @@
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1312386.tgz",
"integrity": "sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA==",
"dev": true,
"license": "BSD-3-Clause"
"license": "BSD-3-Clause",
"peer": true
},
"node_modules/dezalgo": {
"version": "1.0.4",
@ -17450,6 +17418,7 @@
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"bin": {
"esbuild": "bin/esbuild"
},
@ -18663,8 +18632,7 @@
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true
"optional": true
},
"node_modules/figlet": {
"version": "1.9.4",
@ -18844,8 +18812,7 @@
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
"dev": true,
"license": "ISC",
"optional": true,
"peer": true
"optional": true
},
"node_modules/follow-redirects": {
"version": "1.15.11",
@ -20573,6 +20540,7 @@
"integrity": "sha512-wF3j/DmkM8q5E+OtfdQhCRw8/0ahkc8CUTgEddxZzpEWPslu7YPL3t64MWRoI9m6upVGpfAg4ms2BBvxCdKRLQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@alcalzone/ansi-tokenize": "^0.2.1",
"ansi-escapes": "^7.2.0",
@ -21801,6 +21769,7 @@
"integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@jest/core": "^29.7.0",
"@jest/types": "^29.6.3",
@ -23672,6 +23641,7 @@
"integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 10.16.0"
}
@ -24081,7 +24051,6 @@
"os": [
"android"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
@ -24103,7 +24072,6 @@
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
@ -24125,7 +24093,6 @@
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
@ -24147,7 +24114,6 @@
"os": [
"freebsd"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
@ -24169,7 +24135,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
@ -24191,7 +24156,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
@ -24213,7 +24177,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
@ -24235,7 +24198,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
@ -24257,7 +24219,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
@ -24279,7 +24240,6 @@
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
@ -24301,7 +24261,6 @@
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
@ -24580,7 +24539,6 @@
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
@ -24593,8 +24551,7 @@
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/lowercase-keys": {
"version": "3.0.0",
@ -24741,6 +24698,7 @@
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz",
"integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==",
"license": "MIT",
"peer": true,
"bin": {
"marked": "bin/marked.js"
},
@ -26474,7 +26432,6 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=10"
}
@ -27907,6 +27864,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@ -28639,6 +28597,7 @@
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@ -28649,6 +28608,7 @@
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@ -29648,6 +29608,7 @@
"integrity": "sha512-iMmuD72XXLf26Tqrv1cryNYLX6NNPLhZ3AmNkSf8+xda0H+yijjGJ+wVT9UdBUHOpKzq9RjKtQKRCWoEKQQBZQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@oxc-project/types": "=0.95.0",
"@rolldown/pluginutils": "1.0.0-beta.45"
@ -30467,7 +30428,6 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@polka/url": "^1.0.0-next.24",
"mrmime": "^2.0.0",
@ -31825,7 +31785,6 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=6"
}
@ -32683,20 +32642,6 @@
"integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
"license": "MIT"
},
"node_modules/tsup/node_modules/yaml": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
"license": "ISC",
"optional": true,
"peer": true,
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
}
},
"node_modules/tsx": {
"version": "4.20.6",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz",
@ -32995,6 +32940,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@ -33153,6 +33099,7 @@
"integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/unist": "^3.0.0",
"bail": "^2.0.0",
@ -33667,6 +33614,7 @@
"integrity": "sha512-pmW4GCKQ8t5Ko1jYjC3SqOr7TUKN7uHOHB/XGsAIb69eYu6d1ionGSsb5H9chmPf+WeXt0VE7jTXsB1IvWoNbw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vitest/expect": "4.0.12",
"@vitest/mocker": "4.0.12",
@ -33783,6 +33731,7 @@
"integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@ -33852,21 +33801,6 @@
}
}
},
"node_modules/vitest/node_modules/yaml": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
}
},
"node_modules/walker": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
@ -34615,6 +34549,7 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz",
"integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}

View File

@ -72,7 +72,7 @@
"@streamparser/json": "^0.0.22",
"@supabase/supabase-js": "^2.57.4",
"ai": "^5.0.51",
"ai-sdk-provider-claude-code": "^2.1.0",
"ai-sdk-provider-claude-code": "^2.2.0",
"ai-sdk-provider-codex-cli": "^0.3.0",
"ai-sdk-provider-gemini-cli": "^1.1.1",
"ajv": "^8.17.1",

View File

@ -0,0 +1,366 @@
import { jest } from '@jest/globals';
import { z } from 'zod';
/**
* Tests for Claude Code native structured output support (v2.2.0+)
*
* ai-sdk-provider-claude-code v2.2.0 introduced native structured outputs via
* the Claude Agent SDK's outputFormat option. When using generateObject() with
* a schema, the SDK now guarantees schema-compliant JSON through constrained decoding.
*
* Key behaviors tested:
* 1. Schema is passed correctly to the SDK
* 2. mode: 'json' is used (which enables native outputFormat in the SDK)
* 3. SDK error handling for schema validation failures
*/
// Mock generateObject from 'ai' SDK
const mockGenerateObject = jest.fn();
jest.unstable_mockModule('ai', () => ({
generateObject: mockGenerateObject,
generateText: jest.fn(),
streamText: jest.fn(),
streamObject: jest.fn(),
zodSchema: jest.fn((schema) => schema),
JSONParseError: class JSONParseError extends Error {
constructor(message, text) {
super(message);
this.text = text;
}
},
NoObjectGeneratedError: class NoObjectGeneratedError extends Error {
static isInstance(error) {
return error instanceof NoObjectGeneratedError;
}
}
}));
// Mock jsonrepair
jest.unstable_mockModule('jsonrepair', () => ({
jsonrepair: jest.fn((text) => text)
}));
// Mock the ai-sdk-provider-claude-code package
jest.unstable_mockModule('ai-sdk-provider-claude-code', () => ({
createClaudeCode: jest.fn(() => {
const provider = (modelId) => ({
id: modelId,
specificationVersion: 'v1',
provider: 'claude-code',
modelId
});
provider.languageModel = provider;
provider.chat = provider;
return provider;
})
}));
// Mock config getters
jest.unstable_mockModule('../../../scripts/modules/config-manager.js', () => ({
getClaudeCodeSettingsForCommand: jest.fn(() => ({})),
getSupportedModelsForProvider: jest.fn(() => ['opus', 'sonnet', 'haiku']),
getDebugFlag: jest.fn(() => false),
getLogLevel: jest.fn(() => 'info'),
isProxyEnabled: jest.fn(() => false)
}));
// Mock utils
jest.unstable_mockModule('../../../scripts/modules/utils.js', () => ({
log: jest.fn(),
findProjectRoot: jest.fn(() => '/test/project')
}));
// Import after mocking
const { ClaudeCodeProvider } = await import(
'../../../src/ai-providers/claude-code.js'
);
describe('ClaudeCodeProvider structured outputs (v2.2.0+)', () => {
let provider;
beforeEach(() => {
provider = new ClaudeCodeProvider();
jest.clearAllMocks();
});
describe('needsExplicitJsonSchema flag', () => {
it('should have needsExplicitJsonSchema set to true', () => {
// This flag triggers mode: 'json' in base-provider.js generateObject()
// which in turn enables the SDK's native outputFormat with constrained decoding
expect(provider.needsExplicitJsonSchema).toBe(true);
});
it('should not support temperature parameter', () => {
// Claude Code SDK doesn't support temperature
expect(provider.supportsTemperature).toBe(false);
});
});
describe('generateObject with schema', () => {
const testSchema = z.object({
name: z.string(),
age: z.number(),
email: z.string().email()
});
const testMessages = [
{ role: 'system', content: 'You are a helpful assistant.' },
{ role: 'user', content: 'Generate a user profile' }
];
beforeEach(() => {
// Mock successful generateObject response
mockGenerateObject.mockResolvedValue({
object: { name: 'Test User', age: 25, email: 'test@example.com' },
usage: {
promptTokens: 100,
completionTokens: 50,
totalTokens: 150
}
});
});
it('should pass schema to generateObject call', async () => {
await provider.generateObject({
apiKey: 'test-key',
modelId: 'sonnet',
messages: testMessages,
schema: testSchema,
objectName: 'user_profile'
});
expect(mockGenerateObject).toHaveBeenCalledWith(
expect.objectContaining({
schema: testSchema
})
);
});
it('should use json mode for Claude Code (enables native outputFormat)', async () => {
await provider.generateObject({
apiKey: 'test-key',
modelId: 'sonnet',
messages: testMessages,
schema: testSchema,
objectName: 'user_profile'
});
// mode: 'json' is set when needsExplicitJsonSchema is true
// This triggers the SDK to use outputFormat: { type: 'json_schema', schema: ... }
expect(mockGenerateObject).toHaveBeenCalledWith(
expect.objectContaining({
mode: 'json'
})
);
});
it('should pass schemaName for better SDK context', async () => {
await provider.generateObject({
apiKey: 'test-key',
modelId: 'sonnet',
messages: testMessages,
schema: testSchema,
objectName: 'user_profile'
});
expect(mockGenerateObject).toHaveBeenCalledWith(
expect.objectContaining({
schemaName: 'user_profile'
})
);
});
it('should return structured object from SDK', async () => {
const result = await provider.generateObject({
apiKey: 'test-key',
modelId: 'sonnet',
messages: testMessages,
schema: testSchema,
objectName: 'user_profile'
});
expect(result.object).toEqual({
name: 'Test User',
age: 25,
email: 'test@example.com'
});
});
it('should return usage information', async () => {
const result = await provider.generateObject({
apiKey: 'test-key',
modelId: 'sonnet',
messages: testMessages,
schema: testSchema,
objectName: 'user_profile'
});
expect(result.usage).toEqual({
inputTokens: 100,
outputTokens: 50,
totalTokens: 150
});
});
});
describe('complex schemas', () => {
it('should handle nested object schemas', async () => {
const complexSchema = z.object({
tasks: z.array(
z.object({
id: z.number(),
title: z.string(),
subtasks: z.array(
z.object({
id: z.number(),
title: z.string()
})
)
})
)
});
mockGenerateObject.mockResolvedValue({
object: {
tasks: [
{
id: 1,
title: 'Main Task',
subtasks: [{ id: 1, title: 'Subtask 1' }]
}
]
},
usage: { promptTokens: 50, completionTokens: 30, totalTokens: 80 }
});
const result = await provider.generateObject({
apiKey: 'test-key',
modelId: 'sonnet',
messages: [{ role: 'user', content: 'Generate tasks' }],
schema: complexSchema,
objectName: 'task_list'
});
expect(result.object.tasks).toHaveLength(1);
expect(result.object.tasks[0].subtasks).toHaveLength(1);
});
it('should handle enum schemas (like task priority)', async () => {
const prioritySchema = z.object({
priority: z.enum(['high', 'medium', 'low']),
title: z.string()
});
mockGenerateObject.mockResolvedValue({
object: { priority: 'high', title: 'Important Task' },
usage: { promptTokens: 30, completionTokens: 20, totalTokens: 50 }
});
const result = await provider.generateObject({
apiKey: 'test-key',
modelId: 'sonnet',
messages: [{ role: 'user', content: 'Create a task' }],
schema: prioritySchema,
objectName: 'task'
});
expect(result.object.priority).toBe('high');
});
});
describe('error handling', () => {
it('should throw error when schema is missing', async () => {
await expect(
provider.generateObject({
apiKey: 'test-key',
modelId: 'sonnet',
messages: [{ role: 'user', content: 'test' }],
objectName: 'test'
// schema is missing
})
).rejects.toThrow('Schema is required');
});
it('should throw error when objectName is missing', async () => {
await expect(
provider.generateObject({
apiKey: 'test-key',
modelId: 'sonnet',
messages: [{ role: 'user', content: 'test' }],
schema: z.object({ name: z.string() })
// objectName is missing
})
).rejects.toThrow('Object name is required');
});
it('should handle SDK errors gracefully', async () => {
mockGenerateObject.mockRejectedValue(
new Error('SDK error: Failed to generate')
);
await expect(
provider.generateObject({
apiKey: 'test-key',
modelId: 'sonnet',
messages: [{ role: 'user', content: 'test' }],
schema: z.object({ name: z.string() }),
objectName: 'test'
})
).rejects.toThrow();
});
});
describe('v2.2.0 native structured output benefits', () => {
/**
* These tests document the expected behavior with v2.2.0's native schema support.
* The SDK now handles schema validation internally through constrained decoding,
* so the jsonrepair fallback in base-provider.js should rarely be triggered
* for Claude Code operations.
*/
it('should work with Task Master command schemas', async () => {
// This simulates the expand-task schema pattern
const expandTaskSchema = z.object({
subtasks: z.array(
z.object({
id: z.number().int().positive(),
title: z.string().min(1),
description: z.string().min(1),
dependencies: z.array(z.number().int()),
details: z.string(),
testStrategy: z.string()
})
)
});
mockGenerateObject.mockResolvedValue({
object: {
subtasks: [
{
id: 1,
title: 'Implement feature X',
description: 'Description for feature X',
dependencies: [],
details: 'Implementation details',
testStrategy: 'Unit tests for feature X'
}
]
},
usage: { promptTokens: 200, completionTokens: 100, totalTokens: 300 }
});
const result = await provider.generateObject({
apiKey: 'test-key',
modelId: 'sonnet',
messages: [{ role: 'user', content: 'Expand task into subtasks' }],
schema: expandTaskSchema,
objectName: 'subtasks'
});
expect(result.object.subtasks).toHaveLength(1);
expect(result.object.subtasks[0].id).toBe(1);
expect(result.object.subtasks[0].title).toBe('Implement feature X');
});
});
});