@gitkraken/core-gitlens 0.5.110 → 0.5.111
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -1
- package/dist/git/cache.d.ts +111 -6
- package/dist/git/cache.d.ts.map +1 -1
- package/dist/git/cache.js +312 -43
- package/dist/git/cache.js.map +1 -1
- package/dist/git/context.d.ts +11 -0
- package/dist/git/context.d.ts.map +1 -1
- package/dist/git/context.js.map +1 -1
- package/dist/git/models/fileStatus.d.ts +1 -0
- package/dist/git/models/fileStatus.d.ts.map +1 -1
- package/dist/git/models/fileStatus.js +1 -0
- package/dist/git/models/fileStatus.js.map +1 -1
- package/dist/git/models/issue.d.ts +4 -0
- package/dist/git/models/issue.d.ts.map +1 -1
- package/dist/git/models/mergeConflicts.d.ts +2 -0
- package/dist/git/models/mergeConflicts.d.ts.map +1 -1
- package/dist/git/models/pullRequest.d.ts +38 -2
- package/dist/git/models/pullRequest.d.ts.map +1 -1
- package/dist/git/models/pullRequest.js +12 -1
- package/dist/git/models/pullRequest.js.map +1 -1
- package/dist/git/models/repository.d.ts +32 -5
- package/dist/git/models/repository.d.ts.map +1 -1
- package/dist/git/models/repository.js +58 -12
- package/dist/git/models/repository.js.map +1 -1
- package/dist/git/models/statusFile.d.ts.map +1 -1
- package/dist/git/models/statusFile.js +3 -0
- package/dist/git/models/statusFile.js.map +1 -1
- package/dist/git/providers/branches.d.ts +2 -0
- package/dist/git/providers/branches.d.ts.map +1 -1
- package/dist/git/providers/commits.d.ts +8 -0
- package/dist/git/providers/commits.d.ts.map +1 -1
- package/dist/git/providers/config.d.ts +19 -1
- package/dist/git/providers/config.d.ts.map +1 -1
- package/dist/git/providers/operations.d.ts +1 -0
- package/dist/git/providers/operations.d.ts.map +1 -1
- package/dist/git/providers/pausedOperations.d.ts +2 -0
- package/dist/git/providers/pausedOperations.d.ts.map +1 -1
- package/dist/git/providers/status.d.ts +1 -0
- package/dist/git/providers/status.d.ts.map +1 -1
- package/dist/git/utils/config.utils.d.ts +3 -0
- package/dist/git/utils/config.utils.d.ts.map +1 -0
- package/dist/git/utils/config.utils.js +8 -0
- package/dist/git/utils/config.utils.js.map +1 -0
- package/dist/git/utils/issue.utils.d.ts.map +1 -1
- package/dist/git/utils/issue.utils.js +2 -0
- package/dist/git/utils/issue.utils.js.map +1 -1
- package/dist/git/utils/pausedOperationStatus.utils.d.ts +14 -0
- package/dist/git/utils/pausedOperationStatus.utils.d.ts.map +1 -1
- package/dist/git/utils/pausedOperationStatus.utils.js +23 -0
- package/dist/git/utils/pausedOperationStatus.utils.js.map +1 -1
- package/dist/git/utils/pullRequest.utils.d.ts +14 -1
- package/dist/git/utils/pullRequest.utils.d.ts.map +1 -1
- package/dist/git/utils/pullRequest.utils.js +31 -0
- package/dist/git/utils/pullRequest.utils.js.map +1 -1
- package/dist/git/watching/changeEvent.d.ts +6 -1
- package/dist/git/watching/changeEvent.d.ts.map +1 -1
- package/dist/git/watching/classifyChange.d.ts.map +1 -1
- package/dist/git/watching/classifyChange.js +14 -1
- package/dist/git/watching/classifyChange.js.map +1 -1
- package/dist/git/watching/watchService.d.ts.map +1 -1
- package/dist/git/watching/watchService.js +18 -3
- package/dist/git/watching/watchService.js.map +1 -1
- package/dist/git/watching/watchSession.d.ts +8 -1
- package/dist/git/watching/watchSession.d.ts.map +1 -1
- package/dist/git/watching/watchSession.js +38 -6
- package/dist/git/watching/watchSession.js.map +1 -1
- package/dist/git/watching/watcherPatterns.d.ts +2 -2
- package/dist/git/watching/watcherPatterns.d.ts.map +1 -1
- package/dist/git/watching/watcherPatterns.js +1 -1
- package/dist/git/watching/watcherPatterns.js.map +1 -1
- package/dist/git-cli/exec/git.d.ts +1 -1
- package/dist/git-cli/exec/git.d.ts.map +1 -1
- package/dist/git-cli/exec/git.js +13 -3
- package/dist/git-cli/exec/git.js.map +1 -1
- package/dist/git-cli/providers/branches.d.ts +19 -0
- package/dist/git-cli/providers/branches.d.ts.map +1 -1
- package/dist/git-cli/providers/branches.js +282 -78
- package/dist/git-cli/providers/branches.js.map +1 -1
- package/dist/git-cli/providers/commits.d.ts +4 -0
- package/dist/git-cli/providers/commits.d.ts.map +1 -1
- package/dist/git-cli/providers/commits.js +84 -11
- package/dist/git-cli/providers/commits.js.map +1 -1
- package/dist/git-cli/providers/config.d.ts +42 -0
- package/dist/git-cli/providers/config.d.ts.map +1 -1
- package/dist/git-cli/providers/config.js +186 -5
- package/dist/git-cli/providers/config.js.map +1 -1
- package/dist/git-cli/providers/contributors.d.ts.map +1 -1
- package/dist/git-cli/providers/contributors.js +1 -1
- package/dist/git-cli/providers/contributors.js.map +1 -1
- package/dist/git-cli/providers/graph.d.ts +2 -2
- package/dist/git-cli/providers/graph.d.ts.map +1 -1
- package/dist/git-cli/providers/graph.js +27 -9
- package/dist/git-cli/providers/graph.js.map +1 -1
- package/dist/git-cli/providers/operations.d.ts.map +1 -1
- package/dist/git-cli/providers/operations.js +40 -12
- package/dist/git-cli/providers/operations.js.map +1 -1
- package/dist/git-cli/providers/pausedOperations.d.ts +5 -0
- package/dist/git-cli/providers/pausedOperations.d.ts.map +1 -1
- package/dist/git-cli/providers/pausedOperations.js +87 -16
- package/dist/git-cli/providers/pausedOperations.js.map +1 -1
- package/dist/git-cli/providers/refs.d.ts +5 -3
- package/dist/git-cli/providers/refs.d.ts.map +1 -1
- package/dist/git-cli/providers/refs.js +33 -4
- package/dist/git-cli/providers/refs.js.map +1 -1
- package/dist/git-cli/providers/status.d.ts +1 -0
- package/dist/git-cli/providers/status.d.ts.map +1 -1
- package/dist/git-cli/providers/status.js +18 -3
- package/dist/git-cli/providers/status.js.map +1 -1
- package/dist/git-cli/providers/worktrees.d.ts.map +1 -1
- package/dist/git-cli/providers/worktrees.js +4 -0
- package/dist/git-cli/providers/worktrees.js.map +1 -1
- package/dist/git-cli/service.d.ts.map +1 -1
- package/dist/git-cli/service.js.map +1 -1
- package/dist/plus/agents/providers/claudeCodeProvider.d.ts +39 -1
- package/dist/plus/agents/providers/claudeCodeProvider.d.ts.map +1 -1
- package/dist/plus/agents/providers/claudeCodeProvider.js +978 -71
- package/dist/plus/agents/providers/claudeCodeProvider.js.map +1 -1
- package/dist/plus/agents/providers/claudeCodeTranscript.d.ts +24 -2
- package/dist/plus/agents/providers/claudeCodeTranscript.d.ts.map +1 -1
- package/dist/plus/agents/providers/claudeCodeTranscript.js +84 -9
- package/dist/plus/agents/providers/claudeCodeTranscript.js.map +1 -1
- package/dist/plus/agents/types.d.ts +37 -2
- package/dist/plus/agents/types.d.ts.map +1 -1
- package/dist/plus/agents/types.js +2 -0
- package/dist/plus/agents/types.js.map +1 -1
- package/dist/plus/ai/errors.d.ts +10 -0
- package/dist/plus/ai/errors.d.ts.map +1 -1
- package/dist/plus/ai/errors.js +29 -0
- package/dist/plus/ai/errors.js.map +1 -1
- package/dist/plus/ai/models/model.d.ts +4 -0
- package/dist/plus/ai/models/model.d.ts.map +1 -1
- package/dist/plus/ai/models/model.js +51 -6
- package/dist/plus/ai/models/model.js.map +1 -1
- package/dist/plus/ai/models/promptTemplates.d.ts +1 -1
- package/dist/plus/ai/models/promptTemplates.d.ts.map +1 -1
- package/dist/plus/ai/models/provider.d.ts +60 -2
- package/dist/plus/ai/models/provider.d.ts.map +1 -1
- package/dist/plus/ai/prompts.d.ts +12 -0
- package/dist/plus/ai/prompts.d.ts.map +1 -1
- package/dist/plus/ai/prompts.js +177 -61
- package/dist/plus/ai/prompts.js.map +1 -1
- package/dist/plus/ai/providers/anthropicProvider.d.ts +10 -2
- package/dist/plus/ai/providers/anthropicProvider.d.ts.map +1 -1
- package/dist/plus/ai/providers/anthropicProvider.js +139 -46
- package/dist/plus/ai/providers/anthropicProvider.js.map +1 -1
- package/dist/plus/ai/providers/deepSeekProvider.d.ts +2 -0
- package/dist/plus/ai/providers/deepSeekProvider.d.ts.map +1 -1
- package/dist/plus/ai/providers/deepSeekProvider.js +10 -21
- package/dist/plus/ai/providers/deepSeekProvider.js.map +1 -1
- package/dist/plus/ai/providers/geminiProvider.d.ts +1 -0
- package/dist/plus/ai/providers/geminiProvider.d.ts.map +1 -1
- package/dist/plus/ai/providers/geminiProvider.js +19 -0
- package/dist/plus/ai/providers/geminiProvider.js.map +1 -1
- package/dist/plus/ai/providers/gitkrakenProvider.d.ts +5 -1
- package/dist/plus/ai/providers/gitkrakenProvider.d.ts.map +1 -1
- package/dist/plus/ai/providers/gitkrakenProvider.js +118 -3
- package/dist/plus/ai/providers/gitkrakenProvider.js.map +1 -1
- package/dist/plus/ai/providers/huggingFaceProvider.d.ts +1 -0
- package/dist/plus/ai/providers/huggingFaceProvider.d.ts.map +1 -1
- package/dist/plus/ai/providers/huggingFaceProvider.js +5 -0
- package/dist/plus/ai/providers/huggingFaceProvider.js.map +1 -1
- package/dist/plus/ai/providers/mistralProvider.d.ts +3 -1
- package/dist/plus/ai/providers/mistralProvider.d.ts.map +1 -1
- package/dist/plus/ai/providers/mistralProvider.js +99 -19
- package/dist/plus/ai/providers/mistralProvider.js.map +1 -1
- package/dist/plus/ai/providers/ollamaProvider.d.ts +4 -3
- package/dist/plus/ai/providers/ollamaProvider.d.ts.map +1 -1
- package/dist/plus/ai/providers/ollamaProvider.js +38 -3
- package/dist/plus/ai/providers/ollamaProvider.js.map +1 -1
- package/dist/plus/ai/providers/openAICompatibleProvider.d.ts +1 -0
- package/dist/plus/ai/providers/openAICompatibleProvider.d.ts.map +1 -1
- package/dist/plus/ai/providers/openAICompatibleProvider.js +4 -0
- package/dist/plus/ai/providers/openAICompatibleProvider.js.map +1 -1
- package/dist/plus/ai/providers/openAICompatibleProviderBase.d.ts +154 -5
- package/dist/plus/ai/providers/openAICompatibleProviderBase.d.ts.map +1 -1
- package/dist/plus/ai/providers/openAICompatibleProviderBase.js +259 -10
- package/dist/plus/ai/providers/openAICompatibleProviderBase.js.map +1 -1
- package/dist/plus/ai/providers/openRouterProvider.d.ts +4 -0
- package/dist/plus/ai/providers/openRouterProvider.d.ts.map +1 -1
- package/dist/plus/ai/providers/openRouterProvider.js +9 -0
- package/dist/plus/ai/providers/openRouterProvider.js.map +1 -1
- package/dist/plus/ai/providers/openaiProvider.d.ts +1 -0
- package/dist/plus/ai/providers/openaiProvider.d.ts.map +1 -1
- package/dist/plus/ai/providers/openaiProvider.js +1 -0
- package/dist/plus/ai/providers/openaiProvider.js.map +1 -1
- package/dist/plus/ai/providers/responseFormatCache.d.ts +8 -0
- package/dist/plus/ai/providers/responseFormatCache.d.ts.map +1 -0
- package/dist/plus/ai/providers/responseFormatCache.js +25 -0
- package/dist/plus/ai/providers/responseFormatCache.js.map +1 -0
- package/dist/plus/ai/providers/xaiProvider.d.ts +1 -0
- package/dist/plus/ai/providers/xaiProvider.d.ts.map +1 -1
- package/dist/plus/ai/providers/xaiProvider.js +10 -1
- package/dist/plus/ai/providers/xaiProvider.js.map +1 -1
- package/dist/plus/ai/utils/ai.utils.d.ts +3 -0
- package/dist/plus/ai/utils/ai.utils.d.ts.map +1 -1
- package/dist/plus/ai/utils/ai.utils.js +13 -0
- package/dist/plus/ai/utils/ai.utils.js.map +1 -1
- package/dist/plus/ai/utils/results.utils.d.ts +16 -2
- package/dist/plus/ai/utils/results.utils.d.ts.map +1 -1
- package/dist/plus/ai/utils/results.utils.js +204 -28
- package/dist/plus/ai/utils/results.utils.js.map +1 -1
- package/dist/plus/git-github/api/github.d.ts +55 -0
- package/dist/plus/git-github/api/github.d.ts.map +1 -1
- package/dist/plus/git-github/api/github.js +177 -4
- package/dist/plus/git-github/api/github.js.map +1 -1
- package/dist/plus/git-github/models.d.ts +16 -0
- package/dist/plus/git-github/models.d.ts.map +1 -1
- package/dist/plus/git-github/models.js +29 -3
- package/dist/plus/git-github/models.js.map +1 -1
- package/dist/plus/git-github/providers/github/status.d.ts +1 -0
- package/dist/plus/git-github/providers/github/status.d.ts.map +1 -1
- package/dist/plus/git-github/providers/github/status.js.map +1 -1
- package/dist/plus/integrations/context.d.ts +4 -0
- package/dist/plus/integrations/context.d.ts.map +1 -1
- package/dist/plus/integrations/index.d.ts.map +1 -1
- package/dist/plus/integrations/index.js +2 -0
- package/dist/plus/integrations/index.js.map +1 -1
- package/dist/plus/integrations/models/gitHostIntegration.d.ts +6 -3
- package/dist/plus/integrations/models/gitHostIntegration.d.ts.map +1 -1
- package/dist/plus/integrations/models/gitHostIntegration.js +12 -2
- package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
- package/dist/plus/integrations/models/integration.d.ts +3 -1
- package/dist/plus/integrations/models/integration.d.ts.map +1 -1
- package/dist/plus/integrations/models/integration.js +2 -2
- package/dist/plus/integrations/models/integration.js.map +1 -1
- package/dist/plus/integrations/providers/accounts.d.ts.map +1 -1
- package/dist/plus/integrations/providers/accounts.js +4 -0
- package/dist/plus/integrations/providers/accounts.js.map +1 -1
- package/dist/plus/integrations/providers/azure/models.d.ts.map +1 -1
- package/dist/plus/integrations/providers/azure/models.js +3 -0
- package/dist/plus/integrations/providers/azure/models.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket/models.d.ts.map +1 -1
- package/dist/plus/integrations/providers/bitbucket/models.js +1 -0
- package/dist/plus/integrations/providers/bitbucket/models.js.map +1 -1
- package/dist/plus/integrations/providers/github.d.ts +14 -2
- package/dist/plus/integrations/providers/github.d.ts.map +1 -1
- package/dist/plus/integrations/providers/github.js +55 -3
- package/dist/plus/integrations/providers/github.js.map +1 -1
- package/dist/plus/integrations/providers/gitlab.d.ts.map +1 -1
- package/dist/plus/integrations/providers/gitlab.js.map +1 -1
- package/dist/plus/integrations/providers/models.d.ts.map +1 -1
- package/dist/plus/integrations/providers/models.js +30 -4
- package/dist/plus/integrations/providers/models.js.map +1 -1
- package/dist/plus/integrations/reads/broaden.d.ts.map +1 -1
- package/dist/plus/integrations/reads/broaden.js +6 -1
- package/dist/plus/integrations/reads/broaden.js.map +1 -1
- package/dist/plus/integrations/reads/drains.d.ts.map +1 -1
- package/dist/plus/integrations/reads/drains.js +7 -4
- package/dist/plus/integrations/reads/drains.js.map +1 -1
- package/dist/plus/integrations/reads/hierarchy.d.ts.map +1 -1
- package/dist/plus/integrations/reads/hierarchy.js +3 -1
- package/dist/plus/integrations/reads/hierarchy.js.map +1 -1
- package/dist/plus/integrations/reads/issueTracker.d.ts.map +1 -1
- package/dist/plus/integrations/reads/issueTracker.js +8 -3
- package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
- package/dist/plus/integrations/reads/issues.d.ts.map +1 -1
- package/dist/plus/integrations/reads/issues.js +9 -4
- package/dist/plus/integrations/reads/issues.js.map +1 -1
- package/dist/plus/integrations/reads/pullRequests.d.ts.map +1 -1
- package/dist/plus/integrations/reads/pullRequests.js +9 -4
- package/dist/plus/integrations/reads/pullRequests.js.map +1 -1
- package/dist/utils/env/browser/base64.d.ts +1 -1
- package/dist/utils/env/browser/base64.d.ts.map +1 -1
- package/dist/utils/env/node/base64.d.ts +1 -1
- package/dist/utils/env/node/base64.d.ts.map +1 -1
- package/dist/utils/env/node/base64.js +2 -0
- package/dist/utils/env/node/base64.js.map +1 -1
- package/dist/utils/keys/chord.d.ts +52 -0
- package/dist/utils/keys/chord.d.ts.map +1 -0
- package/dist/utils/keys/chord.js +135 -0
- package/dist/utils/keys/chord.js.map +1 -0
- package/dist/utils/keys/keybinding.d.ts +53 -0
- package/dist/utils/keys/keybinding.d.ts.map +1 -0
- package/dist/utils/keys/keybinding.js +58 -0
- package/dist/utils/keys/keybinding.js.map +1 -0
- package/dist/utils/path.d.ts +11 -0
- package/dist/utils/path.d.ts.map +1 -1
- package/dist/utils/path.js +13 -0
- package/dist/utils/path.js.map +1 -1
- package/package.json +13 -5
- package/src/git/cache.ts +383 -47
- package/src/git/context.ts +11 -0
- package/src/git/models/fileStatus.ts +1 -0
- package/src/git/models/issue.ts +4 -0
- package/src/git/models/mergeConflicts.ts +3 -2
- package/src/git/models/pullRequest.ts +33 -1
- package/src/git/models/repository.ts +58 -17
- package/src/git/models/statusFile.ts +3 -0
- package/src/git/providers/branches.ts +6 -0
- package/src/git/providers/commits.ts +11 -0
- package/src/git/providers/config.ts +18 -0
- package/src/git/providers/operations.ts +1 -0
- package/src/git/providers/pausedOperations.ts +2 -0
- package/src/git/providers/status.ts +1 -0
- package/src/git/utils/config.utils.ts +8 -0
- package/src/git/utils/issue.utils.ts +2 -0
- package/src/git/utils/pausedOperationStatus.utils.ts +28 -0
- package/src/git/utils/pullRequest.utils.ts +36 -0
- package/src/git/watching/changeEvent.ts +6 -1
- package/src/git/watching/classifyChange.ts +15 -1
- package/src/git/watching/watchService.ts +20 -3
- package/src/git/watching/watchSession.ts +41 -6
- package/src/git/watching/watcherPatterns.ts +1 -1
- package/src/git-cli/exec/git.ts +13 -3
- package/src/git-cli/providers/branches.ts +384 -132
- package/src/git-cli/providers/commits.ts +106 -15
- package/src/git-cli/providers/config.ts +229 -8
- package/src/git-cli/providers/contributors.ts +5 -1
- package/src/git-cli/providers/graph.ts +26 -13
- package/src/git-cli/providers/operations.ts +45 -20
- package/src/git-cli/providers/pausedOperations.ts +109 -17
- package/src/git-cli/providers/refs.ts +33 -3
- package/src/git-cli/providers/status.ts +25 -4
- package/src/git-cli/providers/worktrees.ts +5 -0
- package/src/git-cli/service.ts +0 -1
- package/src/plus/agents/providers/claudeCodeProvider.ts +1130 -87
- package/src/plus/agents/providers/claudeCodeTranscript.ts +95 -9
- package/src/plus/agents/types.ts +46 -2
- package/src/plus/ai/errors.ts +30 -0
- package/src/plus/ai/models/model.ts +56 -6
- package/src/plus/ai/models/promptTemplates.ts +1 -1
- package/src/plus/ai/models/provider.ts +69 -2
- package/src/plus/ai/prompts.ts +186 -61
- package/src/plus/ai/providers/anthropicProvider.ts +155 -47
- package/src/plus/ai/providers/deepSeekProvider.ts +11 -21
- package/src/plus/ai/providers/geminiProvider.ts +19 -0
- package/src/plus/ai/providers/gitkrakenProvider.ts +126 -3
- package/src/plus/ai/providers/huggingFaceProvider.ts +6 -0
- package/src/plus/ai/providers/mistralProvider.ts +102 -19
- package/src/plus/ai/providers/ollamaProvider.ts +55 -4
- package/src/plus/ai/providers/openAICompatibleProvider.ts +5 -0
- package/src/plus/ai/providers/openAICompatibleProviderBase.ts +360 -17
- package/src/plus/ai/providers/openRouterProvider.ts +17 -0
- package/src/plus/ai/providers/openaiProvider.ts +1 -0
- package/src/plus/ai/providers/responseFormatCache.ts +39 -0
- package/src/plus/ai/providers/xaiProvider.ts +10 -1
- package/src/plus/ai/utils/ai.utils.ts +14 -0
- package/src/plus/ai/utils/results.utils.ts +255 -31
- package/src/plus/git-github/api/github.ts +291 -7
- package/src/plus/git-github/models.ts +48 -2
- package/src/plus/git-github/providers/github/status.ts +1 -0
- package/src/plus/integrations/context.ts +4 -0
- package/src/plus/integrations/index.ts +2 -0
- package/src/plus/integrations/models/gitHostIntegration.ts +25 -2
- package/src/plus/integrations/models/integration.ts +23 -13
- package/src/plus/integrations/providers/accounts.ts +4 -0
- package/src/plus/integrations/providers/azure/models.ts +3 -0
- package/src/plus/integrations/providers/bitbucket/models.ts +1 -0
- package/src/plus/integrations/providers/github.ts +85 -1
- package/src/plus/integrations/providers/gitlab.ts +2 -10
- package/src/plus/integrations/providers/models.ts +36 -4
- package/src/plus/integrations/reads/broaden.ts +6 -1
- package/src/plus/integrations/reads/drains.ts +7 -4
- package/src/plus/integrations/reads/hierarchy.ts +3 -1
- package/src/plus/integrations/reads/issueTracker.ts +9 -3
- package/src/plus/integrations/reads/issues.ts +9 -4
- package/src/plus/integrations/reads/pullRequests.ts +9 -4
- package/src/utils/env/browser/base64.ts +1 -1
- package/src/utils/env/node/base64.ts +3 -1
- package/src/utils/keys/chord.ts +184 -0
- package/src/utils/keys/keybinding.ts +116 -0
- package/src/utils/path.ts +14 -0
|
@@ -1,13 +1,30 @@
|
|
|
1
1
|
import { CancellationError, isCancellationError } from '../../../utils/cancellation.js';
|
|
2
2
|
import { uuid } from '../../../utils/crypto.js';
|
|
3
|
-
import { getLoggableName } from '../../../utils/logger.js';
|
|
3
|
+
import { getLoggableName, Logger } from '../../../utils/logger.js';
|
|
4
4
|
import { maybeStartScopedLogger } from '../../../utils/logger.scoped.js';
|
|
5
5
|
import type { AIProviders } from '../constants.js';
|
|
6
6
|
import { AIError, AIErrorReason } from '../errors.js';
|
|
7
7
|
import type { AIActionType, AIModel, AIProviderDescriptor } from '../models/model.js';
|
|
8
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
AIChatMessage,
|
|
10
|
+
AIChatMessageRole,
|
|
11
|
+
AIFinishReason,
|
|
12
|
+
AIProvider,
|
|
13
|
+
AIProviderResponse,
|
|
14
|
+
AIResponseFormat,
|
|
15
|
+
AIToolCall,
|
|
16
|
+
AIToolDefinition,
|
|
17
|
+
JSONSchema,
|
|
18
|
+
} from '../models/provider.js';
|
|
9
19
|
import { getActionName, getReducedMaxInputTokens, getValidatedTemperature } from '../utils/ai.utils.js';
|
|
10
20
|
import type { AIProviderContext } from './context.js';
|
|
21
|
+
import { isResponseFormatRejected, rememberResponseFormatRejection } from './responseFormatCache.js';
|
|
22
|
+
|
|
23
|
+
// Native-format rejection wording varies by provider ("response_format", "structured outputs",
|
|
24
|
+
// Gemini's "Json mode is not enabled", and its OpenAI-compat layer's `generation_config.response_schema`
|
|
25
|
+
// path in INVALID_ARGUMENT bodies, ...)
|
|
26
|
+
const formatRejectionRegex =
|
|
27
|
+
/response[_ ]?(?:format|schema)|structured[_ ]?output|output_config|json_schema|json[_ ]?mode/i;
|
|
11
28
|
|
|
12
29
|
export abstract class OpenAICompatibleProviderBase<T extends AIProviders> implements AIProvider<T> {
|
|
13
30
|
constructor(protected readonly context: AIProviderContext) {}
|
|
@@ -54,6 +71,14 @@ export abstract class OpenAICompatibleProviderBase<T extends AIProviders> implem
|
|
|
54
71
|
|
|
55
72
|
abstract getModels(): Promise<readonly AIModel<T>[]>;
|
|
56
73
|
|
|
74
|
+
protected isResponseFormatSessionRejected(model: AIModel<T>, responseFormat: AIResponseFormat): boolean {
|
|
75
|
+
return isResponseFormatRejected(this.id, model, responseFormat);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
protected rememberResponseFormatRejection(model: AIModel<T>, responseFormat: AIResponseFormat): void {
|
|
79
|
+
rememberResponseFormatRejection(this.id, model, responseFormat);
|
|
80
|
+
}
|
|
81
|
+
|
|
57
82
|
protected abstract getUrl(_model: AIModel<T>): string | undefined;
|
|
58
83
|
|
|
59
84
|
protected getHeaders<TAction extends AIActionType>(
|
|
@@ -76,11 +101,13 @@ export abstract class OpenAICompatibleProviderBase<T extends AIProviders> implem
|
|
|
76
101
|
action: TAction,
|
|
77
102
|
model: AIModel<T>,
|
|
78
103
|
apiKey: string,
|
|
79
|
-
getMessages: (maxInputTokens: number, retries: number) => Promise<AIChatMessage[]>,
|
|
104
|
+
getMessages: (maxInputTokens: number, retries: number) => Promise<AIChatMessage<AIChatMessageRole>[]>,
|
|
80
105
|
options: {
|
|
81
106
|
signal: AbortSignal;
|
|
82
107
|
modelOptions?: { outputTokens?: number; temperature?: number };
|
|
83
108
|
conversationId?: string;
|
|
109
|
+
responseFormat?: AIResponseFormat;
|
|
110
|
+
tools?: readonly AIToolDefinition[];
|
|
84
111
|
},
|
|
85
112
|
): Promise<AIProviderResponse<void> | undefined> {
|
|
86
113
|
using scope = maybeStartScopedLogger(`${getLoggableName(this)}.sendRequest`);
|
|
@@ -92,8 +119,10 @@ export abstract class OpenAICompatibleProviderBase<T extends AIProviders> implem
|
|
|
92
119
|
apiKey,
|
|
93
120
|
getMessages,
|
|
94
121
|
options.modelOptions,
|
|
122
|
+
options.responseFormat,
|
|
95
123
|
options.signal,
|
|
96
124
|
options.conversationId,
|
|
125
|
+
options.tools,
|
|
97
126
|
);
|
|
98
127
|
return result;
|
|
99
128
|
} catch (ex) {
|
|
@@ -116,18 +145,27 @@ export abstract class OpenAICompatibleProviderBase<T extends AIProviders> implem
|
|
|
116
145
|
action: TAction,
|
|
117
146
|
model: AIModel<T>,
|
|
118
147
|
apiKey: string,
|
|
119
|
-
messages: (maxInputTokens: number, retries: number) => Promise<AIChatMessage[]>,
|
|
148
|
+
messages: (maxInputTokens: number, retries: number) => Promise<AIChatMessage<AIChatMessageRole>[]>,
|
|
120
149
|
modelOptions?: { outputTokens?: number; temperature?: number },
|
|
150
|
+
responseFormat?: AIResponseFormat,
|
|
121
151
|
signal?: AbortSignal,
|
|
122
152
|
conversationId?: string,
|
|
153
|
+
tools?: readonly AIToolDefinition[],
|
|
123
154
|
): Promise<AIProviderResponse<void>> {
|
|
124
155
|
let retries = 0;
|
|
125
156
|
let maxInputTokens = model.maxTokens.input;
|
|
157
|
+
// Latched when the provider rejects the `tools` field, so the retry drops tools entirely and
|
|
158
|
+
// the caller can stop offering them for the rest of the session.
|
|
159
|
+
let toolsRejected = false;
|
|
126
160
|
|
|
127
161
|
while (true) {
|
|
162
|
+
const useTools = tools?.length && !toolsRejected ? tools : undefined;
|
|
163
|
+
const { messages: msgs, system } = this.extractSystemPrompt(await messages(maxInputTokens, retries));
|
|
128
164
|
const request: ChatCompletionRequest = {
|
|
129
165
|
model: model.id,
|
|
130
|
-
|
|
166
|
+
messages: this.serializeMessages(msgs),
|
|
167
|
+
...(system != null ? { system: system } : undefined),
|
|
168
|
+
...(useTools != null ? this.serializeTools(useTools) : undefined),
|
|
131
169
|
stream: false,
|
|
132
170
|
max_completion_tokens: model.maxTokens.output
|
|
133
171
|
? Math.min(modelOptions?.outputTokens ?? Infinity, model.maxTokens.output)
|
|
@@ -138,22 +176,98 @@ export abstract class OpenAICompatibleProviderBase<T extends AIProviders> implem
|
|
|
138
176
|
this.context.defaultTemperature,
|
|
139
177
|
),
|
|
140
178
|
};
|
|
179
|
+
if (
|
|
180
|
+
responseFormat != null &&
|
|
181
|
+
!this.isResponseFormatSessionRejected(model, responseFormat) &&
|
|
182
|
+
this.supportsResponseFormat(model)
|
|
183
|
+
) {
|
|
184
|
+
this.applyResponseFormat(request, model, responseFormat);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let rsp = await this.fetchCore(action, model, apiKey, request, signal, conversationId);
|
|
188
|
+
// Read once and forward to `handleFetchFailure` so the error body isn't decoded twice
|
|
189
|
+
let errorBody: string | undefined;
|
|
190
|
+
if (!rsp.ok && (request.response_format != null || request.output_config != null)) {
|
|
191
|
+
// If the native format was rejected (stale model snapshot, older Azure api-version,
|
|
192
|
+
// proxy converter gaps, OpenRouter's require_parameters matching no endpoint), strip
|
|
193
|
+
// it and resend the same request — the prompt still describes the JSON. Resending
|
|
194
|
+
// directly avoids re-running the (non-idempotent) prompt build and its side effects
|
|
195
|
+
// Leave it `undefined` (not '') when the read fails, so `handleFetchFailure` falls
|
|
196
|
+
// back to reading the still-unconsumed response instead of parsing an empty string
|
|
197
|
+
errorBody = await rsp
|
|
198
|
+
.clone()
|
|
199
|
+
.text()
|
|
200
|
+
.catch(() => undefined);
|
|
201
|
+
if (this.isResponseFormatRejection(rsp.status, errorBody ?? '')) {
|
|
202
|
+
Logger.warn(
|
|
203
|
+
`(${this.name}) ${model.id}: native response format was rejected (${rsp.status}); retrying without it`,
|
|
204
|
+
);
|
|
205
|
+
const rejected = responseFormat;
|
|
206
|
+
responseFormat = undefined;
|
|
207
|
+
delete request.response_format;
|
|
208
|
+
delete request.output_config;
|
|
209
|
+
delete request.provider;
|
|
210
|
+
rsp = await this.fetchCore(action, model, apiKey, request, signal, conversationId);
|
|
211
|
+
errorBody = undefined;
|
|
212
|
+
// Memoize only when stripping fixed it — proof the format (not e.g. an oversized
|
|
213
|
+
// prompt behind the same wrapped status) caused the failure
|
|
214
|
+
if (rsp.ok && rejected != null) {
|
|
215
|
+
this.rememberResponseFormatRejection(model, rejected);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
141
219
|
|
|
142
|
-
const rsp = await this.fetchCore(action, model, apiKey, request, signal, conversationId);
|
|
143
220
|
if (!rsp.ok) {
|
|
144
|
-
const result = await this.handleFetchFailure(
|
|
221
|
+
const result = await this.handleFetchFailure(
|
|
222
|
+
rsp,
|
|
223
|
+
action,
|
|
224
|
+
model,
|
|
225
|
+
retries,
|
|
226
|
+
maxInputTokens,
|
|
227
|
+
errorBody,
|
|
228
|
+
useTools != null,
|
|
229
|
+
);
|
|
145
230
|
if (result.retry) {
|
|
146
231
|
maxInputTokens = result.maxInputTokens;
|
|
232
|
+
if (result.withoutTools) {
|
|
233
|
+
// Say so explicitly: the caller degrades silently by design, so the only remaining
|
|
234
|
+
// evidence would be `toolCalls=0` — indistinguishable from a model that simply chose
|
|
235
|
+
// not to consult the repository. That ambiguity is the whole reason to log it.
|
|
236
|
+
Logger.warn(
|
|
237
|
+
`(${this.name}) ${model.id}: tools were rejected (${rsp.status}); retrying without them — repository consultation is unavailable for the rest of this session`,
|
|
238
|
+
);
|
|
239
|
+
toolsRejected = true;
|
|
240
|
+
}
|
|
147
241
|
retries++;
|
|
148
242
|
continue;
|
|
149
243
|
}
|
|
150
244
|
}
|
|
151
245
|
|
|
152
246
|
const data: ChatCompletionResponse = (await rsp.json()) as ChatCompletionResponse;
|
|
247
|
+
const toolCalls = this.parseToolCalls(data);
|
|
248
|
+
// A tool-call-only turn carries no text, which `extractContent` correctly reports as '' — it
|
|
249
|
+
// selects the text block by type, so a `tool_use` (or thinking) block is never mistaken for one.
|
|
250
|
+
// The calls ride alongside on `toolCalls` below.
|
|
251
|
+
const content = this.extractContent(data);
|
|
252
|
+
// Some providers/proxies (e.g. OpenRouter) report upstream failures as an OK response
|
|
253
|
+
// carrying an error envelope and no choices — surface that instead of empty content
|
|
254
|
+
if (!content && toolCalls == null && data.error != null) {
|
|
255
|
+
if (data.error.code === 429 || data.error.code === '429') {
|
|
256
|
+
throw new AIError(
|
|
257
|
+
AIErrorReason.RateLimitOrFundsExceeded,
|
|
258
|
+
new Error(`(${this.name}) ${data.error.message || 'Rate limit exceeded'}`),
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
throw new Error(`(${this.name}) ${data.error.message || 'Unknown error'}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
153
264
|
const result: AIProviderResponse<void> = {
|
|
154
265
|
id: data.id ?? uuid(),
|
|
155
|
-
content:
|
|
266
|
+
content: content,
|
|
267
|
+
finishReason: this.extractFinishReason(data),
|
|
156
268
|
model: model,
|
|
269
|
+
...(toolCalls?.length ? { toolCalls: toolCalls } : undefined),
|
|
270
|
+
...(toolsRejected ? { toolsRejected: true } : undefined),
|
|
157
271
|
usage: {
|
|
158
272
|
promptTokens: data.usage?.prompt_tokens ?? data.usage?.input_tokens,
|
|
159
273
|
completionTokens: data.usage?.completion_tokens ?? data.usage?.output_tokens,
|
|
@@ -185,13 +299,196 @@ export abstract class OpenAICompatibleProviderBase<T extends AIProviders> implem
|
|
|
185
299
|
return { messages: messages };
|
|
186
300
|
}
|
|
187
301
|
|
|
302
|
+
/** Whether the given model accepts a native response-format/schema field; when false the
|
|
303
|
+
* prompt's JSON description is the only format control (the request is unchanged).
|
|
304
|
+
* Dynamic-catalog providers (GitKraken, OpenRouter) populate
|
|
305
|
+
* {@link AIModel.supportsStructuredOutputs}; static providers override with id-based gates */
|
|
306
|
+
protected supportsResponseFormat(model: AIModel<T>): boolean {
|
|
307
|
+
return model.supportsStructuredOutputs ?? true;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Whether a failed response reads as a rejection of the `tools` field rather than a problem with the
|
|
312
|
+
* prompt — the trigger for one retry without tools, so a tool-using feature degrades to single-shot
|
|
313
|
+
* instead of failing outright.
|
|
314
|
+
*
|
|
315
|
+
* Providers that wrap upstream rejections in their own envelope (e.g. GitKraken) must override this:
|
|
316
|
+
* the wrapper's prose names neither "tool" nor "function", so the text match here cannot see through
|
|
317
|
+
* it and the fallback would never fire.
|
|
318
|
+
*/
|
|
319
|
+
protected isToolsRejection(status: number, message: string | undefined): boolean {
|
|
320
|
+
if (status !== 400 || !message) return false;
|
|
321
|
+
|
|
322
|
+
const m = message.toLowerCase();
|
|
323
|
+
return (
|
|
324
|
+
(m.includes('tool') || m.includes('function')) &&
|
|
325
|
+
(m.includes('unsupported') ||
|
|
326
|
+
m.includes('not supported') ||
|
|
327
|
+
m.includes('unknown') ||
|
|
328
|
+
m.includes('unrecognized') ||
|
|
329
|
+
m.includes('invalid') ||
|
|
330
|
+
m.includes('unexpected'))
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** Whether a failed response means the native response format itself was rejected — the
|
|
335
|
+
* trigger for stripping it and retrying via the prompt-described JSON. Providers that wrap
|
|
336
|
+
* upstream rejections in their own envelope (e.g. GitKraken) extend this */
|
|
337
|
+
protected isResponseFormatRejection(status: number, body: string): boolean {
|
|
338
|
+
return (
|
|
339
|
+
(status === 400 && formatRejectionRegex.test(body)) ||
|
|
340
|
+
// OpenRouter answers 404 when `require_parameters` matches no serving endpoint
|
|
341
|
+
(status === 404 && /no endpoints found/i.test(body))
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Translates the canonical response format onto the request; called only when
|
|
346
|
+
* {@link supportsResponseFormat} allows it. Base emits the OpenAI `response_format.json_schema`
|
|
347
|
+
* envelope. Overrides must REPLACE this for providers with a different native field
|
|
348
|
+
* (e.g. Anthropic `output_config`) — never leave both on the request. */
|
|
349
|
+
protected applyResponseFormat(
|
|
350
|
+
request: ChatCompletionRequest,
|
|
351
|
+
_model: AIModel<T>,
|
|
352
|
+
responseFormat: AIResponseFormat,
|
|
353
|
+
): void {
|
|
354
|
+
request.response_format = {
|
|
355
|
+
type: 'json_schema',
|
|
356
|
+
json_schema: {
|
|
357
|
+
name: responseFormat.name,
|
|
358
|
+
strict: true,
|
|
359
|
+
schema: responseFormat.schema,
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Finds the text block by type rather than position — with some Anthropic models (e.g. Sonnet 5,
|
|
365
|
+
// where thinking runs by default) a thinking block can precede the text block
|
|
366
|
+
protected extractContent(data: ChatCompletionResponse): string {
|
|
367
|
+
return (
|
|
368
|
+
data.choices?.[0]?.message?.content?.trim() ??
|
|
369
|
+
data.content?.find(b => b.type === 'text')?.text?.trim() ??
|
|
370
|
+
''
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** Normalizes abnormal completion signals (all arrive as HTTP 200) so consumers can
|
|
375
|
+
* distinguish truncated/filtered/refused output from a malformed-but-complete reply */
|
|
376
|
+
protected extractFinishReason(data: ChatCompletionResponse): AIFinishReason | undefined {
|
|
377
|
+
const choice = data.choices?.[0];
|
|
378
|
+
if (choice != null) {
|
|
379
|
+
// Truthy check — some OpenAI-compatible servers serialize `"refusal": ""` on normal replies
|
|
380
|
+
if (choice.message?.refusal) return 'refusal';
|
|
381
|
+
if (choice.finish_reason === 'length') return 'length';
|
|
382
|
+
if (choice.finish_reason === 'content_filter') return 'content_filter';
|
|
383
|
+
return undefined;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
switch (data.stop_reason) {
|
|
387
|
+
// `model_context_window_exceeded` means the context filled before the output limit — the
|
|
388
|
+
// reply is truncated all the same
|
|
389
|
+
case 'max_tokens':
|
|
390
|
+
case 'model_context_window_exceeded':
|
|
391
|
+
return 'length';
|
|
392
|
+
case 'refusal':
|
|
393
|
+
return 'refusal';
|
|
394
|
+
default:
|
|
395
|
+
return undefined;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Maps messages onto the provider's wire shape. The default is OpenAI Chat Completions: assistant
|
|
401
|
+
* tool calls go in `tool_calls` with `arguments` JSON-stringified, and tool results are `tool`-role
|
|
402
|
+
* entries keyed by `tool_call_id`. Providers with a different tool encoding (e.g. Anthropic's
|
|
403
|
+
* content blocks) override this.
|
|
404
|
+
*/
|
|
405
|
+
protected serializeMessages(messages: AIChatMessage<AIChatMessageRole>[]): unknown[] {
|
|
406
|
+
return messages.map(m => {
|
|
407
|
+
if (m.role === 'tool') {
|
|
408
|
+
// A tool result is only addressable by the id of the call it answers. Without one, send it
|
|
409
|
+
// as plain text — the same shape used for providers that can't carry tool calls at all —
|
|
410
|
+
// rather than a `tool_call_id: undefined` the provider rejects with nothing to trace it to.
|
|
411
|
+
if (m.toolCallId == null) return { role: 'user', content: m.content };
|
|
412
|
+
|
|
413
|
+
return { role: 'tool', tool_call_id: m.toolCallId, content: m.content };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (m.role === 'assistant' && m.toolCalls?.length) {
|
|
417
|
+
return {
|
|
418
|
+
role: 'assistant',
|
|
419
|
+
// OpenAI requires `content` to be present, but allows null on a tool-call-only turn
|
|
420
|
+
content: m.content || null,
|
|
421
|
+
tool_calls: m.toolCalls.map(c => ({
|
|
422
|
+
id: c.id,
|
|
423
|
+
type: 'function',
|
|
424
|
+
function: { name: c.name, arguments: JSON.stringify(c.args) },
|
|
425
|
+
// Only the GitKraken proxy mints these, so the field can only ever be present on a
|
|
426
|
+
// conversation with that proxy — no other provider sees it.
|
|
427
|
+
...(c.providerSignature != null ? { thought_signature: c.providerSignature } : undefined),
|
|
428
|
+
})),
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return { role: m.role, content: m.content };
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** Returns the tool-related request fields. The default is the OpenAI `function` envelope. */
|
|
437
|
+
protected serializeTools(tools: readonly AIToolDefinition[]): { tools: unknown[] } {
|
|
438
|
+
return {
|
|
439
|
+
tools: tools.map(t => ({
|
|
440
|
+
type: 'function',
|
|
441
|
+
function: { name: t.name, description: t.description, parameters: t.parameters },
|
|
442
|
+
})),
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Extracts tool calls from a response, handling both the OpenAI shape
|
|
448
|
+
* (`choices[].message.tool_calls`, whose `arguments` is a JSON string) and the Anthropic shape
|
|
449
|
+
* (`content[]` blocks of type `tool_use`) — mirroring how {@link fetch} already reads content
|
|
450
|
+
* from both.
|
|
451
|
+
*/
|
|
452
|
+
protected parseToolCalls(data: ChatCompletionResponse): AIToolCall[] | undefined {
|
|
453
|
+
const calls: AIToolCall[] = [];
|
|
454
|
+
|
|
455
|
+
for (const call of data.choices?.[0]?.message?.tool_calls ?? []) {
|
|
456
|
+
let args: Record<string, unknown> = {};
|
|
457
|
+
try {
|
|
458
|
+
// Providers can emit '' for a no-argument call
|
|
459
|
+
args = call.function.arguments ? (JSON.parse(call.function.arguments) as Record<string, unknown>) : {};
|
|
460
|
+
} catch {
|
|
461
|
+
// Leave args empty — the tool dispatcher reports the resulting validation error back to
|
|
462
|
+
// the model, which is more recoverable than failing the whole request here.
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
calls.push({
|
|
466
|
+
id: call.id,
|
|
467
|
+
name: call.function.name,
|
|
468
|
+
args: args,
|
|
469
|
+
...(call.thought_signature != null ? { providerSignature: call.thought_signature } : undefined),
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
for (const block of data.content ?? []) {
|
|
474
|
+
if (block.type !== 'tool_use' || block.id == null || block.name == null) continue;
|
|
475
|
+
|
|
476
|
+
calls.push({ id: block.id, name: block.name, args: block.input ?? {} });
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return calls.length ? calls : undefined;
|
|
480
|
+
}
|
|
481
|
+
|
|
188
482
|
protected async handleFetchFailure<TAction extends AIActionType>(
|
|
189
483
|
rsp: Response,
|
|
190
484
|
_action: TAction,
|
|
191
485
|
model: AIModel<T>,
|
|
192
486
|
retries: number,
|
|
193
487
|
maxInputTokens: number,
|
|
194
|
-
|
|
488
|
+
/** The already-read error body, when the caller had to read it (avoids a second decode) */
|
|
489
|
+
body?: string,
|
|
490
|
+
sentTools?: boolean,
|
|
491
|
+
): Promise<{ retry: true; maxInputTokens: number; withoutTools?: boolean }> {
|
|
195
492
|
if (rsp.status === 404) {
|
|
196
493
|
throw new AIError(
|
|
197
494
|
AIErrorReason.Unauthorized,
|
|
@@ -209,7 +506,9 @@ export abstract class OpenAICompatibleProviderBase<T extends AIProviders> implem
|
|
|
209
506
|
|
|
210
507
|
let json;
|
|
211
508
|
try {
|
|
212
|
-
json = (await rsp.json()) as
|
|
509
|
+
json = (body != null ? JSON.parse(body) : await rsp.json()) as
|
|
510
|
+
| { error?: { code: string; message: string } }
|
|
511
|
+
| undefined;
|
|
213
512
|
} catch {}
|
|
214
513
|
|
|
215
514
|
if (Array.isArray(json)) {
|
|
@@ -226,6 +525,13 @@ export abstract class OpenAICompatibleProviderBase<T extends AIProviders> implem
|
|
|
226
525
|
);
|
|
227
526
|
}
|
|
228
527
|
|
|
528
|
+
// A provider that advertises the OpenAI shape but rejects `tools` (an older or partial
|
|
529
|
+
// implementation) gets one retry without them, so the caller degrades to text-only instead of
|
|
530
|
+
// failing outright.
|
|
531
|
+
if (sentTools && this.isToolsRejection(rsp.status, json?.error?.message)) {
|
|
532
|
+
return { retry: true, maxInputTokens: maxInputTokens, withoutTools: true };
|
|
533
|
+
}
|
|
534
|
+
|
|
229
535
|
throw new Error(`(${this.name}) ${rsp.status}: ${json?.error?.message || rsp.statusText}`);
|
|
230
536
|
}
|
|
231
537
|
|
|
@@ -242,11 +548,23 @@ export abstract class OpenAICompatibleProviderBase<T extends AIProviders> implem
|
|
|
242
548
|
throw new Error(`(${this.name}) ${getActionName(action)}: No URL configured`);
|
|
243
549
|
}
|
|
244
550
|
|
|
551
|
+
const body = JSON.stringify(request);
|
|
552
|
+
|
|
553
|
+
// A rejected request is otherwise undiagnosable: a proxying backend reports only its own
|
|
554
|
+
// "upstream AI provider error (<provider> HTTP <status>)", so which field the upstream objected to
|
|
555
|
+
// can only be inferred. Log the request's shape — which fields were sent, and how much — but not
|
|
556
|
+
// the messages, which carry the user's source code.
|
|
557
|
+
Logger.trace(
|
|
558
|
+
`(${this.name}) ${getActionName(action)} → ${model.id}: fields=[${Object.keys(request)
|
|
559
|
+
.sort()
|
|
560
|
+
.join(',')}] bytes=${body.length}`,
|
|
561
|
+
);
|
|
562
|
+
|
|
245
563
|
try {
|
|
246
564
|
return await this.context.fetch(url, {
|
|
247
565
|
headers: await this.getHeaders(action, apiKey, model, url, conversationId),
|
|
248
566
|
method: 'POST',
|
|
249
|
-
body:
|
|
567
|
+
body: body,
|
|
250
568
|
signal: signal,
|
|
251
569
|
});
|
|
252
570
|
} catch (ex) {
|
|
@@ -256,13 +574,17 @@ export abstract class OpenAICompatibleProviderBase<T extends AIProviders> implem
|
|
|
256
574
|
}
|
|
257
575
|
}
|
|
258
576
|
|
|
259
|
-
interface ChatCompletionRequest {
|
|
577
|
+
export interface ChatCompletionRequest {
|
|
260
578
|
model: string;
|
|
261
|
-
messages
|
|
579
|
+
/** Provider-shaped messages from {@link OpenAICompatibleProviderBase.serializeMessages} */
|
|
580
|
+
messages: unknown[];
|
|
262
581
|
|
|
263
582
|
/** Anthropic carries the initial system prompt here instead of as a `system`-role message */
|
|
264
583
|
system?: string;
|
|
265
584
|
|
|
585
|
+
/** Tool definitions, shaped by {@link OpenAICompatibleProviderBase.serializeTools} */
|
|
586
|
+
tools?: unknown[];
|
|
587
|
+
|
|
266
588
|
/** @deprecated but used by Anthropic & Gemini */
|
|
267
589
|
max_tokens?: number;
|
|
268
590
|
/** Currently can't be used for Anthropic & Gemini */
|
|
@@ -274,9 +596,16 @@ interface ChatCompletionRequest {
|
|
|
274
596
|
|
|
275
597
|
/** Not supported by many models/providers */
|
|
276
598
|
reasoning_effort?: 'low' | 'medium' | 'high';
|
|
599
|
+
|
|
600
|
+
/** OpenAI-compatible structured output (set via {@link OpenAICompatibleProviderBase.applyResponseFormat}) */
|
|
601
|
+
response_format?: { type: 'json_schema'; json_schema: { name: string; strict: boolean; schema: JSONSchema } };
|
|
602
|
+
/** Anthropic-native structured output (set only by the Anthropic override) */
|
|
603
|
+
output_config?: { format: { type: 'json_schema'; schema: JSONSchema } };
|
|
604
|
+
/** OpenRouter-only routing constraint (set only by the OpenRouter override) */
|
|
605
|
+
provider?: { require_parameters: boolean };
|
|
277
606
|
}
|
|
278
607
|
|
|
279
|
-
interface ChatCompletionResponse {
|
|
608
|
+
export interface ChatCompletionResponse {
|
|
280
609
|
id: string;
|
|
281
610
|
model: string;
|
|
282
611
|
/** OpenAI compatible output */
|
|
@@ -285,12 +614,26 @@ interface ChatCompletionResponse {
|
|
|
285
614
|
message: {
|
|
286
615
|
role: string;
|
|
287
616
|
content: string | null;
|
|
288
|
-
|
|
617
|
+
/** OpenAI-only; most OpenAI-compatible servers omit it entirely */
|
|
618
|
+
refusal?: string | null;
|
|
619
|
+
tool_calls?: {
|
|
620
|
+
id: string;
|
|
621
|
+
type: string;
|
|
622
|
+
function: { name: string; arguments: string };
|
|
623
|
+
/** GitKraken proxy only — an opaque provider reasoning token to round-trip */
|
|
624
|
+
thought_signature?: string;
|
|
625
|
+
}[];
|
|
289
626
|
};
|
|
290
627
|
finish_reason: string;
|
|
291
628
|
}[];
|
|
292
|
-
/** Anthropic output
|
|
293
|
-
|
|
629
|
+
/** Anthropic output (non-text blocks, e.g. thinking, carry no text). `tool_use` blocks add the
|
|
630
|
+
* call fields, which is why `id`/`name` are optional here — {@link parseToolCalls} narrows on
|
|
631
|
+
* `type` and then checks them. */
|
|
632
|
+
content?: { type: string; text?: string; id?: string; name?: string; input?: Record<string, unknown> }[];
|
|
633
|
+
/** Error envelope some providers/proxies return with an OK status (e.g. OpenRouter upstream failures) */
|
|
634
|
+
error?: { message?: string; code?: number | string } | null;
|
|
635
|
+
/** Anthropic stop reason */
|
|
636
|
+
stop_reason?: string;
|
|
294
637
|
usage: {
|
|
295
638
|
/** OpenAI compatible */
|
|
296
639
|
prompt_tokens?: number;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { isCancellationError } from '../../../utils/cancellation.js';
|
|
2
2
|
import { openRouterProviderDescriptor as provider } from '../constants.js';
|
|
3
3
|
import type { AIActionType, AIModel } from '../models/model.js';
|
|
4
|
+
import type { AIResponseFormat } from '../models/provider.js';
|
|
5
|
+
import type { ChatCompletionRequest } from './openAICompatibleProviderBase.js';
|
|
4
6
|
import { OpenAICompatibleProviderBase } from './openAICompatibleProviderBase.js';
|
|
5
7
|
|
|
6
8
|
type OpenRouterModel = AIModel<typeof provider.id>;
|
|
@@ -8,6 +10,7 @@ type OpenRouterModel = AIModel<typeof provider.id>;
|
|
|
8
10
|
export class OpenRouterProvider extends OpenAICompatibleProviderBase<typeof provider.id> {
|
|
9
11
|
readonly id = provider.id;
|
|
10
12
|
readonly name = provider.name;
|
|
13
|
+
readonly supportsTools = true;
|
|
11
14
|
protected readonly descriptor = provider;
|
|
12
15
|
protected readonly config = {
|
|
13
16
|
keyUrl: 'https://openrouter.ai/keys',
|
|
@@ -40,6 +43,7 @@ export class OpenRouterProvider extends OpenAICompatibleProviderBase<typeof prov
|
|
|
40
43
|
top_provider: {
|
|
41
44
|
max_completion_tokens?: number;
|
|
42
45
|
};
|
|
46
|
+
supported_parameters?: string[];
|
|
43
47
|
}[];
|
|
44
48
|
};
|
|
45
49
|
|
|
@@ -55,10 +59,23 @@ export class OpenRouterProvider extends OpenAICompatibleProviderBase<typeof prov
|
|
|
55
59
|
},
|
|
56
60
|
provider: provider,
|
|
57
61
|
temperature: null,
|
|
62
|
+
// OpenRouter hard-errors when `response_format` reaches a model that doesn't advertise support
|
|
63
|
+
supportsStructuredOutputs: m.supported_parameters?.includes('structured_outputs') ?? false,
|
|
58
64
|
}) satisfies OpenRouterModel,
|
|
59
65
|
);
|
|
60
66
|
}
|
|
61
67
|
|
|
68
|
+
protected override applyResponseFormat(
|
|
69
|
+
request: ChatCompletionRequest,
|
|
70
|
+
model: AIModel<typeof provider.id>,
|
|
71
|
+
responseFormat: AIResponseFormat,
|
|
72
|
+
): void {
|
|
73
|
+
super.applyResponseFormat(request, model, responseFormat);
|
|
74
|
+
// `supported_parameters` is a union across the providers serving a model — restrict routing
|
|
75
|
+
// to providers that actually support every parameter sent
|
|
76
|
+
request.provider = { require_parameters: true };
|
|
77
|
+
}
|
|
78
|
+
|
|
62
79
|
protected getUrl(_model: AIModel<typeof provider.id>): string {
|
|
63
80
|
return 'https://openrouter.ai/api/v1/chat/completions';
|
|
64
81
|
}
|
|
@@ -10,6 +10,7 @@ const models: OpenAIModel[] = openAIModels(provider);
|
|
|
10
10
|
export class OpenAIProvider extends OpenAICompatibleProviderBase<typeof provider.id> {
|
|
11
11
|
readonly id = provider.id;
|
|
12
12
|
readonly name = provider.name;
|
|
13
|
+
readonly supportsTools = true;
|
|
13
14
|
protected readonly descriptor = provider;
|
|
14
15
|
protected readonly config = {
|
|
15
16
|
keyUrl: 'https://platform.openai.com/account/api-keys',
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { AIModel } from '../models/model.js';
|
|
2
|
+
import type { AIResponseFormat } from '../models/provider.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `providerId:modelId:schemaName` keys whose native format was rejected. Module-scoped because the
|
|
6
|
+
* service constructs a fresh provider instance per request, so instance state wouldn't survive to
|
|
7
|
+
* skip the doomed probe. Keyed per schema: e.g. GitKraken-Gemini rejects the null-union review
|
|
8
|
+
* schemas but happily enforces the commits schema on the same model.
|
|
9
|
+
*
|
|
10
|
+
* Lives in its own (dependency-free) module so the AI service can clear it without statically
|
|
11
|
+
* importing a provider implementation — every provider is loaded lazily into the `ai` chunk.
|
|
12
|
+
*/
|
|
13
|
+
const rejectedResponseFormats = new Set<string>();
|
|
14
|
+
|
|
15
|
+
function getKey(providerId: string, model: AIModel, responseFormat: AIResponseFormat): string {
|
|
16
|
+
return `${providerId}:${model.id}:${responseFormat.name}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isResponseFormatRejected(
|
|
20
|
+
providerId: string,
|
|
21
|
+
model: AIModel,
|
|
22
|
+
responseFormat: AIResponseFormat,
|
|
23
|
+
): boolean {
|
|
24
|
+
return rejectedResponseFormats.has(getKey(providerId, model, responseFormat));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function rememberResponseFormatRejection(
|
|
28
|
+
providerId: string,
|
|
29
|
+
model: AIModel,
|
|
30
|
+
responseFormat: AIResponseFormat,
|
|
31
|
+
): void {
|
|
32
|
+
rejectedResponseFormats.add(getKey(providerId, model, responseFormat));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Drops the remembered rejections; shares the provider model-list cache's lifetime, so a
|
|
36
|
+
* misclassified rejection can't outlive the capability data it was inferred from */
|
|
37
|
+
export function clearResponseFormatRejections(): void {
|
|
38
|
+
rejectedResponseFormats.clear();
|
|
39
|
+
}
|
|
@@ -4,6 +4,12 @@ import { OpenAICompatibleProviderBase } from './openAICompatibleProviderBase.js'
|
|
|
4
4
|
|
|
5
5
|
type XAIModel = AIModel<typeof provider.id>;
|
|
6
6
|
const models: XAIModel[] = [
|
|
7
|
+
{
|
|
8
|
+
id: 'grok-4.6',
|
|
9
|
+
name: 'Grok 4.6',
|
|
10
|
+
maxTokens: { input: 500000, output: 65536 }, // xAI publishes no output cap; mirrors Grok 4.5 to bound cost
|
|
11
|
+
provider: provider,
|
|
12
|
+
},
|
|
7
13
|
{
|
|
8
14
|
id: 'grok-4.5',
|
|
9
15
|
name: 'Grok 4.5',
|
|
@@ -13,7 +19,7 @@ const models: XAIModel[] = [
|
|
|
13
19
|
{
|
|
14
20
|
id: 'grok-4.3',
|
|
15
21
|
name: 'Grok 4.3',
|
|
16
|
-
maxTokens: { input:
|
|
22
|
+
maxTokens: { input: 1000000, output: 65536 },
|
|
17
23
|
provider: provider,
|
|
18
24
|
default: true,
|
|
19
25
|
},
|
|
@@ -23,12 +29,15 @@ const models: XAIModel[] = [
|
|
|
23
29
|
maxTokens: { input: 131072, output: 4096 },
|
|
24
30
|
provider: provider,
|
|
25
31
|
hidden: true,
|
|
32
|
+
// Structured outputs require grok-2-1212 or newer; grok-beta predates that floor
|
|
33
|
+
supportsStructuredOutputs: false,
|
|
26
34
|
},
|
|
27
35
|
];
|
|
28
36
|
|
|
29
37
|
export class XAIProvider extends OpenAICompatibleProviderBase<typeof provider.id> {
|
|
30
38
|
readonly id = provider.id;
|
|
31
39
|
readonly name = provider.name;
|
|
40
|
+
readonly supportsTools = true;
|
|
32
41
|
protected readonly descriptor = provider;
|
|
33
42
|
protected readonly config = {
|
|
34
43
|
keyUrl: 'https://console.x.ai/',
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { AIActionType, AIModel } from '../models/model.js';
|
|
2
|
+
import type { AIFinishReason } from '../models/provider.js';
|
|
3
|
+
|
|
4
|
+
/** Whether the model declined to fulfill the request (refused or content-filtered) */
|
|
5
|
+
export function didModelDecline(finishReason: AIFinishReason | undefined): boolean {
|
|
6
|
+
return finishReason === 'refusal' || finishReason === 'content_filter';
|
|
7
|
+
}
|
|
2
8
|
|
|
3
9
|
export function getActionName(action: AIActionType): string {
|
|
4
10
|
switch (action) {
|
|
@@ -32,6 +38,14 @@ export function getValidatedTemperature(
|
|
|
32
38
|
modelTemperature?: number | null,
|
|
33
39
|
defaultTemperature?: number,
|
|
34
40
|
): number | undefined {
|
|
41
|
+
// `temperature: null` on the model is a capability statement, not a default: the model rejects the
|
|
42
|
+
// parameter (reasoning models, Anthropic Opus 4.7+/Sonnet 5, Gemini 3.x) or the catalog can't
|
|
43
|
+
// promise it accepts one (every GitKraken-proxy and OpenRouter model). It therefore has to win over
|
|
44
|
+
// a caller-supplied value — callers merge as `modelOptions?.temperature ?? model.temperature`, and
|
|
45
|
+
// `0 ?? null` is `0`, so an explicit 0 would otherwise defeat the opt-out and be sent anyway.
|
|
46
|
+
// It also covers upstreams the id check below can't see: a proxy model is `openai:gpt-5.6-…`, not
|
|
47
|
+
// `gpt-5.6-…`, so `startsWith` misses it while the upstream still 400s on a non-default value.
|
|
48
|
+
if (model.temperature === null) return undefined;
|
|
35
49
|
if (modelTemperature === null) return undefined;
|
|
36
50
|
// GPT5 doesn't support anything but the default temperature
|
|
37
51
|
if (model.id.startsWith('gpt-5')) return undefined;
|