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",
"version": "0.16.1",
"version": "0.16.2-rc.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "task-master-ai",
"version": "0.16.1",
"version": "0.16.2-rc.0",
"license": "MIT WITH Commons-Clause",
"dependencies": {
"@ai-sdk/amazon-bedrock": "^2.2.9",
@@ -29,7 +29,7 @@
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.21.2",
"fastmcp": "^1.20.5",
"fastmcp": "^2.2.2",
"figlet": "^1.8.0",
"fuse.js": "^7.1.0",
"gradient-string": "^3.0.0",
@@ -3593,18 +3593,19 @@
}
},
"node_modules/@modelcontextprotocol/sdk": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.8.0.tgz",
"integrity": "sha512-e06W7SwrontJDHwCawNO5SGxG+nU9AAx+jpHHZqGl/WrDBdWOpvirC+s58VpJTB5QemI4jTRcjWT4Pt3Q1NPQQ==",
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.12.1.tgz",
"integrity": "sha512-KG1CZhZfWg+u8pxeM/mByJDScJSrjjxLc8fwQqbsS8xCjBmQfMNEBTotYdNanKekepnfRI85GtgQlctLFpcYPw==",
"license": "MIT",
"dependencies": {
"ajv": "^6.12.6",
"content-type": "^1.0.5",
"cors": "^2.8.5",
"cross-spawn": "^7.0.3",
"cross-spawn": "^7.0.5",
"eventsource": "^3.0.2",
"express": "^5.0.1",
"express-rate-limit": "^7.5.0",
"pkce-challenge": "^4.1.0",
"pkce-challenge": "^5.0.0",
"raw-body": "^3.0.0",
"zod": "^3.23.8",
"zod-to-json-schema": "^3.24.1"
@@ -3668,84 +3669,45 @@
}
},
"node_modules/@modelcontextprotocol/sdk/node_modules/express": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/express/-/express-5.0.1.tgz",
"integrity": "sha512-ORF7g6qGnD+YtUG9yx4DFoqCShNMmUKiXuT5oWMHiOvt/4WFbHC6yCwQMTSBMno7AqntNCAzzcnnjowRkTL9eQ==",
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
"license": "MIT",
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.0.1",
"body-parser": "^2.2.0",
"content-disposition": "^1.0.0",
"content-type": "~1.0.4",
"cookie": "0.7.1",
"content-type": "^1.0.5",
"cookie": "^0.7.1",
"cookie-signature": "^1.2.1",
"debug": "4.3.6",
"depd": "2.0.0",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "^2.0.0",
"fresh": "2.0.0",
"http-errors": "2.0.0",
"debug": "^4.4.0",
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"etag": "^1.8.1",
"finalhandler": "^2.1.0",
"fresh": "^2.0.0",
"http-errors": "^2.0.0",
"merge-descriptors": "^2.0.0",
"methods": "~1.1.2",
"mime-types": "^3.0.0",
"on-finished": "2.4.1",
"once": "1.4.0",
"parseurl": "~1.3.3",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
"router": "^2.0.0",
"safe-buffer": "5.2.1",
"on-finished": "^2.4.1",
"once": "^1.4.0",
"parseurl": "^1.3.3",
"proxy-addr": "^2.0.7",
"qs": "^6.14.0",
"range-parser": "^1.2.1",
"router": "^2.2.0",
"send": "^1.1.0",
"serve-static": "^2.1.0",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"type-is": "^2.0.0",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
"serve-static": "^2.2.0",
"statuses": "^2.0.1",
"type-is": "^2.0.1",
"vary": "^1.1.2"
},
"engines": {
"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": {
"url": "https://github.com/sponsors/ljharb"
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": {
@@ -4640,6 +4602,12 @@
"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": {
"version": "0.2.7",
"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": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
@@ -6432,9 +6416,9 @@
}
},
"node_modules/eventsource": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.6.tgz",
"integrity": "sha512-l19WpE2m9hSuyP06+FbuUUf1G+R0SFLrtQfbRb9PRr+oimOfxQhgGCbVaXg5IvZyyTThJsxh6L/srkMiCeBPDA==",
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
"integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==",
"license": "MIT",
"dependencies": {
"eventsource-parser": "^3.0.1"
@@ -6636,6 +6620,12 @@
"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": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
@@ -6657,7 +6647,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
"dev": true,
"license": "MIT"
},
"node_modules/fast-safe-stringify": {
@@ -6690,22 +6679,24 @@
}
},
"node_modules/fastmcp": {
"version": "1.20.5",
"resolved": "https://registry.npmjs.org/fastmcp/-/fastmcp-1.20.5.tgz",
"integrity": "sha512-jwcPgMF9bcE9qsEG82YMlAG26/n5CSYsr95e60ntqWWd+3kgTBbUIasB3HfpqHLTNaQuoX6/jl18fpDcybBjcQ==",
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/fastmcp/-/fastmcp-2.2.2.tgz",
"integrity": "sha512-V6qEfOnABo7lDrwHqZQhCYd52KXzK85/ipllmUyaos8WLAjygP9NuuKcm1kiEWa0jjsFxe2kf/Y+T4PRE+0rEw==",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.6.0",
"@modelcontextprotocol/sdk": "^1.10.2",
"@standard-schema/spec": "^1.0.0",
"execa": "^9.5.2",
"file-type": "^20.3.0",
"file-type": "^20.4.1",
"fuse.js": "^7.1.0",
"mcp-proxy": "^2.10.4",
"mcp-proxy": "^3.0.3",
"strict-event-emitter-types": "^2.0.0",
"undici": "^7.4.0",
"undici": "^7.8.0",
"uri-templates": "^0.2.0",
"xsschema": "0.3.0-beta.1",
"yargs": "^17.7.2",
"zod": "^3.24.2",
"zod-to-json-schema": "^3.24.3"
"zod": "^3.25.12",
"zod-to-json-schema": "^3.24.5"
},
"bin": {
"fastmcp": "dist/bin/fastmcp.js"
@@ -9104,6 +9095,12 @@
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
"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": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
@@ -9383,19 +9380,31 @@
}
},
"node_modules/mcp-proxy": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/mcp-proxy/-/mcp-proxy-2.12.0.tgz",
"integrity": "sha512-hL2Y6EtK7vkgAOZxOQe9M4Z9g5xEnvR4ZYBKqFi/5tjhz/1jyNEz5NL87Uzv46k8iZQPVNEof/T6arEooBU5bQ==",
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/mcp-proxy/-/mcp-proxy-3.3.0.tgz",
"integrity": "sha512-xyFKQEZ64HC7lxScBHjb5fxiPoyJjjkPhwH5hWUT0oL/ttCpMGZDJrYZRGFKVJiLLkrZPAkHnMGkI+WMlyD/cg==",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.6.0",
"eventsource": "^3.0.5",
"@modelcontextprotocol/sdk": "^1.11.4",
"eventsource": "^4.0.0",
"yargs": "^17.7.2"
},
"bin": {
"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": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -10085,9 +10094,9 @@
}
},
"node_modules/pkce-challenge": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-4.1.0.tgz",
"integrity": "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz",
"integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==",
"license": "MIT",
"engines": {
"node": ">=16.20.0"
@@ -11319,9 +11328,9 @@
}
},
"node_modules/undici": {
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.6.0.tgz",
"integrity": "sha512-gaFsbThjrDGvAaD670r81RZro/s6H2PVZF640Qn0p5kZK+/rim7/mmyfp2W7VB5vOMaFM8vuFBJUaMlaZTYHlA==",
"version": "7.10.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.10.0.tgz",
"integrity": "sha512-u5otvFBOBZvmdjWLVW+5DAc9Nkq8f24g0O9oY7qw2JVIF1VocIFoyz9JFkuVOS2j41AufeO0xnlweJ2RLT8nGw==",
"license": "MIT",
"engines": {
"node": ">=20.18.1"
@@ -11395,6 +11404,15 @@
"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": {
"version": "0.2.0",
"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": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
@@ -11734,9 +11786,9 @@
"license": "MIT"
},
"node_modules/zod": {
"version": "3.24.2",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz",
"integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==",
"version": "3.25.56",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.56.tgz",
"integrity": "sha512-rd6eEF3BTNvQnR2e2wwolfTmUTnp70aUTqr0oaGbHifzC3BKJsoV+Gat8vxUMR1hwOKBs6El+qWehrHbCpW6SQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import { generateText, streamText, generateObject } from 'ai';
import { log } from '../../scripts/modules/index.js';
import { generateText, streamText, generateObject } from "ai";
import { log } from "../../scripts/modules/index.js";
/**
* Base class for all AI providers
@@ -7,7 +7,7 @@ import { log } from '../../scripts/modules/index.js';
export class BaseAIProvider {
constructor() {
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
@@ -51,10 +51,10 @@ export class BaseAIProvider {
params.temperature !== undefined &&
(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) {
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) {
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) {
if (!msg.role || !msg.content) {
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
*/
handleError(operation, error) {
const errorMessage = error.message || 'Unknown error occurred';
log('error', `${this.name} ${operation} failed: ${errorMessage}`, {
error
const errorMessage = error.message || "Unknown error occurred";
log("error", `${this.name} ${operation} failed: ${errorMessage}`, {
error,
});
throw new Error(
`${this.name} API error during ${operation}: ${errorMessage}`
@@ -93,7 +93,7 @@ export class BaseAIProvider {
* @abstract
*/
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);
log(
'debug',
"debug",
`Generating ${this.name} text with model: ${params.modelId}`
);
@@ -114,11 +114,11 @@ export class BaseAIProvider {
model: client(params.modelId),
messages: params.messages,
maxTokens: params.maxTokens,
temperature: params.temperature
temperature: params.temperature,
});
log(
'debug',
"debug",
`${this.name} generateText completed successfully for model: ${params.modelId}`
);
@@ -127,11 +127,11 @@ export class BaseAIProvider {
usage: {
inputTokens: result.usage?.promptTokens,
outputTokens: result.usage?.completionTokens,
totalTokens: result.usage?.totalTokens
}
totalTokens: result.usage?.totalTokens,
},
};
} catch (error) {
this.handleError('text generation', error);
this.handleError("text generation", error);
}
}
@@ -143,24 +143,24 @@ export class BaseAIProvider {
this.validateParams(params);
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 stream = await streamText({
model: client(params.modelId),
messages: params.messages,
maxTokens: params.maxTokens,
temperature: params.temperature
temperature: params.temperature,
});
log(
'debug',
"debug",
`${this.name} streamText initiated successfully for model: ${params.modelId}`
);
return stream;
} catch (error) {
this.handleError('text streaming', error);
this.handleError("text streaming", error);
}
}
@@ -173,14 +173,14 @@ export class BaseAIProvider {
this.validateMessages(params.messages);
if (!params.schema) {
throw new Error('Schema is required for object generation');
throw new Error("Schema is required for object generation");
}
if (!params.objectName) {
throw new Error('Object name is required for object generation');
throw new Error("Object name is required for object generation");
}
log(
'debug',
"debug",
`Generating ${this.name} object ('${params.objectName}') with model: ${params.modelId}`
);
@@ -189,13 +189,13 @@ export class BaseAIProvider {
model: client(params.modelId),
messages: params.messages,
schema: params.schema,
mode: 'tool',
mode: "auto",
maxTokens: params.maxTokens,
temperature: params.temperature
temperature: params.temperature,
});
log(
'debug',
"debug",
`${this.name} generateObject completed successfully for model: ${params.modelId}`
);
@@ -204,11 +204,11 @@ export class BaseAIProvider {
usage: {
inputTokens: result.usage?.promptTokens,
outputTokens: result.usage?.completionTokens,
totalTokens: result.usage?.totalTokens
}
totalTokens: result.usage?.totalTokens,
},
};
} catch (error) {
this.handleError('object generation', error);
this.handleError("object generation", error);
}
}
}