Compare commits

...

16 Commits

Author SHA1 Message Date
Ben Vargas
8df2d50bac chore: add changeset for Claude Code provider feature 2025-06-20 16:21:37 +03:00
Ben Vargas
d0a7deb46c fix(models): add missing --claude-code flag to models command
The models command was missing the --claude-code provider flag, preventing users from setting Claude Code models via CLI. While the backend already supported claude-code as a provider hint, there was no command-line flag to trigger it.

Changes:
- Added --claude-code option to models command alongside existing provider flags
- Updated provider flags validation to include claudeCode option
- Added claude-code to providerHint logic for all three model roles (main, research, fallback)
- Updated error message to include --claude-code in list of mutually exclusive flags
- Added example usage in help text

This allows users to properly set Claude Code models using commands like:
  task-master models --set-main sonnet --claude-code
  task-master models --set-main opus --claude-code

Without this flag, users would get "Model ID not found" errors when trying to set claude-code models, as the system couldn't determine the correct provider for generic model names like "sonnet" or "opus".
2025-06-20 16:21:36 +03:00
Ben Vargas
18a5f63d06 style: apply biome formatting to test files 2025-06-20 16:21:21 +03:00
Ben Vargas
5d82b69610 docs: add Claude Code support information to README
- Added Claude Code to the list of supported providers in Requirements section
- Noted that Claude Code requires no API key but needs Claude Code CLI
- Added example of configuring claude-code/sonnet model
- Created dedicated Claude Code Support section with key information
- Added link to detailed Claude Code setup documentation

This ensures users are aware of the Claude Code option as a no-API-key
alternative for using Claude models.
2025-06-20 16:21:21 +03:00
Ben Vargas
de77826bcc revert: remove maxTokens update functionality from init
This functionality was out of scope for the Claude Code provider PR.
The automatic updating of maxTokens values in config.json during
initialization is a general improvement that should be in a separate PR.

Additionally, Claude Code ignores maxTokens and temperature parameters
anyway, making this change irrelevant for the Claude Code integration.

Removed:
- scripts/modules/update-config-tokens.js
- Import and usage in scripts/init.js
2025-06-20 16:21:21 +03:00
Ben Vargas
4125025abd test: add comprehensive tests for ClaudeCodeProvider
Addresses code review feedback about missing automated tests for the ClaudeCodeProvider.

## Changes

- Added unit tests for ClaudeCodeProvider class covering constructor, validateAuth, and getClient methods
- Added unit tests for ClaudeCodeLanguageModel testing lazy loading behavior and error handling
- Added integration tests verifying optional dependency behavior when @anthropic-ai/claude-code is not installed

## Test Coverage

1. **Unit Tests**:
   - ClaudeCodeProvider: Basic functionality, no API key requirement, client creation
   - ClaudeCodeLanguageModel: Model initialization, lazy loading, error messages, warning generation

2. **Integration Tests**:
   - Optional dependency behavior when package is not installed
   - Clear error messages for users about missing package
   - Provider instantiation works but usage fails gracefully

All tests pass and provide comprehensive coverage for the claude-code provider implementation.
2025-06-20 16:20:56 +03:00
Ben Vargas
72a324075c feat: make @anthropic-ai/claude-code an optional dependency
This change makes the Claude Code SDK package optional, preventing installation failures for users who don't need Claude Code functionality.

Changes:
- Added @anthropic-ai/claude-code to optionalDependencies in package.json
- Implemented lazy loading in language-model.js to only import the SDK when actually used
- Updated documentation to explain the optional installation requirement
- Applied formatting fixes to ensure code consistency

Benefits:
- Users without Claude Code subscriptions don't need to install the dependency
- Reduces package size for users who don't use Claude Code
- Prevents installation failures if the package is unavailable
- Provides clear error messages when the package is needed but not installed

The implementation uses dynamic imports to load the SDK only when doGenerate() or doStream() is called, ensuring the provider can be instantiated without the package present.
2025-06-20 16:20:56 +03:00
Ben Vargas
93271e0a2d fix(docs): correct invalid commands in claude-code usage examples
- Remove non-existent 'do', 'estimate', and 'analyze' commands
- Replace with actual Task Master commands: next, show, set-status
- Use correct syntax for parse-prd and analyze-complexity
2025-06-20 16:20:56 +03:00
Ben Vargas
df9ce457ff feat: add Claude Code provider support
Implements Claude Code as a new AI provider that uses the Claude Code CLI
without requiring API keys. This enables users to leverage Claude models
through their local Claude Code installation.

Key changes:
- Add complete AI SDK v1 implementation for Claude Code provider
  - Custom SDK with streaming/non-streaming support
  - Session management for conversation continuity
  - JSON extraction for object generation mode
  - Support for advanced settings (maxTurns, allowedTools, etc.)

- Integrate Claude Code into Task Master's provider system
  - Update ai-services-unified.js to handle keyless authentication
  - Add provider to supported-models.json with opus/sonnet models
  - Ensure correct maxTokens values are applied (opus: 32000, sonnet: 64000)

- Fix maxTokens configuration issue
  - Add max_tokens property to getAvailableModels() output
  - Update setModel() to properly handle claude-code models
  - Create update-config-tokens.js utility for init process

- Add comprehensive documentation
  - User guide with configuration examples
  - Advanced settings explanation and future integration options

The implementation maintains full backward compatibility with existing
providers while adding seamless Claude Code support to all Task Master
commands.
2025-06-20 16:20:56 +03:00
Ralph Khreish
04f44a2d3d chore: fix package.json 2025-06-20 16:10:52 +03:00
github-actions[bot]
36fe838fd5 Version Packages 2025-06-20 16:10:52 +03:00
github-actions[bot]
415b1835d4 docs: Auto-update and format models.md 2025-06-20 13:05:31 +00:00
Ralph Khreish
78112277b3 fix(bedrock): improve AWS credential handling and add model definitions (#826)
* fix(bedrock): improve AWS credential handling and add model definitions

- Change error to warning when AWS credentials are missing in environment
- Allow fallback to system configuration (aws config files or instance profiles)
- Remove hardcoded region and profile parameters in Bedrock client
- Add Claude 3.7 Sonnet and DeepSeek R1 model definitions for Bedrock
- Update config manager to properly handle Bedrock provider

* chore: cleanup and format and small refactor

---------

Co-authored-by: Ray Krueger <raykrueger@gmail.com>
2025-06-20 15:05:20 +02:00
Ralph Khreish
2bb4260966 fix: Fix external provider support (#726) 2025-06-20 14:59:53 +02:00
Nathan Marley
3a2325a963 fix: switch to ESM export to avoid mixed format (#633)
* fix: switch to ESM export to avoid mixed format

The CLI entrypoint was using `module.exports` alongside ESM `import` statements,
resulting in an invalid mixed module format. Replaced the CommonJS export with
a proper ESM `export` to maintain consistency and prevent module resolution issues.

* chore: add changeset

---------

Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com>
2025-06-20 14:12:36 +02:00
Ralph Khreish
1bd6d4f246 fix: providers config for azure, bedrock, and vertex (#822)
* fix: providers config for azure, bedrock, and vertex

* chore: improve changelog

* chore: fix CI
2025-06-20 13:13:53 +02:00
35 changed files with 2644 additions and 265 deletions

View File

@@ -0,0 +1,5 @@
---
"task-master-ai": patch
---
Improves Amazon Bedrock support

View File

@@ -1,5 +0,0 @@
---
"task-master-ai": patch
---
Fix contextGatherer bug when adding a task `Cannot read properties of undefined (reading 'forEach')`

View File

@@ -0,0 +1,11 @@
---
"task-master-ai": patch
---
Improve provider validation system with clean constants structure
- **Fixed "Invalid provider hint" errors**: Resolved validation failures for Azure, Vertex, and Bedrock providers
- **Improved search UX**: Integrated search for better model discovery with real-time filtering
- **Better organization**: Moved custom provider options to bottom of model selection with clear section separators
This change ensures all custom providers (Azure, Vertex, Bedrock, OpenRouter, Ollama) work correctly in `task-master models --setup`

View File

@@ -0,0 +1,5 @@
---
"task-master-ai": patch
---
Fix weird `task-master init` bug when using in certain environments

View File

@@ -0,0 +1,22 @@
---
"task-master-ai": minor
---
Add Claude Code provider support
Introduces a new provider that enables using Claude models (Opus and Sonnet) through the Claude Code CLI without requiring an API key.
Key features:
- New claude-code provider with support for opus and sonnet models
- No API key required - uses local Claude Code CLI installation
- Optional dependency - won't affect users who don't need Claude Code
- Lazy loading ensures the provider only loads when requested
- Full integration with existing Task Master commands and workflows
- Comprehensive test coverage for reliability
- New --claude-code flag for the models command
Users can now configure Claude Code models with:
task-master models --set-main sonnet --claude-code
task-master models --set-research opus --claude-code
The @anthropic-ai/claude-code package is optional and won't be installed unless explicitly needed.

View File

@@ -1,14 +1,14 @@
{
"models": {
"main": {
"provider": "anthropic",
"modelId": "claude-sonnet-4-20250514",
"provider": "vertex",
"modelId": "gemini-1.5-pro-002",
"maxTokens": 50000,
"temperature": 0.2
},
"research": {
"provider": "perplexity",
"modelId": "sonar-pro",
"modelId": "sonar",
"maxTokens": 8700,
"temperature": 0.1
},
@@ -20,7 +20,6 @@
}
},
"global": {
"userId": "1234567890",
"logLevel": "info",
"debug": false,
"defaultSubtasks": 5,
@@ -28,6 +27,7 @@
"projectName": "Taskmaster",
"ollamaBaseURL": "http://localhost:11434/api",
"bedrockBaseURL": "https://bedrock.us-east-1.amazonaws.com",
"userId": "1234567890",
"azureBaseURL": "https://your-endpoint.azure.com/",
"defaultTag": "master"
}

View File

@@ -1,5 +1,11 @@
# task-master-ai
## 0.17.1
### Patch Changes
- [#789](https://github.com/eyaltoledano/claude-task-master/pull/789) [`8cde6c2`](https://github.com/eyaltoledano/claude-task-master/commit/8cde6c27087f401d085fe267091ae75334309d96) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix contextGatherer bug when adding a task `Cannot read properties of undefined (reading 'forEach')`
## 0.17.0
### Minor Changes

View File

@@ -47,8 +47,9 @@ At least one (1) of the following is required:
- Perplexity API key (for research model)
- xAI API Key (for research or main model)
- OpenRouter API Key (for research or main model)
- Claude Code (no API key required - requires Claude Code CLI)
Using the research model is optional but highly recommended. You will need at least ONE API key. Adding all API keys enables you to seamlessly switch between model providers at will.
Using the research model is optional but highly recommended. You will need at least ONE API key (unless using Claude Code). Adding all API keys enables you to seamlessly switch between model providers at will.
## Quick Start
@@ -131,7 +132,12 @@ In your editor's AI chat pane, say:
Change the main, research and fallback models to <model_name>, <model_name> and <model_name> respectively.
```
[Table of available models](docs/models.md)
For example, to use Claude Code (no API key required):
```txt
Change the main model to claude-code/sonnet
```
[Table of available models](docs/models.md) | [Claude Code setup](docs/examples/claude-code-usage.md)
#### 4. Initialize Task Master
@@ -224,6 +230,16 @@ task-master generate
task-master rules add windsurf,roo,vscode
```
## Claude Code Support
Task Master now supports Claude models through the Claude Code CLI, which requires no API key:
- **Models**: `claude-code/opus` and `claude-code/sonnet`
- **Requirements**: Claude Code CLI installed
- **Benefits**: No API key needed, uses your local Claude instance
[Learn more about Claude Code setup](docs/examples/claude-code-usage.md)
## Troubleshooting
### If `task-master init` doesn't respond

View File

@@ -373,8 +373,4 @@ if (process.argv.length <= 2) {
}
// Add exports at the end of the file
if (typeof module !== 'undefined') {
module.exports = {
detectCamelCaseFlags
};
}
export { detectCamelCaseFlags };

View File

@@ -0,0 +1,169 @@
# Claude Code Provider Usage Example
The Claude Code provider allows you to use Claude models through the Claude Code CLI without requiring an API key.
## Configuration
To use the Claude Code provider, update your `.taskmaster/config.json`:
```json
{
"models": {
"main": {
"provider": "claude-code",
"modelId": "sonnet",
"maxTokens": 64000,
"temperature": 0.2
},
"research": {
"provider": "claude-code",
"modelId": "opus",
"maxTokens": 32000,
"temperature": 0.1
},
"fallback": {
"provider": "claude-code",
"modelId": "sonnet",
"maxTokens": 64000,
"temperature": 0.2
}
}
}
```
## Available Models
- `opus` - Claude Opus model (SWE score: 0.725)
- `sonnet` - Claude Sonnet model (SWE score: 0.727)
## Usage
Once configured, you can use Claude Code with all Task Master commands:
```bash
# Generate tasks from a PRD
task-master parse-prd --input=prd.txt
# Analyze project complexity
task-master analyze-complexity
# Show the next task to work on
task-master next
# View a specific task
task-master show task-001
# Update task status
task-master set-status --id=task-001 --status=in-progress
```
## Requirements
1. Claude Code CLI must be installed and authenticated on your system
2. Install the optional `@anthropic-ai/claude-code` package if you enable this provider:
```bash
npm install @anthropic-ai/claude-code
```
3. No API key is required in your environment variables or MCP configuration
## Advanced Settings
The Claude Code SDK supports additional settings that provide fine-grained control over Claude's behavior. While these settings are implemented in the underlying SDK (`src/ai-providers/custom-sdk/claude-code/`), they are not currently exposed through Task Master's standard API due to architectural constraints.
### Supported Settings
```javascript
const settings = {
// Maximum conversation turns Claude can make in a single request
maxTurns: 5,
// Custom system prompt to override Claude Code's default behavior
customSystemPrompt: "You are a helpful assistant focused on code quality",
// Permission mode for file system operations
permissionMode: 'default', // Options: 'default', 'restricted', 'permissive'
// Explicitly allow only certain tools
allowedTools: ['Read', 'LS'], // Claude can only read files and list directories
// Explicitly disallow certain tools
disallowedTools: ['Write', 'Edit'], // Prevent Claude from modifying files
// MCP servers for additional tool integrations
mcpServers: []
};
```
### Current Limitations
Task Master uses a standardized `BaseAIProvider` interface that only passes through common parameters (modelId, messages, maxTokens, temperature) to maintain consistency across all providers. The Claude Code advanced settings are implemented in the SDK but not accessible through Task Master's high-level commands.
### Future Integration Options
For developers who need to use these advanced settings, there are three potential approaches:
#### Option 1: Extend BaseAIProvider
Modify the core Task Master architecture to support provider-specific settings:
```javascript
// In BaseAIProvider
const result = await generateText({
model: client(params.modelId),
messages: params.messages,
maxTokens: params.maxTokens,
temperature: params.temperature,
...params.providerSettings // New: pass through provider-specific settings
});
```
#### Option 2: Override Methods in ClaudeCodeProvider
Create custom implementations that extract and use Claude-specific settings:
```javascript
// In ClaudeCodeProvider
async generateText(params) {
const { maxTurns, allowedTools, disallowedTools, ...baseParams } = params;
const client = this.getClient({
...baseParams,
settings: { maxTurns, allowedTools, disallowedTools }
});
// Continue with generation...
}
```
#### Option 3: Direct SDK Usage
For immediate access to advanced features, developers can use the Claude Code SDK directly:
```javascript
import { createClaudeCode } from 'task-master-ai/ai-providers/custom-sdk/claude-code';
const claude = createClaudeCode({
defaultSettings: {
maxTurns: 5,
allowedTools: ['Read', 'LS'],
disallowedTools: ['Write', 'Edit']
}
});
const model = claude('sonnet');
const result = await generateText({
model,
messages: [{ role: 'user', content: 'Analyze this code...' }]
});
```
### Why These Settings Matter
- **maxTurns**: Useful for complex refactoring tasks that require multiple iterations
- **customSystemPrompt**: Allows specializing Claude for specific domains or coding standards
- **permissionMode**: Critical for security in production environments
- **allowedTools/disallowedTools**: Enable read-only analysis modes or restrict access to sensitive operations
- **mcpServers**: Future extensibility for custom tool integrations
## Notes
- The Claude Code provider doesn't track usage costs (shown as 0 in telemetry)
- Session management is handled automatically for conversation continuity
- Some AI SDK parameters (temperature, maxTokens) are not supported by Claude Code CLI and will be ignored

View File

@@ -4,6 +4,7 @@
| Provider | Model Name | SWE Score | Input Cost | Output Cost |
| ---------- | ---------------------------------------------- | --------- | ---------- | ----------- |
| bedrock | us.anthropic.claude-3-7-sonnet-20250219-v1:0 | 0.623 | 3 | 15 |
| anthropic | claude-sonnet-4-20250514 | 0.727 | 3 | 15 |
| anthropic | claude-opus-4-20250514 | 0.725 | 15 | 75 |
| anthropic | claude-3-7-sonnet-20250219 | 0.623 | 3 | 15 |
@@ -66,6 +67,7 @@
| Provider | Model Name | SWE Score | Input Cost | Output Cost |
| ---------- | -------------------------- | --------- | ---------- | ----------- |
| bedrock | us.deepseek.r1-v1:0 | — | 1.35 | 5.4 |
| openai | gpt-4o-search-preview | 0.33 | 2.5 | 10 |
| openai | gpt-4o-mini-search-preview | 0.3 | 0.15 | 0.6 |
| perplexity | sonar-pro | — | 3 | 15 |
@@ -80,6 +82,7 @@
| Provider | Model Name | SWE Score | Input Cost | Output Cost |
| ---------- | ---------------------------------------------- | --------- | ---------- | ----------- |
| bedrock | us.anthropic.claude-3-7-sonnet-20250219-v1:0 | 0.623 | 3 | 15 |
| anthropic | claude-sonnet-4-20250514 | 0.727 | 3 | 15 |
| anthropic | claude-opus-4-20250514 | 0.725 | 15 | 75 |
| anthropic | claude-3-7-sonnet-20250219 | 0.623 | 3 | 15 |

View File

@@ -13,6 +13,41 @@ import {
disableSilentMode
} from '../../../../scripts/modules/utils.js';
import { createLogWrapper } from '../../tools/utils.js';
import { CUSTOM_PROVIDERS_ARRAY } from '../../../../src/constants/providers.js';
// Define supported roles for model setting
const MODEL_ROLES = ['main', 'research', 'fallback'];
/**
* Determine provider hint from custom provider flags
* @param {Object} args - Arguments containing provider flags
* @returns {string|undefined} Provider hint or undefined if no custom provider flag is set
*/
function getProviderHint(args) {
return CUSTOM_PROVIDERS_ARRAY.find((provider) => args[provider]);
}
/**
* Handle setting models for different roles
* @param {Object} args - Arguments containing role-specific model IDs
* @param {Object} context - Context object with session, mcpLog, projectRoot
* @returns {Object|null} Result if a model was set, null if no model setting was requested
*/
async function handleModelSetting(args, context) {
for (const role of MODEL_ROLES) {
const roleKey = `set${role.charAt(0).toUpperCase() + role.slice(1)}`; // setMain, setResearch, setFallback
if (args[roleKey]) {
const providerHint = getProviderHint(args);
return await setModel(role, args[roleKey], {
...context,
providerHint
});
}
}
return null; // No model setting was requested
}
/**
* Get or update model configuration
@@ -31,16 +66,21 @@ export async function modelsDirect(args, log, context = {}) {
log.info(`Executing models_direct with args: ${JSON.stringify(args)}`);
log.info(`Using project root: ${projectRoot}`);
// Validate flags: cannot use both openrouter and ollama simultaneously
if (args.openrouter && args.ollama) {
// Validate flags: only one custom provider flag can be used simultaneously
const customProviderFlags = CUSTOM_PROVIDERS_ARRAY.filter(
(provider) => args[provider]
);
if (customProviderFlags.length > 1) {
log.error(
'Error: Cannot use both openrouter and ollama flags simultaneously.'
'Error: Cannot use multiple custom provider flags simultaneously.'
);
return {
success: false,
error: {
code: 'INVALID_ARGS',
message: 'Cannot use both openrouter and ollama flags simultaneously.'
message:
'Cannot use multiple custom provider flags simultaneously. Choose only one: openrouter, ollama, bedrock, azure, or vertex.'
}
};
}
@@ -54,55 +94,22 @@ export async function modelsDirect(args, log, context = {}) {
return await getAvailableModelsList({
session,
mcpLog,
projectRoot // Pass projectRoot to function
projectRoot
});
}
// Handle setting a specific model
if (args.setMain) {
return await setModel('main', args.setMain, {
session,
mcpLog,
projectRoot, // Pass projectRoot to function
providerHint: args.openrouter
? 'openrouter'
: args.ollama
? 'ollama'
: undefined // Pass hint
});
}
if (args.setResearch) {
return await setModel('research', args.setResearch, {
session,
mcpLog,
projectRoot, // Pass projectRoot to function
providerHint: args.openrouter
? 'openrouter'
: args.ollama
? 'ollama'
: undefined // Pass hint
});
}
if (args.setFallback) {
return await setModel('fallback', args.setFallback, {
session,
mcpLog,
projectRoot, // Pass projectRoot to function
providerHint: args.openrouter
? 'openrouter'
: args.ollama
? 'ollama'
: undefined // Pass hint
});
// Handle setting any model role using unified function
const modelContext = { session, mcpLog, projectRoot };
const modelSetResult = await handleModelSetting(args, modelContext);
if (modelSetResult) {
return modelSetResult;
}
// Default action: get current configuration
return await getModelConfiguration({
session,
mcpLog,
projectRoot // Pass projectRoot to function
projectRoot
});
} finally {
disableSilentMode();

View File

@@ -55,7 +55,21 @@ export function registerModelsTool(server) {
ollama: z
.boolean()
.optional()
.describe('Indicates the set model ID is a custom Ollama model.')
.describe('Indicates the set model ID is a custom Ollama model.'),
bedrock: z
.boolean()
.optional()
.describe('Indicates the set model ID is a custom AWS Bedrock model.'),
azure: z
.boolean()
.optional()
.describe('Indicates the set model ID is a custom Azure OpenAI model.'),
vertex: z
.boolean()
.optional()
.describe(
'Indicates the set model ID is a custom Google Vertex AI model.'
)
}),
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
try {

323
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "task-master-ai",
"version": "0.17.0",
"version": "0.17.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "task-master-ai",
"version": "0.17.0",
"version": "0.17.1",
"license": "MIT WITH Commons-Clause",
"dependencies": {
"@ai-sdk/amazon-bedrock": "^2.2.9",
@@ -20,6 +20,7 @@
"@ai-sdk/xai": "^1.2.15",
"@anthropic-ai/sdk": "^0.39.0",
"@aws-sdk/credential-providers": "^3.817.0",
"@inquirer/search": "^3.0.15",
"@openrouter/ai-sdk-provider": "^0.4.5",
"ai": "^4.3.10",
"boxen": "^8.0.1",
@@ -67,6 +68,9 @@
},
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"@anthropic-ai/claude-code": "^1.0.25"
}
},
"node_modules/@ai-sdk/amazon-bedrock": {
@@ -445,6 +449,28 @@
"node": ">=6.0.0"
}
},
"node_modules/@anthropic-ai/claude-code": {
"version": "1.0.25",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-1.0.25.tgz",
"integrity": "sha512-5p4FLlFO4TuRf0zV0axiOxiAkUC8eer0lqJi/A/pA46LESv31Alw6xaNYgwQVkP6oSbP5PydK36u7YrB9QSaXQ==",
"hasInstallScript": true,
"license": "SEE LICENSE IN README.md",
"optional": true,
"bin": {
"claude": "cli.js"
},
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "^0.33.5",
"@img/sharp-darwin-x64": "^0.33.5",
"@img/sharp-linux-arm": "^0.33.5",
"@img/sharp-linux-arm64": "^0.33.5",
"@img/sharp-linux-x64": "^0.33.5",
"@img/sharp-win32-x64": "^0.33.5"
}
},
"node_modules/@anthropic-ai/sdk": {
"version": "0.39.0",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.39.0.tgz",
@@ -2650,6 +2676,215 @@
"node": ">=18"
}
},
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
"integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-arm64": "1.0.4"
}
},
"node_modules/@img/sharp-darwin-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
"integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.0.4"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
"integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
"integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
"integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
"cpu": [
"arm"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
"integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
"integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-linux-arm": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
"integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
"cpu": [
"arm"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.0.5"
}
},
"node_modules/@img/sharp-linux-arm64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
"integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.0.4"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
"integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.0.4"
}
},
"node_modules/@img/sharp-win32-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
"integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@inquirer/checkbox": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.4.tgz",
@@ -2696,13 +2931,13 @@
}
},
"node_modules/@inquirer/core": {
"version": "10.1.9",
"resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.9.tgz",
"integrity": "sha512-sXhVB8n20NYkUBfDYgizGHlpRVaCRjtuzNZA6xpALIUbkgfd2Hjz+DfEN6+h1BRnuxw0/P4jCIMjMsEOAMwAJw==",
"version": "10.1.13",
"resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.13.tgz",
"integrity": "sha512-1viSxebkYN2nJULlzCxES6G9/stgHSepZ9LqqfdIGPHj5OHhiBUXVS0a6R0bEC2A+VL4D9w6QB66ebCr6HGllA==",
"license": "MIT",
"dependencies": {
"@inquirer/figures": "^1.0.11",
"@inquirer/type": "^3.0.5",
"@inquirer/figures": "^1.0.12",
"@inquirer/type": "^3.0.7",
"ansi-escapes": "^4.3.2",
"cli-width": "^4.1.0",
"mute-stream": "^2.0.0",
@@ -2822,9 +3057,9 @@
}
},
"node_modules/@inquirer/figures": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.11.tgz",
"integrity": "sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==",
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.12.tgz",
"integrity": "sha512-MJttijd8rMFcKJC8NYmprWr6hD3r9Gd9qUC0XwPNwoEPWSMVJwA2MlXxF+nhZZNMY+HXsWa+o7KY2emWYIn0jQ==",
"license": "MIT",
"engines": {
"node": ">=18"
@@ -2946,14 +3181,14 @@
}
},
"node_modules/@inquirer/search": {
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.11.tgz",
"integrity": "sha512-9CWQT0ikYcg6Ls3TOa7jljsD7PgjcsYEM0bYE+Gkz+uoW9u8eaJCRHJKkucpRE5+xKtaaDbrND+nPDoxzjYyew==",
"version": "3.0.15",
"resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.15.tgz",
"integrity": "sha512-YBMwPxYBrADqyvP4nNItpwkBnGGglAvCLVW8u4pRmmvOsHUtCAUIMbUrLX5B3tFL1/WsLGdQ2HNzkqswMs5Uaw==",
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.1.9",
"@inquirer/figures": "^1.0.11",
"@inquirer/type": "^3.0.5",
"@inquirer/core": "^10.1.13",
"@inquirer/figures": "^1.0.12",
"@inquirer/type": "^3.0.7",
"yoctocolors-cjs": "^2.1.2"
},
"engines": {
@@ -2993,9 +3228,9 @@
}
},
"node_modules/@inquirer/type": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.5.tgz",
"integrity": "sha512-ZJpeIYYueOz/i/ONzrfof8g89kNdO2hjGuvULROo3O8rlB2CRtSseE5KeirnyE4t/thAn/EwvS/vuQeJCn+NZg==",
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.7.tgz",
"integrity": "sha512-PfunHQcjwnju84L+ycmcMKB/pTPIngjUJvfnRhKY6FKPuYXlM4aQCb/nIdTFR6BEhMjFvngzvng/vBAJMZpLSA==",
"license": "MIT",
"engines": {
"node": ">=18"
@@ -3867,6 +4102,19 @@
"node": ">= 0.6"
}
},
"node_modules/@noble/hashes": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -3965,6 +4213,16 @@
"node": ">=8.0.0"
}
},
"node_modules/@paralleldrive/cuid2": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz",
"integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@noble/hashes": "^1.1.5"
}
},
"node_modules/@sec-ant/readable-stream": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
@@ -5327,9 +5585,9 @@
}
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -7158,16 +7416,19 @@
}
},
"node_modules/formidable": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.2.tgz",
"integrity": "sha512-Jqc1btCy3QzRbJaICGwKcBfGWuLADRerLzDqi2NwSt/UkXLsHJw2TVResiaoBufHVHy9aSgClOHCeJsSsFLTbg==",
"version": "3.5.4",
"resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz",
"integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==",
"dev": true,
"license": "MIT",
"dependencies": {
"@paralleldrive/cuid2": "^2.2.2",
"dezalgo": "^1.0.4",
"hexoid": "^2.0.0",
"once": "^1.4.0"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"url": "https://ko-fi.com/tunnckoCore/commissions"
}
@@ -7671,16 +7932,6 @@
"node": ">=18.0.0"
}
},
"node_modules/hexoid": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/hexoid/-/hexoid-2.0.0.tgz",
"integrity": "sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/highlight.js": {
"version": "10.7.3",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "task-master-ai",
"version": "0.17.0",
"version": "0.17.1",
"description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.",
"main": "index.js",
"type": "module",
@@ -50,6 +50,7 @@
"@ai-sdk/xai": "^1.2.15",
"@anthropic-ai/sdk": "^0.39.0",
"@aws-sdk/credential-providers": "^3.817.0",
"@inquirer/search": "^3.0.15",
"@openrouter/ai-sdk-provider": "^0.4.5",
"ai": "^4.3.10",
"boxen": "^8.0.1",
@@ -75,6 +76,9 @@
"uuid": "^11.1.0",
"zod": "^3.23.8"
},
"optionalDependencies": {
"@anthropic-ai/claude-code": "^1.0.25"
},
"engines": {
"node": ">=18.0.0"
},

View File

@@ -44,7 +44,8 @@ import {
OllamaAIProvider,
BedrockAIProvider,
AzureProvider,
VertexAIProvider
VertexAIProvider,
ClaudeCodeProvider
} from '../../src/ai-providers/index.js';
// Create provider instances
@@ -58,7 +59,8 @@ const PROVIDERS = {
ollama: new OllamaAIProvider(),
bedrock: new BedrockAIProvider(),
azure: new AzureProvider(),
vertex: new VertexAIProvider()
vertex: new VertexAIProvider(),
'claude-code': new ClaudeCodeProvider()
};
// Helper function to get cost for a specific model
@@ -225,6 +227,11 @@ function _extractErrorMessage(error) {
* @throws {Error} If a required API key is missing.
*/
function _resolveApiKey(providerName, session, projectRoot = null) {
// Claude Code doesn't require an API key
if (providerName === 'claude-code') {
return 'claude-code-no-key-required';
}
const keyMap = {
openai: 'OPENAI_API_KEY',
anthropic: 'ANTHROPIC_API_KEY',
@@ -236,7 +243,8 @@ function _resolveApiKey(providerName, session, projectRoot = null) {
xai: 'XAI_API_KEY',
ollama: 'OLLAMA_API_KEY',
bedrock: 'AWS_ACCESS_KEY_ID',
vertex: 'GOOGLE_API_KEY'
vertex: 'GOOGLE_API_KEY',
'claude-code': 'CLAUDE_CODE_API_KEY' // Not actually used, but included for consistency
};
const envVarName = keyMap[providerName];

View File

@@ -11,6 +11,7 @@ import fs from 'fs';
import https from 'https';
import http from 'http';
import inquirer from 'inquirer';
import search from '@inquirer/search';
import ora from 'ora'; // Import ora
import {
@@ -71,6 +72,8 @@ import {
getBaseUrlForRole
} from './config-manager.js';
import { CUSTOM_PROVIDERS } from '../../src/constants/providers.js';
import {
COMPLEXITY_REPORT_FILE,
PRD_FILE,
@@ -291,20 +294,14 @@ async function runInteractiveSetup(projectRoot) {
}
: null;
const customOpenRouterOption = {
name: '* Custom OpenRouter model', // Symbol updated
value: '__CUSTOM_OPENROUTER__'
};
const customOllamaOption = {
name: '* Custom Ollama model', // Symbol updated
value: '__CUSTOM_OLLAMA__'
};
const customBedrockOption = {
name: '* Custom Bedrock model', // Add Bedrock custom option
value: '__CUSTOM_BEDROCK__'
};
// Define custom provider options
const customProviderOptions = [
{ name: '* Custom OpenRouter model', value: '__CUSTOM_OPENROUTER__' },
{ name: '* Custom Ollama model', value: '__CUSTOM_OLLAMA__' },
{ name: '* Custom Bedrock model', value: '__CUSTOM_BEDROCK__' },
{ name: '* Custom Azure model', value: '__CUSTOM_AZURE__' },
{ name: '* Custom Vertex model', value: '__CUSTOM_VERTEX__' }
];
let choices = [];
let defaultIndex = 0; // Default to 'Cancel'
@@ -344,43 +341,42 @@ async function runInteractiveSetup(projectRoot) {
);
}
// Construct final choices list based on whether 'None' is allowed
const commonPrefix = [];
// Construct final choices list with custom options moved to bottom
const systemOptions = [];
if (noChangeOption) {
commonPrefix.push(noChangeOption);
systemOptions.push(noChangeOption);
}
commonPrefix.push(cancelOption);
commonPrefix.push(customOpenRouterOption);
commonPrefix.push(customOllamaOption);
commonPrefix.push(customBedrockOption);
systemOptions.push(cancelOption);
const prefixLength = commonPrefix.length; // Initial prefix length
const systemLength = systemOptions.length;
if (allowNone) {
choices = [
...commonPrefix,
new inquirer.Separator(),
{ name: '⚪ None (disable)', value: null }, // Symbol updated
new inquirer.Separator(),
...roleChoices
...systemOptions,
new inquirer.Separator('\n── Standard Models ──'),
{ name: '⚪ None (disable)', value: null },
...roleChoices,
new inquirer.Separator('\n── Custom Providers ──'),
...customProviderOptions
];
// Adjust default index: Prefix + Sep1 + None + Sep2 (+3)
const noneOptionIndex = prefixLength + 1;
// Adjust default index: System + Sep1 + None (+2)
const noneOptionIndex = systemLength + 1;
defaultIndex =
currentChoiceIndex !== -1
? currentChoiceIndex + prefixLength + 3 // Offset by prefix and separators
? currentChoiceIndex + systemLength + 2 // Offset by system options and separators
: noneOptionIndex; // Default to 'None' if no current model matched
} else {
choices = [
...commonPrefix,
new inquirer.Separator(),
...systemOptions,
new inquirer.Separator('\n── Standard Models ──'),
...roleChoices,
new inquirer.Separator()
new inquirer.Separator('\n── Custom Providers ──'),
...customProviderOptions
];
// Adjust default index: Prefix + Sep (+1)
// Adjust default index: System + Sep (+1)
defaultIndex =
currentChoiceIndex !== -1
? currentChoiceIndex + prefixLength + 1 // Offset by prefix and separator
? currentChoiceIndex + systemLength + 1 // Offset by system options and separator
: noChangeOption
? 1
: 0; // Default to 'No Change' if present, else 'Cancel'
@@ -403,32 +399,63 @@ async function runInteractiveSetup(projectRoot) {
const researchPromptData = getPromptData('research');
const fallbackPromptData = getPromptData('fallback', true); // Allow 'None' for fallback
const answers = await inquirer.prompt([
{
type: 'list',
name: 'mainModel',
message: 'Select the main model for generation/updates:',
choices: mainPromptData.choices,
default: mainPromptData.default
},
{
type: 'list',
name: 'researchModel',
// Display helpful intro message
console.log(chalk.cyan('\n🎯 Interactive Model Setup'));
console.log(chalk.gray('━'.repeat(50)));
console.log(chalk.yellow('💡 Navigation tips:'));
console.log(chalk.gray(' • Type to search and filter options'));
console.log(chalk.gray(' • Use ↑↓ arrow keys to navigate results'));
console.log(
chalk.gray(
' • Standard models are listed first, custom providers at bottom'
)
);
console.log(chalk.gray(' • Press Enter to select\n'));
// Helper function to create search source for models
const createSearchSource = (choices, defaultValue) => {
return (searchTerm = '') => {
const filteredChoices = choices.filter((choice) => {
if (choice.type === 'separator') return true; // Always show separators
const searchText = choice.name || '';
return searchText.toLowerCase().includes(searchTerm.toLowerCase());
});
return Promise.resolve(filteredChoices);
};
};
const answers = {};
// Main model selection
answers.mainModel = await search({
message: 'Select the main model for generation/updates:',
source: createSearchSource(mainPromptData.choices, mainPromptData.default),
pageSize: 15
});
if (answers.mainModel !== '__CANCEL__') {
// Research model selection
answers.researchModel = await search({
message: 'Select the research model:',
choices: researchPromptData.choices,
default: researchPromptData.default,
when: (ans) => ans.mainModel !== '__CANCEL__'
},
{
type: 'list',
name: 'fallbackModel',
message: 'Select the fallback model (optional):',
choices: fallbackPromptData.choices,
default: fallbackPromptData.default,
when: (ans) =>
ans.mainModel !== '__CANCEL__' && ans.researchModel !== '__CANCEL__'
source: createSearchSource(
researchPromptData.choices,
researchPromptData.default
),
pageSize: 15
});
if (answers.researchModel !== '__CANCEL__') {
// Fallback model selection
answers.fallbackModel = await search({
message: 'Select the fallback model (optional):',
source: createSearchSource(
fallbackPromptData.choices,
fallbackPromptData.default
),
pageSize: 15
});
}
]);
}
let setupSuccess = true;
let setupConfigModified = false;
@@ -468,7 +495,7 @@ async function runInteractiveSetup(projectRoot) {
return true; // Continue setup, but don't set this role
}
modelIdToSet = customId;
providerHint = 'openrouter';
providerHint = CUSTOM_PROVIDERS.OPENROUTER;
// Validate against live OpenRouter list
const openRouterModels = await fetchOpenRouterModelsCLI();
if (
@@ -497,7 +524,7 @@ async function runInteractiveSetup(projectRoot) {
return true; // Continue setup, but don't set this role
}
modelIdToSet = customId;
providerHint = 'ollama';
providerHint = CUSTOM_PROVIDERS.OLLAMA;
// Get the Ollama base URL from config for this role
const ollamaBaseURL = getBaseUrlForRole(role, projectRoot);
// Validate against live Ollama list
@@ -538,16 +565,16 @@ async function runInteractiveSetup(projectRoot) {
return true; // Continue setup, but don't set this role
}
modelIdToSet = customId;
providerHint = 'bedrock';
providerHint = CUSTOM_PROVIDERS.BEDROCK;
// Check if AWS environment variables exist
if (
!process.env.AWS_ACCESS_KEY_ID ||
!process.env.AWS_SECRET_ACCESS_KEY
) {
console.error(
chalk.red(
'Error: AWS_ACCESS_KEY_ID and/or AWS_SECRET_ACCESS_KEY environment variables are missing. Please set them before using custom Bedrock models.'
console.warn(
chalk.yellow(
'Warning: AWS_ACCESS_KEY_ID and/or AWS_SECRET_ACCESS_KEY environment variables are missing. Will fallback to system configuration. (ex: aws config files or ec2 instance profiles)'
)
);
setupSuccess = false;
@@ -559,6 +586,76 @@ async function runInteractiveSetup(projectRoot) {
`Custom Bedrock model "${modelIdToSet}" will be used. No validation performed.`
)
);
} else if (selectedValue === '__CUSTOM_AZURE__') {
isCustomSelection = true;
const { customId } = await inquirer.prompt([
{
type: 'input',
name: 'customId',
message: `Enter the custom Azure OpenAI Model ID for the ${role} role (e.g., gpt-4o):`
}
]);
if (!customId) {
console.log(chalk.yellow('No custom ID entered. Skipping role.'));
return true; // Continue setup, but don't set this role
}
modelIdToSet = customId;
providerHint = CUSTOM_PROVIDERS.AZURE;
// Check if Azure environment variables exist
if (
!process.env.AZURE_OPENAI_API_KEY ||
!process.env.AZURE_OPENAI_ENDPOINT
) {
console.error(
chalk.red(
'Error: AZURE_OPENAI_API_KEY and/or AZURE_OPENAI_ENDPOINT environment variables are missing. Please set them before using custom Azure models.'
)
);
setupSuccess = false;
return true; // Continue setup, but mark as failed
}
console.log(
chalk.blue(
`Custom Azure OpenAI model "${modelIdToSet}" will be used. No validation performed.`
)
);
} else if (selectedValue === '__CUSTOM_VERTEX__') {
isCustomSelection = true;
const { customId } = await inquirer.prompt([
{
type: 'input',
name: 'customId',
message: `Enter the custom Vertex AI Model ID for the ${role} role (e.g., gemini-1.5-pro-002):`
}
]);
if (!customId) {
console.log(chalk.yellow('No custom ID entered. Skipping role.'));
return true; // Continue setup, but don't set this role
}
modelIdToSet = customId;
providerHint = CUSTOM_PROVIDERS.VERTEX;
// Check if Google/Vertex environment variables exist
if (
!process.env.GOOGLE_API_KEY &&
!process.env.GOOGLE_APPLICATION_CREDENTIALS
) {
console.error(
chalk.red(
'Error: Either GOOGLE_API_KEY or GOOGLE_APPLICATION_CREDENTIALS environment variable is required. Please set one before using custom Vertex models.'
)
);
setupSuccess = false;
return true; // Continue setup, but mark as failed
}
console.log(
chalk.blue(
`Custom Vertex AI model "${modelIdToSet}" will be used. No validation performed.`
)
);
} else if (
selectedValue &&
typeof selectedValue === 'object' &&
@@ -3307,6 +3404,18 @@ ${result.result}
'--bedrock',
'Allow setting a custom Bedrock model ID (use with --set-*) '
)
.option(
'--claude-code',
'Allow setting a Claude Code model ID (use with --set-*)'
)
.option(
'--azure',
'Allow setting a custom Azure OpenAI model ID (use with --set-*) '
)
.option(
'--vertex',
'Allow setting a custom Vertex AI model ID (use with --set-*) '
)
.addHelpText(
'after',
`
@@ -3318,6 +3427,9 @@ Examples:
$ task-master models --set-main my-custom-model --ollama # Set custom Ollama model for main role
$ task-master models --set-main anthropic.claude-3-sonnet-20240229-v1:0 --bedrock # Set custom Bedrock model for main role
$ task-master models --set-main some/other-model --openrouter # Set custom OpenRouter model for main role
$ task-master models --set-main sonnet --claude-code # Set Claude Code model for main role
$ task-master models --set-main gpt-4o --azure # Set custom Azure OpenAI model for main role
$ task-master models --set-main claude-3-5-sonnet@20241022 --vertex # Set custom Vertex AI model for main role
$ task-master models --setup # Run interactive setup`
)
.action(async (options) => {
@@ -3330,12 +3442,13 @@ Examples:
const providerFlags = [
options.openrouter,
options.ollama,
options.bedrock
options.bedrock,
options.claudeCode
].filter(Boolean).length;
if (providerFlags > 1) {
console.error(
chalk.red(
'Error: Cannot use multiple provider flags (--openrouter, --ollama, --bedrock) simultaneously.'
'Error: Cannot use multiple provider flags (--openrouter, --ollama, --bedrock, --claude-code) simultaneously.'
)
);
process.exit(1);
@@ -3377,7 +3490,9 @@ Examples:
? 'ollama'
: options.bedrock
? 'bedrock'
: undefined
: options.claudeCode
? 'claude-code'
: undefined
});
if (result.success) {
console.log(chalk.green(`${result.data.message}`));
@@ -3399,7 +3514,9 @@ Examples:
? 'ollama'
: options.bedrock
? 'bedrock'
: undefined
: options.claudeCode
? 'claude-code'
: undefined
});
if (result.success) {
console.log(chalk.green(`${result.data.message}`));
@@ -3423,7 +3540,9 @@ Examples:
? 'ollama'
: options.bedrock
? 'bedrock'
: undefined
: options.claudeCode
? 'claude-code'
: undefined
});
if (result.success) {
console.log(chalk.green(`${result.data.message}`));

View File

@@ -5,6 +5,12 @@ import { fileURLToPath } from 'url';
import { log, findProjectRoot, resolveEnvVariable } from './utils.js';
import { LEGACY_CONFIG_FILE } from '../../src/constants/paths.js';
import { findConfigPath } from '../../src/utils/path-utils.js';
import {
VALIDATED_PROVIDERS,
CUSTOM_PROVIDERS,
CUSTOM_PROVIDERS_ARRAY,
ALL_PROVIDERS
} from '../../src/constants/providers.js';
// Calculate __dirname in ESM
const __filename = fileURLToPath(import.meta.url);
@@ -29,9 +35,6 @@ try {
process.exit(1); // Exit if models can't be loaded
}
// Define valid providers dynamically from the loaded MODEL_MAP
const VALID_PROVIDERS = Object.keys(MODEL_MAP || {});
// Default configuration values (used if config file is missing or incomplete)
const DEFAULTS = {
models: {
@@ -233,12 +236,25 @@ function getConfig(explicitRoot = null, forceReload = false) {
}
/**
* Validates if a provider name is in the list of supported providers.
* Validates if a provider name is supported.
* Custom providers (azure, vertex, bedrock, openrouter, ollama) are always allowed.
* Validated providers must exist in the MODEL_MAP from supported-models.json.
* @param {string} providerName The name of the provider.
* @returns {boolean} True if the provider is valid, false otherwise.
*/
function validateProvider(providerName) {
return VALID_PROVIDERS.includes(providerName);
// Custom providers are always allowed
if (CUSTOM_PROVIDERS_ARRAY.includes(providerName)) {
return true;
}
// Validated providers must exist in MODEL_MAP
if (VALIDATED_PROVIDERS.includes(providerName)) {
return !!(MODEL_MAP && MODEL_MAP[providerName]);
}
// Unknown providers are not allowed
return false;
}
/**
@@ -480,10 +496,22 @@ function getParametersForRole(role, explicitRoot = null) {
*/
function isApiKeySet(providerName, session = null, projectRoot = null) {
// Define the expected environment variable name for each provider
if (providerName?.toLowerCase() === 'ollama') {
// Providers that don't require API keys for authentication
const providersWithoutApiKeys = [
CUSTOM_PROVIDERS.OLLAMA,
CUSTOM_PROVIDERS.BEDROCK
];
if (providersWithoutApiKeys.includes(providerName?.toLowerCase())) {
return true; // Indicate key status is effectively "OK"
}
// Claude Code doesn't require an API key
if (providerName?.toLowerCase() === 'claude-code') {
return true; // No API key needed
}
const keyMap = {
openai: 'OPENAI_API_KEY',
anthropic: 'ANTHROPIC_API_KEY',
@@ -493,7 +521,9 @@ function isApiKeySet(providerName, session = null, projectRoot = null) {
azure: 'AZURE_OPENAI_API_KEY',
openrouter: 'OPENROUTER_API_KEY',
xai: 'XAI_API_KEY',
vertex: 'GOOGLE_API_KEY' // Vertex uses the same key as Google
vertex: 'GOOGLE_API_KEY', // Vertex uses the same key as Google
'claude-code': 'CLAUDE_CODE_API_KEY', // Not actually used, but included for consistency
bedrock: 'AWS_ACCESS_KEY_ID' // Bedrock uses AWS credentials
// Add other providers as needed
};
@@ -577,6 +607,8 @@ function getMcpApiKeyStatus(providerName, projectRoot = null) {
break;
case 'ollama':
return true; // No key needed
case 'claude-code':
return true; // No key needed
case 'mistral':
apiKeyToCheck = mcpEnv.MISTRAL_API_KEY;
placeholderValue = 'YOUR_MISTRAL_API_KEY_HERE';
@@ -589,6 +621,10 @@ function getMcpApiKeyStatus(providerName, projectRoot = null) {
apiKeyToCheck = mcpEnv.GOOGLE_API_KEY; // Vertex uses Google API key
placeholderValue = 'YOUR_GOOGLE_API_KEY_HERE';
break;
case 'bedrock':
apiKeyToCheck = mcpEnv.AWS_ACCESS_KEY_ID; // Bedrock uses AWS credentials
placeholderValue = 'YOUR_AWS_ACCESS_KEY_ID_HERE';
break;
default:
return false; // Unknown provider
}
@@ -636,7 +672,8 @@ function getAvailableModels() {
provider: provider,
swe_score: sweScore,
cost_per_1m_tokens: cost,
allowed_roles: allowedRoles
allowed_roles: allowedRoles,
max_tokens: modelObj.max_tokens
});
});
} else {
@@ -736,11 +773,11 @@ function getUserId(explicitRoot = null) {
}
/**
* Gets a list of all provider names defined in the MODEL_MAP.
* @returns {string[]} An array of provider names.
* Gets a list of all known provider names (both validated and custom).
* @returns {string[]} An array of all provider names.
*/
function getAllProviders() {
return Object.keys(MODEL_MAP || {});
return ALL_PROVIDERS;
}
function getBaseUrlForRole(role, explicitRoot = null) {
@@ -759,7 +796,9 @@ export {
// Validation
validateProvider,
validateProviderModelCombination,
VALID_PROVIDERS,
VALIDATED_PROVIDERS,
CUSTOM_PROVIDERS,
ALL_PROVIDERS,
MODEL_MAP,
getAvailableModels,
// Role-specific getters (No env var overrides)

View File

@@ -1,30 +1,58 @@
{
"bedrock": [
{
"id": "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
"swe_score": 0.623,
"cost_per_1m_tokens": { "input": 3, "output": 15 },
"allowed_roles": ["main", "fallback"],
"max_tokens": 65536
},
{
"id": "us.deepseek.r1-v1:0",
"swe_score": 0,
"cost_per_1m_tokens": { "input": 1.35, "output": 5.4 },
"allowed_roles": ["research"],
"max_tokens": 65536
}
],
"anthropic": [
{
"id": "claude-sonnet-4-20250514",
"swe_score": 0.727,
"cost_per_1m_tokens": { "input": 3.0, "output": 15.0 },
"cost_per_1m_tokens": {
"input": 3.0,
"output": 15.0
},
"allowed_roles": ["main", "fallback"],
"max_tokens": 64000
},
{
"id": "claude-opus-4-20250514",
"swe_score": 0.725,
"cost_per_1m_tokens": { "input": 15.0, "output": 75.0 },
"cost_per_1m_tokens": {
"input": 15.0,
"output": 75.0
},
"allowed_roles": ["main", "fallback"],
"max_tokens": 32000
},
{
"id": "claude-3-7-sonnet-20250219",
"swe_score": 0.623,
"cost_per_1m_tokens": { "input": 3.0, "output": 15.0 },
"cost_per_1m_tokens": {
"input": 3.0,
"output": 15.0
},
"allowed_roles": ["main", "fallback"],
"max_tokens": 120000
},
{
"id": "claude-3-5-sonnet-20241022",
"swe_score": 0.49,
"cost_per_1m_tokens": { "input": 3.0, "output": 15.0 },
"cost_per_1m_tokens": {
"input": 3.0,
"output": 15.0
},
"allowed_roles": ["main", "fallback"],
"max_tokens": 64000
}
@@ -33,81 +61,120 @@
{
"id": "gpt-4o",
"swe_score": 0.332,
"cost_per_1m_tokens": { "input": 2.5, "output": 10.0 },
"cost_per_1m_tokens": {
"input": 2.5,
"output": 10.0
},
"allowed_roles": ["main", "fallback"],
"max_tokens": 16384
},
{
"id": "o1",
"swe_score": 0.489,
"cost_per_1m_tokens": { "input": 15.0, "output": 60.0 },
"cost_per_1m_tokens": {
"input": 15.0,
"output": 60.0
},
"allowed_roles": ["main"]
},
{
"id": "o3",
"swe_score": 0.5,
"cost_per_1m_tokens": { "input": 2.0, "output": 8.0 },
"cost_per_1m_tokens": {
"input": 2.0,
"output": 8.0
},
"allowed_roles": ["main", "fallback"]
},
{
"id": "o3-mini",
"swe_score": 0.493,
"cost_per_1m_tokens": { "input": 1.1, "output": 4.4 },
"cost_per_1m_tokens": {
"input": 1.1,
"output": 4.4
},
"allowed_roles": ["main"],
"max_tokens": 100000
},
{
"id": "o4-mini",
"swe_score": 0.45,
"cost_per_1m_tokens": { "input": 1.1, "output": 4.4 },
"cost_per_1m_tokens": {
"input": 1.1,
"output": 4.4
},
"allowed_roles": ["main", "fallback"]
},
{
"id": "o1-mini",
"swe_score": 0.4,
"cost_per_1m_tokens": { "input": 1.1, "output": 4.4 },
"cost_per_1m_tokens": {
"input": 1.1,
"output": 4.4
},
"allowed_roles": ["main"]
},
{
"id": "o1-pro",
"swe_score": 0,
"cost_per_1m_tokens": { "input": 150.0, "output": 600.0 },
"cost_per_1m_tokens": {
"input": 150.0,
"output": 600.0
},
"allowed_roles": ["main"]
},
{
"id": "gpt-4-5-preview",
"swe_score": 0.38,
"cost_per_1m_tokens": { "input": 75.0, "output": 150.0 },
"cost_per_1m_tokens": {
"input": 75.0,
"output": 150.0
},
"allowed_roles": ["main"]
},
{
"id": "gpt-4-1-mini",
"swe_score": 0,
"cost_per_1m_tokens": { "input": 0.4, "output": 1.6 },
"cost_per_1m_tokens": {
"input": 0.4,
"output": 1.6
},
"allowed_roles": ["main"]
},
{
"id": "gpt-4-1-nano",
"swe_score": 0,
"cost_per_1m_tokens": { "input": 0.1, "output": 0.4 },
"cost_per_1m_tokens": {
"input": 0.1,
"output": 0.4
},
"allowed_roles": ["main"]
},
{
"id": "gpt-4o-mini",
"swe_score": 0.3,
"cost_per_1m_tokens": { "input": 0.15, "output": 0.6 },
"cost_per_1m_tokens": {
"input": 0.15,
"output": 0.6
},
"allowed_roles": ["main"]
},
{
"id": "gpt-4o-search-preview",
"swe_score": 0.33,
"cost_per_1m_tokens": { "input": 2.5, "output": 10.0 },
"cost_per_1m_tokens": {
"input": 2.5,
"output": 10.0
},
"allowed_roles": ["research"]
},
{
"id": "gpt-4o-mini-search-preview",
"swe_score": 0.3,
"cost_per_1m_tokens": { "input": 0.15, "output": 0.6 },
"cost_per_1m_tokens": {
"input": 0.15,
"output": 0.6
},
"allowed_roles": ["research"]
}
],
@@ -136,7 +203,10 @@
{
"id": "gemini-2.0-flash",
"swe_score": 0.518,
"cost_per_1m_tokens": { "input": 0.15, "output": 0.6 },
"cost_per_1m_tokens": {
"input": 0.15,
"output": 0.6
},
"allowed_roles": ["main", "fallback"],
"max_tokens": 1048000
},
@@ -152,35 +222,50 @@
{
"id": "sonar-pro",
"swe_score": 0,
"cost_per_1m_tokens": { "input": 3, "output": 15 },
"cost_per_1m_tokens": {
"input": 3,
"output": 15
},
"allowed_roles": ["main", "research"],
"max_tokens": 8700
},
{
"id": "sonar",
"swe_score": 0,
"cost_per_1m_tokens": { "input": 1, "output": 1 },
"cost_per_1m_tokens": {
"input": 1,
"output": 1
},
"allowed_roles": ["research"],
"max_tokens": 8700
},
{
"id": "deep-research",
"swe_score": 0.211,
"cost_per_1m_tokens": { "input": 2, "output": 8 },
"cost_per_1m_tokens": {
"input": 2,
"output": 8
},
"allowed_roles": ["research"],
"max_tokens": 8700
},
{
"id": "sonar-reasoning-pro",
"swe_score": 0.211,
"cost_per_1m_tokens": { "input": 2, "output": 8 },
"cost_per_1m_tokens": {
"input": 2,
"output": 8
},
"allowed_roles": ["main", "research", "fallback"],
"max_tokens": 8700
},
{
"id": "sonar-reasoning",
"swe_score": 0.211,
"cost_per_1m_tokens": { "input": 1, "output": 5 },
"cost_per_1m_tokens": {
"input": 1,
"output": 5
},
"allowed_roles": ["main", "research", "fallback"],
"max_tokens": 8700
}
@@ -190,7 +275,10 @@
"id": "grok-3",
"name": "Grok 3",
"swe_score": null,
"cost_per_1m_tokens": { "input": 3, "output": 15 },
"cost_per_1m_tokens": {
"input": 3,
"output": 15
},
"allowed_roles": ["main", "fallback", "research"],
"max_tokens": 131072
},
@@ -198,7 +286,10 @@
"id": "grok-3-fast",
"name": "Grok 3 Fast",
"swe_score": 0,
"cost_per_1m_tokens": { "input": 5, "output": 25 },
"cost_per_1m_tokens": {
"input": 5,
"output": 25
},
"allowed_roles": ["main", "fallback", "research"],
"max_tokens": 131072
}
@@ -207,43 +298,64 @@
{
"id": "devstral:latest",
"swe_score": 0,
"cost_per_1m_tokens": { "input": 0, "output": 0 },
"cost_per_1m_tokens": {
"input": 0,
"output": 0
},
"allowed_roles": ["main", "fallback"]
},
{
"id": "qwen3:latest",
"swe_score": 0,
"cost_per_1m_tokens": { "input": 0, "output": 0 },
"cost_per_1m_tokens": {
"input": 0,
"output": 0
},
"allowed_roles": ["main", "fallback"]
},
{
"id": "qwen3:14b",
"swe_score": 0,
"cost_per_1m_tokens": { "input": 0, "output": 0 },
"cost_per_1m_tokens": {
"input": 0,
"output": 0
},
"allowed_roles": ["main", "fallback"]
},
{
"id": "qwen3:32b",
"swe_score": 0,
"cost_per_1m_tokens": { "input": 0, "output": 0 },
"cost_per_1m_tokens": {
"input": 0,
"output": 0
},
"allowed_roles": ["main", "fallback"]
},
{
"id": "mistral-small3.1:latest",
"swe_score": 0,
"cost_per_1m_tokens": { "input": 0, "output": 0 },
"cost_per_1m_tokens": {
"input": 0,
"output": 0
},
"allowed_roles": ["main", "fallback"]
},
{
"id": "llama3.3:latest",
"swe_score": 0,
"cost_per_1m_tokens": { "input": 0, "output": 0 },
"cost_per_1m_tokens": {
"input": 0,
"output": 0
},
"allowed_roles": ["main", "fallback"]
},
{
"id": "phi4:latest",
"swe_score": 0,
"cost_per_1m_tokens": { "input": 0, "output": 0 },
"cost_per_1m_tokens": {
"input": 0,
"output": 0
},
"allowed_roles": ["main", "fallback"]
}
],
@@ -251,177 +363,268 @@
{
"id": "google/gemini-2.5-flash-preview-05-20",
"swe_score": 0,
"cost_per_1m_tokens": { "input": 0.15, "output": 0.6 },
"cost_per_1m_tokens": {
"input": 0.15,
"output": 0.6
},
"allowed_roles": ["main", "fallback"],
"max_tokens": 1048576
},
{
"id": "google/gemini-2.5-flash-preview-05-20:thinking",
"swe_score": 0,
"cost_per_1m_tokens": { "input": 0.15, "output": 3.5 },
"cost_per_1m_tokens": {
"input": 0.15,
"output": 3.5
},
"allowed_roles": ["main", "fallback"],
"max_tokens": 1048576
},
{
"id": "google/gemini-2.5-pro-exp-03-25",
"swe_score": 0,
"cost_per_1m_tokens": { "input": 0, "output": 0 },
"cost_per_1m_tokens": {
"input": 0,
"output": 0
},
"allowed_roles": ["main", "fallback"],
"max_tokens": 1000000
},
{
"id": "deepseek/deepseek-chat-v3-0324:free",
"swe_score": 0,
"cost_per_1m_tokens": { "input": 0, "output": 0 },
"cost_per_1m_tokens": {
"input": 0,
"output": 0
},
"allowed_roles": ["main", "fallback"],
"max_tokens": 163840
},
{
"id": "deepseek/deepseek-chat-v3-0324",
"swe_score": 0,
"cost_per_1m_tokens": { "input": 0.27, "output": 1.1 },
"cost_per_1m_tokens": {
"input": 0.27,
"output": 1.1
},
"allowed_roles": ["main"],
"max_tokens": 64000
},
{
"id": "openai/gpt-4.1",
"swe_score": 0,
"cost_per_1m_tokens": { "input": 2, "output": 8 },
"cost_per_1m_tokens": {
"input": 2,
"output": 8
},
"allowed_roles": ["main", "fallback"],
"max_tokens": 1000000
},
{
"id": "openai/gpt-4.1-mini",
"swe_score": 0,
"cost_per_1m_tokens": { "input": 0.4, "output": 1.6 },
"cost_per_1m_tokens": {
"input": 0.4,
"output": 1.6
},
"allowed_roles": ["main", "fallback"],
"max_tokens": 1000000
},
{
"id": "openai/gpt-4.1-nano",
"swe_score": 0,
"cost_per_1m_tokens": { "input": 0.1, "output": 0.4 },
"cost_per_1m_tokens": {
"input": 0.1,
"output": 0.4
},
"allowed_roles": ["main", "fallback"],
"max_tokens": 1000000
},
{
"id": "openai/o3",
"swe_score": 0,
"cost_per_1m_tokens": { "input": 10, "output": 40 },
"cost_per_1m_tokens": {
"input": 10,
"output": 40
},
"allowed_roles": ["main", "fallback"],
"max_tokens": 200000
},
{
"id": "openai/codex-mini",
"swe_score": 0,
"cost_per_1m_tokens": { "input": 1.5, "output": 6 },
"cost_per_1m_tokens": {
"input": 1.5,
"output": 6
},
"allowed_roles": ["main", "fallback"],
"max_tokens": 100000
},
{
"id": "openai/gpt-4o-mini",
"swe_score": 0,
"cost_per_1m_tokens": { "input": 0.15, "output": 0.6 },
"cost_per_1m_tokens": {
"input": 0.15,
"output": 0.6
},
"allowed_roles": ["main", "fallback"],
"max_tokens": 100000
},
{
"id": "openai/o4-mini",
"swe_score": 0.45,
"cost_per_1m_tokens": { "input": 1.1, "output": 4.4 },
"cost_per_1m_tokens": {
"input": 1.1,
"output": 4.4
},
"allowed_roles": ["main", "fallback"],
"max_tokens": 100000
},
{
"id": "openai/o4-mini-high",
"swe_score": 0,
"cost_per_1m_tokens": { "input": 1.1, "output": 4.4 },
"cost_per_1m_tokens": {
"input": 1.1,
"output": 4.4
},
"allowed_roles": ["main", "fallback"],
"max_tokens": 100000
},
{
"id": "openai/o1-pro",
"swe_score": 0,
"cost_per_1m_tokens": { "input": 150, "output": 600 },
"cost_per_1m_tokens": {
"input": 150,
"output": 600
},
"allowed_roles": ["main", "fallback"],
"max_tokens": 100000
},
{
"id": "meta-llama/llama-3.3-70b-instruct",
"swe_score": 0,
"cost_per_1m_tokens": { "input": 120, "output": 600 },
"cost_per_1m_tokens": {
"input": 120,
"output": 600
},
"allowed_roles": ["main", "fallback"],
"max_tokens": 1048576
},
{
"id": "meta-llama/llama-4-maverick",
"swe_score": 0,
"cost_per_1m_tokens": { "input": 0.18, "output": 0.6 },
"cost_per_1m_tokens": {
"input": 0.18,
"output": 0.6
},
"allowed_roles": ["main", "fallback"],
"max_tokens": 1000000
},
{
"id": "meta-llama/llama-4-scout",
"swe_score": 0,
"cost_per_1m_tokens": { "input": 0.08, "output": 0.3 },
"cost_per_1m_tokens": {
"input": 0.08,
"output": 0.3
},
"allowed_roles": ["main", "fallback"],
"max_tokens": 1000000
},
{
"id": "qwen/qwen-max",
"swe_score": 0,
"cost_per_1m_tokens": { "input": 1.6, "output": 6.4 },
"cost_per_1m_tokens": {
"input": 1.6,
"output": 6.4
},
"allowed_roles": ["main", "fallback"],
"max_tokens": 32768
},
{
"id": "qwen/qwen-turbo",
"swe_score": 0,
"cost_per_1m_tokens": { "input": 0.05, "output": 0.2 },
"cost_per_1m_tokens": {
"input": 0.05,
"output": 0.2
},
"allowed_roles": ["main", "fallback"],
"max_tokens": 1000000
},
{
"id": "qwen/qwen3-235b-a22b",
"swe_score": 0,
"cost_per_1m_tokens": { "input": 0.14, "output": 2 },
"cost_per_1m_tokens": {
"input": 0.14,
"output": 2
},
"allowed_roles": ["main", "fallback"],
"max_tokens": 24000
},
{
"id": "mistralai/mistral-small-3.1-24b-instruct:free",
"swe_score": 0,
"cost_per_1m_tokens": { "input": 0, "output": 0 },
"cost_per_1m_tokens": {
"input": 0,
"output": 0
},
"allowed_roles": ["main", "fallback"],
"max_tokens": 96000
},
{
"id": "mistralai/mistral-small-3.1-24b-instruct",
"swe_score": 0,
"cost_per_1m_tokens": { "input": 0.1, "output": 0.3 },
"cost_per_1m_tokens": {
"input": 0.1,
"output": 0.3
},
"allowed_roles": ["main", "fallback"],
"max_tokens": 128000
},
{
"id": "mistralai/devstral-small",
"swe_score": 0,
"cost_per_1m_tokens": { "input": 0.1, "output": 0.3 },
"cost_per_1m_tokens": {
"input": 0.1,
"output": 0.3
},
"allowed_roles": ["main"],
"max_tokens": 110000
},
{
"id": "mistralai/mistral-nemo",
"swe_score": 0,
"cost_per_1m_tokens": { "input": 0.03, "output": 0.07 },
"cost_per_1m_tokens": {
"input": 0.03,
"output": 0.07
},
"allowed_roles": ["main", "fallback"],
"max_tokens": 100000
},
{
"id": "thudm/glm-4-32b:free",
"swe_score": 0,
"cost_per_1m_tokens": { "input": 0, "output": 0 },
"cost_per_1m_tokens": {
"input": 0,
"output": 0
},
"allowed_roles": ["main", "fallback"],
"max_tokens": 32768
}
],
"claude-code": [
{
"id": "opus",
"swe_score": 0.725,
"cost_per_1m_tokens": { "input": 0, "output": 0 },
"allowed_roles": ["main", "fallback", "research"],
"max_tokens": 32000
},
{
"id": "sonnet",
"swe_score": 0.727,
"cost_per_1m_tokens": { "input": 0, "output": 0 },
"allowed_roles": ["main", "fallback", "research"],
"max_tokens": 64000
}
]
}

View File

@@ -23,6 +23,7 @@ import {
} from '../config-manager.js';
import { findConfigPath } from '../../../src/utils/path-utils.js';
import { log } from '../utils.js';
import { CUSTOM_PROVIDERS } from '../../../src/constants/providers.js';
/**
* Fetches the list of models from OpenRouter API.
@@ -424,7 +425,7 @@ async function setModel(role, modelId, options = {}) {
let warningMessage = null;
// Find the model data in internal list initially to see if it exists at all
const modelData = availableModels.find((m) => m.id === modelId);
let modelData = availableModels.find((m) => m.id === modelId);
// --- Revised Logic: Prioritize providerHint --- //
@@ -440,7 +441,7 @@ async function setModel(role, modelId, options = {}) {
} else {
// Either not found internally, OR found but under a DIFFERENT provider than hinted.
// Proceed with custom logic based ONLY on the hint.
if (providerHint === 'openrouter') {
if (providerHint === CUSTOM_PROVIDERS.OPENROUTER) {
// Check OpenRouter ONLY because hint was openrouter
report('info', `Checking OpenRouter for ${modelId} (as hinted)...`);
const openRouterModels = await fetchOpenRouterModels();
@@ -449,7 +450,7 @@ async function setModel(role, modelId, options = {}) {
openRouterModels &&
openRouterModels.some((m) => m.id === modelId)
) {
determinedProvider = 'openrouter';
determinedProvider = CUSTOM_PROVIDERS.OPENROUTER;
// Check if this is a free model (ends with :free)
if (modelId.endsWith(':free')) {
@@ -465,7 +466,7 @@ async function setModel(role, modelId, options = {}) {
`Model ID "${modelId}" not found in the live OpenRouter model list. Please verify the ID and ensure it's available on OpenRouter.`
);
}
} else if (providerHint === 'ollama') {
} else if (providerHint === CUSTOM_PROVIDERS.OLLAMA) {
// Check Ollama ONLY because hint was ollama
report('info', `Checking Ollama for ${modelId} (as hinted)...`);
@@ -479,7 +480,7 @@ async function setModel(role, modelId, options = {}) {
`Unable to connect to Ollama server at ${ollamaBaseURL}. Please ensure Ollama is running and try again.`
);
} else if (ollamaModels.some((m) => m.model === modelId)) {
determinedProvider = 'ollama';
determinedProvider = CUSTOM_PROVIDERS.OLLAMA;
warningMessage = `Warning: Custom Ollama model '${modelId}' set. Ensure your Ollama server is running and has pulled this model. Taskmaster cannot guarantee compatibility.`;
report('warn', warningMessage);
} else {
@@ -489,13 +490,41 @@ async function setModel(role, modelId, options = {}) {
`Model ID "${modelId}" not found in the Ollama instance. Please verify the model is pulled and available. You can check available models with: curl ${tagsUrl}`
);
}
} else if (providerHint === 'bedrock') {
} else if (providerHint === CUSTOM_PROVIDERS.BEDROCK) {
// Set provider without model validation since Bedrock models are managed by AWS
determinedProvider = 'bedrock';
determinedProvider = CUSTOM_PROVIDERS.BEDROCK;
warningMessage = `Warning: Custom Bedrock model '${modelId}' set. Please ensure the model ID is valid and accessible in your AWS account.`;
report('warn', warningMessage);
} else if (providerHint === CUSTOM_PROVIDERS.CLAUDE_CODE) {
// Claude Code provider - check if model exists in our list
determinedProvider = CUSTOM_PROVIDERS.CLAUDE_CODE;
// Re-find modelData specifically for claude-code provider
const claudeCodeModels = availableModels.filter(
(m) => m.provider === 'claude-code'
);
const claudeCodeModelData = claudeCodeModels.find(
(m) => m.id === modelId
);
if (claudeCodeModelData) {
// Update modelData to the found claude-code model
modelData = claudeCodeModelData;
report('info', `Setting Claude Code model '${modelId}'.`);
} else {
warningMessage = `Warning: Claude Code model '${modelId}' not found in supported models. Setting without validation.`;
report('warn', warningMessage);
}
} else if (providerHint === CUSTOM_PROVIDERS.AZURE) {
// Set provider without model validation since Azure models are managed by Azure
determinedProvider = CUSTOM_PROVIDERS.AZURE;
warningMessage = `Warning: Custom Azure model '${modelId}' set. Please ensure the model deployment is valid and accessible in your Azure account.`;
report('warn', warningMessage);
} else if (providerHint === CUSTOM_PROVIDERS.VERTEX) {
// Set provider without model validation since Vertex models are managed by Google Cloud
determinedProvider = CUSTOM_PROVIDERS.VERTEX;
warningMessage = `Warning: Custom Vertex AI model '${modelId}' set. Please ensure the model is valid and accessible in your Google Cloud project.`;
report('warn', warningMessage);
} else {
// Invalid provider hint - should not happen
// Invalid provider hint - should not happen with our constants
throw new Error(`Invalid provider hint received: ${providerHint}`);
}
}
@@ -514,7 +543,7 @@ async function setModel(role, modelId, options = {}) {
success: false,
error: {
code: 'MODEL_NOT_FOUND_NO_HINT',
message: `Model ID "${modelId}" not found in Taskmaster's supported models. If this is a custom model, please specify the provider using --openrouter or --ollama.`
message: `Model ID "${modelId}" not found in Taskmaster's supported models. If this is a custom model, please specify the provider using --openrouter, --ollama, --bedrock, --azure, or --vertex.`
}
};
}
@@ -536,11 +565,16 @@ async function setModel(role, modelId, options = {}) {
// Update configuration
currentConfig.models[role] = {
...currentConfig.models[role], // Keep existing params like maxTokens
...currentConfig.models[role], // Keep existing params like temperature
provider: determinedProvider,
modelId: modelId
};
// If model data is available, update maxTokens from supported-models.json
if (modelData && modelData.max_tokens) {
currentConfig.models[role].maxTokens = modelData.max_tokens;
}
// Write updated configuration
const writeResult = writeConfig(currentConfig, projectRoot);
if (!writeResult) {

View File

@@ -21,18 +21,10 @@ export class BedrockAIProvider extends BaseAIProvider {
*/
getClient(params) {
try {
const {
profile = process.env.AWS_PROFILE || 'default',
region = process.env.AWS_DEFAULT_REGION || 'us-east-1',
baseURL
} = params;
const credentialProvider = fromNodeProviderChain({ profile });
const credentialProvider = fromNodeProviderChain();
return createAmazonBedrock({
region,
credentialProvider,
...(baseURL && { baseURL })
credentialProvider
});
} catch (error) {
this.handleError('client initialization', error);

View File

@@ -0,0 +1,47 @@
/**
* src/ai-providers/claude-code.js
*
* Implementation for interacting with Claude models via Claude Code CLI
* using a custom AI SDK implementation.
*/
import { createClaudeCode } from './custom-sdk/claude-code/index.js';
import { BaseAIProvider } from './base-provider.js';
export class ClaudeCodeProvider extends BaseAIProvider {
constructor() {
super();
this.name = 'Claude Code';
}
/**
* Override validateAuth to skip API key validation for Claude Code
* @param {object} params - Parameters to validate
*/
validateAuth(params) {
// Claude Code doesn't require an API key
// No validation needed
}
/**
* Creates and returns a Claude Code client instance.
* @param {object} params - Parameters for client initialization
* @param {string} [params.baseURL] - Optional custom API endpoint (not used by Claude Code)
* @returns {Function} Claude Code client function
* @throws {Error} If initialization fails
*/
getClient(params) {
try {
// Claude Code doesn't use API keys or base URLs
// Just return the provider factory
return createClaudeCode({
defaultSettings: {
// Add any default settings if needed
// These can be overridden per request
}
});
} catch (error) {
this.handleError('client initialization', error);
}
}
}

View File

@@ -0,0 +1,126 @@
/**
* @fileoverview Error handling utilities for Claude Code provider
*/
import { APICallError, LoadAPIKeyError } from '@ai-sdk/provider';
/**
* @typedef {import('./types.js').ClaudeCodeErrorMetadata} ClaudeCodeErrorMetadata
*/
/**
* Create an API call error with Claude Code specific metadata
* @param {Object} params - Error parameters
* @param {string} params.message - Error message
* @param {string} [params.code] - Error code
* @param {number} [params.exitCode] - Process exit code
* @param {string} [params.stderr] - Standard error output
* @param {string} [params.promptExcerpt] - Excerpt of the prompt
* @param {boolean} [params.isRetryable=false] - Whether the error is retryable
* @returns {APICallError}
*/
export function createAPICallError({
message,
code,
exitCode,
stderr,
promptExcerpt,
isRetryable = false
}) {
/** @type {ClaudeCodeErrorMetadata} */
const metadata = {
code,
exitCode,
stderr,
promptExcerpt
};
return new APICallError({
message,
isRetryable,
url: 'claude-code-cli://command',
requestBodyValues: promptExcerpt ? { prompt: promptExcerpt } : undefined,
data: metadata
});
}
/**
* Create an authentication error
* @param {Object} params - Error parameters
* @param {string} params.message - Error message
* @returns {LoadAPIKeyError}
*/
export function createAuthenticationError({ message }) {
return new LoadAPIKeyError({
message:
message ||
'Authentication failed. Please ensure Claude Code CLI is properly authenticated.'
});
}
/**
* Create a timeout error
* @param {Object} params - Error parameters
* @param {string} params.message - Error message
* @param {string} [params.promptExcerpt] - Excerpt of the prompt
* @param {number} params.timeoutMs - Timeout in milliseconds
* @returns {APICallError}
*/
export function createTimeoutError({ message, promptExcerpt, timeoutMs }) {
// Store timeoutMs in metadata for potential use by error handlers
/** @type {ClaudeCodeErrorMetadata & { timeoutMs: number }} */
const metadata = {
code: 'TIMEOUT',
promptExcerpt,
timeoutMs
};
return new APICallError({
message,
isRetryable: true,
url: 'claude-code-cli://command',
requestBodyValues: promptExcerpt ? { prompt: promptExcerpt } : undefined,
data: metadata
});
}
/**
* Check if an error is an authentication error
* @param {unknown} error - Error to check
* @returns {boolean}
*/
export function isAuthenticationError(error) {
if (error instanceof LoadAPIKeyError) return true;
if (
error instanceof APICallError &&
/** @type {ClaudeCodeErrorMetadata} */ (error.data)?.exitCode === 401
)
return true;
return false;
}
/**
* Check if an error is a timeout error
* @param {unknown} error - Error to check
* @returns {boolean}
*/
export function isTimeoutError(error) {
if (
error instanceof APICallError &&
/** @type {ClaudeCodeErrorMetadata} */ (error.data)?.code === 'TIMEOUT'
)
return true;
return false;
}
/**
* Get error metadata from an error
* @param {unknown} error - Error to extract metadata from
* @returns {ClaudeCodeErrorMetadata|undefined}
*/
export function getErrorMetadata(error) {
if (error instanceof APICallError && error.data) {
return /** @type {ClaudeCodeErrorMetadata} */ (error.data);
}
return undefined;
}

View File

@@ -0,0 +1,83 @@
/**
* @fileoverview Claude Code provider factory and exports
*/
import { NoSuchModelError } from '@ai-sdk/provider';
import { ClaudeCodeLanguageModel } from './language-model.js';
/**
* @typedef {import('./types.js').ClaudeCodeSettings} ClaudeCodeSettings
* @typedef {import('./types.js').ClaudeCodeModelId} ClaudeCodeModelId
* @typedef {import('./types.js').ClaudeCodeProvider} ClaudeCodeProvider
* @typedef {import('./types.js').ClaudeCodeProviderSettings} ClaudeCodeProviderSettings
*/
/**
* Create a Claude Code provider using the official SDK
* @param {ClaudeCodeProviderSettings} [options={}] - Provider configuration options
* @returns {ClaudeCodeProvider} Claude Code provider instance
*/
export function createClaudeCode(options = {}) {
/**
* Create a language model instance
* @param {ClaudeCodeModelId} modelId - Model ID
* @param {ClaudeCodeSettings} [settings={}] - Model settings
* @returns {ClaudeCodeLanguageModel}
*/
const createModel = (modelId, settings = {}) => {
return new ClaudeCodeLanguageModel({
id: modelId,
settings: {
...options.defaultSettings,
...settings
}
});
};
/**
* Provider function
* @param {ClaudeCodeModelId} modelId - Model ID
* @param {ClaudeCodeSettings} [settings] - Model settings
* @returns {ClaudeCodeLanguageModel}
*/
const provider = function (modelId, settings) {
if (new.target) {
throw new Error(
'The Claude Code model function cannot be called with the new keyword.'
);
}
return createModel(modelId, settings);
};
provider.languageModel = createModel;
provider.chat = createModel; // Alias for languageModel
// Add textEmbeddingModel method that throws NoSuchModelError
provider.textEmbeddingModel = (modelId) => {
throw new NoSuchModelError({
modelId,
modelType: 'textEmbeddingModel'
});
};
return /** @type {ClaudeCodeProvider} */ (provider);
}
/**
* Default Claude Code provider instance
*/
export const claudeCode = createClaudeCode();
// Provider exports
export { ClaudeCodeLanguageModel } from './language-model.js';
// Error handling exports
export {
isAuthenticationError,
isTimeoutError,
getErrorMetadata,
createAPICallError,
createAuthenticationError,
createTimeoutError
} from './errors.js';

View File

@@ -0,0 +1,59 @@
/**
* @fileoverview Extract JSON from Claude's response, handling markdown blocks and other formatting
*/
/**
* Extract JSON from Claude's response
* @param {string} text - The text to extract JSON from
* @returns {string} - The extracted JSON string
*/
export function extractJson(text) {
// Remove markdown code blocks if present
let jsonText = text.trim();
// Remove ```json blocks
jsonText = jsonText.replace(/^```json\s*/gm, '');
jsonText = jsonText.replace(/^```\s*/gm, '');
jsonText = jsonText.replace(/```\s*$/gm, '');
// Remove common TypeScript/JavaScript patterns
jsonText = jsonText.replace(/^const\s+\w+\s*=\s*/, ''); // Remove "const varName = "
jsonText = jsonText.replace(/^let\s+\w+\s*=\s*/, ''); // Remove "let varName = "
jsonText = jsonText.replace(/^var\s+\w+\s*=\s*/, ''); // Remove "var varName = "
jsonText = jsonText.replace(/;?\s*$/, ''); // Remove trailing semicolons
// Try to extract JSON object or array
const objectMatch = jsonText.match(/{[\s\S]*}/);
const arrayMatch = jsonText.match(/\[[\s\S]*\]/);
if (objectMatch) {
jsonText = objectMatch[0];
} else if (arrayMatch) {
jsonText = arrayMatch[0];
}
// First try to parse as valid JSON
try {
JSON.parse(jsonText);
return jsonText;
} catch {
// If it's not valid JSON, it might be a JavaScript object literal
// Try to convert it to valid JSON
try {
// This is a simple conversion that handles basic cases
// Replace unquoted keys with quoted keys
const converted = jsonText
.replace(/([{,]\s*)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/g, '$1"$2":')
// Replace single quotes with double quotes
.replace(/'/g, '"');
// Validate the converted JSON
JSON.parse(converted);
return converted;
} catch {
// If all else fails, return the original text
// The AI SDK will handle the error appropriately
return text;
}
}
}

View File

@@ -0,0 +1,458 @@
/**
* @fileoverview Claude Code Language Model implementation
*/
import { NoSuchModelError } from '@ai-sdk/provider';
import { generateId } from '@ai-sdk/provider-utils';
import { convertToClaudeCodeMessages } from './message-converter.js';
import { extractJson } from './json-extractor.js';
import { createAPICallError, createAuthenticationError } from './errors.js';
let query;
let AbortError;
async function loadClaudeCodeModule() {
if (!query || !AbortError) {
try {
const mod = await import('@anthropic-ai/claude-code');
query = mod.query;
AbortError = mod.AbortError;
} catch (err) {
throw new Error(
"Claude Code SDK is not installed. Please install '@anthropic-ai/claude-code' to use the claude-code provider."
);
}
}
}
/**
* @typedef {import('./types.js').ClaudeCodeSettings} ClaudeCodeSettings
* @typedef {import('./types.js').ClaudeCodeModelId} ClaudeCodeModelId
* @typedef {import('./types.js').ClaudeCodeLanguageModelOptions} ClaudeCodeLanguageModelOptions
*/
const modelMap = {
opus: 'opus',
sonnet: 'sonnet'
};
export class ClaudeCodeLanguageModel {
specificationVersion = 'v1';
defaultObjectGenerationMode = 'json';
supportsImageUrls = false;
supportsStructuredOutputs = false;
/** @type {ClaudeCodeModelId} */
modelId;
/** @type {ClaudeCodeSettings} */
settings;
/** @type {string|undefined} */
sessionId;
/**
* @param {ClaudeCodeLanguageModelOptions} options
*/
constructor(options) {
this.modelId = options.id;
this.settings = options.settings ?? {};
// Validate model ID format
if (
!this.modelId ||
typeof this.modelId !== 'string' ||
this.modelId.trim() === ''
) {
throw new NoSuchModelError({
modelId: this.modelId,
modelType: 'languageModel'
});
}
}
get provider() {
return 'claude-code';
}
/**
* Get the model name for Claude Code CLI
* @returns {string}
*/
getModel() {
const mapped = modelMap[this.modelId];
return mapped ?? this.modelId;
}
/**
* Generate unsupported parameter warnings
* @param {Object} options - Generation options
* @returns {Array} Warnings array
*/
generateUnsupportedWarnings(options) {
const warnings = [];
const unsupportedParams = [];
// Check for unsupported parameters
if (options.temperature !== undefined)
unsupportedParams.push('temperature');
if (options.maxTokens !== undefined) unsupportedParams.push('maxTokens');
if (options.topP !== undefined) unsupportedParams.push('topP');
if (options.topK !== undefined) unsupportedParams.push('topK');
if (options.presencePenalty !== undefined)
unsupportedParams.push('presencePenalty');
if (options.frequencyPenalty !== undefined)
unsupportedParams.push('frequencyPenalty');
if (options.stopSequences !== undefined && options.stopSequences.length > 0)
unsupportedParams.push('stopSequences');
if (options.seed !== undefined) unsupportedParams.push('seed');
if (unsupportedParams.length > 0) {
// Add a warning for each unsupported parameter
for (const param of unsupportedParams) {
warnings.push({
type: 'unsupported-setting',
setting: param,
details: `Claude Code CLI does not support the ${param} parameter. It will be ignored.`
});
}
}
return warnings;
}
/**
* Generate text using Claude Code
* @param {Object} options - Generation options
* @returns {Promise<Object>}
*/
async doGenerate(options) {
await loadClaudeCodeModule();
const { messagesPrompt } = convertToClaudeCodeMessages(
options.prompt,
options.mode
);
const abortController = new AbortController();
if (options.abortSignal) {
options.abortSignal.addEventListener('abort', () =>
abortController.abort()
);
}
const queryOptions = {
model: this.getModel(),
abortController,
resume: this.sessionId,
pathToClaudeCodeExecutable: this.settings.pathToClaudeCodeExecutable,
customSystemPrompt: this.settings.customSystemPrompt,
appendSystemPrompt: this.settings.appendSystemPrompt,
maxTurns: this.settings.maxTurns,
maxThinkingTokens: this.settings.maxThinkingTokens,
cwd: this.settings.cwd,
executable: this.settings.executable,
executableArgs: this.settings.executableArgs,
permissionMode: this.settings.permissionMode,
permissionPromptToolName: this.settings.permissionPromptToolName,
continue: this.settings.continue,
allowedTools: this.settings.allowedTools,
disallowedTools: this.settings.disallowedTools,
mcpServers: this.settings.mcpServers
};
let text = '';
let usage = { promptTokens: 0, completionTokens: 0 };
let finishReason = 'stop';
let costUsd;
let durationMs;
let rawUsage;
const warnings = this.generateUnsupportedWarnings(options);
try {
const response = query({
prompt: messagesPrompt,
options: queryOptions
});
for await (const message of response) {
if (message.type === 'assistant') {
text += message.message.content
.map((c) => (c.type === 'text' ? c.text : ''))
.join('');
} else if (message.type === 'result') {
this.sessionId = message.session_id;
costUsd = message.total_cost_usd;
durationMs = message.duration_ms;
if ('usage' in message) {
rawUsage = message.usage;
usage = {
promptTokens:
(message.usage.cache_creation_input_tokens ?? 0) +
(message.usage.cache_read_input_tokens ?? 0) +
(message.usage.input_tokens ?? 0),
completionTokens: message.usage.output_tokens ?? 0
};
}
if (message.subtype === 'error_max_turns') {
finishReason = 'length';
} else if (message.subtype === 'error_during_execution') {
finishReason = 'error';
}
} else if (message.type === 'system' && message.subtype === 'init') {
this.sessionId = message.session_id;
}
}
} catch (error) {
if (error instanceof AbortError) {
throw options.abortSignal?.aborted ? options.abortSignal.reason : error;
}
// Check for authentication errors
if (
error.message?.includes('not logged in') ||
error.message?.includes('authentication') ||
error.exitCode === 401
) {
throw createAuthenticationError({
message:
error.message ||
'Authentication failed. Please ensure Claude Code CLI is properly authenticated.'
});
}
// Wrap other errors with API call error
throw createAPICallError({
message: error.message || 'Claude Code CLI error',
code: error.code,
exitCode: error.exitCode,
stderr: error.stderr,
promptExcerpt: messagesPrompt.substring(0, 200),
isRetryable: error.code === 'ENOENT' || error.code === 'ECONNREFUSED'
});
}
// Extract JSON if in object-json mode
if (options.mode?.type === 'object-json' && text) {
text = extractJson(text);
}
return {
text: text || undefined,
usage,
finishReason,
rawCall: {
rawPrompt: messagesPrompt,
rawSettings: queryOptions
},
warnings: warnings.length > 0 ? warnings : undefined,
response: {
id: generateId(),
timestamp: new Date(),
modelId: this.modelId
},
request: {
body: messagesPrompt
},
providerMetadata: {
'claude-code': {
...(this.sessionId !== undefined && { sessionId: this.sessionId }),
...(costUsd !== undefined && { costUsd }),
...(durationMs !== undefined && { durationMs }),
...(rawUsage !== undefined && { rawUsage })
}
}
};
}
/**
* Stream text using Claude Code
* @param {Object} options - Stream options
* @returns {Promise<Object>}
*/
async doStream(options) {
await loadClaudeCodeModule();
const { messagesPrompt } = convertToClaudeCodeMessages(
options.prompt,
options.mode
);
const abortController = new AbortController();
if (options.abortSignal) {
options.abortSignal.addEventListener('abort', () =>
abortController.abort()
);
}
const queryOptions = {
model: this.getModel(),
abortController,
resume: this.sessionId,
pathToClaudeCodeExecutable: this.settings.pathToClaudeCodeExecutable,
customSystemPrompt: this.settings.customSystemPrompt,
appendSystemPrompt: this.settings.appendSystemPrompt,
maxTurns: this.settings.maxTurns,
maxThinkingTokens: this.settings.maxThinkingTokens,
cwd: this.settings.cwd,
executable: this.settings.executable,
executableArgs: this.settings.executableArgs,
permissionMode: this.settings.permissionMode,
permissionPromptToolName: this.settings.permissionPromptToolName,
continue: this.settings.continue,
allowedTools: this.settings.allowedTools,
disallowedTools: this.settings.disallowedTools,
mcpServers: this.settings.mcpServers
};
const warnings = this.generateUnsupportedWarnings(options);
const stream = new ReadableStream({
start: async (controller) => {
try {
const response = query({
prompt: messagesPrompt,
options: queryOptions
});
let usage = { promptTokens: 0, completionTokens: 0 };
let accumulatedText = '';
for await (const message of response) {
if (message.type === 'assistant') {
const text = message.message.content
.map((c) => (c.type === 'text' ? c.text : ''))
.join('');
if (text) {
accumulatedText += text;
// In object-json mode, we need to accumulate the full text
// and extract JSON at the end, so don't stream individual deltas
if (options.mode?.type !== 'object-json') {
controller.enqueue({
type: 'text-delta',
textDelta: text
});
}
}
} else if (message.type === 'result') {
let rawUsage;
if ('usage' in message) {
rawUsage = message.usage;
usage = {
promptTokens:
(message.usage.cache_creation_input_tokens ?? 0) +
(message.usage.cache_read_input_tokens ?? 0) +
(message.usage.input_tokens ?? 0),
completionTokens: message.usage.output_tokens ?? 0
};
}
let finishReason = 'stop';
if (message.subtype === 'error_max_turns') {
finishReason = 'length';
} else if (message.subtype === 'error_during_execution') {
finishReason = 'error';
}
// Store session ID in the model instance
this.sessionId = message.session_id;
// In object-json mode, extract JSON and send the full text at once
if (options.mode?.type === 'object-json' && accumulatedText) {
const extractedJson = extractJson(accumulatedText);
controller.enqueue({
type: 'text-delta',
textDelta: extractedJson
});
}
controller.enqueue({
type: 'finish',
finishReason,
usage,
providerMetadata: {
'claude-code': {
sessionId: message.session_id,
...(message.total_cost_usd !== undefined && {
costUsd: message.total_cost_usd
}),
...(message.duration_ms !== undefined && {
durationMs: message.duration_ms
}),
...(rawUsage !== undefined && { rawUsage })
}
}
});
} else if (
message.type === 'system' &&
message.subtype === 'init'
) {
// Store session ID for future use
this.sessionId = message.session_id;
// Emit response metadata when session is initialized
controller.enqueue({
type: 'response-metadata',
id: message.session_id,
timestamp: new Date(),
modelId: this.modelId
});
}
}
controller.close();
} catch (error) {
let errorToEmit;
if (error instanceof AbortError) {
errorToEmit = options.abortSignal?.aborted
? options.abortSignal.reason
: error;
} else if (
error.message?.includes('not logged in') ||
error.message?.includes('authentication') ||
error.exitCode === 401
) {
errorToEmit = createAuthenticationError({
message:
error.message ||
'Authentication failed. Please ensure Claude Code CLI is properly authenticated.'
});
} else {
errorToEmit = createAPICallError({
message: error.message || 'Claude Code CLI error',
code: error.code,
exitCode: error.exitCode,
stderr: error.stderr,
promptExcerpt: messagesPrompt.substring(0, 200),
isRetryable:
error.code === 'ENOENT' || error.code === 'ECONNREFUSED'
});
}
// Emit error as a stream part
controller.enqueue({
type: 'error',
error: errorToEmit
});
controller.close();
}
}
});
return {
stream,
rawCall: {
rawPrompt: messagesPrompt,
rawSettings: queryOptions
},
warnings: warnings.length > 0 ? warnings : undefined,
request: {
body: messagesPrompt
}
};
}
}

View File

@@ -0,0 +1,139 @@
/**
* @fileoverview Converts AI SDK prompt format to Claude Code message format
*/
/**
* Convert AI SDK prompt to Claude Code messages format
* @param {Array} prompt - AI SDK prompt array
* @param {Object} [mode] - Generation mode
* @param {string} mode.type - Mode type ('regular', 'object-json', 'object-tool')
* @returns {{messagesPrompt: string, systemPrompt?: string}}
*/
export function convertToClaudeCodeMessages(prompt, mode) {
const messages = [];
let systemPrompt;
for (const message of prompt) {
switch (message.role) {
case 'system':
systemPrompt = message.content;
break;
case 'user':
if (typeof message.content === 'string') {
messages.push(message.content);
} else {
// Handle multi-part content
const textParts = message.content
.filter((part) => part.type === 'text')
.map((part) => part.text)
.join('\n');
if (textParts) {
messages.push(textParts);
}
// Note: Image parts are not supported by Claude Code CLI
const imageParts = message.content.filter(
(part) => part.type === 'image'
);
if (imageParts.length > 0) {
console.warn(
'Claude Code CLI does not support image inputs. Images will be ignored.'
);
}
}
break;
case 'assistant':
if (typeof message.content === 'string') {
messages.push(`Assistant: ${message.content}`);
} else {
const textParts = message.content
.filter((part) => part.type === 'text')
.map((part) => part.text)
.join('\n');
if (textParts) {
messages.push(`Assistant: ${textParts}`);
}
// Handle tool calls if present
const toolCalls = message.content.filter(
(part) => part.type === 'tool-call'
);
if (toolCalls.length > 0) {
// For now, we'll just note that tool calls were made
messages.push(`Assistant: [Tool calls made]`);
}
}
break;
case 'tool':
// Tool results could be included in the conversation
messages.push(
`Tool Result (${message.content[0].toolName}): ${JSON.stringify(
message.content[0].result
)}`
);
break;
}
}
// For the SDK, we need to provide a single prompt string
// Format the conversation history properly
// Combine system prompt with messages
let finalPrompt = '';
// Add system prompt at the beginning if present
if (systemPrompt) {
finalPrompt = systemPrompt;
}
if (messages.length === 0) {
return { messagesPrompt: finalPrompt, systemPrompt };
}
// Format messages
const formattedMessages = [];
for (let i = 0; i < messages.length; i++) {
const msg = messages[i];
// Check if this is a user or assistant message based on content
if (msg.startsWith('Assistant:') || msg.startsWith('Tool Result')) {
formattedMessages.push(msg);
} else {
// User messages
formattedMessages.push(`Human: ${msg}`);
}
}
// Combine system prompt with messages
if (finalPrompt) {
finalPrompt = finalPrompt + '\n\n' + formattedMessages.join('\n\n');
} else {
finalPrompt = formattedMessages.join('\n\n');
}
// For JSON mode, add explicit instruction to ensure JSON output
if (mode?.type === 'object-json') {
// Make the JSON instruction even more explicit
finalPrompt = `${finalPrompt}
CRITICAL INSTRUCTION: You MUST respond with ONLY valid JSON. Follow these rules EXACTLY:
1. Start your response with an opening brace {
2. End your response with a closing brace }
3. Do NOT include any text before the opening brace
4. Do NOT include any text after the closing brace
5. Do NOT use markdown code blocks or backticks
6. Do NOT include explanations or commentary
7. The ENTIRE response must be valid JSON that can be parsed with JSON.parse()
Begin your response with { and end with }`;
}
return {
messagesPrompt: finalPrompt,
systemPrompt
};
}

View File

@@ -0,0 +1,73 @@
/**
* @fileoverview Type definitions for Claude Code AI SDK provider
* These JSDoc types mirror the TypeScript interfaces from the original provider
*/
/**
* Claude Code provider settings
* @typedef {Object} ClaudeCodeSettings
* @property {string} [pathToClaudeCodeExecutable='claude'] - Custom path to Claude Code CLI executable
* @property {string} [customSystemPrompt] - Custom system prompt to use
* @property {string} [appendSystemPrompt] - Append additional content to the system prompt
* @property {number} [maxTurns] - Maximum number of turns for the conversation
* @property {number} [maxThinkingTokens] - Maximum thinking tokens for the model
* @property {string} [cwd] - Working directory for CLI operations
* @property {'bun'|'deno'|'node'} [executable='node'] - JavaScript runtime to use
* @property {string[]} [executableArgs] - Additional arguments for the JavaScript runtime
* @property {'default'|'acceptEdits'|'bypassPermissions'|'plan'} [permissionMode='default'] - Permission mode for tool usage
* @property {string} [permissionPromptToolName] - Custom tool name for permission prompts
* @property {boolean} [continue] - Continue the most recent conversation
* @property {string} [resume] - Resume a specific session by ID
* @property {string[]} [allowedTools] - Tools to explicitly allow during execution (e.g., ['Read', 'LS', 'Bash(git log:*)'])
* @property {string[]} [disallowedTools] - Tools to disallow during execution (e.g., ['Write', 'Edit', 'Bash(rm:*)'])
* @property {Object.<string, MCPServerConfig>} [mcpServers] - MCP server configuration
* @property {boolean} [verbose] - Enable verbose logging for debugging
*/
/**
* MCP Server configuration
* @typedef {Object} MCPServerConfig
* @property {'stdio'|'sse'} [type='stdio'] - Server type
* @property {string} command - Command to execute (for stdio type)
* @property {string[]} [args] - Arguments for the command
* @property {Object.<string, string>} [env] - Environment variables
* @property {string} url - URL for SSE type servers
* @property {Object.<string, string>} [headers] - Headers for SSE type servers
*/
/**
* Model ID type - either 'opus', 'sonnet', or any string
* @typedef {'opus'|'sonnet'|string} ClaudeCodeModelId
*/
/**
* Language model options
* @typedef {Object} ClaudeCodeLanguageModelOptions
* @property {ClaudeCodeModelId} id - The model ID
* @property {ClaudeCodeSettings} [settings] - Optional settings
*/
/**
* Error metadata for Claude Code errors
* @typedef {Object} ClaudeCodeErrorMetadata
* @property {string} [code] - Error code
* @property {number} [exitCode] - Process exit code
* @property {string} [stderr] - Standard error output
* @property {string} [promptExcerpt] - Excerpt of the prompt that caused the error
*/
/**
* Claude Code provider interface
* @typedef {Object} ClaudeCodeProvider
* @property {function(ClaudeCodeModelId, ClaudeCodeSettings=): Object} languageModel - Create a language model
* @property {function(ClaudeCodeModelId, ClaudeCodeSettings=): Object} chat - Alias for languageModel
* @property {function(string): never} textEmbeddingModel - Throws NoSuchModelError (not supported)
*/
/**
* Claude Code provider settings
* @typedef {Object} ClaudeCodeProviderSettings
* @property {ClaudeCodeSettings} [defaultSettings] - Default settings to use for all models
*/
export {}; // This ensures the file is treated as a module

View File

@@ -13,3 +13,4 @@ export { OllamaAIProvider } from './ollama.js';
export { BedrockAIProvider } from './bedrock.js';
export { AzureProvider } from './azure.js';
export { VertexAIProvider } from './google-vertex.js';
export { ClaudeCodeProvider } from './claude-code.js';

View File

@@ -0,0 +1,33 @@
/**
* Provider validation constants
* Defines which providers should be validated against the supported-models.json file
*/
// Providers that have predefined model lists and should be validated
export const VALIDATED_PROVIDERS = [
'anthropic',
'openai',
'google',
'perplexity',
'xai',
'mistral'
];
// Custom providers object for easy named access
export const CUSTOM_PROVIDERS = {
AZURE: 'azure',
VERTEX: 'vertex',
BEDROCK: 'bedrock',
OPENROUTER: 'openrouter',
OLLAMA: 'ollama',
CLAUDE_CODE: 'claude-code'
};
// Custom providers array (for backward compatibility and iteration)
export const CUSTOM_PROVIDERS_ARRAY = Object.values(CUSTOM_PROVIDERS);
// All known providers (for reference)
export const ALL_PROVIDERS = [
...VALIDATED_PROVIDERS,
...CUSTOM_PROVIDERS_ARRAY
];

View File

@@ -0,0 +1,95 @@
import { jest } from '@jest/globals';
// Mock the base provider to avoid circular dependencies
jest.unstable_mockModule('../../src/ai-providers/base-provider.js', () => ({
BaseAIProvider: class {
constructor() {
this.name = 'Base Provider';
}
handleError(context, error) {
throw error;
}
}
}));
// Mock the claude-code SDK to simulate it not being installed
jest.unstable_mockModule('@anthropic-ai/claude-code', () => {
throw new Error("Cannot find module '@anthropic-ai/claude-code'");
});
// Import after mocking
const { ClaudeCodeProvider } = await import(
'../../src/ai-providers/claude-code.js'
);
describe('Claude Code Optional Dependency Integration', () => {
describe('when @anthropic-ai/claude-code is not installed', () => {
it('should allow provider instantiation', () => {
// Provider should instantiate without error
const provider = new ClaudeCodeProvider();
expect(provider).toBeDefined();
expect(provider.name).toBe('Claude Code');
});
it('should allow client creation', () => {
const provider = new ClaudeCodeProvider();
// Client creation should work
const client = provider.getClient({});
expect(client).toBeDefined();
expect(typeof client).toBe('function');
});
it('should fail with clear error when trying to use the model', async () => {
const provider = new ClaudeCodeProvider();
const client = provider.getClient({});
const model = client('opus');
// The actual usage should fail with the lazy loading error
await expect(
model.doGenerate({
prompt: [{ role: 'user', content: 'Hello' }],
mode: { type: 'regular' }
})
).rejects.toThrow(
"Claude Code SDK is not installed. Please install '@anthropic-ai/claude-code' to use the claude-code provider."
);
});
it('should provide helpful error message for streaming', async () => {
const provider = new ClaudeCodeProvider();
const client = provider.getClient({});
const model = client('sonnet');
await expect(
model.doStream({
prompt: [{ role: 'user', content: 'Hello' }],
mode: { type: 'regular' }
})
).rejects.toThrow(
"Claude Code SDK is not installed. Please install '@anthropic-ai/claude-code' to use the claude-code provider."
);
});
});
describe('provider behavior', () => {
it('should not require API key', () => {
const provider = new ClaudeCodeProvider();
// Should not throw
expect(() => provider.validateAuth()).not.toThrow();
expect(() => provider.validateAuth({ apiKey: null })).not.toThrow();
});
it('should work with ai-services-unified when provider is configured', async () => {
// This tests that the provider can be selected but will fail appropriately
// when the actual model is used
const provider = new ClaudeCodeProvider();
expect(provider).toBeDefined();
// In real usage, ai-services-unified would:
// 1. Get the provider instance (works)
// 2. Call provider.getClient() (works)
// 3. Create a model (works)
// 4. Try to generate (fails with clear error)
});
});
});

View File

@@ -0,0 +1,115 @@
import { jest } from '@jest/globals';
// Mock the claude-code SDK module
jest.unstable_mockModule(
'../../../src/ai-providers/custom-sdk/claude-code/index.js',
() => ({
createClaudeCode: jest.fn(() => {
const provider = (modelId, settings) => ({
// Mock language model
id: modelId,
settings
});
provider.languageModel = jest.fn((id, settings) => ({ id, settings }));
provider.chat = provider.languageModel;
return provider;
})
})
);
// Mock the base provider
jest.unstable_mockModule('../../../src/ai-providers/base-provider.js', () => ({
BaseAIProvider: class {
constructor() {
this.name = 'Base Provider';
}
handleError(context, error) {
throw error;
}
}
}));
// Import after mocking
const { ClaudeCodeProvider } = await import(
'../../../src/ai-providers/claude-code.js'
);
describe('ClaudeCodeProvider', () => {
let provider;
beforeEach(() => {
provider = new ClaudeCodeProvider();
jest.clearAllMocks();
});
describe('constructor', () => {
it('should set the provider name to Claude Code', () => {
expect(provider.name).toBe('Claude Code');
});
});
describe('validateAuth', () => {
it('should not throw an error (no API key required)', () => {
expect(() => provider.validateAuth({})).not.toThrow();
});
it('should not require any parameters', () => {
expect(() => provider.validateAuth()).not.toThrow();
});
it('should work with any params passed', () => {
expect(() =>
provider.validateAuth({
apiKey: 'some-key',
baseURL: 'https://example.com'
})
).not.toThrow();
});
});
describe('getClient', () => {
it('should return a claude code client', () => {
const client = provider.getClient({});
expect(client).toBeDefined();
expect(typeof client).toBe('function');
});
it('should create client without API key or base URL', () => {
const client = provider.getClient({});
expect(client).toBeDefined();
});
it('should handle params even though they are not used', () => {
const client = provider.getClient({
baseURL: 'https://example.com',
apiKey: 'unused-key'
});
expect(client).toBeDefined();
});
it('should have languageModel and chat methods', () => {
const client = provider.getClient({});
expect(client.languageModel).toBeDefined();
expect(client.chat).toBeDefined();
expect(client.chat).toBe(client.languageModel);
});
});
describe('error handling', () => {
it('should handle client initialization errors', async () => {
// Force an error by making createClaudeCode throw
const { createClaudeCode } = await import(
'../../../src/ai-providers/custom-sdk/claude-code/index.js'
);
createClaudeCode.mockImplementationOnce(() => {
throw new Error('Mock initialization error');
});
// Create a new provider instance to use the mocked createClaudeCode
const errorProvider = new ClaudeCodeProvider();
expect(() => errorProvider.getClient({})).toThrow(
'Mock initialization error'
);
});
});
});

View File

@@ -0,0 +1,237 @@
import { jest } from '@jest/globals';
// Mock modules before importing
jest.unstable_mockModule('@ai-sdk/provider', () => ({
NoSuchModelError: class NoSuchModelError extends Error {
constructor({ modelId, modelType }) {
super(`No such model: ${modelId}`);
this.modelId = modelId;
this.modelType = modelType;
}
}
}));
jest.unstable_mockModule('@ai-sdk/provider-utils', () => ({
generateId: jest.fn(() => 'test-id-123')
}));
jest.unstable_mockModule(
'../../../../../src/ai-providers/custom-sdk/claude-code/message-converter.js',
() => ({
convertToClaudeCodeMessages: jest.fn((prompt) => ({
messagesPrompt: 'converted-prompt',
systemPrompt: 'system'
}))
})
);
jest.unstable_mockModule(
'../../../../../src/ai-providers/custom-sdk/claude-code/json-extractor.js',
() => ({
extractJson: jest.fn((text) => text)
})
);
jest.unstable_mockModule(
'../../../../../src/ai-providers/custom-sdk/claude-code/errors.js',
() => ({
createAPICallError: jest.fn((opts) => new Error(opts.message)),
createAuthenticationError: jest.fn((opts) => new Error(opts.message))
})
);
// This mock will be controlled by tests
let mockClaudeCodeModule = null;
jest.unstable_mockModule('@anthropic-ai/claude-code', () => {
if (mockClaudeCodeModule) {
return mockClaudeCodeModule;
}
throw new Error("Cannot find module '@anthropic-ai/claude-code'");
});
// Import the module under test
const { ClaudeCodeLanguageModel } = await import(
'../../../../../src/ai-providers/custom-sdk/claude-code/language-model.js'
);
describe('ClaudeCodeLanguageModel', () => {
beforeEach(() => {
jest.clearAllMocks();
// Reset the module mock
mockClaudeCodeModule = null;
// Clear module cache to ensure fresh imports
jest.resetModules();
});
describe('constructor', () => {
it('should initialize with valid model ID', () => {
const model = new ClaudeCodeLanguageModel({
id: 'opus',
settings: { maxTurns: 5 }
});
expect(model.modelId).toBe('opus');
expect(model.settings).toEqual({ maxTurns: 5 });
expect(model.provider).toBe('claude-code');
});
it('should throw NoSuchModelError for invalid model ID', async () => {
expect(
() =>
new ClaudeCodeLanguageModel({
id: '',
settings: {}
})
).toThrow('No such model: ');
expect(
() =>
new ClaudeCodeLanguageModel({
id: null,
settings: {}
})
).toThrow('No such model: null');
});
});
describe('lazy loading of @anthropic-ai/claude-code', () => {
it('should throw error when package is not installed', async () => {
// Keep mockClaudeCodeModule as null to simulate missing package
const model = new ClaudeCodeLanguageModel({
id: 'opus',
settings: {}
});
await expect(
model.doGenerate({
prompt: [{ role: 'user', content: 'test' }],
mode: { type: 'regular' }
})
).rejects.toThrow(
"Claude Code SDK is not installed. Please install '@anthropic-ai/claude-code' to use the claude-code provider."
);
});
it('should load package successfully when available', async () => {
// Mock successful package load
const mockQuery = jest.fn(async function* () {
yield {
type: 'assistant',
message: { content: [{ type: 'text', text: 'Hello' }] }
};
yield {
type: 'result',
subtype: 'done',
usage: { output_tokens: 10, input_tokens: 5 }
};
});
mockClaudeCodeModule = {
query: mockQuery,
AbortError: class AbortError extends Error {}
};
// Need to re-import to get fresh module with mocks
jest.resetModules();
const { ClaudeCodeLanguageModel: FreshModel } = await import(
'../../../../../src/ai-providers/custom-sdk/claude-code/language-model.js'
);
const model = new FreshModel({
id: 'opus',
settings: {}
});
const result = await model.doGenerate({
prompt: [{ role: 'user', content: 'test' }],
mode: { type: 'regular' }
});
expect(result.text).toBe('Hello');
expect(mockQuery).toHaveBeenCalled();
});
it('should only attempt to load package once', async () => {
// Get a fresh import to ensure clean state
jest.resetModules();
const { ClaudeCodeLanguageModel: TestModel } = await import(
'../../../../../src/ai-providers/custom-sdk/claude-code/language-model.js'
);
const model = new TestModel({
id: 'opus',
settings: {}
});
// First call should throw
await expect(
model.doGenerate({
prompt: [{ role: 'user', content: 'test' }],
mode: { type: 'regular' }
})
).rejects.toThrow('Claude Code SDK is not installed');
// Second call should also throw without trying to load again
await expect(
model.doGenerate({
prompt: [{ role: 'user', content: 'test' }],
mode: { type: 'regular' }
})
).rejects.toThrow('Claude Code SDK is not installed');
});
});
describe('generateUnsupportedWarnings', () => {
it('should generate warnings for unsupported parameters', () => {
const model = new ClaudeCodeLanguageModel({
id: 'opus',
settings: {}
});
const warnings = model.generateUnsupportedWarnings({
temperature: 0.7,
maxTokens: 1000,
topP: 0.9,
seed: 42
});
expect(warnings).toHaveLength(4);
expect(warnings[0]).toEqual({
type: 'unsupported-setting',
setting: 'temperature',
details:
'Claude Code CLI does not support the temperature parameter. It will be ignored.'
});
});
it('should return empty array when no unsupported parameters', () => {
const model = new ClaudeCodeLanguageModel({
id: 'opus',
settings: {}
});
const warnings = model.generateUnsupportedWarnings({});
expect(warnings).toEqual([]);
});
});
describe('getModel', () => {
it('should map model IDs correctly', () => {
const model = new ClaudeCodeLanguageModel({
id: 'opus',
settings: {}
});
expect(model.getModel()).toBe('opus');
});
it('should return unmapped model IDs as-is', () => {
const model = new ClaudeCodeLanguageModel({
id: 'custom-model',
settings: {}
});
expect(model.getModel()).toBe('custom-model');
});
});
});

View File

@@ -180,6 +180,11 @@ jest.unstable_mockModule('../../src/ai-providers/index.js', () => ({
generateText: jest.fn(),
streamText: jest.fn(),
generateObject: jest.fn()
})),
ClaudeCodeProvider: jest.fn(() => ({
generateText: jest.fn(),
streamText: jest.fn(),
generateObject: jest.fn()
}))
}));

View File

@@ -266,6 +266,7 @@ describe('Validation Functions', () => {
expect(configManager.validateProvider('perplexity')).toBe(true);
expect(configManager.validateProvider('ollama')).toBe(true);
expect(configManager.validateProvider('openrouter')).toBe(true);
expect(configManager.validateProvider('bedrock')).toBe(true);
});
test('validateProvider should return false for invalid providers', () => {
@@ -713,17 +714,25 @@ describe('isConfigFilePresent', () => {
// --- getAllProviders Tests ---
describe('getAllProviders', () => {
test('should return list of providers from supported-models.json', () => {
test('should return all providers from ALL_PROVIDERS constant', () => {
// Arrange: Ensure config is loaded with real data
configManager.getConfig(null, true); // Force load using the mock that returns real data
// Act
const providers = configManager.getAllProviders();
// Assert
// Assert against the actual keys in the REAL loaded data
const expectedProviders = Object.keys(REAL_SUPPORTED_MODELS_DATA);
expect(providers).toEqual(expect.arrayContaining(expectedProviders));
expect(providers.length).toBe(expectedProviders.length);
// getAllProviders() should return the same as the ALL_PROVIDERS constant
expect(providers).toEqual(configManager.ALL_PROVIDERS);
expect(providers.length).toBe(configManager.ALL_PROVIDERS.length);
// Verify it includes both validated and custom providers
expect(providers).toEqual(
expect.arrayContaining(configManager.VALIDATED_PROVIDERS)
);
expect(providers).toEqual(
expect.arrayContaining(Object.values(configManager.CUSTOM_PROVIDERS))
);
});
});