fix(ai-providers): change generateObject mode from 'tool' to 'auto' for better provider compatibility

Fixes Perplexity research role failing with 'tool-mode object generation' error

The hardcoded 'tool' mode was incompatible with providers like Perplexity that support structured JSON output but not function calling/tool use

Using 'auto' mode allows the AI SDK to choose the best approach for each provider
This commit is contained in:
Eyal Toledano
2025-06-07 15:02:48 -04:00
parent 7db7cf3859
commit f533fd0931
7 changed files with 1688 additions and 2477 deletions

View File

@@ -0,0 +1,37 @@
# Task ID: 96
# Title: Create Export Command for On-Demand Task File and PDF Generation
# Status: pending
# Dependencies: 2, 4, 95
# Priority: medium
# Description: Develop an 'export' CLI command that generates task files and comprehensive PDF exports on-demand, replacing automatic file generation and providing users with flexible export options.
# Details:
Implement a new 'export' command in the CLI that supports two primary modes: (1) generating individual task files on-demand (superseding the current automatic generation system), and (2) producing a comprehensive PDF export. The PDF should include: a first page with the output of 'tm list --with-subtasks', followed by individual pages for each task (using 'tm show <task_id>') and each subtask (using 'tm show <subtask_id>'). Integrate PDF generation using a robust library (e.g., pdfkit, Puppeteer, or jsPDF) to ensure high-quality output and proper pagination. Refactor or disable any existing automatic file generation logic to avoid performance overhead. Ensure the command supports flexible output paths and options for exporting only files, only PDF, or both. Update documentation and help output to reflect the new export capabilities. Consider concurrency and error handling for large projects. Ensure the export process is efficient and does not block the main CLI thread unnecessarily.
# Test Strategy:
1. Run the 'export' command with various options and verify that task files are generated only on-demand, not automatically. 2. Generate a PDF export and confirm that the first page contains the correct 'tm list --with-subtasks' output, and that each subsequent page accurately reflects the output of 'tm show <task_id>' and 'tm show <subtask_id>' for all tasks and subtasks. 3. Test exporting in projects with large numbers of tasks and subtasks to ensure performance and correctness. 4. Attempt exports with invalid paths or missing data to verify robust error handling. 5. Confirm that no automatic file generation occurs during normal task operations. 6. Review CLI help output and documentation for accuracy regarding the new export functionality.
# Subtasks:
## 1. Remove Automatic Task File Generation from Task Operations [pending]
### Dependencies: None
### Description: Eliminate all calls to generateTaskFiles() from task operations such as add-task, remove-task, set-status, and similar commands to prevent unnecessary performance overhead.
### Details:
Audit the codebase for any automatic invocations of generateTaskFiles() and remove or refactor them to ensure task files are not generated automatically during task operations.
## 2. Implement Export Command Infrastructure with On-Demand Task File Generation [pending]
### Dependencies: 96.1
### Description: Develop the CLI 'export' command infrastructure, enabling users to generate task files on-demand by invoking the preserved generateTaskFiles function only when requested.
### Details:
Create the export command with options for output paths and modes (files, PDF, or both). Ensure generateTaskFiles is only called within this command and not elsewhere.
## 3. Implement Comprehensive PDF Export Functionality [pending]
### Dependencies: 96.2
### Description: Add PDF export capability to the export command, generating a structured PDF with a first page listing all tasks and subtasks, followed by individual pages for each task and subtask, using a robust PDF library.
### Details:
Integrate a PDF generation library (e.g., pdfkit, Puppeteer, or jsPDF). Ensure the PDF includes the output of 'tm list --with-subtasks' on the first page, and uses 'tm show <task_id>' and 'tm show <subtask_id>' for subsequent pages. Handle pagination, concurrency, and error handling for large projects.
## 4. Update Documentation, Tests, and CLI Help for Export Workflow [pending]
### Dependencies: 96.2, 96.3
### Description: Revise all relevant documentation, automated tests, and CLI help output to reflect the new export-based workflow and available options.
### Details:
Update user guides, README files, and CLI help text. Add or modify tests to cover the new export command and its options. Ensure all documentation accurately describes the new workflow and usage.

File diff suppressed because one or more lines are too long

252
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "task-master-ai", "name": "task-master-ai",
"version": "0.16.1", "version": "0.16.2-rc.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "task-master-ai", "name": "task-master-ai",
"version": "0.16.1", "version": "0.16.2-rc.0",
"license": "MIT WITH Commons-Clause", "license": "MIT WITH Commons-Clause",
"dependencies": { "dependencies": {
"@ai-sdk/amazon-bedrock": "^2.2.9", "@ai-sdk/amazon-bedrock": "^2.2.9",
@@ -29,7 +29,7 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"express": "^4.21.2", "express": "^4.21.2",
"fastmcp": "^1.20.5", "fastmcp": "^2.2.2",
"figlet": "^1.8.0", "figlet": "^1.8.0",
"fuse.js": "^7.1.0", "fuse.js": "^7.1.0",
"gradient-string": "^3.0.0", "gradient-string": "^3.0.0",
@@ -3593,18 +3593,19 @@
} }
}, },
"node_modules/@modelcontextprotocol/sdk": { "node_modules/@modelcontextprotocol/sdk": {
"version": "1.8.0", "version": "1.12.1",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.8.0.tgz", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.12.1.tgz",
"integrity": "sha512-e06W7SwrontJDHwCawNO5SGxG+nU9AAx+jpHHZqGl/WrDBdWOpvirC+s58VpJTB5QemI4jTRcjWT4Pt3Q1NPQQ==", "integrity": "sha512-KG1CZhZfWg+u8pxeM/mByJDScJSrjjxLc8fwQqbsS8xCjBmQfMNEBTotYdNanKekepnfRI85GtgQlctLFpcYPw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ajv": "^6.12.6",
"content-type": "^1.0.5", "content-type": "^1.0.5",
"cors": "^2.8.5", "cors": "^2.8.5",
"cross-spawn": "^7.0.3", "cross-spawn": "^7.0.5",
"eventsource": "^3.0.2", "eventsource": "^3.0.2",
"express": "^5.0.1", "express": "^5.0.1",
"express-rate-limit": "^7.5.0", "express-rate-limit": "^7.5.0",
"pkce-challenge": "^4.1.0", "pkce-challenge": "^5.0.0",
"raw-body": "^3.0.0", "raw-body": "^3.0.0",
"zod": "^3.23.8", "zod": "^3.23.8",
"zod-to-json-schema": "^3.24.1" "zod-to-json-schema": "^3.24.1"
@@ -3668,84 +3669,45 @@
} }
}, },
"node_modules/@modelcontextprotocol/sdk/node_modules/express": { "node_modules/@modelcontextprotocol/sdk/node_modules/express": {
"version": "5.0.1", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/express/-/express-5.0.1.tgz", "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
"integrity": "sha512-ORF7g6qGnD+YtUG9yx4DFoqCShNMmUKiXuT5oWMHiOvt/4WFbHC6yCwQMTSBMno7AqntNCAzzcnnjowRkTL9eQ==", "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"accepts": "^2.0.0", "accepts": "^2.0.0",
"body-parser": "^2.0.1", "body-parser": "^2.2.0",
"content-disposition": "^1.0.0", "content-disposition": "^1.0.0",
"content-type": "~1.0.4", "content-type": "^1.0.5",
"cookie": "0.7.1", "cookie": "^0.7.1",
"cookie-signature": "^1.2.1", "cookie-signature": "^1.2.1",
"debug": "4.3.6", "debug": "^4.4.0",
"depd": "2.0.0", "encodeurl": "^2.0.0",
"encodeurl": "~2.0.0", "escape-html": "^1.0.3",
"escape-html": "~1.0.3", "etag": "^1.8.1",
"etag": "~1.8.1", "finalhandler": "^2.1.0",
"finalhandler": "^2.0.0", "fresh": "^2.0.0",
"fresh": "2.0.0", "http-errors": "^2.0.0",
"http-errors": "2.0.0",
"merge-descriptors": "^2.0.0", "merge-descriptors": "^2.0.0",
"methods": "~1.1.2",
"mime-types": "^3.0.0", "mime-types": "^3.0.0",
"on-finished": "2.4.1", "on-finished": "^2.4.1",
"once": "1.4.0", "once": "^1.4.0",
"parseurl": "~1.3.3", "parseurl": "^1.3.3",
"proxy-addr": "~2.0.7", "proxy-addr": "^2.0.7",
"qs": "6.13.0", "qs": "^6.14.0",
"range-parser": "~1.2.1", "range-parser": "^1.2.1",
"router": "^2.0.0", "router": "^2.2.0",
"safe-buffer": "5.2.1",
"send": "^1.1.0", "send": "^1.1.0",
"serve-static": "^2.1.0", "serve-static": "^2.2.0",
"setprototypeof": "1.2.0", "statuses": "^2.0.1",
"statuses": "2.0.1", "type-is": "^2.0.1",
"type-is": "^2.0.0", "vary": "^1.1.2"
"utils-merge": "1.0.1",
"vary": "~1.1.2"
}, },
"engines": { "engines": {
"node": ">= 18" "node": ">= 18"
}
},
"node_modules/@modelcontextprotocol/sdk/node_modules/express/node_modules/debug": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
"integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
"license": "MIT",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/@modelcontextprotocol/sdk/node_modules/express/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"license": "MIT"
},
"node_modules/@modelcontextprotocol/sdk/node_modules/express/node_modules/qs": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.0.6"
},
"engines": {
"node": ">=0.6"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "type": "opencollective",
"url": "https://opencollective.com/express"
} }
}, },
"node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": { "node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": {
@@ -4640,6 +4602,12 @@
"node": ">=18.0.0" "node": ">=18.0.0"
} }
}, },
"node_modules/@standard-schema/spec": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
"integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
"license": "MIT"
},
"node_modules/@tokenizer/inflate": { "node_modules/@tokenizer/inflate": {
"version": "0.2.7", "version": "0.2.7",
"resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz",
@@ -4884,6 +4852,22 @@
} }
} }
}, },
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ansi-align": { "node_modules/ansi-align": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
@@ -6432,9 +6416,9 @@
} }
}, },
"node_modules/eventsource": { "node_modules/eventsource": {
"version": "3.0.6", "version": "3.0.7",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.6.tgz", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
"integrity": "sha512-l19WpE2m9hSuyP06+FbuUUf1G+R0SFLrtQfbRb9PRr+oimOfxQhgGCbVaXg5IvZyyTThJsxh6L/srkMiCeBPDA==", "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"eventsource-parser": "^3.0.1" "eventsource-parser": "^3.0.1"
@@ -6636,6 +6620,12 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"license": "MIT"
},
"node_modules/fast-glob": { "node_modules/fast-glob": {
"version": "3.3.3", "version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
@@ -6657,7 +6647,6 @@
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-safe-stringify": { "node_modules/fast-safe-stringify": {
@@ -6690,22 +6679,24 @@
} }
}, },
"node_modules/fastmcp": { "node_modules/fastmcp": {
"version": "1.20.5", "version": "2.2.2",
"resolved": "https://registry.npmjs.org/fastmcp/-/fastmcp-1.20.5.tgz", "resolved": "https://registry.npmjs.org/fastmcp/-/fastmcp-2.2.2.tgz",
"integrity": "sha512-jwcPgMF9bcE9qsEG82YMlAG26/n5CSYsr95e60ntqWWd+3kgTBbUIasB3HfpqHLTNaQuoX6/jl18fpDcybBjcQ==", "integrity": "sha512-V6qEfOnABo7lDrwHqZQhCYd52KXzK85/ipllmUyaos8WLAjygP9NuuKcm1kiEWa0jjsFxe2kf/Y+T4PRE+0rEw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.6.0", "@modelcontextprotocol/sdk": "^1.10.2",
"@standard-schema/spec": "^1.0.0",
"execa": "^9.5.2", "execa": "^9.5.2",
"file-type": "^20.3.0", "file-type": "^20.4.1",
"fuse.js": "^7.1.0", "fuse.js": "^7.1.0",
"mcp-proxy": "^2.10.4", "mcp-proxy": "^3.0.3",
"strict-event-emitter-types": "^2.0.0", "strict-event-emitter-types": "^2.0.0",
"undici": "^7.4.0", "undici": "^7.8.0",
"uri-templates": "^0.2.0", "uri-templates": "^0.2.0",
"xsschema": "0.3.0-beta.1",
"yargs": "^17.7.2", "yargs": "^17.7.2",
"zod": "^3.24.2", "zod": "^3.25.12",
"zod-to-json-schema": "^3.24.3" "zod-to-json-schema": "^3.24.5"
}, },
"bin": { "bin": {
"fastmcp": "dist/bin/fastmcp.js" "fastmcp": "dist/bin/fastmcp.js"
@@ -9104,6 +9095,12 @@
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
"license": "(AFL-2.1 OR BSD-3-Clause)" "license": "(AFL-2.1 OR BSD-3-Clause)"
}, },
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"license": "MIT"
},
"node_modules/json5": { "node_modules/json5": {
"version": "2.2.3", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
@@ -9383,19 +9380,31 @@
} }
}, },
"node_modules/mcp-proxy": { "node_modules/mcp-proxy": {
"version": "2.12.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/mcp-proxy/-/mcp-proxy-2.12.0.tgz", "resolved": "https://registry.npmjs.org/mcp-proxy/-/mcp-proxy-3.3.0.tgz",
"integrity": "sha512-hL2Y6EtK7vkgAOZxOQe9M4Z9g5xEnvR4ZYBKqFi/5tjhz/1jyNEz5NL87Uzv46k8iZQPVNEof/T6arEooBU5bQ==", "integrity": "sha512-xyFKQEZ64HC7lxScBHjb5fxiPoyJjjkPhwH5hWUT0oL/ttCpMGZDJrYZRGFKVJiLLkrZPAkHnMGkI+WMlyD/cg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.6.0", "@modelcontextprotocol/sdk": "^1.11.4",
"eventsource": "^3.0.5", "eventsource": "^4.0.0",
"yargs": "^17.7.2" "yargs": "^17.7.2"
}, },
"bin": { "bin": {
"mcp-proxy": "dist/bin/mcp-proxy.js" "mcp-proxy": "dist/bin/mcp-proxy.js"
} }
}, },
"node_modules/mcp-proxy/node_modules/eventsource": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-4.0.0.tgz",
"integrity": "sha512-fvIkb9qZzdMxgZrEQDyll+9oJsyaVvY92I2Re+qK0qEJ+w5s0X3dtz+M0VAPOjP1gtU3iqWyjQ0G3nvd5CLZ2g==",
"license": "MIT",
"dependencies": {
"eventsource-parser": "^3.0.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/media-typer": { "node_modules/media-typer": {
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -10085,9 +10094,9 @@
} }
}, },
"node_modules/pkce-challenge": { "node_modules/pkce-challenge": {
"version": "4.1.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-4.1.0.tgz", "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz",
"integrity": "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ==", "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=16.20.0" "node": ">=16.20.0"
@@ -11319,9 +11328,9 @@
} }
}, },
"node_modules/undici": { "node_modules/undici": {
"version": "7.6.0", "version": "7.10.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.6.0.tgz", "resolved": "https://registry.npmjs.org/undici/-/undici-7.10.0.tgz",
"integrity": "sha512-gaFsbThjrDGvAaD670r81RZro/s6H2PVZF640Qn0p5kZK+/rim7/mmyfp2W7VB5vOMaFM8vuFBJUaMlaZTYHlA==", "integrity": "sha512-u5otvFBOBZvmdjWLVW+5DAc9Nkq8f24g0O9oY7qw2JVIF1VocIFoyz9JFkuVOS2j41AufeO0xnlweJ2RLT8nGw==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=20.18.1" "node": ">=20.18.1"
@@ -11395,6 +11404,15 @@
"browserslist": ">= 4.21.0" "browserslist": ">= 4.21.0"
} }
}, },
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"license": "BSD-2-Clause",
"dependencies": {
"punycode": "^2.1.0"
}
},
"node_modules/uri-templates": { "node_modules/uri-templates": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/uri-templates/-/uri-templates-0.2.0.tgz", "resolved": "https://registry.npmjs.org/uri-templates/-/uri-templates-0.2.0.tgz",
@@ -11605,6 +11623,40 @@
} }
} }
}, },
"node_modules/xsschema": {
"version": "0.3.0-beta.1",
"resolved": "https://registry.npmjs.org/xsschema/-/xsschema-0.3.0-beta.1.tgz",
"integrity": "sha512-Z7ZlPKLTc8iUKVfic0Lr66NB777wJqZl3JVLIy1vaNxx6NNTuylYm4wbK78Sgg7kHwaPRqFnuT4IliQM1sDxvg==",
"license": "MIT",
"peerDependencies": {
"@valibot/to-json-schema": "^1.0.0",
"arktype": "^2.1.16",
"effect": "^3.14.5",
"sury": "^10.0.0-rc",
"zod": "^3.25.0",
"zod-to-json-schema": "^3.24.5"
},
"peerDependenciesMeta": {
"@valibot/to-json-schema": {
"optional": true
},
"arktype": {
"optional": true
},
"effect": {
"optional": true
},
"sury": {
"optional": true
},
"zod": {
"optional": true
},
"zod-to-json-schema": {
"optional": true
}
}
},
"node_modules/y18n": { "node_modules/y18n": {
"version": "5.0.8", "version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
@@ -11734,9 +11786,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/zod": { "node_modules/zod": {
"version": "3.24.2", "version": "3.25.56",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.56.tgz",
"integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", "integrity": "sha512-rd6eEF3BTNvQnR2e2wwolfTmUTnp70aUTqr0oaGbHifzC3BKJsoV+Gat8vxUMR1hwOKBs6El+qWehrHbCpW6SQ==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"

View File

@@ -24,9 +24,9 @@ import {
getAzureBaseURL, getAzureBaseURL,
getBedrockBaseURL, getBedrockBaseURL,
getVertexProjectId, getVertexProjectId,
getVertexLocation getVertexLocation,
} from './config-manager.js'; } from "./config-manager.js";
import { log, findProjectRoot, resolveEnvVariable } from './utils.js'; import { log, findProjectRoot, resolveEnvVariable } from "./utils.js";
// Import provider classes // Import provider classes
import { import {
@@ -39,8 +39,8 @@ import {
OllamaAIProvider, OllamaAIProvider,
BedrockAIProvider, BedrockAIProvider,
AzureProvider, AzureProvider,
VertexAIProvider VertexAIProvider,
} from '../../src/ai-providers/index.js'; } from "../../src/ai-providers/index.js";
// Create provider instances // Create provider instances
const PROVIDERS = { const PROVIDERS = {
@@ -53,36 +53,36 @@ const PROVIDERS = {
ollama: new OllamaAIProvider(), ollama: new OllamaAIProvider(),
bedrock: new BedrockAIProvider(), bedrock: new BedrockAIProvider(),
azure: new AzureProvider(), azure: new AzureProvider(),
vertex: new VertexAIProvider() vertex: new VertexAIProvider(),
}; };
// Helper function to get cost for a specific model // Helper function to get cost for a specific model
function _getCostForModel(providerName, modelId) { function _getCostForModel(providerName, modelId) {
if (!MODEL_MAP || !MODEL_MAP[providerName]) { if (!MODEL_MAP || !MODEL_MAP[providerName]) {
log( log(
'warn', "warn",
`Provider "${providerName}" not found in MODEL_MAP. Cannot determine cost for model ${modelId}.` `Provider "${providerName}" not found in MODEL_MAP. Cannot determine cost for model ${modelId}.`
); );
return { inputCost: 0, outputCost: 0, currency: 'USD' }; // Default to zero cost return { inputCost: 0, outputCost: 0, currency: "USD" }; // Default to zero cost
} }
const modelData = MODEL_MAP[providerName].find((m) => m.id === modelId); const modelData = MODEL_MAP[providerName].find((m) => m.id === modelId);
if (!modelData || !modelData.cost_per_1m_tokens) { if (!modelData || !modelData.cost_per_1m_tokens) {
log( log(
'debug', "debug",
`Cost data not found for model "${modelId}" under provider "${providerName}". Assuming zero cost.` `Cost data not found for model "${modelId}" under provider "${providerName}". Assuming zero cost.`
); );
return { inputCost: 0, outputCost: 0, currency: 'USD' }; // Default to zero cost return { inputCost: 0, outputCost: 0, currency: "USD" }; // Default to zero cost
} }
// Ensure currency is part of the returned object, defaulting if not present // Ensure currency is part of the returned object, defaulting if not present
const currency = modelData.cost_per_1m_tokens.currency || 'USD'; const currency = modelData.cost_per_1m_tokens.currency || "USD";
return { return {
inputCost: modelData.cost_per_1m_tokens.input || 0, inputCost: modelData.cost_per_1m_tokens.input || 0,
outputCost: modelData.cost_per_1m_tokens.output || 0, outputCost: modelData.cost_per_1m_tokens.output || 0,
currency: currency currency: currency,
}; };
} }
@@ -92,13 +92,13 @@ const INITIAL_RETRY_DELAY_MS = 1000;
// Helper function to check if an error is retryable // Helper function to check if an error is retryable
function isRetryableError(error) { function isRetryableError(error) {
const errorMessage = error.message?.toLowerCase() || ''; const errorMessage = error.message?.toLowerCase() || "";
return ( return (
errorMessage.includes('rate limit') || errorMessage.includes("rate limit") ||
errorMessage.includes('overloaded') || errorMessage.includes("overloaded") ||
errorMessage.includes('service temporarily unavailable') || errorMessage.includes("service temporarily unavailable") ||
errorMessage.includes('timeout') || errorMessage.includes("timeout") ||
errorMessage.includes('network error') || errorMessage.includes("network error") ||
error.status === 429 || error.status === 429 ||
error.status >= 500 error.status >= 500
); );
@@ -123,7 +123,7 @@ function _extractErrorMessage(error) {
} }
// Attempt 3: Look for nested error message in response body if it's JSON string // Attempt 3: Look for nested error message in response body if it's JSON string
if (typeof error?.responseBody === 'string') { if (typeof error?.responseBody === "string") {
try { try {
const body = JSON.parse(error.responseBody); const body = JSON.parse(error.responseBody);
if (body?.error?.message) { if (body?.error?.message) {
@@ -135,20 +135,20 @@ function _extractErrorMessage(error) {
} }
// Attempt 4: Use the top-level message if it exists // Attempt 4: Use the top-level message if it exists
if (typeof error?.message === 'string' && error.message) { if (typeof error?.message === "string" && error.message) {
return error.message; return error.message;
} }
// Attempt 5: Handle simple string errors // Attempt 5: Handle simple string errors
if (typeof error === 'string') { if (typeof error === "string") {
return error; return error;
} }
// Fallback // Fallback
return 'An unknown AI service error occurred.'; return "An unknown AI service error occurred.";
} catch (e) { } catch (e) {
// Safety net // Safety net
return 'Failed to extract error message.'; return "Failed to extract error message.";
} }
} }
@@ -162,17 +162,17 @@ function _extractErrorMessage(error) {
*/ */
function _resolveApiKey(providerName, session, projectRoot = null) { function _resolveApiKey(providerName, session, projectRoot = null) {
const keyMap = { const keyMap = {
openai: 'OPENAI_API_KEY', openai: "OPENAI_API_KEY",
anthropic: 'ANTHROPIC_API_KEY', anthropic: "ANTHROPIC_API_KEY",
google: 'GOOGLE_API_KEY', google: "GOOGLE_API_KEY",
perplexity: 'PERPLEXITY_API_KEY', perplexity: "PERPLEXITY_API_KEY",
mistral: 'MISTRAL_API_KEY', mistral: "MISTRAL_API_KEY",
azure: 'AZURE_OPENAI_API_KEY', azure: "AZURE_OPENAI_API_KEY",
openrouter: 'OPENROUTER_API_KEY', openrouter: "OPENROUTER_API_KEY",
xai: 'XAI_API_KEY', xai: "XAI_API_KEY",
ollama: 'OLLAMA_API_KEY', ollama: "OLLAMA_API_KEY",
bedrock: 'AWS_ACCESS_KEY_ID', bedrock: "AWS_ACCESS_KEY_ID",
vertex: 'GOOGLE_API_KEY' vertex: "GOOGLE_API_KEY",
}; };
const envVarName = keyMap[providerName]; const envVarName = keyMap[providerName];
@@ -185,7 +185,7 @@ function _resolveApiKey(providerName, session, projectRoot = null) {
const apiKey = resolveEnvVariable(envVarName, session, projectRoot); const apiKey = resolveEnvVariable(envVarName, session, projectRoot);
// Special handling for providers that can use alternative auth // Special handling for providers that can use alternative auth
if (providerName === 'ollama' || providerName === 'bedrock') { if (providerName === "ollama" || providerName === "bedrock") {
return apiKey || null; return apiKey || null;
} }
@@ -223,7 +223,7 @@ async function _attemptProviderCallWithRetries(
try { try {
if (getDebugFlag()) { if (getDebugFlag()) {
log( log(
'info', "info",
`Attempt ${retries + 1}/${MAX_RETRIES + 1} calling ${fnName} (Provider: ${providerName}, Model: ${modelId}, Role: ${attemptRole})` `Attempt ${retries + 1}/${MAX_RETRIES + 1} calling ${fnName} (Provider: ${providerName}, Model: ${modelId}, Role: ${attemptRole})`
); );
} }
@@ -233,14 +233,14 @@ async function _attemptProviderCallWithRetries(
if (getDebugFlag()) { if (getDebugFlag()) {
log( log(
'info', "info",
`${fnName} succeeded for role ${attemptRole} (Provider: ${providerName}) on attempt ${retries + 1}` `${fnName} succeeded for role ${attemptRole} (Provider: ${providerName}) on attempt ${retries + 1}`
); );
} }
return result; return result;
} catch (error) { } catch (error) {
log( log(
'warn', "warn",
`Attempt ${retries + 1} failed for role ${attemptRole} (${fnName} / ${providerName}): ${error.message}` `Attempt ${retries + 1} failed for role ${attemptRole} (${fnName} / ${providerName}): ${error.message}`
); );
@@ -248,13 +248,13 @@ async function _attemptProviderCallWithRetries(
retries++; retries++;
const delay = INITIAL_RETRY_DELAY_MS * Math.pow(2, retries - 1); const delay = INITIAL_RETRY_DELAY_MS * Math.pow(2, retries - 1);
log( log(
'info', "info",
`Something went wrong on the provider side. Retrying in ${delay / 1000}s...` `Something went wrong on the provider side. Retrying in ${delay / 1000}s...`
); );
await new Promise((resolve) => setTimeout(resolve, delay)); await new Promise((resolve) => setTimeout(resolve, delay));
} else { } else {
log( log(
'error', "error",
`Something went wrong on the provider side. Max retries reached for role ${attemptRole} (${fnName} / ${providerName}).` `Something went wrong on the provider side. Max retries reached for role ${attemptRole} (${fnName} / ${providerName}).`
); );
throw error; throw error;
@@ -296,11 +296,11 @@ async function _unifiedServiceRunner(serviceType, params) {
...restApiParams ...restApiParams
} = params; } = params;
if (getDebugFlag()) { if (getDebugFlag()) {
log('info', `${serviceType}Service called`, { log("info", `${serviceType}Service called`, {
role: initialRole, role: initialRole,
commandName, commandName,
outputType, outputType,
projectRoot projectRoot,
}); });
} }
@@ -308,23 +308,23 @@ async function _unifiedServiceRunner(serviceType, params) {
const userId = getUserId(effectiveProjectRoot); const userId = getUserId(effectiveProjectRoot);
let sequence; let sequence;
if (initialRole === 'main') { if (initialRole === "main") {
sequence = ['main', 'fallback', 'research']; sequence = ["main", "fallback", "research"];
} else if (initialRole === 'research') { } else if (initialRole === "research") {
sequence = ['research', 'fallback', 'main']; sequence = ["research", "fallback", "main"];
} else if (initialRole === 'fallback') { } else if (initialRole === "fallback") {
sequence = ['fallback', 'main', 'research']; sequence = ["fallback", "main", "research"];
} else { } else {
log( log(
'warn', "warn",
`Unknown initial role: ${initialRole}. Defaulting to main -> fallback -> research sequence.` `Unknown initial role: ${initialRole}. Defaulting to main -> fallback -> research sequence.`
); );
sequence = ['main', 'fallback', 'research']; sequence = ["main", "fallback", "research"];
} }
let lastError = null; let lastError = null;
let lastCleanErrorMessage = let lastCleanErrorMessage =
'AI service call failed for all configured roles.'; "AI service call failed for all configured roles.";
for (const currentRole of sequence) { for (const currentRole of sequence) {
let providerName, let providerName,
@@ -337,20 +337,20 @@ async function _unifiedServiceRunner(serviceType, params) {
telemetryData = null; telemetryData = null;
try { try {
log('info', `New AI service call with role: ${currentRole}`); log("info", `New AI service call with role: ${currentRole}`);
if (currentRole === 'main') { if (currentRole === "main") {
providerName = getMainProvider(effectiveProjectRoot); providerName = getMainProvider(effectiveProjectRoot);
modelId = getMainModelId(effectiveProjectRoot); modelId = getMainModelId(effectiveProjectRoot);
} else if (currentRole === 'research') { } else if (currentRole === "research") {
providerName = getResearchProvider(effectiveProjectRoot); providerName = getResearchProvider(effectiveProjectRoot);
modelId = getResearchModelId(effectiveProjectRoot); modelId = getResearchModelId(effectiveProjectRoot);
} else if (currentRole === 'fallback') { } else if (currentRole === "fallback") {
providerName = getFallbackProvider(effectiveProjectRoot); providerName = getFallbackProvider(effectiveProjectRoot);
modelId = getFallbackModelId(effectiveProjectRoot); modelId = getFallbackModelId(effectiveProjectRoot);
} else { } else {
log( log(
'error', "error",
`Unknown role encountered in _unifiedServiceRunner: ${currentRole}` `Unknown role encountered in _unifiedServiceRunner: ${currentRole}`
); );
lastError = lastError =
@@ -360,7 +360,7 @@ async function _unifiedServiceRunner(serviceType, params) {
if (!providerName || !modelId) { if (!providerName || !modelId) {
log( log(
'warn', "warn",
`Skipping role '${currentRole}': Provider or Model ID not configured.` `Skipping role '${currentRole}': Provider or Model ID not configured.`
); );
lastError = lastError =
@@ -375,7 +375,7 @@ async function _unifiedServiceRunner(serviceType, params) {
provider = PROVIDERS[providerName?.toLowerCase()]; provider = PROVIDERS[providerName?.toLowerCase()];
if (!provider) { if (!provider) {
log( log(
'warn', "warn",
`Skipping role '${currentRole}': Provider '${providerName}' not supported.` `Skipping role '${currentRole}': Provider '${providerName}' not supported.`
); );
lastError = lastError =
@@ -385,10 +385,10 @@ async function _unifiedServiceRunner(serviceType, params) {
} }
// Check API key if needed // Check API key if needed
if (providerName?.toLowerCase() !== 'ollama') { if (providerName?.toLowerCase() !== "ollama") {
if (!isApiKeySet(providerName, session, effectiveProjectRoot)) { if (!isApiKeySet(providerName, session, effectiveProjectRoot)) {
log( log(
'warn', "warn",
`Skipping role '${currentRole}' (Provider: ${providerName}): API key not set or invalid.` `Skipping role '${currentRole}' (Provider: ${providerName}): API key not set or invalid.`
); );
lastError = lastError =
@@ -404,17 +404,17 @@ async function _unifiedServiceRunner(serviceType, params) {
baseURL = getBaseUrlForRole(currentRole, effectiveProjectRoot); baseURL = getBaseUrlForRole(currentRole, effectiveProjectRoot);
// For Azure, use the global Azure base URL if role-specific URL is not configured // For Azure, use the global Azure base URL if role-specific URL is not configured
if (providerName?.toLowerCase() === 'azure' && !baseURL) { if (providerName?.toLowerCase() === "azure" && !baseURL) {
baseURL = getAzureBaseURL(effectiveProjectRoot); baseURL = getAzureBaseURL(effectiveProjectRoot);
log('debug', `Using global Azure base URL: ${baseURL}`); log("debug", `Using global Azure base URL: ${baseURL}`);
} else if (providerName?.toLowerCase() === 'ollama' && !baseURL) { } else if (providerName?.toLowerCase() === "ollama" && !baseURL) {
// For Ollama, use the global Ollama base URL if role-specific URL is not configured // For Ollama, use the global Ollama base URL if role-specific URL is not configured
baseURL = getOllamaBaseURL(effectiveProjectRoot); baseURL = getOllamaBaseURL(effectiveProjectRoot);
log('debug', `Using global Ollama base URL: ${baseURL}`); log("debug", `Using global Ollama base URL: ${baseURL}`);
} else if (providerName?.toLowerCase() === 'bedrock' && !baseURL) { } else if (providerName?.toLowerCase() === "bedrock" && !baseURL) {
// For Bedrock, use the global Bedrock base URL if role-specific URL is not configured // For Bedrock, use the global Bedrock base URL if role-specific URL is not configured
baseURL = getBedrockBaseURL(effectiveProjectRoot); baseURL = getBedrockBaseURL(effectiveProjectRoot);
log('debug', `Using global Bedrock base URL: ${baseURL}`); log("debug", `Using global Bedrock base URL: ${baseURL}`);
} }
// Get AI parameters for the current role // Get AI parameters for the current role
@@ -429,12 +429,12 @@ async function _unifiedServiceRunner(serviceType, params) {
let providerSpecificParams = {}; let providerSpecificParams = {};
// Handle Vertex AI specific configuration // Handle Vertex AI specific configuration
if (providerName?.toLowerCase() === 'vertex') { if (providerName?.toLowerCase() === "vertex") {
// Get Vertex project ID and location // Get Vertex project ID and location
const projectId = const projectId =
getVertexProjectId(effectiveProjectRoot) || getVertexProjectId(effectiveProjectRoot) ||
resolveEnvVariable( resolveEnvVariable(
'VERTEX_PROJECT_ID', "VERTEX_PROJECT_ID",
session, session,
effectiveProjectRoot effectiveProjectRoot
); );
@@ -442,15 +442,15 @@ async function _unifiedServiceRunner(serviceType, params) {
const location = const location =
getVertexLocation(effectiveProjectRoot) || getVertexLocation(effectiveProjectRoot) ||
resolveEnvVariable( resolveEnvVariable(
'VERTEX_LOCATION', "VERTEX_LOCATION",
session, session,
effectiveProjectRoot effectiveProjectRoot
) || ) ||
'us-central1'; "us-central1";
// Get credentials path if available // Get credentials path if available
const credentialsPath = resolveEnvVariable( const credentialsPath = resolveEnvVariable(
'GOOGLE_APPLICATION_CREDENTIALS', "GOOGLE_APPLICATION_CREDENTIALS",
session, session,
effectiveProjectRoot effectiveProjectRoot
); );
@@ -459,18 +459,18 @@ async function _unifiedServiceRunner(serviceType, params) {
providerSpecificParams = { providerSpecificParams = {
projectId, projectId,
location, location,
...(credentialsPath && { credentials: { credentialsFromEnv: true } }) ...(credentialsPath && { credentials: { credentialsFromEnv: true } }),
}; };
log( log(
'debug', "debug",
`Using Vertex AI configuration: Project ID=${projectId}, Location=${location}` `Using Vertex AI configuration: Project ID=${projectId}, Location=${location}`
); );
} }
const messages = []; const messages = [];
if (systemPrompt) { if (systemPrompt) {
messages.push({ role: 'system', content: systemPrompt }); messages.push({ role: "system", content: systemPrompt });
} }
// IN THE FUTURE WHEN DOING CONTEXT IMPROVEMENTS // IN THE FUTURE WHEN DOING CONTEXT IMPROVEMENTS
@@ -492,9 +492,9 @@ async function _unifiedServiceRunner(serviceType, params) {
// } // }
if (prompt) { if (prompt) {
messages.push({ role: 'user', content: prompt }); messages.push({ role: "user", content: prompt });
} else { } else {
throw new Error('User prompt content is missing.'); throw new Error("User prompt content is missing.");
} }
const callParams = { const callParams = {
@@ -504,9 +504,9 @@ async function _unifiedServiceRunner(serviceType, params) {
temperature: roleParams.temperature, temperature: roleParams.temperature,
messages, messages,
...(baseURL && { baseURL }), ...(baseURL && { baseURL }),
...(serviceType === 'generateObject' && { schema, objectName }), ...(serviceType === "generateObject" && { schema, objectName }),
...providerSpecificParams, ...providerSpecificParams,
...restApiParams ...restApiParams,
}; };
providerResponse = await _attemptProviderCallWithRetries( providerResponse = await _attemptProviderCallWithRetries(
@@ -527,7 +527,7 @@ async function _unifiedServiceRunner(serviceType, params) {
modelId, modelId,
inputTokens: providerResponse.usage.inputTokens, inputTokens: providerResponse.usage.inputTokens,
outputTokens: providerResponse.usage.outputTokens, outputTokens: providerResponse.usage.outputTokens,
outputType outputType,
}); });
} catch (telemetryError) { } catch (telemetryError) {
// logAiUsage already logs its own errors and returns null on failure // logAiUsage already logs its own errors and returns null on failure
@@ -535,21 +535,21 @@ async function _unifiedServiceRunner(serviceType, params) {
} }
} else if (userId && providerResponse && !providerResponse.usage) { } else if (userId && providerResponse && !providerResponse.usage) {
log( log(
'warn', "warn",
`Cannot log telemetry for ${commandName} (${providerName}/${modelId}): AI result missing 'usage' data. (May be expected for streams)` `Cannot log telemetry for ${commandName} (${providerName}/${modelId}): AI result missing 'usage' data. (May be expected for streams)`
); );
} }
let finalMainResult; let finalMainResult;
if (serviceType === 'generateText') { if (serviceType === "generateText") {
finalMainResult = providerResponse.text; finalMainResult = providerResponse.text;
} else if (serviceType === 'generateObject') { } else if (serviceType === "generateObject") {
finalMainResult = providerResponse.object; finalMainResult = providerResponse.object;
} else if (serviceType === 'streamText') { } else if (serviceType === "streamText") {
finalMainResult = providerResponse; finalMainResult = providerResponse;
} else { } else {
log( log(
'error', "error",
`Unknown serviceType in _unifiedServiceRunner: ${serviceType}` `Unknown serviceType in _unifiedServiceRunner: ${serviceType}`
); );
finalMainResult = providerResponse; finalMainResult = providerResponse;
@@ -557,37 +557,38 @@ async function _unifiedServiceRunner(serviceType, params) {
return { return {
mainResult: finalMainResult, mainResult: finalMainResult,
telemetryData: telemetryData telemetryData: telemetryData,
}; };
} catch (error) { } catch (error) {
const cleanMessage = _extractErrorMessage(error); const cleanMessage = _extractErrorMessage(error);
log( log(
'error', "error",
`Service call failed for role ${currentRole} (Provider: ${providerName || 'unknown'}, Model: ${modelId || 'unknown'}): ${cleanMessage}` `Service call failed for role ${currentRole} (Provider: ${providerName || "unknown"}, Model: ${modelId || "unknown"}): ${cleanMessage}`
); );
lastError = error; lastError = error;
lastCleanErrorMessage = cleanMessage; lastCleanErrorMessage = cleanMessage;
if (serviceType === 'generateObject') { if (serviceType === "generateObject") {
const lowerCaseMessage = cleanMessage.toLowerCase(); const lowerCaseMessage = cleanMessage.toLowerCase();
if ( if (
lowerCaseMessage.includes( lowerCaseMessage.includes(
'no endpoints found that support tool use' "no endpoints found that support tool use"
) || ) ||
lowerCaseMessage.includes('does not support tool_use') || lowerCaseMessage.includes("does not support tool_use") ||
lowerCaseMessage.includes('tool use is not supported') || lowerCaseMessage.includes("tool use is not supported") ||
lowerCaseMessage.includes('tools are not supported') || lowerCaseMessage.includes("tools are not supported") ||
lowerCaseMessage.includes('function calling is not supported') lowerCaseMessage.includes("function calling is not supported") ||
lowerCaseMessage.includes("tool use is not supported")
) { ) {
const specificErrorMsg = `Model '${modelId || 'unknown'}' via provider '${providerName || 'unknown'}' does not support the 'tool use' required by generateObjectService. Please configure a model that supports tool/function calling for the '${currentRole}' role, or use generateTextService if structured output is not strictly required.`; const specificErrorMsg = `Model '${modelId || "unknown"}' via provider '${providerName || "unknown"}' does not support the 'tool use' required by generateObjectService. Please configure a model that supports tool/function calling for the '${currentRole}' role, or use generateTextService if structured output is not strictly required.`;
log('error', `[Tool Support Error] ${specificErrorMsg}`); log("error", `[Tool Support Error] ${specificErrorMsg}`);
throw new Error(specificErrorMsg); throw new Error(specificErrorMsg);
} }
} }
} }
} }
log('error', `All roles in the sequence [${sequence.join(', ')}] failed.`); log("error", `All roles in the sequence [${sequence.join(", ")}] failed.`);
throw new Error(lastCleanErrorMessage); throw new Error(lastCleanErrorMessage);
} }
@@ -607,10 +608,10 @@ async function _unifiedServiceRunner(serviceType, params) {
*/ */
async function generateTextService(params) { async function generateTextService(params) {
// Ensure default outputType if not provided // Ensure default outputType if not provided
const defaults = { outputType: 'cli' }; const defaults = { outputType: "cli" };
const combinedParams = { ...defaults, ...params }; const combinedParams = { ...defaults, ...params };
// TODO: Validate commandName exists? // TODO: Validate commandName exists?
return _unifiedServiceRunner('generateText', combinedParams); return _unifiedServiceRunner("generateText", combinedParams);
} }
/** /**
@@ -628,13 +629,13 @@ async function generateTextService(params) {
* @returns {Promise<object>} Result object containing the stream and usage data. * @returns {Promise<object>} Result object containing the stream and usage data.
*/ */
async function streamTextService(params) { async function streamTextService(params) {
const defaults = { outputType: 'cli' }; const defaults = { outputType: "cli" };
const combinedParams = { ...defaults, ...params }; const combinedParams = { ...defaults, ...params };
// TODO: Validate commandName exists? // TODO: Validate commandName exists?
// NOTE: Telemetry for streaming might be tricky as usage data often comes at the end. // NOTE: Telemetry for streaming might be tricky as usage data often comes at the end.
// The current implementation logs *after* the stream is returned. // The current implementation logs *after* the stream is returned.
// We might need to adjust how usage is captured/logged for streams. // We might need to adjust how usage is captured/logged for streams.
return _unifiedServiceRunner('streamText', combinedParams); return _unifiedServiceRunner("streamText", combinedParams);
} }
/** /**
@@ -656,13 +657,13 @@ async function streamTextService(params) {
*/ */
async function generateObjectService(params) { async function generateObjectService(params) {
const defaults = { const defaults = {
objectName: 'generated_object', objectName: "generated_object",
maxRetries: 3, maxRetries: 3,
outputType: 'cli' outputType: "cli",
}; };
const combinedParams = { ...defaults, ...params }; const combinedParams = { ...defaults, ...params };
// TODO: Validate commandName exists? // TODO: Validate commandName exists?
return _unifiedServiceRunner('generateObject', combinedParams); return _unifiedServiceRunner("generateObject", combinedParams);
} }
// --- Telemetry Function --- // --- Telemetry Function ---
@@ -684,10 +685,10 @@ async function logAiUsage({
modelId, modelId,
inputTokens, inputTokens,
outputTokens, outputTokens,
outputType outputType,
}) { }) {
try { try {
const isMCP = outputType === 'mcp'; const isMCP = outputType === "mcp";
const timestamp = new Date().toISOString(); const timestamp = new Date().toISOString();
const totalTokens = (inputTokens || 0) + (outputTokens || 0); const totalTokens = (inputTokens || 0) + (outputTokens || 0);
@@ -711,19 +712,19 @@ async function logAiUsage({
outputTokens: outputTokens || 0, outputTokens: outputTokens || 0,
totalTokens, totalTokens,
totalCost: parseFloat(totalCost.toFixed(6)), totalCost: parseFloat(totalCost.toFixed(6)),
currency // Add currency to the telemetry data currency, // Add currency to the telemetry data
}; };
if (getDebugFlag()) { if (getDebugFlag()) {
log('info', 'AI Usage Telemetry:', telemetryData); log("info", "AI Usage Telemetry:", telemetryData);
} }
// TODO (Subtask 77.2): Send telemetryData securely to the external endpoint. // TODO (Subtask 77.2): Send telemetryData securely to the external endpoint.
return telemetryData; return telemetryData;
} catch (error) { } catch (error) {
log('error', `Failed to log AI usage telemetry: ${error.message}`, { log("error", `Failed to log AI usage telemetry: ${error.message}`, {
error error,
}); });
// Don't re-throw; telemetry failure shouldn't block core functionality. // Don't re-throw; telemetry failure shouldn't block core functionality.
return null; return null;
@@ -734,5 +735,5 @@ export {
generateTextService, generateTextService,
streamTextService, streamTextService,
generateObjectService, generateObjectService,
logAiUsage logAiUsage,
}; };

View File

@@ -153,7 +153,7 @@
"id": "sonar-pro", "id": "sonar-pro",
"swe_score": 0, "swe_score": 0,
"cost_per_1m_tokens": { "input": 3, "output": 15 }, "cost_per_1m_tokens": { "input": 3, "output": 15 },
"allowed_roles": ["research"], "allowed_roles": ["main", "research"],
"max_tokens": 8700 "max_tokens": 8700
}, },
{ {
@@ -174,14 +174,14 @@
"id": "sonar-reasoning-pro", "id": "sonar-reasoning-pro",
"swe_score": 0.211, "swe_score": 0.211,
"cost_per_1m_tokens": { "input": 2, "output": 8 }, "cost_per_1m_tokens": { "input": 2, "output": 8 },
"allowed_roles": ["main", "fallback"], "allowed_roles": ["main", "research", "fallback"],
"max_tokens": 8700 "max_tokens": 8700
}, },
{ {
"id": "sonar-reasoning", "id": "sonar-reasoning",
"swe_score": 0.211, "swe_score": 0.211,
"cost_per_1m_tokens": { "input": 1, "output": 5 }, "cost_per_1m_tokens": { "input": 1, "output": 5 },
"allowed_roles": ["main", "fallback"], "allowed_roles": ["main", "research", "fallback"],
"max_tokens": 8700 "max_tokens": 8700
} }
], ],

View File

@@ -1,5 +1,5 @@
import { generateText, streamText, generateObject } from 'ai'; import { generateText, streamText, generateObject } from "ai";
import { log } from '../../scripts/modules/index.js'; import { log } from "../../scripts/modules/index.js";
/** /**
* Base class for all AI providers * Base class for all AI providers
@@ -7,7 +7,7 @@ import { log } from '../../scripts/modules/index.js';
export class BaseAIProvider { export class BaseAIProvider {
constructor() { constructor() {
if (this.constructor === BaseAIProvider) { if (this.constructor === BaseAIProvider) {
throw new Error('BaseAIProvider cannot be instantiated directly'); throw new Error("BaseAIProvider cannot be instantiated directly");
} }
// Each provider must set their name // Each provider must set their name
@@ -51,10 +51,10 @@ export class BaseAIProvider {
params.temperature !== undefined && params.temperature !== undefined &&
(params.temperature < 0 || params.temperature > 1) (params.temperature < 0 || params.temperature > 1)
) { ) {
throw new Error('Temperature must be between 0 and 1'); throw new Error("Temperature must be between 0 and 1");
} }
if (params.maxTokens !== undefined && params.maxTokens <= 0) { if (params.maxTokens !== undefined && params.maxTokens <= 0) {
throw new Error('maxTokens must be greater than 0'); throw new Error("maxTokens must be greater than 0");
} }
} }
@@ -63,13 +63,13 @@ export class BaseAIProvider {
*/ */
validateMessages(messages) { validateMessages(messages) {
if (!messages || !Array.isArray(messages) || messages.length === 0) { if (!messages || !Array.isArray(messages) || messages.length === 0) {
throw new Error('Invalid or empty messages array provided'); throw new Error("Invalid or empty messages array provided");
} }
for (const msg of messages) { for (const msg of messages) {
if (!msg.role || !msg.content) { if (!msg.role || !msg.content) {
throw new Error( throw new Error(
'Invalid message format. Each message must have role and content' "Invalid message format. Each message must have role and content"
); );
} }
} }
@@ -79,9 +79,9 @@ export class BaseAIProvider {
* Common error handler * Common error handler
*/ */
handleError(operation, error) { handleError(operation, error) {
const errorMessage = error.message || 'Unknown error occurred'; const errorMessage = error.message || "Unknown error occurred";
log('error', `${this.name} ${operation} failed: ${errorMessage}`, { log("error", `${this.name} ${operation} failed: ${errorMessage}`, {
error error,
}); });
throw new Error( throw new Error(
`${this.name} API error during ${operation}: ${errorMessage}` `${this.name} API error during ${operation}: ${errorMessage}`
@@ -93,7 +93,7 @@ export class BaseAIProvider {
* @abstract * @abstract
*/ */
getClient(params) { getClient(params) {
throw new Error('getClient must be implemented by provider'); throw new Error("getClient must be implemented by provider");
} }
/** /**
@@ -105,7 +105,7 @@ export class BaseAIProvider {
this.validateMessages(params.messages); this.validateMessages(params.messages);
log( log(
'debug', "debug",
`Generating ${this.name} text with model: ${params.modelId}` `Generating ${this.name} text with model: ${params.modelId}`
); );
@@ -114,11 +114,11 @@ export class BaseAIProvider {
model: client(params.modelId), model: client(params.modelId),
messages: params.messages, messages: params.messages,
maxTokens: params.maxTokens, maxTokens: params.maxTokens,
temperature: params.temperature temperature: params.temperature,
}); });
log( log(
'debug', "debug",
`${this.name} generateText completed successfully for model: ${params.modelId}` `${this.name} generateText completed successfully for model: ${params.modelId}`
); );
@@ -127,11 +127,11 @@ export class BaseAIProvider {
usage: { usage: {
inputTokens: result.usage?.promptTokens, inputTokens: result.usage?.promptTokens,
outputTokens: result.usage?.completionTokens, outputTokens: result.usage?.completionTokens,
totalTokens: result.usage?.totalTokens totalTokens: result.usage?.totalTokens,
} },
}; };
} catch (error) { } catch (error) {
this.handleError('text generation', error); this.handleError("text generation", error);
} }
} }
@@ -143,24 +143,24 @@ export class BaseAIProvider {
this.validateParams(params); this.validateParams(params);
this.validateMessages(params.messages); this.validateMessages(params.messages);
log('debug', `Streaming ${this.name} text with model: ${params.modelId}`); log("debug", `Streaming ${this.name} text with model: ${params.modelId}`);
const client = this.getClient(params); const client = this.getClient(params);
const stream = await streamText({ const stream = await streamText({
model: client(params.modelId), model: client(params.modelId),
messages: params.messages, messages: params.messages,
maxTokens: params.maxTokens, maxTokens: params.maxTokens,
temperature: params.temperature temperature: params.temperature,
}); });
log( log(
'debug', "debug",
`${this.name} streamText initiated successfully for model: ${params.modelId}` `${this.name} streamText initiated successfully for model: ${params.modelId}`
); );
return stream; return stream;
} catch (error) { } catch (error) {
this.handleError('text streaming', error); this.handleError("text streaming", error);
} }
} }
@@ -173,14 +173,14 @@ export class BaseAIProvider {
this.validateMessages(params.messages); this.validateMessages(params.messages);
if (!params.schema) { if (!params.schema) {
throw new Error('Schema is required for object generation'); throw new Error("Schema is required for object generation");
} }
if (!params.objectName) { if (!params.objectName) {
throw new Error('Object name is required for object generation'); throw new Error("Object name is required for object generation");
} }
log( log(
'debug', "debug",
`Generating ${this.name} object ('${params.objectName}') with model: ${params.modelId}` `Generating ${this.name} object ('${params.objectName}') with model: ${params.modelId}`
); );
@@ -189,13 +189,13 @@ export class BaseAIProvider {
model: client(params.modelId), model: client(params.modelId),
messages: params.messages, messages: params.messages,
schema: params.schema, schema: params.schema,
mode: 'tool', mode: "auto",
maxTokens: params.maxTokens, maxTokens: params.maxTokens,
temperature: params.temperature temperature: params.temperature,
}); });
log( log(
'debug', "debug",
`${this.name} generateObject completed successfully for model: ${params.modelId}` `${this.name} generateObject completed successfully for model: ${params.modelId}`
); );
@@ -204,11 +204,11 @@ export class BaseAIProvider {
usage: { usage: {
inputTokens: result.usage?.promptTokens, inputTokens: result.usage?.promptTokens,
outputTokens: result.usage?.completionTokens, outputTokens: result.usage?.completionTokens,
totalTokens: result.usage?.totalTokens totalTokens: result.usage?.totalTokens,
} },
}; };
} catch (error) { } catch (error) {
this.handleError('object generation', error); this.handleError("object generation", error);
} }
} }
} }