bonecode 1.0.0 → 1.1.0
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/LICENSE +21 -0
- package/README.md +64 -50
- package/bone/output/agent/.dockerignore +7 -7
- package/bone/output/agent/.env.example +36 -36
- package/bone/output/agent/.github/workflows/ci.yaml +58 -58
- package/bone/output/agent/AgentDomain.bone.map +349 -349
- package/bone/output/agent/AgentDomain.postman_collection.json +957 -957
- package/bone/output/agent/Dockerfile +22 -22
- package/bone/output/agent/README.md +47 -47
- package/bone/output/agent/admin/index.html +739 -739
- package/bone/output/agent/docker-compose.yaml +22 -22
- package/bone/output/agent/k8s/deployment.yaml +75 -75
- package/bone/output/agent/migrations/agent.sql +36 -36
- package/bone/output/agent/migrations/agent_instance.sql +36 -36
- package/bone/output/agent/migrations/audit_log.sql +18 -18
- package/bone/output/agent/migrations/build_step.sql +34 -34
- package/bone/output/agent/migrations/event_outbox.sql +31 -31
- package/bone/output/agent/migrations/plan.sql +30 -30
- package/bone/output/agent/migrations/task.sql +30 -30
- package/bone/output/agent/migrations/tool_call.sql +33 -33
- package/bone/output/agent/openapi.yaml +1116 -1116
- package/bone/output/agent/package.json +35 -35
- package/bone/output/agent/schema.graphql +233 -233
- package/bone/output/agent/sdk/client.ts +231 -231
- package/bone/output/agent/src/algorithms.ts +2 -2
- package/bone/output/agent/src/audit.ts +44 -44
- package/bone/output/agent/src/auth.ts +57 -57
- package/bone/output/agent/src/cron.ts +12 -12
- package/bone/output/agent/src/db.ts +31 -31
- package/bone/output/agent/src/debug.ts +66 -66
- package/bone/output/agent/src/events.ts +243 -243
- package/bone/output/agent/src/extensions.ts +54 -54
- package/bone/output/agent/src/failure_rules.ts +322 -322
- package/bone/output/agent/src/flows.ts +168 -168
- package/bone/output/agent/src/health.ts +43 -43
- package/bone/output/agent/src/index.ts +99 -99
- package/bone/output/agent/src/logger.ts +69 -66
- package/bone/output/agent/src/metrics.ts +75 -75
- package/bone/output/agent/src/migrate.ts +351 -351
- package/bone/output/agent/src/migration_diff.ts +108 -108
- package/bone/output/agent/src/notify.ts +125 -125
- package/bone/output/agent/src/routes/plan.ts +91 -91
- package/bone/output/agent/src/routes/task.ts +105 -105
- package/bone/output/agent/src/routes/tool_call.ts +166 -166
- package/bone/output/agent/src/schemas.ts +384 -384
- package/bone/output/agent/src/state_machines/agent_instance.ts +24 -24
- package/bone/output/agent/src/state_machines/build_step.ts +22 -22
- package/bone/output/agent/src/state_machines/plan.ts +22 -22
- package/bone/output/agent/src/state_machines/task.ts +22 -22
- package/bone/output/agent/src/state_machines/tool_call.ts +22 -22
- package/bone/output/agent/src/tests.ts +361 -361
- package/bone/output/agent/src/websocket.ts +200 -200
- package/bone/output/agent/tsconfig.json +24 -24
- package/bone/output/rag/.dockerignore +7 -7
- package/bone/output/rag/.env.example +36 -36
- package/bone/output/rag/.github/workflows/ci.yaml +58 -58
- package/bone/output/rag/Dockerfile +22 -22
- package/bone/output/rag/RAGDomain.bone.map +286 -286
- package/bone/output/rag/RAGDomain.postman_collection.json +922 -922
- package/bone/output/rag/README.md +47 -47
- package/bone/output/rag/admin/index.html +817 -817
- package/bone/output/rag/docker-compose.yaml +22 -22
- package/bone/output/rag/k8s/deployment.yaml +75 -75
- package/bone/output/rag/migrations/audit_log.sql +18 -18
- package/bone/output/rag/migrations/code_chunk.sql +34 -34
- package/bone/output/rag/migrations/code_file.sql +33 -33
- package/bone/output/rag/migrations/event_outbox.sql +31 -31
- package/bone/output/rag/migrations/indexing_job.sql +33 -33
- package/bone/output/rag/migrations/knowledge_base.sql +35 -35
- package/bone/output/rag/migrations/memory_entry.sql +34 -34
- package/bone/output/rag/openapi.yaml +1097 -1097
- package/bone/output/rag/package.json +35 -35
- package/bone/output/rag/schema.graphql +245 -245
- package/bone/output/rag/sdk/client.ts +234 -234
- package/bone/output/rag/src/algorithms.ts +2 -2
- package/bone/output/rag/src/audit.ts +37 -37
- package/bone/output/rag/src/auth.ts +57 -57
- package/bone/output/rag/src/cron.ts +12 -12
- package/bone/output/rag/src/db.ts +31 -31
- package/bone/output/rag/src/debug.ts +66 -66
- package/bone/output/rag/src/events.ts +243 -243
- package/bone/output/rag/src/extensions.ts +350 -350
- package/bone/output/rag/src/failure_rules.ts +314 -314
- package/bone/output/rag/src/flows.ts +239 -239
- package/bone/output/rag/src/health.ts +43 -43
- package/bone/output/rag/src/index.ts +94 -94
- package/bone/output/rag/src/logger.ts +69 -66
- package/bone/output/rag/src/metrics.ts +75 -75
- package/bone/output/rag/src/migrate.ts +363 -363
- package/bone/output/rag/src/migration_diff.ts +108 -108
- package/bone/output/rag/src/notify.ts +99 -99
- package/bone/output/rag/src/routes/code_chunk.ts +75 -75
- package/bone/output/rag/src/routes/code_file.ts +101 -101
- package/bone/output/rag/src/routes/indexing_job.ts +87 -87
- package/bone/output/rag/src/routes/knowledge_base.ts +230 -230
- package/bone/output/rag/src/routes/memory_entry.ts +87 -87
- package/bone/output/rag/src/schemas.ts +394 -394
- package/bone/output/rag/src/state_machines/code_file.ts +23 -23
- package/bone/output/rag/src/state_machines/indexing_job.ts +22 -22
- package/bone/output/rag/src/state_machines/knowledge_base.ts +23 -23
- package/bone/output/rag/src/state_machines/memory_entry.ts +20 -20
- package/bone/output/rag/src/tests.ts +339 -339
- package/bone/output/rag/tsconfig.json +24 -24
- package/bone/output/session/.dockerignore +7 -7
- package/bone/output/session/.env.example +36 -36
- package/bone/output/session/.github/workflows/ci.yaml +58 -58
- package/bone/output/session/Dockerfile +22 -22
- package/bone/output/session/README.md +47 -47
- package/bone/output/session/SessionDomain.bone.map +349 -349
- package/bone/output/session/SessionDomain.postman_collection.json +957 -957
- package/bone/output/session/admin/index.html +666 -666
- package/bone/output/session/docker-compose.yaml +22 -22
- package/bone/output/session/k8s/deployment.yaml +75 -75
- package/bone/output/session/migrations/audit_log.sql +18 -18
- package/bone/output/session/migrations/event_outbox.sql +31 -31
- package/bone/output/session/migrations/message.sql +31 -31
- package/bone/output/session/migrations/part.sql +28 -28
- package/bone/output/session/migrations/permission.sql +28 -28
- package/bone/output/session/migrations/project.sql +28 -28
- package/bone/output/session/migrations/session.sql +38 -38
- package/bone/output/session/openapi.yaml +1101 -1101
- package/bone/output/session/package.json +35 -35
- package/bone/output/session/schema.graphql +222 -222
- package/bone/output/session/sdk/client.ts +225 -225
- package/bone/output/session/src/algorithms.ts +2 -2
- package/bone/output/session/src/audit.ts +44 -44
- package/bone/output/session/src/auth.ts +57 -57
- package/bone/output/session/src/cron.ts +12 -12
- package/bone/output/session/src/db.ts +31 -31
- package/bone/output/session/src/debug.ts +66 -66
- package/bone/output/session/src/events.ts +270 -270
- package/bone/output/session/src/extensions.ts +215 -215
- package/bone/output/session/src/failure_rules.ts +283 -283
- package/bone/output/session/src/flows.ts +168 -168
- package/bone/output/session/src/health.ts +43 -43
- package/bone/output/session/src/index.ts +99 -99
- package/bone/output/session/src/logger.ts +67 -66
- package/bone/output/session/src/metrics.ts +75 -75
- package/bone/output/session/src/migrate.ts +331 -331
- package/bone/output/session/src/migration_diff.ts +108 -108
- package/bone/output/session/src/notify.ts +112 -112
- package/bone/output/session/src/routes/message.ts +93 -93
- package/bone/output/session/src/routes/part.ts +79 -79
- package/bone/output/session/src/routes/permission.ts +79 -79
- package/bone/output/session/src/routes/project.ts +79 -79
- package/bone/output/session/src/routes/session.ts +294 -294
- package/bone/output/session/src/schemas.ts +357 -357
- package/bone/output/session/src/state_machines/session.ts +23 -23
- package/bone/output/session/src/tests.ts +325 -325
- package/bone/output/session/src/websocket.ts +223 -200
- package/bone/output/session/tsconfig.json +24 -24
- package/bone/output/workspace/.dockerignore +7 -7
- package/bone/output/workspace/.env.example +36 -36
- package/bone/output/workspace/.github/workflows/ci.yaml +58 -58
- package/bone/output/workspace/Dockerfile +22 -22
- package/bone/output/workspace/README.md +45 -45
- package/bone/output/workspace/WorkspaceDomain.bone.map +188 -188
- package/bone/output/workspace/WorkspaceDomain.postman_collection.json +620 -620
- package/bone/output/workspace/admin/index.html +484 -484
- package/bone/output/workspace/docker-compose.yaml +22 -22
- package/bone/output/workspace/k8s/deployment.yaml +75 -75
- package/bone/output/workspace/migrations/audit_log.sql +18 -18
- package/bone/output/workspace/migrations/codebase.sql +34 -34
- package/bone/output/workspace/migrations/event_outbox.sql +31 -31
- package/bone/output/workspace/migrations/snapshot.sql +32 -32
- package/bone/output/workspace/migrations/workspace.sql +33 -33
- package/bone/output/workspace/openapi.yaml +721 -721
- package/bone/output/workspace/package.json +35 -35
- package/bone/output/workspace/schema.graphql +153 -153
- package/bone/output/workspace/sdk/client.ts +155 -155
- package/bone/output/workspace/src/algorithms.ts +2 -2
- package/bone/output/workspace/src/audit.ts +37 -37
- package/bone/output/workspace/src/auth.ts +57 -57
- package/bone/output/workspace/src/cron.ts +12 -12
- package/bone/output/workspace/src/db.ts +31 -31
- package/bone/output/workspace/src/debug.ts +66 -66
- package/bone/output/workspace/src/events.ts +243 -243
- package/bone/output/workspace/src/extensions.ts +44 -44
- package/bone/output/workspace/src/failure_rules.ts +152 -152
- package/bone/output/workspace/src/health.ts +43 -43
- package/bone/output/workspace/src/index.ts +88 -88
- package/bone/output/workspace/src/logger.ts +69 -66
- package/bone/output/workspace/src/metrics.ts +75 -75
- package/bone/output/workspace/src/migrate.ts +219 -219
- package/bone/output/workspace/src/migration_diff.ts +108 -108
- package/bone/output/workspace/src/notify.ts +73 -73
- package/bone/output/workspace/src/routes/codebase.ts +87 -87
- package/bone/output/workspace/src/routes/snapshot.ts +127 -127
- package/bone/output/workspace/src/routes/workspace.ts +190 -190
- package/bone/output/workspace/src/schemas.ts +231 -231
- package/bone/output/workspace/src/state_machines/codebase.ts +21 -21
- package/bone/output/workspace/src/state_machines/snapshot.ts +20 -20
- package/bone/output/workspace/src/state_machines/workspace.ts +21 -21
- package/bone/output/workspace/src/tests.ts +248 -248
- package/bone/output/workspace/tsconfig.json +24 -24
- package/compat/opencode_adapter.ts +94 -17
- package/package.json +15 -2
- package/src/cli.ts +66 -107
- package/src/db_adapter.ts +354 -0
- package/src/engine/account/account.sql.ts +39 -39
- package/src/engine/account/account.ts +456 -456
- package/src/engine/account/repo.ts +166 -166
- package/src/engine/account/schema.ts +99 -99
- package/src/engine/account/url.ts +8 -8
- package/src/engine/acp/README.md +174 -174
- package/src/engine/acp/agent.ts +1968 -1968
- package/src/engine/acp/runtime.ts +22 -22
- package/src/engine/acp/session.ts +122 -122
- package/src/engine/acp/types.ts +24 -24
- package/src/engine/agent/agent.ts +463 -463
- package/src/engine/agent/generate.txt +75 -75
- package/src/engine/agent/prompt/compaction.txt +9 -9
- package/src/engine/agent/prompt/explore.txt +18 -18
- package/src/engine/agent/prompt/scout.txt +36 -36
- package/src/engine/agent/prompt/summary.txt +11 -11
- package/src/engine/agent/prompt/title.txt +44 -44
- package/src/engine/agent/subagent-permissions.ts +34 -34
- package/src/engine/auth/index.ts +96 -96
- package/src/engine/background/background/job.ts +200 -200
- package/src/engine/background/job.ts +200 -200
- package/src/engine/bus/bus-event.ts +45 -45
- package/src/engine/bus/global.ts +22 -22
- package/src/engine/bus/index.ts +203 -203
- package/src/engine/command/command/index.ts +181 -181
- package/src/engine/command/command/template/initialize.txt +66 -66
- package/src/engine/command/command/template/review.txt +101 -101
- package/src/engine/command/index.ts +181 -181
- package/src/engine/command/template/initialize.txt +66 -66
- package/src/engine/command/template/review.txt +101 -101
- package/src/engine/config/agent.ts +172 -172
- package/src/engine/config/attachment.ts +25 -25
- package/src/engine/config/command.ts +62 -62
- package/src/engine/config/config.ts +833 -833
- package/src/engine/config/console-state.ts +14 -14
- package/src/engine/config/entry-name.ts +16 -16
- package/src/engine/config/error.ts +23 -23
- package/src/engine/config/formatter.ts +13 -13
- package/src/engine/config/layout.ts +6 -6
- package/src/engine/config/lsp.ts +43 -43
- package/src/engine/config/managed.ts +71 -71
- package/src/engine/config/markdown.ts +96 -96
- package/src/engine/config/mcp.ts +56 -56
- package/src/engine/config/model-id.ts +5 -5
- package/src/engine/config/parse.ts +79 -79
- package/src/engine/config/paths.ts +45 -45
- package/src/engine/config/permission.ts +58 -58
- package/src/engine/config/plugin.ts +84 -84
- package/src/engine/config/provider.ts +111 -111
- package/src/engine/config/reference.ts +23 -23
- package/src/engine/config/server.ts +19 -19
- package/src/engine/config/skills.ts +14 -14
- package/src/engine/config/variable.ts +90 -90
- package/src/engine/control-plane/adapters/index.ts +41 -41
- package/src/engine/control-plane/adapters/worktree.ts +96 -96
- package/src/engine/control-plane/dev/README.md +19 -19
- package/src/engine/control-plane/dev/debug-workspace-plugin.ts +73 -73
- package/src/engine/control-plane/schema.ts +14 -14
- package/src/engine/control-plane/types.ts +59 -59
- package/src/engine/control-plane/util.ts +39 -39
- package/src/engine/control-plane/workspace-adapter-runtime.ts +51 -51
- package/src/engine/control-plane/workspace-context.ts +26 -26
- package/src/engine/control-plane/workspace.sql.ts +20 -20
- package/src/engine/control-plane/workspace.ts +1072 -1072
- package/src/engine/data-migration.ts +161 -161
- package/src/engine/effect/app-runtime.ts +143 -143
- package/src/engine/effect/bootstrap-runtime.ts +29 -29
- package/src/engine/effect/bridge.ts +84 -84
- package/src/engine/effect/config-service.ts +67 -67
- package/src/engine/effect/instance-ref.ts +11 -11
- package/src/engine/effect/instance-registry.ts +12 -12
- package/src/engine/effect/instance-state.ts +72 -72
- package/src/engine/effect/promise.ts +17 -17
- package/src/engine/effect/run-service.ts +47 -47
- package/src/engine/effect/runner.ts +217 -217
- package/src/engine/effect/runtime-flags.ts +74 -74
- package/src/engine/effect/service-use.ts +38 -38
- package/src/engine/env/index.ts +37 -37
- package/src/engine/event-v2-bridge.ts +89 -89
- package/src/engine/file/file/ignore.ts +81 -81
- package/src/engine/file/file/index.ts +651 -651
- package/src/engine/file/file/protected.ts +59 -59
- package/src/engine/file/file/ripgrep.ts +481 -481
- package/src/engine/file/file/watcher.ts +167 -167
- package/src/engine/file/ignore.ts +81 -81
- package/src/engine/file/index.ts +651 -651
- package/src/engine/file/protected.ts +59 -59
- package/src/engine/file/ripgrep.ts +481 -481
- package/src/engine/file/watcher.ts +167 -167
- package/src/engine/format/format/formatter.ts +404 -404
- package/src/engine/format/format/index.ts +209 -209
- package/src/engine/format/formatter.ts +404 -404
- package/src/engine/format/index.ts +209 -209
- package/src/engine/git/git/index.ts +347 -347
- package/src/engine/git/index.ts +347 -347
- package/src/engine/id/id.ts +80 -80
- package/src/engine/ide/index.ts +70 -70
- package/src/engine/image/image/image.ts +176 -176
- package/src/engine/image/image.ts +176 -176
- package/src/engine/index.ts +251 -251
- package/src/engine/installation/index.ts +327 -327
- package/src/engine/lsp/client.ts +707 -707
- package/src/engine/lsp/diagnostic.ts +29 -29
- package/src/engine/lsp/language.ts +121 -121
- package/src/engine/lsp/launch.ts +21 -21
- package/src/engine/lsp/lsp/client.ts +707 -707
- package/src/engine/lsp/lsp/diagnostic.ts +29 -29
- package/src/engine/lsp/lsp/language.ts +121 -121
- package/src/engine/lsp/lsp/launch.ts +21 -21
- package/src/engine/lsp/lsp/lsp.ts +507 -507
- package/src/engine/lsp/lsp/server.ts +2064 -2064
- package/src/engine/lsp/lsp.ts +507 -507
- package/src/engine/lsp/server.ts +2064 -2064
- package/src/engine/mcp/auth.ts +146 -146
- package/src/engine/mcp/index.ts +958 -958
- package/src/engine/mcp/mcp/auth.ts +146 -146
- package/src/engine/mcp/mcp/index.ts +958 -958
- package/src/engine/mcp/mcp/oauth-callback.ts +232 -232
- package/src/engine/mcp/mcp/oauth-provider.ts +214 -214
- package/src/engine/mcp/oauth-callback.ts +232 -232
- package/src/engine/mcp/oauth-provider.ts +214 -214
- package/src/engine/node.ts +6 -6
- package/src/engine/patch/index.ts +689 -689
- package/src/engine/patch/patch/index.ts +689 -689
- package/src/engine/permission/arity.ts +163 -163
- package/src/engine/permission/evaluate.ts +15 -15
- package/src/engine/permission/index.ts +306 -306
- package/src/engine/permission/permission/arity.ts +163 -163
- package/src/engine/permission/permission/evaluate.ts +15 -15
- package/src/engine/permission/permission/index.ts +306 -306
- package/src/engine/permission/permission/schema.ts +13 -13
- package/src/engine/permission/schema.ts +13 -13
- package/src/engine/plugin/azure.ts +26 -26
- package/src/engine/plugin/cloudflare.ts +76 -76
- package/src/engine/plugin/codex.ts +622 -622
- package/src/engine/plugin/digitalocean.ts +411 -411
- package/src/engine/plugin/github-copilot/copilot.ts +394 -394
- package/src/engine/plugin/github-copilot/models.ts +196 -196
- package/src/engine/plugin/index.ts +295 -295
- package/src/engine/plugin/install.ts +439 -439
- package/src/engine/plugin/loader.ts +216 -216
- package/src/engine/plugin/meta.ts +188 -188
- package/src/engine/plugin/shared.ts +323 -323
- package/src/engine/project/bootstrap-service.ts +9 -9
- package/src/engine/project/bootstrap.ts +75 -75
- package/src/engine/project/instance-context.ts +24 -24
- package/src/engine/project/instance-layer.ts +11 -11
- package/src/engine/project/instance-runtime.ts +16 -16
- package/src/engine/project/instance-store.ts +193 -193
- package/src/engine/project/project.sql.ts +17 -17
- package/src/engine/project/project.ts +537 -537
- package/src/engine/project/schema.ts +13 -13
- package/src/engine/project/vcs.ts +405 -405
- package/src/engine/provider/auth.ts +225 -225
- package/src/engine/provider/error.ts +204 -204
- package/src/engine/provider/model-status.ts +8 -8
- package/src/engine/provider/provider.ts +1843 -1843
- package/src/engine/provider/schema.ts +30 -30
- package/src/engine/provider/transform.ts +1376 -1376
- package/src/engine/pty/index.ts +365 -365
- package/src/engine/pty/input.ts +24 -24
- package/src/engine/pty/pty/index.ts +365 -365
- package/src/engine/pty/pty/input.ts +24 -24
- package/src/engine/pty/pty/pty.bun.ts +26 -26
- package/src/engine/pty/pty/pty.node.ts +27 -27
- package/src/engine/pty/pty/pty.ts +25 -25
- package/src/engine/pty/pty/schema.ts +14 -14
- package/src/engine/pty/pty/ticket.ts +68 -68
- package/src/engine/pty/pty.bun.ts +26 -26
- package/src/engine/pty/pty.node.ts +27 -27
- package/src/engine/pty/pty.ts +25 -25
- package/src/engine/pty/schema.ts +14 -14
- package/src/engine/pty/ticket.ts +68 -68
- package/src/engine/question/index.ts +213 -213
- package/src/engine/question/question/index.ts +213 -213
- package/src/engine/question/question/schema.ts +10 -10
- package/src/engine/question/schema.ts +10 -10
- package/src/engine/reference/reference/reference.ts +241 -241
- package/src/engine/reference/reference/repository-cache.ts +147 -147
- package/src/engine/reference/reference.ts +241 -241
- package/src/engine/reference/repository-cache.ts +147 -147
- package/src/engine/session/compaction.ts +651 -651
- package/src/engine/session/instruction.ts +238 -238
- package/src/engine/session/llm.ts +459 -459
- package/src/engine/session/message-error.ts +14 -14
- package/src/engine/session/message-v2.ts +1202 -1202
- package/src/engine/session/message.ts +146 -146
- package/src/engine/session/overflow.ts +32 -32
- package/src/engine/session/processor.ts +823 -823
- package/src/engine/session/prompt/anthropic.txt +105 -105
- package/src/engine/session/prompt/beast.txt +147 -147
- package/src/engine/session/prompt/build-switch.txt +5 -5
- package/src/engine/session/prompt/codex.txt +79 -79
- package/src/engine/session/prompt/copilot-gpt-5.txt +143 -143
- package/src/engine/session/prompt/default.txt +105 -105
- package/src/engine/session/prompt/gemini.txt +155 -155
- package/src/engine/session/prompt/gpt.txt +107 -107
- package/src/engine/session/prompt/kimi.txt +95 -95
- package/src/engine/session/prompt/max-steps.txt +15 -15
- package/src/engine/session/prompt/plan-reminder-anthropic.txt +67 -67
- package/src/engine/session/prompt/plan.txt +26 -26
- package/src/engine/session/prompt/trinity.txt +97 -97
- package/src/engine/session/prompt.ts +66 -9
- package/src/engine/session/retry.ts +200 -200
- package/src/engine/session/revert.ts +162 -162
- package/src/engine/session/run-state.ts +153 -153
- package/src/engine/session/schema.ts +26 -26
- package/src/engine/session/session.sql.ts +137 -137
- package/src/engine/session/session.ts +1011 -1011
- package/src/engine/session/status.ts +94 -94
- package/src/engine/session/summary.ts +164 -164
- package/src/engine/session/system.ts +84 -84
- package/src/engine/session/todo.ts +81 -81
- package/src/engine/share/session.ts +61 -61
- package/src/engine/share/share-next.ts +376 -376
- package/src/engine/share/share.sql.ts +13 -13
- package/src/engine/shell/shell/shell.ts +215 -215
- package/src/engine/shell/shell.ts +215 -215
- package/src/engine/skill/discovery.ts +116 -116
- package/src/engine/skill/index.ts +336 -336
- package/src/engine/skill/prompt/customize-opencode.md +377 -377
- package/src/engine/skill/skill/discovery.ts +116 -116
- package/src/engine/skill/skill/index.ts +336 -336
- package/src/engine/skill/skill/prompt/customize-opencode.md +377 -377
- package/src/engine/snapshot/index.ts +762 -762
- package/src/engine/snapshot/snapshot/index.ts +762 -762
- package/src/engine/sync/README.md +179 -179
- package/src/engine/sync/event.sql.ts +17 -17
- package/src/engine/sync/index.ts +410 -410
- package/src/engine/sync/schema.ts +11 -11
- package/src/engine/temporary.ts +33 -33
- package/src/engine/tool/apply_patch.ts +313 -313
- package/src/engine/tool/apply_patch.txt +33 -33
- package/src/engine/tool/edit.ts +711 -711
- package/src/engine/tool/edit.txt +10 -10
- package/src/engine/tool/external-directory.ts +49 -49
- package/src/engine/tool/glob.ts +103 -103
- package/src/engine/tool/glob.txt +6 -6
- package/src/engine/tool/grep.ts +156 -156
- package/src/engine/tool/grep.txt +8 -8
- package/src/engine/tool/invalid.ts +21 -21
- package/src/engine/tool/json-schema.ts +164 -164
- package/src/engine/tool/lsp.ts +113 -113
- package/src/engine/tool/lsp.txt +24 -24
- package/src/engine/tool/mcp-websearch.ts +96 -96
- package/src/engine/tool/plan-enter.txt +14 -14
- package/src/engine/tool/plan-exit.txt +13 -13
- package/src/engine/tool/plan.ts +78 -78
- package/src/engine/tool/question.ts +44 -44
- package/src/engine/tool/question.txt +10 -10
- package/src/engine/tool/read.ts +337 -337
- package/src/engine/tool/read.txt +14 -14
- package/src/engine/tool/registry.ts +472 -472
- package/src/engine/tool/repo_clone.ts +80 -80
- package/src/engine/tool/repo_clone.txt +5 -5
- package/src/engine/tool/repo_overview.ts +279 -279
- package/src/engine/tool/repo_overview.txt +4 -4
- package/src/engine/tool/schema.ts +14 -14
- package/src/engine/tool/shell/id.ts +19 -19
- package/src/engine/tool/shell/prompt.ts +295 -295
- package/src/engine/tool/shell/shell.txt +77 -77
- package/src/engine/tool/shell.ts +647 -647
- package/src/engine/tool/skill.ts +75 -75
- package/src/engine/tool/skill.txt +5 -5
- package/src/engine/tool/task.ts +337 -337
- package/src/engine/tool/task.txt +58 -58
- package/src/engine/tool/task_status.ts +179 -179
- package/src/engine/tool/task_status.txt +13 -13
- package/src/engine/tool/todo.ts +57 -57
- package/src/engine/tool/todowrite.txt +167 -167
- package/src/engine/tool/tool/apply_patch.ts +313 -313
- package/src/engine/tool/tool/apply_patch.txt +33 -33
- package/src/engine/tool/tool/edit.ts +711 -711
- package/src/engine/tool/tool/edit.txt +10 -10
- package/src/engine/tool/tool/external-directory.ts +49 -49
- package/src/engine/tool/tool/glob.ts +103 -103
- package/src/engine/tool/tool/glob.txt +6 -6
- package/src/engine/tool/tool/grep.ts +156 -156
- package/src/engine/tool/tool/grep.txt +8 -8
- package/src/engine/tool/tool/invalid.ts +21 -21
- package/src/engine/tool/tool/json-schema.ts +164 -164
- package/src/engine/tool/tool/lsp.ts +113 -113
- package/src/engine/tool/tool/lsp.txt +24 -24
- package/src/engine/tool/tool/mcp-websearch.ts +96 -96
- package/src/engine/tool/tool/plan-enter.txt +14 -14
- package/src/engine/tool/tool/plan-exit.txt +13 -13
- package/src/engine/tool/tool/plan.ts +78 -78
- package/src/engine/tool/tool/question.ts +44 -44
- package/src/engine/tool/tool/question.txt +10 -10
- package/src/engine/tool/tool/read.ts +337 -337
- package/src/engine/tool/tool/read.txt +14 -14
- package/src/engine/tool/tool/registry.ts +472 -472
- package/src/engine/tool/tool/repo_clone.ts +80 -80
- package/src/engine/tool/tool/repo_clone.txt +5 -5
- package/src/engine/tool/tool/repo_overview.ts +279 -279
- package/src/engine/tool/tool/repo_overview.txt +4 -4
- package/src/engine/tool/tool/schema.ts +14 -14
- package/src/engine/tool/tool/shell/id.ts +19 -19
- package/src/engine/tool/tool/shell/prompt.ts +295 -295
- package/src/engine/tool/tool/shell/shell.txt +77 -77
- package/src/engine/tool/tool/shell.ts +647 -647
- package/src/engine/tool/tool/skill.ts +75 -75
- package/src/engine/tool/tool/skill.txt +5 -5
- package/src/engine/tool/tool/task.ts +337 -337
- package/src/engine/tool/tool/task.txt +58 -58
- package/src/engine/tool/tool/task_status.ts +179 -179
- package/src/engine/tool/tool/task_status.txt +13 -13
- package/src/engine/tool/tool/todo.ts +57 -57
- package/src/engine/tool/tool/todowrite.txt +167 -167
- package/src/engine/tool/tool/tool.ts +164 -164
- package/src/engine/tool/tool/truncate.ts +160 -160
- package/src/engine/tool/tool/truncation-dir.ts +4 -4
- package/src/engine/tool/tool/webfetch.ts +192 -192
- package/src/engine/tool/tool/webfetch.txt +13 -13
- package/src/engine/tool/tool/websearch.ts +143 -143
- package/src/engine/tool/tool/websearch.txt +14 -14
- package/src/engine/tool/tool/write.ts +104 -104
- package/src/engine/tool/tool/write.txt +8 -8
- package/src/engine/tool/tool.ts +164 -164
- package/src/engine/tool/truncate.ts +160 -160
- package/src/engine/tool/truncation-dir.ts +4 -4
- package/src/engine/tool/webfetch.ts +192 -192
- package/src/engine/tool/webfetch.txt +13 -13
- package/src/engine/tool/websearch.ts +143 -143
- package/src/engine/tool/websearch.txt +14 -14
- package/src/engine/tool/write.ts +104 -104
- package/src/engine/tool/write.txt +8 -8
- package/src/engine/util/archive.ts +17 -17
- package/src/engine/util/bom.ts +31 -31
- package/src/engine/util/data-url.ts +9 -9
- package/src/engine/util/defer.ts +10 -10
- package/src/engine/util/effect-http-client.ts +11 -11
- package/src/engine/util/error.ts +88 -88
- package/src/engine/util/filesystem.ts +252 -252
- package/src/engine/util/format.ts +20 -20
- package/src/engine/util/iife.ts +3 -3
- package/src/engine/util/lazy.ts +20 -20
- package/src/engine/util/local-context.ts +25 -25
- package/src/engine/util/locale.ts +86 -86
- package/src/engine/util/media.ts +26 -26
- package/src/engine/util/process.ts +176 -176
- package/src/engine/util/queue.ts +32 -32
- package/src/engine/util/record.ts +3 -3
- package/src/engine/util/repository.ts +158 -158
- package/src/engine/util/rpc.ts +66 -66
- package/src/engine/util/signal.ts +12 -12
- package/src/engine/util/timeout.ts +13 -13
- package/src/engine/util/token.ts +7 -7
- package/src/engine/util/util/archive.ts +17 -17
- package/src/engine/util/util/bom.ts +31 -31
- package/src/engine/util/util/data-url.ts +9 -9
- package/src/engine/util/util/defer.ts +10 -10
- package/src/engine/util/util/effect-http-client.ts +11 -11
- package/src/engine/util/util/error.ts +88 -88
- package/src/engine/util/util/filesystem.ts +252 -252
- package/src/engine/util/util/format.ts +20 -20
- package/src/engine/util/util/iife.ts +3 -3
- package/src/engine/util/util/lazy.ts +20 -20
- package/src/engine/util/util/local-context.ts +25 -25
- package/src/engine/util/util/locale.ts +86 -86
- package/src/engine/util/util/media.ts +26 -26
- package/src/engine/util/util/process.ts +176 -176
- package/src/engine/util/util/queue.ts +32 -32
- package/src/engine/util/util/record.ts +3 -3
- package/src/engine/util/util/repository.ts +158 -158
- package/src/engine/util/util/rpc.ts +66 -66
- package/src/engine/util/util/signal.ts +12 -12
- package/src/engine/util/util/timeout.ts +13 -13
- package/src/engine/util/util/token.ts +7 -7
- package/src/engine/util/util/which.ts +14 -14
- package/src/engine/util/util/wildcard.ts +59 -59
- package/src/engine/util/which.ts +14 -14
- package/src/engine/util/wildcard.ts +59 -59
- package/src/engine/worktree/index.ts +621 -621
- package/src/server.ts +121 -158
- package/src/tui.ts +485 -502
|
@@ -1,1376 +1,1376 @@
|
|
|
1
|
-
import type { ModelMessage, ToolResultPart } from "ai"
|
|
2
|
-
import { mergeDeep, unique } from "remeda"
|
|
3
|
-
import type { JSONSchema7 } from "@ai-sdk/provider"
|
|
4
|
-
import type * as Provider from "./provider"
|
|
5
|
-
import type * as ModelsDev from "@opencode-ai/core/models"
|
|
6
|
-
import { iife } from "@/util/iife"
|
|
7
|
-
|
|
8
|
-
type Modality = NonNullable<ModelsDev.Model["modalities"]>["input"][number]
|
|
9
|
-
|
|
10
|
-
function mimeToModality(mime: string): Modality | undefined {
|
|
11
|
-
if (mime.startsWith("image/")) return "image"
|
|
12
|
-
if (mime.startsWith("audio/")) return "audio"
|
|
13
|
-
if (mime.startsWith("video/")) return "video"
|
|
14
|
-
if (mime === "application/pdf") return "pdf"
|
|
15
|
-
return undefined
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export const OUTPUT_TOKEN_MAX = 32_000
|
|
19
|
-
|
|
20
|
-
export function sanitizeSurrogates(content: string) {
|
|
21
|
-
return content.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "\uFFFD")
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Maps npm package to the key the AI SDK expects for providerOptions
|
|
25
|
-
function sdkKey(npm: string): string | undefined {
|
|
26
|
-
switch (npm) {
|
|
27
|
-
case "@ai-sdk/github-copilot":
|
|
28
|
-
return "copilot"
|
|
29
|
-
case "@ai-sdk/azure":
|
|
30
|
-
return "azure"
|
|
31
|
-
case "@ai-sdk/openai":
|
|
32
|
-
return "openai"
|
|
33
|
-
case "@ai-sdk/amazon-bedrock":
|
|
34
|
-
return "bedrock"
|
|
35
|
-
case "@ai-sdk/anthropic":
|
|
36
|
-
case "@ai-sdk/google-vertex/anthropic":
|
|
37
|
-
return "anthropic"
|
|
38
|
-
case "@ai-sdk/google-vertex":
|
|
39
|
-
return "vertex"
|
|
40
|
-
case "@ai-sdk/google":
|
|
41
|
-
return "google"
|
|
42
|
-
case "@ai-sdk/gateway":
|
|
43
|
-
return "gateway"
|
|
44
|
-
case "@openrouter/ai-sdk-provider":
|
|
45
|
-
return "openrouter"
|
|
46
|
-
case "ai-gateway-provider":
|
|
47
|
-
// ai-gateway-provider/unified wraps createOpenAICompatible({ name: "Unified" }),
|
|
48
|
-
// and @ai-sdk/openai-compatible parses compatibleOptions from one of
|
|
49
|
-
// "openai-compatible" / "openaiCompatible" / "Unified" / "unified". The
|
|
50
|
-
// "openai-compatible" key emits a deprecation warning at runtime, so we
|
|
51
|
-
// pick the camelCase form the SDK now treats as canonical.
|
|
52
|
-
return "openaiCompatible"
|
|
53
|
-
}
|
|
54
|
-
return undefined
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// TODO: fix this stupid inefficient dogshit function
|
|
58
|
-
function normalizeMessages(
|
|
59
|
-
msgs: ModelMessage[],
|
|
60
|
-
model: Provider.Model,
|
|
61
|
-
_options: Record<string, unknown>,
|
|
62
|
-
): ModelMessage[] {
|
|
63
|
-
const sanitizeToolResultOutput = (content: ToolResultPart) => {
|
|
64
|
-
if (content.output.type === "text" || content.output.type === "error-text") {
|
|
65
|
-
content.output.value = sanitizeSurrogates(content.output.value)
|
|
66
|
-
}
|
|
67
|
-
if (content.output.type === "content") {
|
|
68
|
-
content.output.value = content.output.value.map((item) => {
|
|
69
|
-
if (item.type === "text") {
|
|
70
|
-
item.text = sanitizeSurrogates(item.text)
|
|
71
|
-
}
|
|
72
|
-
return item
|
|
73
|
-
})
|
|
74
|
-
}
|
|
75
|
-
return content
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
msgs = msgs.map((msg) => {
|
|
79
|
-
switch (msg.role) {
|
|
80
|
-
case "tool":
|
|
81
|
-
if (!Array.isArray(msg.content)) return msg
|
|
82
|
-
msg.content = msg.content.map((content) => {
|
|
83
|
-
if (content.type === "tool-result") {
|
|
84
|
-
return sanitizeToolResultOutput(content)
|
|
85
|
-
}
|
|
86
|
-
return content
|
|
87
|
-
})
|
|
88
|
-
return msg
|
|
89
|
-
|
|
90
|
-
case "system":
|
|
91
|
-
msg.content = sanitizeSurrogates(msg.content)
|
|
92
|
-
return msg
|
|
93
|
-
|
|
94
|
-
case "user":
|
|
95
|
-
if (typeof msg.content === "string") {
|
|
96
|
-
msg.content = sanitizeSurrogates(msg.content)
|
|
97
|
-
} else {
|
|
98
|
-
msg.content = msg.content.map((content) => {
|
|
99
|
-
if (content.type === "text") {
|
|
100
|
-
content.text = sanitizeSurrogates(content.text)
|
|
101
|
-
}
|
|
102
|
-
return content
|
|
103
|
-
})
|
|
104
|
-
}
|
|
105
|
-
return msg
|
|
106
|
-
|
|
107
|
-
case "assistant":
|
|
108
|
-
if (typeof msg.content === "string") {
|
|
109
|
-
msg.content = sanitizeSurrogates(msg.content)
|
|
110
|
-
} else {
|
|
111
|
-
msg.content = msg.content.map((content) => {
|
|
112
|
-
if (content.type === "text" || content.type === "reasoning") {
|
|
113
|
-
content.text = sanitizeSurrogates(content.text)
|
|
114
|
-
}
|
|
115
|
-
if (content.type === "tool-result") {
|
|
116
|
-
return sanitizeToolResultOutput(content)
|
|
117
|
-
}
|
|
118
|
-
return content
|
|
119
|
-
})
|
|
120
|
-
}
|
|
121
|
-
return msg
|
|
122
|
-
}
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
// Anthropic rejects messages with empty content - filter out empty string messages
|
|
126
|
-
// and remove empty text/reasoning parts from array content
|
|
127
|
-
if (model.api.npm === "@ai-sdk/anthropic") {
|
|
128
|
-
msgs = msgs
|
|
129
|
-
.map((msg) => {
|
|
130
|
-
if (typeof msg.content === "string") {
|
|
131
|
-
if (msg.content === "") return undefined
|
|
132
|
-
return msg
|
|
133
|
-
}
|
|
134
|
-
if (!Array.isArray(msg.content)) return msg
|
|
135
|
-
const filtered = msg.content.filter((part) => {
|
|
136
|
-
if (part.type === "text") {
|
|
137
|
-
return part.text !== ""
|
|
138
|
-
}
|
|
139
|
-
if (part.type === "reasoning") {
|
|
140
|
-
return (
|
|
141
|
-
part.text.trim().length > 0 ||
|
|
142
|
-
part.providerOptions?.anthropic?.signature != null ||
|
|
143
|
-
part.providerOptions?.anthropic?.redactedData != null
|
|
144
|
-
)
|
|
145
|
-
}
|
|
146
|
-
return true
|
|
147
|
-
})
|
|
148
|
-
if (filtered.length === 0) return undefined
|
|
149
|
-
return { ...msg, content: filtered }
|
|
150
|
-
})
|
|
151
|
-
.filter((msg): msg is ModelMessage => msg !== undefined && msg.content !== "")
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Bedrock specific transforms
|
|
155
|
-
if (model.api.npm === "@ai-sdk/amazon-bedrock") {
|
|
156
|
-
msgs = msgs
|
|
157
|
-
.map((msg) => {
|
|
158
|
-
if (typeof msg.content === "string") {
|
|
159
|
-
if (msg.content === "") return undefined
|
|
160
|
-
return msg
|
|
161
|
-
}
|
|
162
|
-
if (!Array.isArray(msg.content)) return msg
|
|
163
|
-
const filtered = msg.content.filter((part) => {
|
|
164
|
-
if (part.type === "text") {
|
|
165
|
-
return part.text !== ""
|
|
166
|
-
}
|
|
167
|
-
if (part.type === "reasoning") {
|
|
168
|
-
return (
|
|
169
|
-
part.text.trim().length > 0 ||
|
|
170
|
-
part.providerOptions?.bedrock?.signature != null ||
|
|
171
|
-
part.providerOptions?.bedrock?.redactedData != null
|
|
172
|
-
)
|
|
173
|
-
}
|
|
174
|
-
return true
|
|
175
|
-
})
|
|
176
|
-
if (filtered.length === 0) return undefined
|
|
177
|
-
return { ...msg, content: filtered }
|
|
178
|
-
})
|
|
179
|
-
.filter((msg): msg is ModelMessage => msg !== undefined && msg.content !== "")
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (model.api.id.includes("claude")) {
|
|
183
|
-
const scrub = (id: string) => id.replace(/[^a-zA-Z0-9_-]/g, "_")
|
|
184
|
-
msgs = msgs.map((msg) => {
|
|
185
|
-
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
|
186
|
-
return {
|
|
187
|
-
...msg,
|
|
188
|
-
content: msg.content.map((part) => {
|
|
189
|
-
if (part.type === "tool-call" || part.type === "tool-result") {
|
|
190
|
-
return { ...part, toolCallId: scrub(part.toolCallId) }
|
|
191
|
-
}
|
|
192
|
-
return part
|
|
193
|
-
}),
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
if (msg.role === "tool" && Array.isArray(msg.content)) {
|
|
197
|
-
return {
|
|
198
|
-
...msg,
|
|
199
|
-
content: msg.content.map((part) => {
|
|
200
|
-
if (part.type === "tool-result") {
|
|
201
|
-
return { ...part, toolCallId: scrub(part.toolCallId) }
|
|
202
|
-
}
|
|
203
|
-
return part
|
|
204
|
-
}),
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
return msg
|
|
208
|
-
})
|
|
209
|
-
}
|
|
210
|
-
if (["@ai-sdk/anthropic", "@ai-sdk/google-vertex/anthropic"].includes(model.api.npm)) {
|
|
211
|
-
// Anthropic rejects assistant turns where tool_use blocks are followed by non-tool
|
|
212
|
-
// content, e.g. [tool_use, tool_use, text], with:
|
|
213
|
-
// `tool_use` ids were found without `tool_result` blocks immediately after...
|
|
214
|
-
//
|
|
215
|
-
// Reorder that invalid shape into [text] + [tool_use, tool_use]. Consecutive
|
|
216
|
-
// assistant messages are later merged by the provider/SDK, so preserving the
|
|
217
|
-
// original [tool_use...] then [text] order still produces the invalid payload.
|
|
218
|
-
//
|
|
219
|
-
// The root cause appears to be somewhere upstream where the stream is originally
|
|
220
|
-
// processed. We were unable to locate an exact narrower reproduction elsewhere,
|
|
221
|
-
// so we keep this transform in place for the time being.
|
|
222
|
-
msgs = msgs.flatMap((msg) => {
|
|
223
|
-
if (msg.role !== "assistant" || !Array.isArray(msg.content)) return [msg]
|
|
224
|
-
|
|
225
|
-
const parts = msg.content
|
|
226
|
-
const first = parts.findIndex((part) => part.type === "tool-call")
|
|
227
|
-
if (first === -1) return [msg]
|
|
228
|
-
if (!parts.slice(first).some((part) => part.type !== "tool-call")) return [msg]
|
|
229
|
-
return [
|
|
230
|
-
{ ...msg, content: parts.filter((part) => part.type !== "tool-call") },
|
|
231
|
-
{ ...msg, content: parts.filter((part) => part.type === "tool-call") },
|
|
232
|
-
]
|
|
233
|
-
})
|
|
234
|
-
}
|
|
235
|
-
if (
|
|
236
|
-
model.providerID === "mistral" ||
|
|
237
|
-
model.api.id.toLowerCase().includes("mistral") ||
|
|
238
|
-
model.api.id.toLocaleLowerCase().includes("devstral")
|
|
239
|
-
) {
|
|
240
|
-
const scrub = (id: string) => {
|
|
241
|
-
return id
|
|
242
|
-
.replace(/[^a-zA-Z0-9]/g, "") // Remove non-alphanumeric characters
|
|
243
|
-
.substring(0, 9) // Take first 9 characters
|
|
244
|
-
.padEnd(9, "0") // Pad with zeros if less than 9 characters
|
|
245
|
-
}
|
|
246
|
-
const result: ModelMessage[] = []
|
|
247
|
-
for (let i = 0; i < msgs.length; i++) {
|
|
248
|
-
const msg = msgs[i]
|
|
249
|
-
const nextMsg = msgs[i + 1]
|
|
250
|
-
|
|
251
|
-
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
|
252
|
-
msg.content = msg.content.map((part) => {
|
|
253
|
-
if (part.type === "tool-call" || part.type === "tool-result") {
|
|
254
|
-
return { ...part, toolCallId: scrub(part.toolCallId) }
|
|
255
|
-
}
|
|
256
|
-
return part
|
|
257
|
-
})
|
|
258
|
-
}
|
|
259
|
-
if (msg.role === "tool" && Array.isArray(msg.content)) {
|
|
260
|
-
msg.content = msg.content.map((part) => {
|
|
261
|
-
if (part.type === "tool-result") {
|
|
262
|
-
return { ...part, toolCallId: scrub(part.toolCallId) }
|
|
263
|
-
}
|
|
264
|
-
return part
|
|
265
|
-
})
|
|
266
|
-
}
|
|
267
|
-
result.push(msg)
|
|
268
|
-
|
|
269
|
-
// Fix message sequence: tool messages cannot be followed by user messages
|
|
270
|
-
if (msg.role === "tool" && nextMsg?.role === "user") {
|
|
271
|
-
result.push({
|
|
272
|
-
role: "assistant",
|
|
273
|
-
content: [
|
|
274
|
-
{
|
|
275
|
-
type: "text",
|
|
276
|
-
text: "Done.",
|
|
277
|
-
},
|
|
278
|
-
],
|
|
279
|
-
})
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
return result
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Deepseek requires all assistant messages to have reasoning on them
|
|
286
|
-
if (model.api.id.toLowerCase().includes("deepseek")) {
|
|
287
|
-
msgs = msgs.map((msg) => {
|
|
288
|
-
if (msg.role !== "assistant") return msg
|
|
289
|
-
if (Array.isArray(msg.content)) {
|
|
290
|
-
if (msg.content.some((part) => part.type === "reasoning")) return msg
|
|
291
|
-
return { ...msg, content: [...msg.content, { type: "reasoning", text: "" }] }
|
|
292
|
-
}
|
|
293
|
-
return {
|
|
294
|
-
...msg,
|
|
295
|
-
content: [
|
|
296
|
-
...(msg.content ? [{ type: "text" as const, text: msg.content }] : []),
|
|
297
|
-
{ type: "reasoning" as const, text: "" },
|
|
298
|
-
],
|
|
299
|
-
}
|
|
300
|
-
})
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
if (
|
|
304
|
-
typeof model.capabilities.interleaved === "object" &&
|
|
305
|
-
model.capabilities.interleaved.field &&
|
|
306
|
-
model.api.npm !== "@openrouter/ai-sdk-provider"
|
|
307
|
-
) {
|
|
308
|
-
const field = model.capabilities.interleaved.field
|
|
309
|
-
return msgs.map((msg) => {
|
|
310
|
-
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
|
311
|
-
const reasoningParts = msg.content.filter((part: any) => part.type === "reasoning")
|
|
312
|
-
const reasoningText = reasoningParts.map((part: any) => part.text).join("")
|
|
313
|
-
|
|
314
|
-
// Filter out reasoning parts from content
|
|
315
|
-
const filteredContent = msg.content.filter((part: any) => part.type !== "reasoning")
|
|
316
|
-
|
|
317
|
-
// Include reasoning_content | reasoning_details directly on the message for all assistant messages.
|
|
318
|
-
// Always set the field even when empty — some providers (e.g. DeepSeek) may return empty
|
|
319
|
-
// reasoning_content which still needs to be sent back in subsequent requests.
|
|
320
|
-
return {
|
|
321
|
-
...msg,
|
|
322
|
-
content: filteredContent,
|
|
323
|
-
providerOptions: {
|
|
324
|
-
...msg.providerOptions,
|
|
325
|
-
openaiCompatible: {
|
|
326
|
-
...msg.providerOptions?.openaiCompatible,
|
|
327
|
-
[field]: reasoningText,
|
|
328
|
-
},
|
|
329
|
-
},
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
return msg
|
|
334
|
-
})
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
return msgs
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function applyCaching(msgs: ModelMessage[], model: Provider.Model): ModelMessage[] {
|
|
341
|
-
const system = msgs.filter((msg) => msg.role === "system").slice(0, 2)
|
|
342
|
-
const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
|
|
343
|
-
|
|
344
|
-
const providerOptions = {
|
|
345
|
-
anthropic: {
|
|
346
|
-
cacheControl: { type: "ephemeral" },
|
|
347
|
-
},
|
|
348
|
-
openrouter: {
|
|
349
|
-
cacheControl: { type: "ephemeral" },
|
|
350
|
-
},
|
|
351
|
-
bedrock: {
|
|
352
|
-
cachePoint: { type: "default" },
|
|
353
|
-
},
|
|
354
|
-
openaiCompatible: {
|
|
355
|
-
cache_control: { type: "ephemeral" },
|
|
356
|
-
},
|
|
357
|
-
copilot: {
|
|
358
|
-
copilot_cache_control: { type: "ephemeral" },
|
|
359
|
-
},
|
|
360
|
-
alibaba: {
|
|
361
|
-
cacheControl: { type: "ephemeral" },
|
|
362
|
-
},
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
for (const msg of unique([...system, ...final])) {
|
|
366
|
-
const useMessageLevelOptions =
|
|
367
|
-
model.providerID === "anthropic" ||
|
|
368
|
-
model.providerID.includes("bedrock") ||
|
|
369
|
-
model.api.npm === "@ai-sdk/amazon-bedrock"
|
|
370
|
-
const shouldUseContentOptions = !useMessageLevelOptions && Array.isArray(msg.content) && msg.content.length > 0
|
|
371
|
-
|
|
372
|
-
if (shouldUseContentOptions) {
|
|
373
|
-
const lastContent = msg.content[msg.content.length - 1]
|
|
374
|
-
if (
|
|
375
|
-
lastContent &&
|
|
376
|
-
typeof lastContent === "object" &&
|
|
377
|
-
lastContent.type !== "tool-approval-request" &&
|
|
378
|
-
lastContent.type !== "tool-approval-response"
|
|
379
|
-
) {
|
|
380
|
-
lastContent.providerOptions = mergeDeep(lastContent.providerOptions ?? {}, providerOptions)
|
|
381
|
-
continue
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
msg.providerOptions = mergeDeep(msg.providerOptions ?? {}, providerOptions)
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
return msgs
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
function unsupportedParts(msgs: ModelMessage[], model: Provider.Model): ModelMessage[] {
|
|
392
|
-
return msgs.map((msg) => {
|
|
393
|
-
if (msg.role !== "user" || !Array.isArray(msg.content)) return msg
|
|
394
|
-
|
|
395
|
-
const filtered = msg.content.map((part) => {
|
|
396
|
-
if (part.type !== "file" && part.type !== "image") return part
|
|
397
|
-
|
|
398
|
-
// Check for empty base64 image data
|
|
399
|
-
if (part.type === "image") {
|
|
400
|
-
const imageStr = String(part.image)
|
|
401
|
-
if (imageStr.startsWith("data:")) {
|
|
402
|
-
const match = imageStr.match(/^data:([^;]+);base64,(.*)$/)
|
|
403
|
-
if (match && (!match[2] || match[2].length === 0)) {
|
|
404
|
-
return {
|
|
405
|
-
type: "text" as const,
|
|
406
|
-
text: "ERROR: Image file is empty or corrupted. Please provide a valid image.",
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
const mime = part.type === "image" ? String(part.image).split(";")[0].replace("data:", "") : part.mediaType
|
|
413
|
-
const filename = part.type === "file" ? part.filename : undefined
|
|
414
|
-
const modality = mimeToModality(mime)
|
|
415
|
-
if (!modality) return part
|
|
416
|
-
if (model.capabilities.input[modality]) return part
|
|
417
|
-
|
|
418
|
-
const name = filename ? `"${filename}"` : modality
|
|
419
|
-
return {
|
|
420
|
-
type: "text" as const,
|
|
421
|
-
text: `ERROR: Cannot read ${name} (this model does not support ${modality} input). Inform the user.`,
|
|
422
|
-
}
|
|
423
|
-
})
|
|
424
|
-
|
|
425
|
-
return { ...msg, content: filtered }
|
|
426
|
-
})
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
export function message(msgs: ModelMessage[], model: Provider.Model, options: Record<string, unknown>) {
|
|
430
|
-
msgs = unsupportedParts(msgs, model)
|
|
431
|
-
msgs = normalizeMessages(msgs, model, options)
|
|
432
|
-
if (
|
|
433
|
-
(model.providerID === "anthropic" ||
|
|
434
|
-
model.providerID === "google-vertex-anthropic" ||
|
|
435
|
-
model.api.id.includes("anthropic") ||
|
|
436
|
-
model.api.id.includes("claude") ||
|
|
437
|
-
model.id.includes("anthropic") ||
|
|
438
|
-
model.id.includes("claude") ||
|
|
439
|
-
model.api.npm === "@ai-sdk/anthropic" ||
|
|
440
|
-
model.api.npm === "@ai-sdk/alibaba") &&
|
|
441
|
-
model.api.npm !== "@ai-sdk/gateway"
|
|
442
|
-
) {
|
|
443
|
-
msgs = applyCaching(msgs, model)
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// Remap providerOptions keys from stored providerID to expected SDK key
|
|
447
|
-
const key = sdkKey(model.api.npm)
|
|
448
|
-
if (key && key !== model.providerID) {
|
|
449
|
-
const remap = (opts: Record<string, any> | undefined) => {
|
|
450
|
-
if (!opts) return opts
|
|
451
|
-
if (!(model.providerID in opts)) return opts
|
|
452
|
-
const result = { ...opts }
|
|
453
|
-
result[key] = result[model.providerID]
|
|
454
|
-
delete result[model.providerID]
|
|
455
|
-
return result
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
msgs = msgs.map((msg) => {
|
|
459
|
-
if (!Array.isArray(msg.content)) return { ...msg, providerOptions: remap(msg.providerOptions) }
|
|
460
|
-
return {
|
|
461
|
-
...msg,
|
|
462
|
-
providerOptions: remap(msg.providerOptions),
|
|
463
|
-
content: msg.content.map((part) => {
|
|
464
|
-
if (part.type === "tool-approval-request" || part.type === "tool-approval-response") {
|
|
465
|
-
return { ...part }
|
|
466
|
-
}
|
|
467
|
-
return { ...part, providerOptions: remap(part.providerOptions) }
|
|
468
|
-
}),
|
|
469
|
-
} as typeof msg
|
|
470
|
-
})
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
return msgs
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
export function temperature(model: Provider.Model) {
|
|
477
|
-
const id = model.id.toLowerCase()
|
|
478
|
-
if (id.includes("qwen")) return 0.55
|
|
479
|
-
if (id.includes("claude")) return undefined
|
|
480
|
-
if (id.includes("gemini")) return 1.0
|
|
481
|
-
if (id.includes("glm-4.6")) return 1.0
|
|
482
|
-
if (id.includes("glm-4.7")) return 1.0
|
|
483
|
-
if (id.includes("minimax-m2")) return 1.0
|
|
484
|
-
if (id.includes("kimi-k2")) {
|
|
485
|
-
// kimi-k2-thinking & kimi-k2.5 && kimi-k2p5 && kimi-k2-5
|
|
486
|
-
if (["thinking", "k2.", "k2p", "k2-5"].some((s) => id.includes(s))) {
|
|
487
|
-
return 1.0
|
|
488
|
-
}
|
|
489
|
-
return 0.6
|
|
490
|
-
}
|
|
491
|
-
return undefined
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
export function topP(model: Provider.Model) {
|
|
495
|
-
const id = model.id.toLowerCase()
|
|
496
|
-
if (id.includes("qwen")) return 1
|
|
497
|
-
if (["minimax-m2", "gemini", "kimi-k2.5", "kimi-k2p5", "kimi-k2-5"].some((s) => id.includes(s))) {
|
|
498
|
-
return 0.95
|
|
499
|
-
}
|
|
500
|
-
return undefined
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
export function topK(model: Provider.Model) {
|
|
504
|
-
const id = model.id.toLowerCase()
|
|
505
|
-
if (id.includes("minimax-m2")) {
|
|
506
|
-
if (["m2.", "m25", "m21"].some((s) => id.includes(s))) return 40
|
|
507
|
-
return 20
|
|
508
|
-
}
|
|
509
|
-
if (id.includes("gemini")) return 64
|
|
510
|
-
return undefined
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
const WIDELY_SUPPORTED_EFFORTS = ["low", "medium", "high"]
|
|
514
|
-
const OPENAI_EFFORTS = ["none", "minimal", ...WIDELY_SUPPORTED_EFFORTS, "xhigh"]
|
|
515
|
-
const OPENAI_GPT5_1_EFFORTS = ["none", ...WIDELY_SUPPORTED_EFFORTS]
|
|
516
|
-
const OPENAI_GPT5_2_PLUS_EFFORTS = [...OPENAI_GPT5_1_EFFORTS, "xhigh"]
|
|
517
|
-
const OPENAI_GPT5_PRO_EFFORTS = ["high"]
|
|
518
|
-
const OPENAI_GPT5_PRO_2_PLUS_EFFORTS = ["medium", "high", "xhigh"]
|
|
519
|
-
const OPENAI_GPT5_CHAT_EFFORTS = ["medium"]
|
|
520
|
-
const OPENAI_GPT5_CODEX_XHIGH_EFFORTS = [...WIDELY_SUPPORTED_EFFORTS, "xhigh"]
|
|
521
|
-
const OPENAI_GPT5_CODEX_3_PLUS_EFFORTS = ["none", ...OPENAI_GPT5_CODEX_XHIGH_EFFORTS]
|
|
522
|
-
|
|
523
|
-
// OpenAI rolled out the `none` reasoning_effort tier on this date (Responses API).
|
|
524
|
-
// Models released before it 400 on `reasoning_effort: "none"`, so we only expose
|
|
525
|
-
// it as a variant for models new enough to accept it.
|
|
526
|
-
const OPENAI_NONE_EFFORT_RELEASE_DATE = "2025-11-13"
|
|
527
|
-
|
|
528
|
-
// OpenAI rolled out the `xhigh` reasoning_effort tier on this date. Same reasoning.
|
|
529
|
-
const OPENAI_XHIGH_EFFORT_RELEASE_DATE = "2025-12-04"
|
|
530
|
-
|
|
531
|
-
// Matches members of the gpt-5 family across the id formats we encounter:
|
|
532
|
-
// "gpt-5", "gpt-5-nano", "gpt-5.4", "openai/gpt-5.4-codex".
|
|
533
|
-
// Anchored to start-of-string or "/" so it doesn't false-match "gpt-50" or "gpt-5o".
|
|
534
|
-
const GPT5_FAMILY_RE = /(?:^|\/)gpt-5(?:[.-]|$)/
|
|
535
|
-
const GPT5_VERSION_RE = /(?:^|\/)gpt-5[.-](\d+)(?:[.-]|$)/
|
|
536
|
-
const GPT5_PRO_RE = /(?:^|\/)gpt-5[.-]?pro(?:[.-]|$)/
|
|
537
|
-
const GPT5_VERSIONED_PRO_RE = /(?:^|\/)gpt-5[.-]\d+[.-]pro(?:[.-]|$)/
|
|
538
|
-
|
|
539
|
-
function gpt5Version(apiId: string) {
|
|
540
|
-
return Number(GPT5_VERSION_RE.exec(apiId)?.[1]) || undefined
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
function versionedGpt5ReasoningEfforts(apiId: string) {
|
|
544
|
-
if (GPT5_VERSIONED_PRO_RE.test(apiId)) return OPENAI_GPT5_PRO_2_PLUS_EFFORTS
|
|
545
|
-
const version = gpt5Version(apiId)
|
|
546
|
-
if (version === undefined) return undefined
|
|
547
|
-
if (version === 1) return OPENAI_GPT5_1_EFFORTS
|
|
548
|
-
return OPENAI_GPT5_2_PLUS_EFFORTS
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
function gpt5CodexReasoningEfforts(apiId: string) {
|
|
552
|
-
if (!GPT5_FAMILY_RE.test(apiId) || !apiId.includes("codex")) return undefined
|
|
553
|
-
const version = gpt5Version(apiId)
|
|
554
|
-
if (version !== undefined && version >= 3) return OPENAI_GPT5_CODEX_3_PLUS_EFFORTS
|
|
555
|
-
if (apiId.includes("codex-max") || (version !== undefined && version >= 2)) return OPENAI_GPT5_CODEX_XHIGH_EFFORTS
|
|
556
|
-
return WIDELY_SUPPORTED_EFFORTS
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
function gpt5ChatReasoningEfforts(apiId: string) {
|
|
560
|
-
if (!GPT5_FAMILY_RE.test(apiId) || !apiId.includes("-chat")) return undefined
|
|
561
|
-
return gpt5Version(apiId) === undefined ? [] : OPENAI_GPT5_CHAT_EFFORTS
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
// Computes the reasoning_effort tiers an OpenAI (or OpenAI-compatible upstream
|
|
565
|
-
// routed through it, e.g. cf-ai-gateway) model exposes. Effort order: weakest
|
|
566
|
-
// to strongest.
|
|
567
|
-
function openaiReasoningEfforts(apiId: string, releaseDate: string) {
|
|
568
|
-
const id = apiId.toLowerCase()
|
|
569
|
-
if (id.includes("deep-research")) return ["medium"]
|
|
570
|
-
const chatEfforts = gpt5ChatReasoningEfforts(id)
|
|
571
|
-
if (chatEfforts) return chatEfforts
|
|
572
|
-
if (GPT5_PRO_RE.test(id)) return OPENAI_GPT5_PRO_EFFORTS
|
|
573
|
-
const codexEfforts = gpt5CodexReasoningEfforts(id)
|
|
574
|
-
if (codexEfforts) return codexEfforts
|
|
575
|
-
const versionedEfforts = versionedGpt5ReasoningEfforts(id)
|
|
576
|
-
// GPT-5.1 replaced GPT-5's `minimal` effort with `none`; GPT-5.2+
|
|
577
|
-
// additionally accepts `xhigh`. Model pages list the supported subset.
|
|
578
|
-
if (versionedEfforts) return versionedEfforts
|
|
579
|
-
const efforts = [...WIDELY_SUPPORTED_EFFORTS]
|
|
580
|
-
if (GPT5_FAMILY_RE.test(id)) efforts.unshift("minimal")
|
|
581
|
-
if (releaseDate >= OPENAI_NONE_EFFORT_RELEASE_DATE) efforts.unshift("none")
|
|
582
|
-
if (releaseDate >= OPENAI_XHIGH_EFFORT_RELEASE_DATE) efforts.push("xhigh")
|
|
583
|
-
return efforts
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
function openaiCompatibleReasoningEfforts(id: string) {
|
|
587
|
-
const apiId = id.toLowerCase()
|
|
588
|
-
const chatEfforts = gpt5ChatReasoningEfforts(apiId)
|
|
589
|
-
if (chatEfforts) return chatEfforts
|
|
590
|
-
if (GPT5_PRO_RE.test(apiId)) return OPENAI_GPT5_PRO_EFFORTS
|
|
591
|
-
return gpt5CodexReasoningEfforts(apiId) ?? versionedGpt5ReasoningEfforts(apiId) ?? OPENAI_EFFORTS
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
function anthropicAdaptiveEfforts(apiId: string): string[] | null {
|
|
595
|
-
if (["opus-4-7", "opus-4.7"].some((v) => apiId.includes(v))) {
|
|
596
|
-
return ["low", "medium", "high", "xhigh", "max"]
|
|
597
|
-
}
|
|
598
|
-
if (["opus-4-6", "opus-4.6", "sonnet-4-6", "sonnet-4.6"].some((v) => apiId.includes(v))) {
|
|
599
|
-
return ["low", "medium", "high", "max"]
|
|
600
|
-
}
|
|
601
|
-
return null
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
function googleThinkingLevelEfforts(apiId: string) {
|
|
605
|
-
const id = apiId.toLowerCase()
|
|
606
|
-
if (!id.includes("gemini-3")) return ["low", "high"]
|
|
607
|
-
if (id.includes("flash-image")) return ["minimal", "high"]
|
|
608
|
-
if (id.includes("pro-image")) return ["high"]
|
|
609
|
-
if (id.includes("flash")) return ["minimal", "low", "medium", "high"]
|
|
610
|
-
return ["low", "medium", "high"]
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
function googleThinkingBudgetMax(apiId: string) {
|
|
614
|
-
const id = apiId.toLowerCase()
|
|
615
|
-
if (id.includes("2.5") && id.includes("pro") && !id.includes("flash")) return 32_768
|
|
616
|
-
return 24_576
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
export function variants(model: Provider.Model): Record<string, Record<string, any>> {
|
|
620
|
-
if (!model.capabilities.reasoning) return {}
|
|
621
|
-
|
|
622
|
-
const id = model.id.toLowerCase()
|
|
623
|
-
const adaptiveEfforts = anthropicAdaptiveEfforts(model.api.id)
|
|
624
|
-
if (
|
|
625
|
-
id.includes("deepseek-chat") ||
|
|
626
|
-
id.includes("deepseek-reasoner") ||
|
|
627
|
-
id.includes("deepseek-r1") ||
|
|
628
|
-
id.includes("deepseek-v3") ||
|
|
629
|
-
id.includes("minimax") ||
|
|
630
|
-
id.includes("glm") ||
|
|
631
|
-
id.includes("kimi") ||
|
|
632
|
-
id.includes("k2p") ||
|
|
633
|
-
id.includes("qwen") ||
|
|
634
|
-
id.includes("big-pickle")
|
|
635
|
-
)
|
|
636
|
-
return {}
|
|
637
|
-
|
|
638
|
-
// see: https://docs.x.ai/docs/guides/reasoning#control-how-hard-the-model-thinks
|
|
639
|
-
if (id.includes("grok") && id.includes("grok-3-mini")) {
|
|
640
|
-
if (model.api.npm === "@openrouter/ai-sdk-provider") {
|
|
641
|
-
return {
|
|
642
|
-
low: { reasoning: { effort: "low" } },
|
|
643
|
-
high: { reasoning: { effort: "high" } },
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
return {
|
|
647
|
-
low: { reasoningEffort: "low" },
|
|
648
|
-
high: { reasoningEffort: "high" },
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
if (id.includes("grok")) return {}
|
|
652
|
-
|
|
653
|
-
switch (model.api.npm) {
|
|
654
|
-
case "@openrouter/ai-sdk-provider":
|
|
655
|
-
if (!id.includes("gpt") && !id.includes("gemini-3") && !id.includes("claude")) return {}
|
|
656
|
-
return Object.fromEntries(
|
|
657
|
-
(id.includes("gpt") ? openaiCompatibleReasoningEfforts(id) : OPENAI_EFFORTS).map((effort) => [
|
|
658
|
-
effort,
|
|
659
|
-
{ reasoning: { effort } },
|
|
660
|
-
]),
|
|
661
|
-
)
|
|
662
|
-
|
|
663
|
-
case "ai-gateway-provider": {
|
|
664
|
-
// Cloudflare AI Gateway routes every upstream through its OpenAI-compatible
|
|
665
|
-
// /v1/compat endpoint, so the body is always OAI-shaped. The gateway
|
|
666
|
-
// translates `reasoning_effort` to the upstream provider's native control
|
|
667
|
-
// (e.g. Anthropic thinking budgets) when needed. Variants therefore stay
|
|
668
|
-
// OAI-style for all upstreams, with an extended effort set for OpenAI
|
|
669
|
-
// models that support it.
|
|
670
|
-
if (model.api.id.startsWith("openai/")) {
|
|
671
|
-
const efforts = openaiReasoningEfforts(model.api.id, model.release_date)
|
|
672
|
-
return Object.fromEntries(efforts.map((effort) => [effort, { reasoningEffort: effort }]))
|
|
673
|
-
}
|
|
674
|
-
return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
case "@ai-sdk/gateway":
|
|
678
|
-
if (model.id.includes("anthropic")) {
|
|
679
|
-
if (adaptiveEfforts) {
|
|
680
|
-
return Object.fromEntries(
|
|
681
|
-
adaptiveEfforts.map((effort) => [
|
|
682
|
-
effort,
|
|
683
|
-
{
|
|
684
|
-
thinking: {
|
|
685
|
-
type: "adaptive",
|
|
686
|
-
},
|
|
687
|
-
effort,
|
|
688
|
-
},
|
|
689
|
-
]),
|
|
690
|
-
)
|
|
691
|
-
}
|
|
692
|
-
return {
|
|
693
|
-
high: {
|
|
694
|
-
thinking: {
|
|
695
|
-
type: "enabled",
|
|
696
|
-
budgetTokens: 16000,
|
|
697
|
-
},
|
|
698
|
-
},
|
|
699
|
-
max: {
|
|
700
|
-
thinking: {
|
|
701
|
-
type: "enabled",
|
|
702
|
-
budgetTokens: 31999,
|
|
703
|
-
},
|
|
704
|
-
},
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
if (model.id.includes("google")) {
|
|
708
|
-
if (id.includes("2.5")) {
|
|
709
|
-
return {
|
|
710
|
-
high: {
|
|
711
|
-
thinkingConfig: {
|
|
712
|
-
includeThoughts: true,
|
|
713
|
-
thinkingBudget: 16000,
|
|
714
|
-
},
|
|
715
|
-
},
|
|
716
|
-
max: {
|
|
717
|
-
thinkingConfig: {
|
|
718
|
-
includeThoughts: true,
|
|
719
|
-
thinkingBudget: 24576,
|
|
720
|
-
},
|
|
721
|
-
},
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
return Object.fromEntries(
|
|
725
|
-
["low", "high"].map((effort) => [
|
|
726
|
-
effort,
|
|
727
|
-
{
|
|
728
|
-
includeThoughts: true,
|
|
729
|
-
thinkingLevel: effort,
|
|
730
|
-
},
|
|
731
|
-
]),
|
|
732
|
-
)
|
|
733
|
-
}
|
|
734
|
-
return Object.fromEntries(
|
|
735
|
-
openaiCompatibleReasoningEfforts(model.api.id).map((effort) => [effort, { reasoningEffort: effort }]),
|
|
736
|
-
)
|
|
737
|
-
|
|
738
|
-
case "@ai-sdk/github-copilot":
|
|
739
|
-
if (model.id.includes("gemini")) {
|
|
740
|
-
// currently github copilot only returns thinking
|
|
741
|
-
return {}
|
|
742
|
-
}
|
|
743
|
-
if (model.id.includes("claude")) {
|
|
744
|
-
return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
|
|
745
|
-
}
|
|
746
|
-
const copilotEfforts = iife(() => {
|
|
747
|
-
if (id.includes("5.1-codex-max") || id.includes("5.2") || id.includes("5.3"))
|
|
748
|
-
return [...WIDELY_SUPPORTED_EFFORTS, "xhigh"]
|
|
749
|
-
const arr = [...WIDELY_SUPPORTED_EFFORTS]
|
|
750
|
-
if (id.includes("gpt-5") && model.release_date >= "2025-12-04") arr.push("xhigh")
|
|
751
|
-
return arr
|
|
752
|
-
})
|
|
753
|
-
return Object.fromEntries(
|
|
754
|
-
copilotEfforts.map((effort) => [
|
|
755
|
-
effort,
|
|
756
|
-
{
|
|
757
|
-
reasoningEffort: effort,
|
|
758
|
-
reasoningSummary: "auto",
|
|
759
|
-
include: ["reasoning.encrypted_content"],
|
|
760
|
-
},
|
|
761
|
-
]),
|
|
762
|
-
)
|
|
763
|
-
|
|
764
|
-
case "@ai-sdk/cerebras":
|
|
765
|
-
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/cerebras
|
|
766
|
-
case "@ai-sdk/togetherai":
|
|
767
|
-
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/togetherai
|
|
768
|
-
case "@ai-sdk/xai":
|
|
769
|
-
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/xai
|
|
770
|
-
case "@ai-sdk/deepinfra":
|
|
771
|
-
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/deepinfra
|
|
772
|
-
case "venice-ai-sdk-provider":
|
|
773
|
-
// https://docs.venice.ai/overview/guides/reasoning-models#reasoning-effort
|
|
774
|
-
case "@ai-sdk/openai-compatible":
|
|
775
|
-
const efforts = [...WIDELY_SUPPORTED_EFFORTS]
|
|
776
|
-
if (model.api.id.toLowerCase().includes("deepseek-v4")) {
|
|
777
|
-
efforts.push("max")
|
|
778
|
-
}
|
|
779
|
-
return Object.fromEntries(efforts.map((effort) => [effort, { reasoningEffort: effort }]))
|
|
780
|
-
|
|
781
|
-
case "@ai-sdk/azure":
|
|
782
|
-
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/azure
|
|
783
|
-
if (id === "o1-mini") return {}
|
|
784
|
-
return Object.fromEntries(
|
|
785
|
-
(GPT5_FAMILY_RE.test(id) && gpt5Version(id) === undefined
|
|
786
|
-
? ["minimal", ...WIDELY_SUPPORTED_EFFORTS]
|
|
787
|
-
: WIDELY_SUPPORTED_EFFORTS
|
|
788
|
-
).map((effort) => [
|
|
789
|
-
effort,
|
|
790
|
-
{
|
|
791
|
-
reasoningEffort: effort,
|
|
792
|
-
reasoningSummary: "auto",
|
|
793
|
-
include: ["reasoning.encrypted_content"],
|
|
794
|
-
},
|
|
795
|
-
]),
|
|
796
|
-
)
|
|
797
|
-
case "@ai-sdk/openai": {
|
|
798
|
-
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/openai
|
|
799
|
-
const efforts = openaiReasoningEfforts(model.api.id, model.release_date)
|
|
800
|
-
return Object.fromEntries(
|
|
801
|
-
efforts.map((effort) => [
|
|
802
|
-
effort,
|
|
803
|
-
{
|
|
804
|
-
reasoningEffort: effort,
|
|
805
|
-
reasoningSummary: "auto",
|
|
806
|
-
include: ["reasoning.encrypted_content"],
|
|
807
|
-
},
|
|
808
|
-
]),
|
|
809
|
-
)
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
case "@ai-sdk/anthropic":
|
|
813
|
-
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/anthropic
|
|
814
|
-
case "@ai-sdk/google-vertex/anthropic":
|
|
815
|
-
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-vertex#anthropic-provider
|
|
816
|
-
if (adaptiveEfforts) {
|
|
817
|
-
let efforts = [...adaptiveEfforts]
|
|
818
|
-
if (model.providerID === "github-copilot") {
|
|
819
|
-
if (model.api.id.includes("opus-4.7")) {
|
|
820
|
-
efforts = ["medium"]
|
|
821
|
-
}
|
|
822
|
-
// Efforts currently supported are: low, medium, high
|
|
823
|
-
efforts = efforts.filter((v) => v !== "max" && v !== "xhigh")
|
|
824
|
-
}
|
|
825
|
-
return Object.fromEntries(
|
|
826
|
-
efforts.map((effort) => [
|
|
827
|
-
effort,
|
|
828
|
-
{
|
|
829
|
-
thinking: {
|
|
830
|
-
type: "adaptive",
|
|
831
|
-
...(model.api.id.includes("opus-4-7") || model.api.id.includes("opus-4.7")
|
|
832
|
-
? { display: "summarized" }
|
|
833
|
-
: {}),
|
|
834
|
-
},
|
|
835
|
-
effort,
|
|
836
|
-
},
|
|
837
|
-
]),
|
|
838
|
-
)
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
if (["opus-4-5", "opus-4.5"].some((v) => model.api.id.includes(v))) {
|
|
842
|
-
return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { effort }]))
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
return {
|
|
846
|
-
high: {
|
|
847
|
-
thinking: {
|
|
848
|
-
type: "enabled",
|
|
849
|
-
budgetTokens: Math.min(16_000, Math.floor(model.limit.output / 2 - 1)),
|
|
850
|
-
},
|
|
851
|
-
},
|
|
852
|
-
max: {
|
|
853
|
-
thinking: {
|
|
854
|
-
type: "enabled",
|
|
855
|
-
budgetTokens: Math.min(31_999, model.limit.output - 1),
|
|
856
|
-
},
|
|
857
|
-
},
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
case "@ai-sdk/amazon-bedrock":
|
|
861
|
-
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/amazon-bedrock
|
|
862
|
-
if (adaptiveEfforts) {
|
|
863
|
-
return Object.fromEntries(
|
|
864
|
-
adaptiveEfforts.map((effort) => [
|
|
865
|
-
effort,
|
|
866
|
-
{
|
|
867
|
-
reasoningConfig: {
|
|
868
|
-
type: "adaptive",
|
|
869
|
-
maxReasoningEffort: effort,
|
|
870
|
-
...(model.api.id.includes("opus-4-7") || model.api.id.includes("opus-4.7")
|
|
871
|
-
? { display: "summarized" }
|
|
872
|
-
: {}),
|
|
873
|
-
},
|
|
874
|
-
},
|
|
875
|
-
]),
|
|
876
|
-
)
|
|
877
|
-
}
|
|
878
|
-
// For Anthropic models on Bedrock, use reasoningConfig with budgetTokens
|
|
879
|
-
if (model.api.id.includes("anthropic")) {
|
|
880
|
-
return {
|
|
881
|
-
high: {
|
|
882
|
-
reasoningConfig: {
|
|
883
|
-
type: "enabled",
|
|
884
|
-
budgetTokens: 16000,
|
|
885
|
-
},
|
|
886
|
-
},
|
|
887
|
-
max: {
|
|
888
|
-
reasoningConfig: {
|
|
889
|
-
type: "enabled",
|
|
890
|
-
budgetTokens: 31999,
|
|
891
|
-
},
|
|
892
|
-
},
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
// For Amazon Nova models, use reasoningConfig with maxReasoningEffort
|
|
897
|
-
return Object.fromEntries(
|
|
898
|
-
WIDELY_SUPPORTED_EFFORTS.map((effort) => [
|
|
899
|
-
effort,
|
|
900
|
-
{
|
|
901
|
-
reasoningConfig: {
|
|
902
|
-
type: "enabled",
|
|
903
|
-
maxReasoningEffort: effort,
|
|
904
|
-
},
|
|
905
|
-
},
|
|
906
|
-
]),
|
|
907
|
-
)
|
|
908
|
-
|
|
909
|
-
case "@ai-sdk/google-vertex":
|
|
910
|
-
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-vertex
|
|
911
|
-
case "@ai-sdk/google":
|
|
912
|
-
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai
|
|
913
|
-
if (id.includes("2.5")) {
|
|
914
|
-
return {
|
|
915
|
-
high: {
|
|
916
|
-
thinkingConfig: {
|
|
917
|
-
includeThoughts: true,
|
|
918
|
-
thinkingBudget: 16000,
|
|
919
|
-
},
|
|
920
|
-
},
|
|
921
|
-
max: {
|
|
922
|
-
thinkingConfig: {
|
|
923
|
-
includeThoughts: true,
|
|
924
|
-
thinkingBudget: googleThinkingBudgetMax(id),
|
|
925
|
-
},
|
|
926
|
-
},
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
return Object.fromEntries(
|
|
931
|
-
googleThinkingLevelEfforts(id).map((effort) => [
|
|
932
|
-
effort,
|
|
933
|
-
{
|
|
934
|
-
thinkingConfig: {
|
|
935
|
-
includeThoughts: true,
|
|
936
|
-
thinkingLevel: effort,
|
|
937
|
-
},
|
|
938
|
-
},
|
|
939
|
-
]),
|
|
940
|
-
)
|
|
941
|
-
|
|
942
|
-
case "@ai-sdk/mistral":
|
|
943
|
-
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/mistral
|
|
944
|
-
// https://docs.mistral.ai/capabilities/reasoning/adjustable
|
|
945
|
-
if (!model.capabilities.reasoning) return {}
|
|
946
|
-
// Only Mistral Small 4 and Medium 3.5 support reasoning
|
|
947
|
-
const MISTRAL_REASONING_IDS = [
|
|
948
|
-
"mistral-small-2603",
|
|
949
|
-
"mistral-small-latest",
|
|
950
|
-
"mistral-medium-3.5",
|
|
951
|
-
"mistral-medium-2604",
|
|
952
|
-
]
|
|
953
|
-
const mistralId = model.api.id.toLowerCase()
|
|
954
|
-
if (!MISTRAL_REASONING_IDS.some((id) => mistralId.includes(id))) return {}
|
|
955
|
-
return {
|
|
956
|
-
high: { reasoningEffort: "high" },
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
case "@ai-sdk/cohere":
|
|
960
|
-
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/cohere
|
|
961
|
-
return {}
|
|
962
|
-
|
|
963
|
-
case "@ai-sdk/groq":
|
|
964
|
-
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/groq
|
|
965
|
-
const groqEffort = ["none", ...WIDELY_SUPPORTED_EFFORTS]
|
|
966
|
-
return Object.fromEntries(
|
|
967
|
-
groqEffort.map((effort) => [
|
|
968
|
-
effort,
|
|
969
|
-
{
|
|
970
|
-
reasoningEffort: effort,
|
|
971
|
-
},
|
|
972
|
-
]),
|
|
973
|
-
)
|
|
974
|
-
|
|
975
|
-
case "@ai-sdk/perplexity":
|
|
976
|
-
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/perplexity
|
|
977
|
-
return {}
|
|
978
|
-
|
|
979
|
-
case "@jerome-benoit/sap-ai-provider-v2":
|
|
980
|
-
if (model.api.id.includes("anthropic")) {
|
|
981
|
-
if (adaptiveEfforts) {
|
|
982
|
-
return Object.fromEntries(
|
|
983
|
-
adaptiveEfforts.map((effort) => [
|
|
984
|
-
effort,
|
|
985
|
-
{
|
|
986
|
-
thinking: {
|
|
987
|
-
type: "adaptive",
|
|
988
|
-
},
|
|
989
|
-
effort,
|
|
990
|
-
},
|
|
991
|
-
]),
|
|
992
|
-
)
|
|
993
|
-
}
|
|
994
|
-
return {
|
|
995
|
-
high: {
|
|
996
|
-
thinking: {
|
|
997
|
-
type: "enabled",
|
|
998
|
-
budgetTokens: 16000,
|
|
999
|
-
},
|
|
1000
|
-
},
|
|
1001
|
-
max: {
|
|
1002
|
-
thinking: {
|
|
1003
|
-
type: "enabled",
|
|
1004
|
-
budgetTokens: 31999,
|
|
1005
|
-
},
|
|
1006
|
-
},
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
if (model.api.id.includes("gemini") && id.includes("2.5")) {
|
|
1010
|
-
return {
|
|
1011
|
-
high: {
|
|
1012
|
-
thinkingConfig: {
|
|
1013
|
-
includeThoughts: true,
|
|
1014
|
-
thinkingBudget: 16000,
|
|
1015
|
-
},
|
|
1016
|
-
},
|
|
1017
|
-
max: {
|
|
1018
|
-
thinkingConfig: {
|
|
1019
|
-
includeThoughts: true,
|
|
1020
|
-
thinkingBudget: 24576,
|
|
1021
|
-
},
|
|
1022
|
-
},
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
if (model.api.id.includes("gpt") || /\bo[1-9]/.test(model.api.id)) {
|
|
1026
|
-
return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
|
|
1027
|
-
}
|
|
1028
|
-
return {}
|
|
1029
|
-
}
|
|
1030
|
-
return {}
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
export function options(input: {
|
|
1034
|
-
model: Provider.Model
|
|
1035
|
-
sessionID: string
|
|
1036
|
-
providerOptions?: Record<string, any>
|
|
1037
|
-
}): Record<string, any> {
|
|
1038
|
-
const result: Record<string, any> = {}
|
|
1039
|
-
|
|
1040
|
-
if (
|
|
1041
|
-
input.model.api.npm === "@ai-sdk/google-vertex/anthropic" ||
|
|
1042
|
-
(!input.model.api.id.includes("claude") && input.model.api.npm === "@ai-sdk/anthropic")
|
|
1043
|
-
) {
|
|
1044
|
-
result["toolStreaming"] = false
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
// openai and providers using openai package should set store to false by default.
|
|
1048
|
-
if (
|
|
1049
|
-
input.model.providerID === "openai" ||
|
|
1050
|
-
input.model.api.npm === "@ai-sdk/openai" ||
|
|
1051
|
-
input.model.api.npm === "@ai-sdk/github-copilot"
|
|
1052
|
-
) {
|
|
1053
|
-
result["store"] = false
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
if (input.model.api.npm === "@ai-sdk/azure") {
|
|
1057
|
-
result["store"] = false
|
|
1058
|
-
result["promptCacheKey"] = input.sessionID
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
if (input.model.api.npm === "@openrouter/ai-sdk-provider" || input.model.api.npm === "@llmgateway/ai-sdk-provider") {
|
|
1062
|
-
result["usage"] = {
|
|
1063
|
-
include: true,
|
|
1064
|
-
}
|
|
1065
|
-
if (input.model.api.id.includes("gemini-3")) {
|
|
1066
|
-
result["reasoning"] = { effort: "high" }
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
if (
|
|
1071
|
-
input.model.providerID === "baseten" ||
|
|
1072
|
-
(input.model.providerID === "opencode" && ["kimi-k2-thinking", "glm-4.6"].includes(input.model.api.id))
|
|
1073
|
-
) {
|
|
1074
|
-
result["chat_template_args"] = { enable_thinking: true }
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
if (
|
|
1078
|
-
["zai", "zhipuai"].some((id) => input.model.providerID.includes(id)) &&
|
|
1079
|
-
input.model.api.npm === "@ai-sdk/openai-compatible"
|
|
1080
|
-
) {
|
|
1081
|
-
result["thinking"] = {
|
|
1082
|
-
type: "enabled",
|
|
1083
|
-
clear_thinking: false,
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
if (input.model.providerID === "openai" || input.providerOptions?.setCacheKey) {
|
|
1088
|
-
result["promptCacheKey"] = input.sessionID
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
if (input.model.api.npm === "@ai-sdk/google" || input.model.api.npm === "@ai-sdk/google-vertex") {
|
|
1092
|
-
if (input.model.capabilities.reasoning) {
|
|
1093
|
-
result["thinkingConfig"] = {
|
|
1094
|
-
includeThoughts: true,
|
|
1095
|
-
}
|
|
1096
|
-
if (input.model.api.id.includes("gemini-3")) {
|
|
1097
|
-
result["thinkingConfig"]["thinkingLevel"] = "high"
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
// Enable thinking by default for kimi models using anthropic SDK
|
|
1103
|
-
const modelId = input.model.api.id.toLowerCase()
|
|
1104
|
-
if (
|
|
1105
|
-
(input.model.api.npm === "@ai-sdk/anthropic" || input.model.api.npm === "@ai-sdk/google-vertex/anthropic") &&
|
|
1106
|
-
(modelId.includes("k2p") || modelId.includes("kimi-k2.") || modelId.includes("kimi-k2p"))
|
|
1107
|
-
) {
|
|
1108
|
-
result["thinking"] = {
|
|
1109
|
-
type: "enabled",
|
|
1110
|
-
budgetTokens: Math.min(16_000, Math.floor(input.model.limit.output / 2 - 1)),
|
|
1111
|
-
}
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
// Enable thinking for reasoning models on alibaba-cn (DashScope).
|
|
1115
|
-
// DashScope's OpenAI-compatible API requires `enable_thinking: true` in the request body
|
|
1116
|
-
// to return reasoning_content. Without it, models like kimi-k2.5, qwen-plus, qwen3, qwq,
|
|
1117
|
-
// deepseek-r1, etc. never output thinking/reasoning tokens.
|
|
1118
|
-
// Note: kimi-k2-thinking is excluded as it returns reasoning_content by default.
|
|
1119
|
-
if (
|
|
1120
|
-
input.model.providerID === "alibaba-cn" &&
|
|
1121
|
-
input.model.capabilities.reasoning &&
|
|
1122
|
-
input.model.api.npm === "@ai-sdk/openai-compatible" &&
|
|
1123
|
-
!modelId.includes("kimi-k2-thinking")
|
|
1124
|
-
) {
|
|
1125
|
-
result["enable_thinking"] = true
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
if (input.model.api.npm === "@ai-sdk/azure" && input.model.api.id.includes("gpt-5.5")) {
|
|
1129
|
-
result["reasoningSummary"] = "auto"
|
|
1130
|
-
return result
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
if (input.model.api.id.includes("gpt-5") && !input.model.api.id.includes("gpt-5-chat")) {
|
|
1134
|
-
if (!input.model.api.id.includes("gpt-5-pro")) {
|
|
1135
|
-
result["reasoningEffort"] = "medium"
|
|
1136
|
-
result["reasoningSummary"] = "auto"
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
// Only set textVerbosity for non-chat gpt-5.x models
|
|
1140
|
-
// Chat models (e.g. gpt-5.2-chat-latest) only support "medium" verbosity
|
|
1141
|
-
if (
|
|
1142
|
-
input.model.api.id.includes("gpt-5.") &&
|
|
1143
|
-
!input.model.api.id.includes("codex") &&
|
|
1144
|
-
!input.model.api.id.includes("-chat") &&
|
|
1145
|
-
input.model.providerID !== "azure"
|
|
1146
|
-
) {
|
|
1147
|
-
result["textVerbosity"] = "low"
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
if (input.model.providerID.startsWith("opencode")) {
|
|
1151
|
-
result["promptCacheKey"] = input.sessionID
|
|
1152
|
-
result["include"] = ["reasoning.encrypted_content"]
|
|
1153
|
-
result["reasoningSummary"] = "auto"
|
|
1154
|
-
}
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
if (input.model.providerID === "venice") {
|
|
1158
|
-
result["promptCacheKey"] = input.sessionID
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
if (input.model.providerID === "openrouter") {
|
|
1162
|
-
result["prompt_cache_key"] = input.sessionID
|
|
1163
|
-
}
|
|
1164
|
-
if (input.model.api.npm === "@ai-sdk/gateway") {
|
|
1165
|
-
result["gateway"] = {
|
|
1166
|
-
caching: "auto",
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
return result
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
export function smallOptions(model: Provider.Model) {
|
|
1174
|
-
const small = Object.values(model.variants ?? {})[0] ?? {}
|
|
1175
|
-
if (
|
|
1176
|
-
model.providerID === "openai" ||
|
|
1177
|
-
model.api.npm === "@ai-sdk/openai" ||
|
|
1178
|
-
model.api.npm === "@ai-sdk/github-copilot"
|
|
1179
|
-
) {
|
|
1180
|
-
const base = { store: false }
|
|
1181
|
-
return mergeDeep(base, small)
|
|
1182
|
-
}
|
|
1183
|
-
if (model.providerID === "openrouter" || model.providerID === "llmgateway") {
|
|
1184
|
-
if (Object.keys(small).length === 0 && model.api.id.includes("google")) {
|
|
1185
|
-
return { reasoning: { enabled: false } }
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
if (model.providerID === "venice") {
|
|
1190
|
-
if (Object.keys(small).length > 0) return small
|
|
1191
|
-
return { veniceParameters: { disableThinking: true } }
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
return small
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
// Maps model ID prefix to provider slug used in providerOptions.
|
|
1198
|
-
// Example: "amazon/nova-2-lite" → "bedrock"
|
|
1199
|
-
const SLUG_OVERRIDES: Record<string, string> = {
|
|
1200
|
-
amazon: "bedrock",
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
export function providerOptions(model: Provider.Model, options: { [x: string]: any }) {
|
|
1204
|
-
if (model.api.npm === "@ai-sdk/gateway") {
|
|
1205
|
-
// Gateway providerOptions are split across two namespaces:
|
|
1206
|
-
// - `gateway`: gateway-native routing/caching controls (order, only, byok, etc.)
|
|
1207
|
-
// - `<upstream slug>`: provider-specific model options (anthropic/openai/...)
|
|
1208
|
-
// We keep `gateway` as-is and route every other top-level option under the
|
|
1209
|
-
// model-derived upstream slug.
|
|
1210
|
-
const i = model.api.id.indexOf("/")
|
|
1211
|
-
const rawSlug = i > 0 ? model.api.id.slice(0, i) : undefined
|
|
1212
|
-
const slug = rawSlug ? (SLUG_OVERRIDES[rawSlug] ?? rawSlug) : undefined
|
|
1213
|
-
const gateway = options.gateway
|
|
1214
|
-
const rest = Object.fromEntries(Object.entries(options).filter(([k]) => k !== "gateway"))
|
|
1215
|
-
const has = Object.keys(rest).length > 0
|
|
1216
|
-
|
|
1217
|
-
const result: Record<string, any> = {}
|
|
1218
|
-
if (gateway !== undefined) result.gateway = gateway
|
|
1219
|
-
|
|
1220
|
-
if (has) {
|
|
1221
|
-
if (slug) {
|
|
1222
|
-
// Route model-specific options under the provider slug
|
|
1223
|
-
result[slug] = rest
|
|
1224
|
-
} else if (gateway && typeof gateway === "object" && !Array.isArray(gateway)) {
|
|
1225
|
-
result.gateway = { ...gateway, ...rest }
|
|
1226
|
-
} else {
|
|
1227
|
-
result.gateway = rest
|
|
1228
|
-
}
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
return result
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
|
-
// AI SDK packages that resolve providerOptionsName by splitting the
|
|
1235
|
-
// provider name on "." (e.g. "wafer.ai" -> "wafer") need the same
|
|
1236
|
-
// logic here so the key we write matches the key they read.
|
|
1237
|
-
// Other SDKs (xai, mistral, groq, cohere, etc.) use hardcoded keys
|
|
1238
|
-
// like "xai" or "cohere" - applying .split(".")[0] would break those.
|
|
1239
|
-
const usesDotSplitOptions =
|
|
1240
|
-
model.api.npm === "@ai-sdk/openai-compatible" ||
|
|
1241
|
-
model.api.npm === "@ai-sdk/openai" ||
|
|
1242
|
-
model.api.npm === "@ai-sdk/anthropic"
|
|
1243
|
-
const key = sdkKey(model.api.npm) ?? (usesDotSplitOptions ? model.providerID.split(".")[0] : model.providerID)
|
|
1244
|
-
// @ai-sdk/azure delegates to OpenAIChatLanguageModel which reads from
|
|
1245
|
-
// providerOptions["openai"], but OpenAIResponsesLanguageModel checks
|
|
1246
|
-
// "azure" first. Pass both so model options work on either code path.
|
|
1247
|
-
if (model.api.npm === "@ai-sdk/azure") {
|
|
1248
|
-
return { openai: options, azure: options }
|
|
1249
|
-
}
|
|
1250
|
-
return { [key]: options }
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
export function maxOutputTokens(model: Provider.Model, outputTokenMax = OUTPUT_TOKEN_MAX): number {
|
|
1254
|
-
return Math.min(model.limit.output, outputTokenMax) || outputTokenMax
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
export function schema(model: Provider.Model, schema: JSONSchema7): JSONSchema7 {
|
|
1258
|
-
/*
|
|
1259
|
-
if (["openai", "azure"].includes(providerID)) {
|
|
1260
|
-
if (schema.type === "object" && schema.properties) {
|
|
1261
|
-
for (const [key, value] of Object.entries(schema.properties)) {
|
|
1262
|
-
if (schema.required?.includes(key)) continue
|
|
1263
|
-
schema.properties[key] = {
|
|
1264
|
-
anyOf: [
|
|
1265
|
-
value as JSONSchema.JSONSchema,
|
|
1266
|
-
{
|
|
1267
|
-
type: "null",
|
|
1268
|
-
},
|
|
1269
|
-
],
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
*/
|
|
1275
|
-
|
|
1276
|
-
if (model.providerID === "moonshotai" || model.api.id.toLowerCase().includes("kimi")) {
|
|
1277
|
-
const sanitizeMoonshot = (obj: unknown): unknown => {
|
|
1278
|
-
if (obj === null || typeof obj !== "object") return obj
|
|
1279
|
-
if (Array.isArray(obj)) return obj.map(sanitizeMoonshot)
|
|
1280
|
-
// Moonshot expands $ref before validation and rejects sibling keywords like description on the same node.
|
|
1281
|
-
if ("$ref" in obj && typeof obj.$ref === "string") return { $ref: obj.$ref }
|
|
1282
|
-
const result = Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, sanitizeMoonshot(value)]))
|
|
1283
|
-
// MFJS does not support tuple-style `items` arrays; it requires one schema object for all array items.
|
|
1284
|
-
if (Array.isArray(result.items)) result.items = result.items[0] ?? {}
|
|
1285
|
-
return result
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
const sanitized = sanitizeMoonshot(schema)
|
|
1289
|
-
if (typeof sanitized === "object" && sanitized !== null && !Array.isArray(sanitized)) {
|
|
1290
|
-
schema = sanitized
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
// Convert integer enums to string enums for Google/Gemini
|
|
1295
|
-
if (model.providerID === "google" || model.api.id.includes("gemini")) {
|
|
1296
|
-
const isPlainObject = (node: unknown): node is Record<string, any> =>
|
|
1297
|
-
typeof node === "object" && node !== null && !Array.isArray(node)
|
|
1298
|
-
const hasCombiner = (node: unknown) =>
|
|
1299
|
-
isPlainObject(node) && (Array.isArray(node.anyOf) || Array.isArray(node.oneOf) || Array.isArray(node.allOf))
|
|
1300
|
-
const hasSchemaIntent = (node: unknown) => {
|
|
1301
|
-
if (!isPlainObject(node)) return false
|
|
1302
|
-
if (hasCombiner(node)) return true
|
|
1303
|
-
return [
|
|
1304
|
-
"type",
|
|
1305
|
-
"properties",
|
|
1306
|
-
"items",
|
|
1307
|
-
"prefixItems",
|
|
1308
|
-
"enum",
|
|
1309
|
-
"const",
|
|
1310
|
-
"$ref",
|
|
1311
|
-
"additionalProperties",
|
|
1312
|
-
"patternProperties",
|
|
1313
|
-
"required",
|
|
1314
|
-
"not",
|
|
1315
|
-
"if",
|
|
1316
|
-
"then",
|
|
1317
|
-
"else",
|
|
1318
|
-
].some((key) => key in node)
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
const sanitizeGemini = (obj: any): any => {
|
|
1322
|
-
if (obj === null || typeof obj !== "object") {
|
|
1323
|
-
return obj
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
if (Array.isArray(obj)) {
|
|
1327
|
-
return obj.map(sanitizeGemini)
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
const result: any = {}
|
|
1331
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
1332
|
-
if (key === "enum" && Array.isArray(value)) {
|
|
1333
|
-
// Convert all enum values to strings
|
|
1334
|
-
result[key] = value.map((v) => String(v))
|
|
1335
|
-
// If we have integer type with enum, change type to string
|
|
1336
|
-
if (result.type === "integer" || result.type === "number") {
|
|
1337
|
-
result.type = "string"
|
|
1338
|
-
}
|
|
1339
|
-
} else if (typeof value === "object" && value !== null) {
|
|
1340
|
-
result[key] = sanitizeGemini(value)
|
|
1341
|
-
} else {
|
|
1342
|
-
result[key] = value
|
|
1343
|
-
}
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
// Filter required array to only include fields that exist in properties
|
|
1347
|
-
if (result.type === "object" && result.properties && Array.isArray(result.required)) {
|
|
1348
|
-
result.required = result.required.filter((field: any) => field in result.properties)
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
if (result.type === "array" && !hasCombiner(result)) {
|
|
1352
|
-
if (result.items == null) {
|
|
1353
|
-
result.items = {}
|
|
1354
|
-
}
|
|
1355
|
-
// Ensure items has a type only when it's still schema-empty.
|
|
1356
|
-
if (isPlainObject(result.items) && !hasSchemaIntent(result.items)) {
|
|
1357
|
-
result.items.type = "string"
|
|
1358
|
-
}
|
|
1359
|
-
}
|
|
1360
|
-
|
|
1361
|
-
// Remove properties/required from non-object types (Gemini rejects these)
|
|
1362
|
-
if (result.type && result.type !== "object" && !hasCombiner(result)) {
|
|
1363
|
-
delete result.properties
|
|
1364
|
-
delete result.required
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
return result
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
schema = sanitizeGemini(schema)
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1373
|
-
return schema
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
export * as ProviderTransform from "./transform"
|
|
1
|
+
import type { ModelMessage, ToolResultPart } from "ai"
|
|
2
|
+
import { mergeDeep, unique } from "remeda"
|
|
3
|
+
import type { JSONSchema7 } from "@ai-sdk/provider"
|
|
4
|
+
import type * as Provider from "./provider"
|
|
5
|
+
import type * as ModelsDev from "@opencode-ai/core/models"
|
|
6
|
+
import { iife } from "@/util/iife"
|
|
7
|
+
|
|
8
|
+
type Modality = NonNullable<ModelsDev.Model["modalities"]>["input"][number]
|
|
9
|
+
|
|
10
|
+
function mimeToModality(mime: string): Modality | undefined {
|
|
11
|
+
if (mime.startsWith("image/")) return "image"
|
|
12
|
+
if (mime.startsWith("audio/")) return "audio"
|
|
13
|
+
if (mime.startsWith("video/")) return "video"
|
|
14
|
+
if (mime === "application/pdf") return "pdf"
|
|
15
|
+
return undefined
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const OUTPUT_TOKEN_MAX = 32_000
|
|
19
|
+
|
|
20
|
+
export function sanitizeSurrogates(content: string) {
|
|
21
|
+
return content.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "\uFFFD")
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Maps npm package to the key the AI SDK expects for providerOptions
|
|
25
|
+
function sdkKey(npm: string): string | undefined {
|
|
26
|
+
switch (npm) {
|
|
27
|
+
case "@ai-sdk/github-copilot":
|
|
28
|
+
return "copilot"
|
|
29
|
+
case "@ai-sdk/azure":
|
|
30
|
+
return "azure"
|
|
31
|
+
case "@ai-sdk/openai":
|
|
32
|
+
return "openai"
|
|
33
|
+
case "@ai-sdk/amazon-bedrock":
|
|
34
|
+
return "bedrock"
|
|
35
|
+
case "@ai-sdk/anthropic":
|
|
36
|
+
case "@ai-sdk/google-vertex/anthropic":
|
|
37
|
+
return "anthropic"
|
|
38
|
+
case "@ai-sdk/google-vertex":
|
|
39
|
+
return "vertex"
|
|
40
|
+
case "@ai-sdk/google":
|
|
41
|
+
return "google"
|
|
42
|
+
case "@ai-sdk/gateway":
|
|
43
|
+
return "gateway"
|
|
44
|
+
case "@openrouter/ai-sdk-provider":
|
|
45
|
+
return "openrouter"
|
|
46
|
+
case "ai-gateway-provider":
|
|
47
|
+
// ai-gateway-provider/unified wraps createOpenAICompatible({ name: "Unified" }),
|
|
48
|
+
// and @ai-sdk/openai-compatible parses compatibleOptions from one of
|
|
49
|
+
// "openai-compatible" / "openaiCompatible" / "Unified" / "unified". The
|
|
50
|
+
// "openai-compatible" key emits a deprecation warning at runtime, so we
|
|
51
|
+
// pick the camelCase form the SDK now treats as canonical.
|
|
52
|
+
return "openaiCompatible"
|
|
53
|
+
}
|
|
54
|
+
return undefined
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// TODO: fix this stupid inefficient dogshit function
|
|
58
|
+
function normalizeMessages(
|
|
59
|
+
msgs: ModelMessage[],
|
|
60
|
+
model: Provider.Model,
|
|
61
|
+
_options: Record<string, unknown>,
|
|
62
|
+
): ModelMessage[] {
|
|
63
|
+
const sanitizeToolResultOutput = (content: ToolResultPart) => {
|
|
64
|
+
if (content.output.type === "text" || content.output.type === "error-text") {
|
|
65
|
+
content.output.value = sanitizeSurrogates(content.output.value)
|
|
66
|
+
}
|
|
67
|
+
if (content.output.type === "content") {
|
|
68
|
+
content.output.value = content.output.value.map((item) => {
|
|
69
|
+
if (item.type === "text") {
|
|
70
|
+
item.text = sanitizeSurrogates(item.text)
|
|
71
|
+
}
|
|
72
|
+
return item
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
return content
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
msgs = msgs.map((msg) => {
|
|
79
|
+
switch (msg.role) {
|
|
80
|
+
case "tool":
|
|
81
|
+
if (!Array.isArray(msg.content)) return msg
|
|
82
|
+
msg.content = msg.content.map((content) => {
|
|
83
|
+
if (content.type === "tool-result") {
|
|
84
|
+
return sanitizeToolResultOutput(content)
|
|
85
|
+
}
|
|
86
|
+
return content
|
|
87
|
+
})
|
|
88
|
+
return msg
|
|
89
|
+
|
|
90
|
+
case "system":
|
|
91
|
+
msg.content = sanitizeSurrogates(msg.content)
|
|
92
|
+
return msg
|
|
93
|
+
|
|
94
|
+
case "user":
|
|
95
|
+
if (typeof msg.content === "string") {
|
|
96
|
+
msg.content = sanitizeSurrogates(msg.content)
|
|
97
|
+
} else {
|
|
98
|
+
msg.content = msg.content.map((content) => {
|
|
99
|
+
if (content.type === "text") {
|
|
100
|
+
content.text = sanitizeSurrogates(content.text)
|
|
101
|
+
}
|
|
102
|
+
return content
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
return msg
|
|
106
|
+
|
|
107
|
+
case "assistant":
|
|
108
|
+
if (typeof msg.content === "string") {
|
|
109
|
+
msg.content = sanitizeSurrogates(msg.content)
|
|
110
|
+
} else {
|
|
111
|
+
msg.content = msg.content.map((content) => {
|
|
112
|
+
if (content.type === "text" || content.type === "reasoning") {
|
|
113
|
+
content.text = sanitizeSurrogates(content.text)
|
|
114
|
+
}
|
|
115
|
+
if (content.type === "tool-result") {
|
|
116
|
+
return sanitizeToolResultOutput(content)
|
|
117
|
+
}
|
|
118
|
+
return content
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
return msg
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
// Anthropic rejects messages with empty content - filter out empty string messages
|
|
126
|
+
// and remove empty text/reasoning parts from array content
|
|
127
|
+
if (model.api.npm === "@ai-sdk/anthropic") {
|
|
128
|
+
msgs = msgs
|
|
129
|
+
.map((msg) => {
|
|
130
|
+
if (typeof msg.content === "string") {
|
|
131
|
+
if (msg.content === "") return undefined
|
|
132
|
+
return msg
|
|
133
|
+
}
|
|
134
|
+
if (!Array.isArray(msg.content)) return msg
|
|
135
|
+
const filtered = msg.content.filter((part) => {
|
|
136
|
+
if (part.type === "text") {
|
|
137
|
+
return part.text !== ""
|
|
138
|
+
}
|
|
139
|
+
if (part.type === "reasoning") {
|
|
140
|
+
return (
|
|
141
|
+
part.text.trim().length > 0 ||
|
|
142
|
+
part.providerOptions?.anthropic?.signature != null ||
|
|
143
|
+
part.providerOptions?.anthropic?.redactedData != null
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
return true
|
|
147
|
+
})
|
|
148
|
+
if (filtered.length === 0) return undefined
|
|
149
|
+
return { ...msg, content: filtered }
|
|
150
|
+
})
|
|
151
|
+
.filter((msg): msg is ModelMessage => msg !== undefined && msg.content !== "")
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Bedrock specific transforms
|
|
155
|
+
if (model.api.npm === "@ai-sdk/amazon-bedrock") {
|
|
156
|
+
msgs = msgs
|
|
157
|
+
.map((msg) => {
|
|
158
|
+
if (typeof msg.content === "string") {
|
|
159
|
+
if (msg.content === "") return undefined
|
|
160
|
+
return msg
|
|
161
|
+
}
|
|
162
|
+
if (!Array.isArray(msg.content)) return msg
|
|
163
|
+
const filtered = msg.content.filter((part) => {
|
|
164
|
+
if (part.type === "text") {
|
|
165
|
+
return part.text !== ""
|
|
166
|
+
}
|
|
167
|
+
if (part.type === "reasoning") {
|
|
168
|
+
return (
|
|
169
|
+
part.text.trim().length > 0 ||
|
|
170
|
+
part.providerOptions?.bedrock?.signature != null ||
|
|
171
|
+
part.providerOptions?.bedrock?.redactedData != null
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
return true
|
|
175
|
+
})
|
|
176
|
+
if (filtered.length === 0) return undefined
|
|
177
|
+
return { ...msg, content: filtered }
|
|
178
|
+
})
|
|
179
|
+
.filter((msg): msg is ModelMessage => msg !== undefined && msg.content !== "")
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (model.api.id.includes("claude")) {
|
|
183
|
+
const scrub = (id: string) => id.replace(/[^a-zA-Z0-9_-]/g, "_")
|
|
184
|
+
msgs = msgs.map((msg) => {
|
|
185
|
+
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
|
186
|
+
return {
|
|
187
|
+
...msg,
|
|
188
|
+
content: msg.content.map((part) => {
|
|
189
|
+
if (part.type === "tool-call" || part.type === "tool-result") {
|
|
190
|
+
return { ...part, toolCallId: scrub(part.toolCallId) }
|
|
191
|
+
}
|
|
192
|
+
return part
|
|
193
|
+
}),
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (msg.role === "tool" && Array.isArray(msg.content)) {
|
|
197
|
+
return {
|
|
198
|
+
...msg,
|
|
199
|
+
content: msg.content.map((part) => {
|
|
200
|
+
if (part.type === "tool-result") {
|
|
201
|
+
return { ...part, toolCallId: scrub(part.toolCallId) }
|
|
202
|
+
}
|
|
203
|
+
return part
|
|
204
|
+
}),
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return msg
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
if (["@ai-sdk/anthropic", "@ai-sdk/google-vertex/anthropic"].includes(model.api.npm)) {
|
|
211
|
+
// Anthropic rejects assistant turns where tool_use blocks are followed by non-tool
|
|
212
|
+
// content, e.g. [tool_use, tool_use, text], with:
|
|
213
|
+
// `tool_use` ids were found without `tool_result` blocks immediately after...
|
|
214
|
+
//
|
|
215
|
+
// Reorder that invalid shape into [text] + [tool_use, tool_use]. Consecutive
|
|
216
|
+
// assistant messages are later merged by the provider/SDK, so preserving the
|
|
217
|
+
// original [tool_use...] then [text] order still produces the invalid payload.
|
|
218
|
+
//
|
|
219
|
+
// The root cause appears to be somewhere upstream where the stream is originally
|
|
220
|
+
// processed. We were unable to locate an exact narrower reproduction elsewhere,
|
|
221
|
+
// so we keep this transform in place for the time being.
|
|
222
|
+
msgs = msgs.flatMap((msg) => {
|
|
223
|
+
if (msg.role !== "assistant" || !Array.isArray(msg.content)) return [msg]
|
|
224
|
+
|
|
225
|
+
const parts = msg.content
|
|
226
|
+
const first = parts.findIndex((part) => part.type === "tool-call")
|
|
227
|
+
if (first === -1) return [msg]
|
|
228
|
+
if (!parts.slice(first).some((part) => part.type !== "tool-call")) return [msg]
|
|
229
|
+
return [
|
|
230
|
+
{ ...msg, content: parts.filter((part) => part.type !== "tool-call") },
|
|
231
|
+
{ ...msg, content: parts.filter((part) => part.type === "tool-call") },
|
|
232
|
+
]
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
if (
|
|
236
|
+
model.providerID === "mistral" ||
|
|
237
|
+
model.api.id.toLowerCase().includes("mistral") ||
|
|
238
|
+
model.api.id.toLocaleLowerCase().includes("devstral")
|
|
239
|
+
) {
|
|
240
|
+
const scrub = (id: string) => {
|
|
241
|
+
return id
|
|
242
|
+
.replace(/[^a-zA-Z0-9]/g, "") // Remove non-alphanumeric characters
|
|
243
|
+
.substring(0, 9) // Take first 9 characters
|
|
244
|
+
.padEnd(9, "0") // Pad with zeros if less than 9 characters
|
|
245
|
+
}
|
|
246
|
+
const result: ModelMessage[] = []
|
|
247
|
+
for (let i = 0; i < msgs.length; i++) {
|
|
248
|
+
const msg = msgs[i]
|
|
249
|
+
const nextMsg = msgs[i + 1]
|
|
250
|
+
|
|
251
|
+
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
|
252
|
+
msg.content = msg.content.map((part) => {
|
|
253
|
+
if (part.type === "tool-call" || part.type === "tool-result") {
|
|
254
|
+
return { ...part, toolCallId: scrub(part.toolCallId) }
|
|
255
|
+
}
|
|
256
|
+
return part
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
if (msg.role === "tool" && Array.isArray(msg.content)) {
|
|
260
|
+
msg.content = msg.content.map((part) => {
|
|
261
|
+
if (part.type === "tool-result") {
|
|
262
|
+
return { ...part, toolCallId: scrub(part.toolCallId) }
|
|
263
|
+
}
|
|
264
|
+
return part
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
result.push(msg)
|
|
268
|
+
|
|
269
|
+
// Fix message sequence: tool messages cannot be followed by user messages
|
|
270
|
+
if (msg.role === "tool" && nextMsg?.role === "user") {
|
|
271
|
+
result.push({
|
|
272
|
+
role: "assistant",
|
|
273
|
+
content: [
|
|
274
|
+
{
|
|
275
|
+
type: "text",
|
|
276
|
+
text: "Done.",
|
|
277
|
+
},
|
|
278
|
+
],
|
|
279
|
+
})
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return result
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Deepseek requires all assistant messages to have reasoning on them
|
|
286
|
+
if (model.api.id.toLowerCase().includes("deepseek")) {
|
|
287
|
+
msgs = msgs.map((msg) => {
|
|
288
|
+
if (msg.role !== "assistant") return msg
|
|
289
|
+
if (Array.isArray(msg.content)) {
|
|
290
|
+
if (msg.content.some((part) => part.type === "reasoning")) return msg
|
|
291
|
+
return { ...msg, content: [...msg.content, { type: "reasoning", text: "" }] }
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
...msg,
|
|
295
|
+
content: [
|
|
296
|
+
...(msg.content ? [{ type: "text" as const, text: msg.content }] : []),
|
|
297
|
+
{ type: "reasoning" as const, text: "" },
|
|
298
|
+
],
|
|
299
|
+
}
|
|
300
|
+
})
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (
|
|
304
|
+
typeof model.capabilities.interleaved === "object" &&
|
|
305
|
+
model.capabilities.interleaved.field &&
|
|
306
|
+
model.api.npm !== "@openrouter/ai-sdk-provider"
|
|
307
|
+
) {
|
|
308
|
+
const field = model.capabilities.interleaved.field
|
|
309
|
+
return msgs.map((msg) => {
|
|
310
|
+
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
|
311
|
+
const reasoningParts = msg.content.filter((part: any) => part.type === "reasoning")
|
|
312
|
+
const reasoningText = reasoningParts.map((part: any) => part.text).join("")
|
|
313
|
+
|
|
314
|
+
// Filter out reasoning parts from content
|
|
315
|
+
const filteredContent = msg.content.filter((part: any) => part.type !== "reasoning")
|
|
316
|
+
|
|
317
|
+
// Include reasoning_content | reasoning_details directly on the message for all assistant messages.
|
|
318
|
+
// Always set the field even when empty — some providers (e.g. DeepSeek) may return empty
|
|
319
|
+
// reasoning_content which still needs to be sent back in subsequent requests.
|
|
320
|
+
return {
|
|
321
|
+
...msg,
|
|
322
|
+
content: filteredContent,
|
|
323
|
+
providerOptions: {
|
|
324
|
+
...msg.providerOptions,
|
|
325
|
+
openaiCompatible: {
|
|
326
|
+
...msg.providerOptions?.openaiCompatible,
|
|
327
|
+
[field]: reasoningText,
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return msg
|
|
334
|
+
})
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return msgs
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function applyCaching(msgs: ModelMessage[], model: Provider.Model): ModelMessage[] {
|
|
341
|
+
const system = msgs.filter((msg) => msg.role === "system").slice(0, 2)
|
|
342
|
+
const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
|
|
343
|
+
|
|
344
|
+
const providerOptions = {
|
|
345
|
+
anthropic: {
|
|
346
|
+
cacheControl: { type: "ephemeral" },
|
|
347
|
+
},
|
|
348
|
+
openrouter: {
|
|
349
|
+
cacheControl: { type: "ephemeral" },
|
|
350
|
+
},
|
|
351
|
+
bedrock: {
|
|
352
|
+
cachePoint: { type: "default" },
|
|
353
|
+
},
|
|
354
|
+
openaiCompatible: {
|
|
355
|
+
cache_control: { type: "ephemeral" },
|
|
356
|
+
},
|
|
357
|
+
copilot: {
|
|
358
|
+
copilot_cache_control: { type: "ephemeral" },
|
|
359
|
+
},
|
|
360
|
+
alibaba: {
|
|
361
|
+
cacheControl: { type: "ephemeral" },
|
|
362
|
+
},
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
for (const msg of unique([...system, ...final])) {
|
|
366
|
+
const useMessageLevelOptions =
|
|
367
|
+
model.providerID === "anthropic" ||
|
|
368
|
+
model.providerID.includes("bedrock") ||
|
|
369
|
+
model.api.npm === "@ai-sdk/amazon-bedrock"
|
|
370
|
+
const shouldUseContentOptions = !useMessageLevelOptions && Array.isArray(msg.content) && msg.content.length > 0
|
|
371
|
+
|
|
372
|
+
if (shouldUseContentOptions) {
|
|
373
|
+
const lastContent = msg.content[msg.content.length - 1]
|
|
374
|
+
if (
|
|
375
|
+
lastContent &&
|
|
376
|
+
typeof lastContent === "object" &&
|
|
377
|
+
lastContent.type !== "tool-approval-request" &&
|
|
378
|
+
lastContent.type !== "tool-approval-response"
|
|
379
|
+
) {
|
|
380
|
+
lastContent.providerOptions = mergeDeep(lastContent.providerOptions ?? {}, providerOptions)
|
|
381
|
+
continue
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
msg.providerOptions = mergeDeep(msg.providerOptions ?? {}, providerOptions)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return msgs
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function unsupportedParts(msgs: ModelMessage[], model: Provider.Model): ModelMessage[] {
|
|
392
|
+
return msgs.map((msg) => {
|
|
393
|
+
if (msg.role !== "user" || !Array.isArray(msg.content)) return msg
|
|
394
|
+
|
|
395
|
+
const filtered = msg.content.map((part) => {
|
|
396
|
+
if (part.type !== "file" && part.type !== "image") return part
|
|
397
|
+
|
|
398
|
+
// Check for empty base64 image data
|
|
399
|
+
if (part.type === "image") {
|
|
400
|
+
const imageStr = String(part.image)
|
|
401
|
+
if (imageStr.startsWith("data:")) {
|
|
402
|
+
const match = imageStr.match(/^data:([^;]+);base64,(.*)$/)
|
|
403
|
+
if (match && (!match[2] || match[2].length === 0)) {
|
|
404
|
+
return {
|
|
405
|
+
type: "text" as const,
|
|
406
|
+
text: "ERROR: Image file is empty or corrupted. Please provide a valid image.",
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const mime = part.type === "image" ? String(part.image).split(";")[0].replace("data:", "") : part.mediaType
|
|
413
|
+
const filename = part.type === "file" ? part.filename : undefined
|
|
414
|
+
const modality = mimeToModality(mime)
|
|
415
|
+
if (!modality) return part
|
|
416
|
+
if (model.capabilities.input[modality]) return part
|
|
417
|
+
|
|
418
|
+
const name = filename ? `"${filename}"` : modality
|
|
419
|
+
return {
|
|
420
|
+
type: "text" as const,
|
|
421
|
+
text: `ERROR: Cannot read ${name} (this model does not support ${modality} input). Inform the user.`,
|
|
422
|
+
}
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
return { ...msg, content: filtered }
|
|
426
|
+
})
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export function message(msgs: ModelMessage[], model: Provider.Model, options: Record<string, unknown>) {
|
|
430
|
+
msgs = unsupportedParts(msgs, model)
|
|
431
|
+
msgs = normalizeMessages(msgs, model, options)
|
|
432
|
+
if (
|
|
433
|
+
(model.providerID === "anthropic" ||
|
|
434
|
+
model.providerID === "google-vertex-anthropic" ||
|
|
435
|
+
model.api.id.includes("anthropic") ||
|
|
436
|
+
model.api.id.includes("claude") ||
|
|
437
|
+
model.id.includes("anthropic") ||
|
|
438
|
+
model.id.includes("claude") ||
|
|
439
|
+
model.api.npm === "@ai-sdk/anthropic" ||
|
|
440
|
+
model.api.npm === "@ai-sdk/alibaba") &&
|
|
441
|
+
model.api.npm !== "@ai-sdk/gateway"
|
|
442
|
+
) {
|
|
443
|
+
msgs = applyCaching(msgs, model)
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Remap providerOptions keys from stored providerID to expected SDK key
|
|
447
|
+
const key = sdkKey(model.api.npm)
|
|
448
|
+
if (key && key !== model.providerID) {
|
|
449
|
+
const remap = (opts: Record<string, any> | undefined) => {
|
|
450
|
+
if (!opts) return opts
|
|
451
|
+
if (!(model.providerID in opts)) return opts
|
|
452
|
+
const result = { ...opts }
|
|
453
|
+
result[key] = result[model.providerID]
|
|
454
|
+
delete result[model.providerID]
|
|
455
|
+
return result
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
msgs = msgs.map((msg) => {
|
|
459
|
+
if (!Array.isArray(msg.content)) return { ...msg, providerOptions: remap(msg.providerOptions) }
|
|
460
|
+
return {
|
|
461
|
+
...msg,
|
|
462
|
+
providerOptions: remap(msg.providerOptions),
|
|
463
|
+
content: msg.content.map((part) => {
|
|
464
|
+
if (part.type === "tool-approval-request" || part.type === "tool-approval-response") {
|
|
465
|
+
return { ...part }
|
|
466
|
+
}
|
|
467
|
+
return { ...part, providerOptions: remap(part.providerOptions) }
|
|
468
|
+
}),
|
|
469
|
+
} as typeof msg
|
|
470
|
+
})
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return msgs
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export function temperature(model: Provider.Model) {
|
|
477
|
+
const id = model.id.toLowerCase()
|
|
478
|
+
if (id.includes("qwen")) return 0.55
|
|
479
|
+
if (id.includes("claude")) return undefined
|
|
480
|
+
if (id.includes("gemini")) return 1.0
|
|
481
|
+
if (id.includes("glm-4.6")) return 1.0
|
|
482
|
+
if (id.includes("glm-4.7")) return 1.0
|
|
483
|
+
if (id.includes("minimax-m2")) return 1.0
|
|
484
|
+
if (id.includes("kimi-k2")) {
|
|
485
|
+
// kimi-k2-thinking & kimi-k2.5 && kimi-k2p5 && kimi-k2-5
|
|
486
|
+
if (["thinking", "k2.", "k2p", "k2-5"].some((s) => id.includes(s))) {
|
|
487
|
+
return 1.0
|
|
488
|
+
}
|
|
489
|
+
return 0.6
|
|
490
|
+
}
|
|
491
|
+
return undefined
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export function topP(model: Provider.Model) {
|
|
495
|
+
const id = model.id.toLowerCase()
|
|
496
|
+
if (id.includes("qwen")) return 1
|
|
497
|
+
if (["minimax-m2", "gemini", "kimi-k2.5", "kimi-k2p5", "kimi-k2-5"].some((s) => id.includes(s))) {
|
|
498
|
+
return 0.95
|
|
499
|
+
}
|
|
500
|
+
return undefined
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export function topK(model: Provider.Model) {
|
|
504
|
+
const id = model.id.toLowerCase()
|
|
505
|
+
if (id.includes("minimax-m2")) {
|
|
506
|
+
if (["m2.", "m25", "m21"].some((s) => id.includes(s))) return 40
|
|
507
|
+
return 20
|
|
508
|
+
}
|
|
509
|
+
if (id.includes("gemini")) return 64
|
|
510
|
+
return undefined
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const WIDELY_SUPPORTED_EFFORTS = ["low", "medium", "high"]
|
|
514
|
+
const OPENAI_EFFORTS = ["none", "minimal", ...WIDELY_SUPPORTED_EFFORTS, "xhigh"]
|
|
515
|
+
const OPENAI_GPT5_1_EFFORTS = ["none", ...WIDELY_SUPPORTED_EFFORTS]
|
|
516
|
+
const OPENAI_GPT5_2_PLUS_EFFORTS = [...OPENAI_GPT5_1_EFFORTS, "xhigh"]
|
|
517
|
+
const OPENAI_GPT5_PRO_EFFORTS = ["high"]
|
|
518
|
+
const OPENAI_GPT5_PRO_2_PLUS_EFFORTS = ["medium", "high", "xhigh"]
|
|
519
|
+
const OPENAI_GPT5_CHAT_EFFORTS = ["medium"]
|
|
520
|
+
const OPENAI_GPT5_CODEX_XHIGH_EFFORTS = [...WIDELY_SUPPORTED_EFFORTS, "xhigh"]
|
|
521
|
+
const OPENAI_GPT5_CODEX_3_PLUS_EFFORTS = ["none", ...OPENAI_GPT5_CODEX_XHIGH_EFFORTS]
|
|
522
|
+
|
|
523
|
+
// OpenAI rolled out the `none` reasoning_effort tier on this date (Responses API).
|
|
524
|
+
// Models released before it 400 on `reasoning_effort: "none"`, so we only expose
|
|
525
|
+
// it as a variant for models new enough to accept it.
|
|
526
|
+
const OPENAI_NONE_EFFORT_RELEASE_DATE = "2025-11-13"
|
|
527
|
+
|
|
528
|
+
// OpenAI rolled out the `xhigh` reasoning_effort tier on this date. Same reasoning.
|
|
529
|
+
const OPENAI_XHIGH_EFFORT_RELEASE_DATE = "2025-12-04"
|
|
530
|
+
|
|
531
|
+
// Matches members of the gpt-5 family across the id formats we encounter:
|
|
532
|
+
// "gpt-5", "gpt-5-nano", "gpt-5.4", "openai/gpt-5.4-codex".
|
|
533
|
+
// Anchored to start-of-string or "/" so it doesn't false-match "gpt-50" or "gpt-5o".
|
|
534
|
+
const GPT5_FAMILY_RE = /(?:^|\/)gpt-5(?:[.-]|$)/
|
|
535
|
+
const GPT5_VERSION_RE = /(?:^|\/)gpt-5[.-](\d+)(?:[.-]|$)/
|
|
536
|
+
const GPT5_PRO_RE = /(?:^|\/)gpt-5[.-]?pro(?:[.-]|$)/
|
|
537
|
+
const GPT5_VERSIONED_PRO_RE = /(?:^|\/)gpt-5[.-]\d+[.-]pro(?:[.-]|$)/
|
|
538
|
+
|
|
539
|
+
function gpt5Version(apiId: string) {
|
|
540
|
+
return Number(GPT5_VERSION_RE.exec(apiId)?.[1]) || undefined
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function versionedGpt5ReasoningEfforts(apiId: string) {
|
|
544
|
+
if (GPT5_VERSIONED_PRO_RE.test(apiId)) return OPENAI_GPT5_PRO_2_PLUS_EFFORTS
|
|
545
|
+
const version = gpt5Version(apiId)
|
|
546
|
+
if (version === undefined) return undefined
|
|
547
|
+
if (version === 1) return OPENAI_GPT5_1_EFFORTS
|
|
548
|
+
return OPENAI_GPT5_2_PLUS_EFFORTS
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function gpt5CodexReasoningEfforts(apiId: string) {
|
|
552
|
+
if (!GPT5_FAMILY_RE.test(apiId) || !apiId.includes("codex")) return undefined
|
|
553
|
+
const version = gpt5Version(apiId)
|
|
554
|
+
if (version !== undefined && version >= 3) return OPENAI_GPT5_CODEX_3_PLUS_EFFORTS
|
|
555
|
+
if (apiId.includes("codex-max") || (version !== undefined && version >= 2)) return OPENAI_GPT5_CODEX_XHIGH_EFFORTS
|
|
556
|
+
return WIDELY_SUPPORTED_EFFORTS
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function gpt5ChatReasoningEfforts(apiId: string) {
|
|
560
|
+
if (!GPT5_FAMILY_RE.test(apiId) || !apiId.includes("-chat")) return undefined
|
|
561
|
+
return gpt5Version(apiId) === undefined ? [] : OPENAI_GPT5_CHAT_EFFORTS
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Computes the reasoning_effort tiers an OpenAI (or OpenAI-compatible upstream
|
|
565
|
+
// routed through it, e.g. cf-ai-gateway) model exposes. Effort order: weakest
|
|
566
|
+
// to strongest.
|
|
567
|
+
function openaiReasoningEfforts(apiId: string, releaseDate: string) {
|
|
568
|
+
const id = apiId.toLowerCase()
|
|
569
|
+
if (id.includes("deep-research")) return ["medium"]
|
|
570
|
+
const chatEfforts = gpt5ChatReasoningEfforts(id)
|
|
571
|
+
if (chatEfforts) return chatEfforts
|
|
572
|
+
if (GPT5_PRO_RE.test(id)) return OPENAI_GPT5_PRO_EFFORTS
|
|
573
|
+
const codexEfforts = gpt5CodexReasoningEfforts(id)
|
|
574
|
+
if (codexEfforts) return codexEfforts
|
|
575
|
+
const versionedEfforts = versionedGpt5ReasoningEfforts(id)
|
|
576
|
+
// GPT-5.1 replaced GPT-5's `minimal` effort with `none`; GPT-5.2+
|
|
577
|
+
// additionally accepts `xhigh`. Model pages list the supported subset.
|
|
578
|
+
if (versionedEfforts) return versionedEfforts
|
|
579
|
+
const efforts = [...WIDELY_SUPPORTED_EFFORTS]
|
|
580
|
+
if (GPT5_FAMILY_RE.test(id)) efforts.unshift("minimal")
|
|
581
|
+
if (releaseDate >= OPENAI_NONE_EFFORT_RELEASE_DATE) efforts.unshift("none")
|
|
582
|
+
if (releaseDate >= OPENAI_XHIGH_EFFORT_RELEASE_DATE) efforts.push("xhigh")
|
|
583
|
+
return efforts
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function openaiCompatibleReasoningEfforts(id: string) {
|
|
587
|
+
const apiId = id.toLowerCase()
|
|
588
|
+
const chatEfforts = gpt5ChatReasoningEfforts(apiId)
|
|
589
|
+
if (chatEfforts) return chatEfforts
|
|
590
|
+
if (GPT5_PRO_RE.test(apiId)) return OPENAI_GPT5_PRO_EFFORTS
|
|
591
|
+
return gpt5CodexReasoningEfforts(apiId) ?? versionedGpt5ReasoningEfforts(apiId) ?? OPENAI_EFFORTS
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function anthropicAdaptiveEfforts(apiId: string): string[] | null {
|
|
595
|
+
if (["opus-4-7", "opus-4.7"].some((v) => apiId.includes(v))) {
|
|
596
|
+
return ["low", "medium", "high", "xhigh", "max"]
|
|
597
|
+
}
|
|
598
|
+
if (["opus-4-6", "opus-4.6", "sonnet-4-6", "sonnet-4.6"].some((v) => apiId.includes(v))) {
|
|
599
|
+
return ["low", "medium", "high", "max"]
|
|
600
|
+
}
|
|
601
|
+
return null
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function googleThinkingLevelEfforts(apiId: string) {
|
|
605
|
+
const id = apiId.toLowerCase()
|
|
606
|
+
if (!id.includes("gemini-3")) return ["low", "high"]
|
|
607
|
+
if (id.includes("flash-image")) return ["minimal", "high"]
|
|
608
|
+
if (id.includes("pro-image")) return ["high"]
|
|
609
|
+
if (id.includes("flash")) return ["minimal", "low", "medium", "high"]
|
|
610
|
+
return ["low", "medium", "high"]
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function googleThinkingBudgetMax(apiId: string) {
|
|
614
|
+
const id = apiId.toLowerCase()
|
|
615
|
+
if (id.includes("2.5") && id.includes("pro") && !id.includes("flash")) return 32_768
|
|
616
|
+
return 24_576
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
export function variants(model: Provider.Model): Record<string, Record<string, any>> {
|
|
620
|
+
if (!model.capabilities.reasoning) return {}
|
|
621
|
+
|
|
622
|
+
const id = model.id.toLowerCase()
|
|
623
|
+
const adaptiveEfforts = anthropicAdaptiveEfforts(model.api.id)
|
|
624
|
+
if (
|
|
625
|
+
id.includes("deepseek-chat") ||
|
|
626
|
+
id.includes("deepseek-reasoner") ||
|
|
627
|
+
id.includes("deepseek-r1") ||
|
|
628
|
+
id.includes("deepseek-v3") ||
|
|
629
|
+
id.includes("minimax") ||
|
|
630
|
+
id.includes("glm") ||
|
|
631
|
+
id.includes("kimi") ||
|
|
632
|
+
id.includes("k2p") ||
|
|
633
|
+
id.includes("qwen") ||
|
|
634
|
+
id.includes("big-pickle")
|
|
635
|
+
)
|
|
636
|
+
return {}
|
|
637
|
+
|
|
638
|
+
// see: https://docs.x.ai/docs/guides/reasoning#control-how-hard-the-model-thinks
|
|
639
|
+
if (id.includes("grok") && id.includes("grok-3-mini")) {
|
|
640
|
+
if (model.api.npm === "@openrouter/ai-sdk-provider") {
|
|
641
|
+
return {
|
|
642
|
+
low: { reasoning: { effort: "low" } },
|
|
643
|
+
high: { reasoning: { effort: "high" } },
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
return {
|
|
647
|
+
low: { reasoningEffort: "low" },
|
|
648
|
+
high: { reasoningEffort: "high" },
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
if (id.includes("grok")) return {}
|
|
652
|
+
|
|
653
|
+
switch (model.api.npm) {
|
|
654
|
+
case "@openrouter/ai-sdk-provider":
|
|
655
|
+
if (!id.includes("gpt") && !id.includes("gemini-3") && !id.includes("claude")) return {}
|
|
656
|
+
return Object.fromEntries(
|
|
657
|
+
(id.includes("gpt") ? openaiCompatibleReasoningEfforts(id) : OPENAI_EFFORTS).map((effort) => [
|
|
658
|
+
effort,
|
|
659
|
+
{ reasoning: { effort } },
|
|
660
|
+
]),
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
case "ai-gateway-provider": {
|
|
664
|
+
// Cloudflare AI Gateway routes every upstream through its OpenAI-compatible
|
|
665
|
+
// /v1/compat endpoint, so the body is always OAI-shaped. The gateway
|
|
666
|
+
// translates `reasoning_effort` to the upstream provider's native control
|
|
667
|
+
// (e.g. Anthropic thinking budgets) when needed. Variants therefore stay
|
|
668
|
+
// OAI-style for all upstreams, with an extended effort set for OpenAI
|
|
669
|
+
// models that support it.
|
|
670
|
+
if (model.api.id.startsWith("openai/")) {
|
|
671
|
+
const efforts = openaiReasoningEfforts(model.api.id, model.release_date)
|
|
672
|
+
return Object.fromEntries(efforts.map((effort) => [effort, { reasoningEffort: effort }]))
|
|
673
|
+
}
|
|
674
|
+
return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
case "@ai-sdk/gateway":
|
|
678
|
+
if (model.id.includes("anthropic")) {
|
|
679
|
+
if (adaptiveEfforts) {
|
|
680
|
+
return Object.fromEntries(
|
|
681
|
+
adaptiveEfforts.map((effort) => [
|
|
682
|
+
effort,
|
|
683
|
+
{
|
|
684
|
+
thinking: {
|
|
685
|
+
type: "adaptive",
|
|
686
|
+
},
|
|
687
|
+
effort,
|
|
688
|
+
},
|
|
689
|
+
]),
|
|
690
|
+
)
|
|
691
|
+
}
|
|
692
|
+
return {
|
|
693
|
+
high: {
|
|
694
|
+
thinking: {
|
|
695
|
+
type: "enabled",
|
|
696
|
+
budgetTokens: 16000,
|
|
697
|
+
},
|
|
698
|
+
},
|
|
699
|
+
max: {
|
|
700
|
+
thinking: {
|
|
701
|
+
type: "enabled",
|
|
702
|
+
budgetTokens: 31999,
|
|
703
|
+
},
|
|
704
|
+
},
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
if (model.id.includes("google")) {
|
|
708
|
+
if (id.includes("2.5")) {
|
|
709
|
+
return {
|
|
710
|
+
high: {
|
|
711
|
+
thinkingConfig: {
|
|
712
|
+
includeThoughts: true,
|
|
713
|
+
thinkingBudget: 16000,
|
|
714
|
+
},
|
|
715
|
+
},
|
|
716
|
+
max: {
|
|
717
|
+
thinkingConfig: {
|
|
718
|
+
includeThoughts: true,
|
|
719
|
+
thinkingBudget: 24576,
|
|
720
|
+
},
|
|
721
|
+
},
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return Object.fromEntries(
|
|
725
|
+
["low", "high"].map((effort) => [
|
|
726
|
+
effort,
|
|
727
|
+
{
|
|
728
|
+
includeThoughts: true,
|
|
729
|
+
thinkingLevel: effort,
|
|
730
|
+
},
|
|
731
|
+
]),
|
|
732
|
+
)
|
|
733
|
+
}
|
|
734
|
+
return Object.fromEntries(
|
|
735
|
+
openaiCompatibleReasoningEfforts(model.api.id).map((effort) => [effort, { reasoningEffort: effort }]),
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
case "@ai-sdk/github-copilot":
|
|
739
|
+
if (model.id.includes("gemini")) {
|
|
740
|
+
// currently github copilot only returns thinking
|
|
741
|
+
return {}
|
|
742
|
+
}
|
|
743
|
+
if (model.id.includes("claude")) {
|
|
744
|
+
return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
|
|
745
|
+
}
|
|
746
|
+
const copilotEfforts = iife(() => {
|
|
747
|
+
if (id.includes("5.1-codex-max") || id.includes("5.2") || id.includes("5.3"))
|
|
748
|
+
return [...WIDELY_SUPPORTED_EFFORTS, "xhigh"]
|
|
749
|
+
const arr = [...WIDELY_SUPPORTED_EFFORTS]
|
|
750
|
+
if (id.includes("gpt-5") && model.release_date >= "2025-12-04") arr.push("xhigh")
|
|
751
|
+
return arr
|
|
752
|
+
})
|
|
753
|
+
return Object.fromEntries(
|
|
754
|
+
copilotEfforts.map((effort) => [
|
|
755
|
+
effort,
|
|
756
|
+
{
|
|
757
|
+
reasoningEffort: effort,
|
|
758
|
+
reasoningSummary: "auto",
|
|
759
|
+
include: ["reasoning.encrypted_content"],
|
|
760
|
+
},
|
|
761
|
+
]),
|
|
762
|
+
)
|
|
763
|
+
|
|
764
|
+
case "@ai-sdk/cerebras":
|
|
765
|
+
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/cerebras
|
|
766
|
+
case "@ai-sdk/togetherai":
|
|
767
|
+
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/togetherai
|
|
768
|
+
case "@ai-sdk/xai":
|
|
769
|
+
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/xai
|
|
770
|
+
case "@ai-sdk/deepinfra":
|
|
771
|
+
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/deepinfra
|
|
772
|
+
case "venice-ai-sdk-provider":
|
|
773
|
+
// https://docs.venice.ai/overview/guides/reasoning-models#reasoning-effort
|
|
774
|
+
case "@ai-sdk/openai-compatible":
|
|
775
|
+
const efforts = [...WIDELY_SUPPORTED_EFFORTS]
|
|
776
|
+
if (model.api.id.toLowerCase().includes("deepseek-v4")) {
|
|
777
|
+
efforts.push("max")
|
|
778
|
+
}
|
|
779
|
+
return Object.fromEntries(efforts.map((effort) => [effort, { reasoningEffort: effort }]))
|
|
780
|
+
|
|
781
|
+
case "@ai-sdk/azure":
|
|
782
|
+
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/azure
|
|
783
|
+
if (id === "o1-mini") return {}
|
|
784
|
+
return Object.fromEntries(
|
|
785
|
+
(GPT5_FAMILY_RE.test(id) && gpt5Version(id) === undefined
|
|
786
|
+
? ["minimal", ...WIDELY_SUPPORTED_EFFORTS]
|
|
787
|
+
: WIDELY_SUPPORTED_EFFORTS
|
|
788
|
+
).map((effort) => [
|
|
789
|
+
effort,
|
|
790
|
+
{
|
|
791
|
+
reasoningEffort: effort,
|
|
792
|
+
reasoningSummary: "auto",
|
|
793
|
+
include: ["reasoning.encrypted_content"],
|
|
794
|
+
},
|
|
795
|
+
]),
|
|
796
|
+
)
|
|
797
|
+
case "@ai-sdk/openai": {
|
|
798
|
+
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/openai
|
|
799
|
+
const efforts = openaiReasoningEfforts(model.api.id, model.release_date)
|
|
800
|
+
return Object.fromEntries(
|
|
801
|
+
efforts.map((effort) => [
|
|
802
|
+
effort,
|
|
803
|
+
{
|
|
804
|
+
reasoningEffort: effort,
|
|
805
|
+
reasoningSummary: "auto",
|
|
806
|
+
include: ["reasoning.encrypted_content"],
|
|
807
|
+
},
|
|
808
|
+
]),
|
|
809
|
+
)
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
case "@ai-sdk/anthropic":
|
|
813
|
+
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/anthropic
|
|
814
|
+
case "@ai-sdk/google-vertex/anthropic":
|
|
815
|
+
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-vertex#anthropic-provider
|
|
816
|
+
if (adaptiveEfforts) {
|
|
817
|
+
let efforts = [...adaptiveEfforts]
|
|
818
|
+
if (model.providerID === "github-copilot") {
|
|
819
|
+
if (model.api.id.includes("opus-4.7")) {
|
|
820
|
+
efforts = ["medium"]
|
|
821
|
+
}
|
|
822
|
+
// Efforts currently supported are: low, medium, high
|
|
823
|
+
efforts = efforts.filter((v) => v !== "max" && v !== "xhigh")
|
|
824
|
+
}
|
|
825
|
+
return Object.fromEntries(
|
|
826
|
+
efforts.map((effort) => [
|
|
827
|
+
effort,
|
|
828
|
+
{
|
|
829
|
+
thinking: {
|
|
830
|
+
type: "adaptive",
|
|
831
|
+
...(model.api.id.includes("opus-4-7") || model.api.id.includes("opus-4.7")
|
|
832
|
+
? { display: "summarized" }
|
|
833
|
+
: {}),
|
|
834
|
+
},
|
|
835
|
+
effort,
|
|
836
|
+
},
|
|
837
|
+
]),
|
|
838
|
+
)
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
if (["opus-4-5", "opus-4.5"].some((v) => model.api.id.includes(v))) {
|
|
842
|
+
return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { effort }]))
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
return {
|
|
846
|
+
high: {
|
|
847
|
+
thinking: {
|
|
848
|
+
type: "enabled",
|
|
849
|
+
budgetTokens: Math.min(16_000, Math.floor(model.limit.output / 2 - 1)),
|
|
850
|
+
},
|
|
851
|
+
},
|
|
852
|
+
max: {
|
|
853
|
+
thinking: {
|
|
854
|
+
type: "enabled",
|
|
855
|
+
budgetTokens: Math.min(31_999, model.limit.output - 1),
|
|
856
|
+
},
|
|
857
|
+
},
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
case "@ai-sdk/amazon-bedrock":
|
|
861
|
+
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/amazon-bedrock
|
|
862
|
+
if (adaptiveEfforts) {
|
|
863
|
+
return Object.fromEntries(
|
|
864
|
+
adaptiveEfforts.map((effort) => [
|
|
865
|
+
effort,
|
|
866
|
+
{
|
|
867
|
+
reasoningConfig: {
|
|
868
|
+
type: "adaptive",
|
|
869
|
+
maxReasoningEffort: effort,
|
|
870
|
+
...(model.api.id.includes("opus-4-7") || model.api.id.includes("opus-4.7")
|
|
871
|
+
? { display: "summarized" }
|
|
872
|
+
: {}),
|
|
873
|
+
},
|
|
874
|
+
},
|
|
875
|
+
]),
|
|
876
|
+
)
|
|
877
|
+
}
|
|
878
|
+
// For Anthropic models on Bedrock, use reasoningConfig with budgetTokens
|
|
879
|
+
if (model.api.id.includes("anthropic")) {
|
|
880
|
+
return {
|
|
881
|
+
high: {
|
|
882
|
+
reasoningConfig: {
|
|
883
|
+
type: "enabled",
|
|
884
|
+
budgetTokens: 16000,
|
|
885
|
+
},
|
|
886
|
+
},
|
|
887
|
+
max: {
|
|
888
|
+
reasoningConfig: {
|
|
889
|
+
type: "enabled",
|
|
890
|
+
budgetTokens: 31999,
|
|
891
|
+
},
|
|
892
|
+
},
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// For Amazon Nova models, use reasoningConfig with maxReasoningEffort
|
|
897
|
+
return Object.fromEntries(
|
|
898
|
+
WIDELY_SUPPORTED_EFFORTS.map((effort) => [
|
|
899
|
+
effort,
|
|
900
|
+
{
|
|
901
|
+
reasoningConfig: {
|
|
902
|
+
type: "enabled",
|
|
903
|
+
maxReasoningEffort: effort,
|
|
904
|
+
},
|
|
905
|
+
},
|
|
906
|
+
]),
|
|
907
|
+
)
|
|
908
|
+
|
|
909
|
+
case "@ai-sdk/google-vertex":
|
|
910
|
+
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-vertex
|
|
911
|
+
case "@ai-sdk/google":
|
|
912
|
+
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai
|
|
913
|
+
if (id.includes("2.5")) {
|
|
914
|
+
return {
|
|
915
|
+
high: {
|
|
916
|
+
thinkingConfig: {
|
|
917
|
+
includeThoughts: true,
|
|
918
|
+
thinkingBudget: 16000,
|
|
919
|
+
},
|
|
920
|
+
},
|
|
921
|
+
max: {
|
|
922
|
+
thinkingConfig: {
|
|
923
|
+
includeThoughts: true,
|
|
924
|
+
thinkingBudget: googleThinkingBudgetMax(id),
|
|
925
|
+
},
|
|
926
|
+
},
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
return Object.fromEntries(
|
|
931
|
+
googleThinkingLevelEfforts(id).map((effort) => [
|
|
932
|
+
effort,
|
|
933
|
+
{
|
|
934
|
+
thinkingConfig: {
|
|
935
|
+
includeThoughts: true,
|
|
936
|
+
thinkingLevel: effort,
|
|
937
|
+
},
|
|
938
|
+
},
|
|
939
|
+
]),
|
|
940
|
+
)
|
|
941
|
+
|
|
942
|
+
case "@ai-sdk/mistral":
|
|
943
|
+
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/mistral
|
|
944
|
+
// https://docs.mistral.ai/capabilities/reasoning/adjustable
|
|
945
|
+
if (!model.capabilities.reasoning) return {}
|
|
946
|
+
// Only Mistral Small 4 and Medium 3.5 support reasoning
|
|
947
|
+
const MISTRAL_REASONING_IDS = [
|
|
948
|
+
"mistral-small-2603",
|
|
949
|
+
"mistral-small-latest",
|
|
950
|
+
"mistral-medium-3.5",
|
|
951
|
+
"mistral-medium-2604",
|
|
952
|
+
]
|
|
953
|
+
const mistralId = model.api.id.toLowerCase()
|
|
954
|
+
if (!MISTRAL_REASONING_IDS.some((id) => mistralId.includes(id))) return {}
|
|
955
|
+
return {
|
|
956
|
+
high: { reasoningEffort: "high" },
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
case "@ai-sdk/cohere":
|
|
960
|
+
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/cohere
|
|
961
|
+
return {}
|
|
962
|
+
|
|
963
|
+
case "@ai-sdk/groq":
|
|
964
|
+
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/groq
|
|
965
|
+
const groqEffort = ["none", ...WIDELY_SUPPORTED_EFFORTS]
|
|
966
|
+
return Object.fromEntries(
|
|
967
|
+
groqEffort.map((effort) => [
|
|
968
|
+
effort,
|
|
969
|
+
{
|
|
970
|
+
reasoningEffort: effort,
|
|
971
|
+
},
|
|
972
|
+
]),
|
|
973
|
+
)
|
|
974
|
+
|
|
975
|
+
case "@ai-sdk/perplexity":
|
|
976
|
+
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/perplexity
|
|
977
|
+
return {}
|
|
978
|
+
|
|
979
|
+
case "@jerome-benoit/sap-ai-provider-v2":
|
|
980
|
+
if (model.api.id.includes("anthropic")) {
|
|
981
|
+
if (adaptiveEfforts) {
|
|
982
|
+
return Object.fromEntries(
|
|
983
|
+
adaptiveEfforts.map((effort) => [
|
|
984
|
+
effort,
|
|
985
|
+
{
|
|
986
|
+
thinking: {
|
|
987
|
+
type: "adaptive",
|
|
988
|
+
},
|
|
989
|
+
effort,
|
|
990
|
+
},
|
|
991
|
+
]),
|
|
992
|
+
)
|
|
993
|
+
}
|
|
994
|
+
return {
|
|
995
|
+
high: {
|
|
996
|
+
thinking: {
|
|
997
|
+
type: "enabled",
|
|
998
|
+
budgetTokens: 16000,
|
|
999
|
+
},
|
|
1000
|
+
},
|
|
1001
|
+
max: {
|
|
1002
|
+
thinking: {
|
|
1003
|
+
type: "enabled",
|
|
1004
|
+
budgetTokens: 31999,
|
|
1005
|
+
},
|
|
1006
|
+
},
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
if (model.api.id.includes("gemini") && id.includes("2.5")) {
|
|
1010
|
+
return {
|
|
1011
|
+
high: {
|
|
1012
|
+
thinkingConfig: {
|
|
1013
|
+
includeThoughts: true,
|
|
1014
|
+
thinkingBudget: 16000,
|
|
1015
|
+
},
|
|
1016
|
+
},
|
|
1017
|
+
max: {
|
|
1018
|
+
thinkingConfig: {
|
|
1019
|
+
includeThoughts: true,
|
|
1020
|
+
thinkingBudget: 24576,
|
|
1021
|
+
},
|
|
1022
|
+
},
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
if (model.api.id.includes("gpt") || /\bo[1-9]/.test(model.api.id)) {
|
|
1026
|
+
return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
|
|
1027
|
+
}
|
|
1028
|
+
return {}
|
|
1029
|
+
}
|
|
1030
|
+
return {}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
export function options(input: {
|
|
1034
|
+
model: Provider.Model
|
|
1035
|
+
sessionID: string
|
|
1036
|
+
providerOptions?: Record<string, any>
|
|
1037
|
+
}): Record<string, any> {
|
|
1038
|
+
const result: Record<string, any> = {}
|
|
1039
|
+
|
|
1040
|
+
if (
|
|
1041
|
+
input.model.api.npm === "@ai-sdk/google-vertex/anthropic" ||
|
|
1042
|
+
(!input.model.api.id.includes("claude") && input.model.api.npm === "@ai-sdk/anthropic")
|
|
1043
|
+
) {
|
|
1044
|
+
result["toolStreaming"] = false
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// openai and providers using openai package should set store to false by default.
|
|
1048
|
+
if (
|
|
1049
|
+
input.model.providerID === "openai" ||
|
|
1050
|
+
input.model.api.npm === "@ai-sdk/openai" ||
|
|
1051
|
+
input.model.api.npm === "@ai-sdk/github-copilot"
|
|
1052
|
+
) {
|
|
1053
|
+
result["store"] = false
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
if (input.model.api.npm === "@ai-sdk/azure") {
|
|
1057
|
+
result["store"] = false
|
|
1058
|
+
result["promptCacheKey"] = input.sessionID
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
if (input.model.api.npm === "@openrouter/ai-sdk-provider" || input.model.api.npm === "@llmgateway/ai-sdk-provider") {
|
|
1062
|
+
result["usage"] = {
|
|
1063
|
+
include: true,
|
|
1064
|
+
}
|
|
1065
|
+
if (input.model.api.id.includes("gemini-3")) {
|
|
1066
|
+
result["reasoning"] = { effort: "high" }
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
if (
|
|
1071
|
+
input.model.providerID === "baseten" ||
|
|
1072
|
+
(input.model.providerID === "opencode" && ["kimi-k2-thinking", "glm-4.6"].includes(input.model.api.id))
|
|
1073
|
+
) {
|
|
1074
|
+
result["chat_template_args"] = { enable_thinking: true }
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
if (
|
|
1078
|
+
["zai", "zhipuai"].some((id) => input.model.providerID.includes(id)) &&
|
|
1079
|
+
input.model.api.npm === "@ai-sdk/openai-compatible"
|
|
1080
|
+
) {
|
|
1081
|
+
result["thinking"] = {
|
|
1082
|
+
type: "enabled",
|
|
1083
|
+
clear_thinking: false,
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
if (input.model.providerID === "openai" || input.providerOptions?.setCacheKey) {
|
|
1088
|
+
result["promptCacheKey"] = input.sessionID
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
if (input.model.api.npm === "@ai-sdk/google" || input.model.api.npm === "@ai-sdk/google-vertex") {
|
|
1092
|
+
if (input.model.capabilities.reasoning) {
|
|
1093
|
+
result["thinkingConfig"] = {
|
|
1094
|
+
includeThoughts: true,
|
|
1095
|
+
}
|
|
1096
|
+
if (input.model.api.id.includes("gemini-3")) {
|
|
1097
|
+
result["thinkingConfig"]["thinkingLevel"] = "high"
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// Enable thinking by default for kimi models using anthropic SDK
|
|
1103
|
+
const modelId = input.model.api.id.toLowerCase()
|
|
1104
|
+
if (
|
|
1105
|
+
(input.model.api.npm === "@ai-sdk/anthropic" || input.model.api.npm === "@ai-sdk/google-vertex/anthropic") &&
|
|
1106
|
+
(modelId.includes("k2p") || modelId.includes("kimi-k2.") || modelId.includes("kimi-k2p"))
|
|
1107
|
+
) {
|
|
1108
|
+
result["thinking"] = {
|
|
1109
|
+
type: "enabled",
|
|
1110
|
+
budgetTokens: Math.min(16_000, Math.floor(input.model.limit.output / 2 - 1)),
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// Enable thinking for reasoning models on alibaba-cn (DashScope).
|
|
1115
|
+
// DashScope's OpenAI-compatible API requires `enable_thinking: true` in the request body
|
|
1116
|
+
// to return reasoning_content. Without it, models like kimi-k2.5, qwen-plus, qwen3, qwq,
|
|
1117
|
+
// deepseek-r1, etc. never output thinking/reasoning tokens.
|
|
1118
|
+
// Note: kimi-k2-thinking is excluded as it returns reasoning_content by default.
|
|
1119
|
+
if (
|
|
1120
|
+
input.model.providerID === "alibaba-cn" &&
|
|
1121
|
+
input.model.capabilities.reasoning &&
|
|
1122
|
+
input.model.api.npm === "@ai-sdk/openai-compatible" &&
|
|
1123
|
+
!modelId.includes("kimi-k2-thinking")
|
|
1124
|
+
) {
|
|
1125
|
+
result["enable_thinking"] = true
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
if (input.model.api.npm === "@ai-sdk/azure" && input.model.api.id.includes("gpt-5.5")) {
|
|
1129
|
+
result["reasoningSummary"] = "auto"
|
|
1130
|
+
return result
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
if (input.model.api.id.includes("gpt-5") && !input.model.api.id.includes("gpt-5-chat")) {
|
|
1134
|
+
if (!input.model.api.id.includes("gpt-5-pro")) {
|
|
1135
|
+
result["reasoningEffort"] = "medium"
|
|
1136
|
+
result["reasoningSummary"] = "auto"
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// Only set textVerbosity for non-chat gpt-5.x models
|
|
1140
|
+
// Chat models (e.g. gpt-5.2-chat-latest) only support "medium" verbosity
|
|
1141
|
+
if (
|
|
1142
|
+
input.model.api.id.includes("gpt-5.") &&
|
|
1143
|
+
!input.model.api.id.includes("codex") &&
|
|
1144
|
+
!input.model.api.id.includes("-chat") &&
|
|
1145
|
+
input.model.providerID !== "azure"
|
|
1146
|
+
) {
|
|
1147
|
+
result["textVerbosity"] = "low"
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
if (input.model.providerID.startsWith("opencode")) {
|
|
1151
|
+
result["promptCacheKey"] = input.sessionID
|
|
1152
|
+
result["include"] = ["reasoning.encrypted_content"]
|
|
1153
|
+
result["reasoningSummary"] = "auto"
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
if (input.model.providerID === "venice") {
|
|
1158
|
+
result["promptCacheKey"] = input.sessionID
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
if (input.model.providerID === "openrouter") {
|
|
1162
|
+
result["prompt_cache_key"] = input.sessionID
|
|
1163
|
+
}
|
|
1164
|
+
if (input.model.api.npm === "@ai-sdk/gateway") {
|
|
1165
|
+
result["gateway"] = {
|
|
1166
|
+
caching: "auto",
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
return result
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
export function smallOptions(model: Provider.Model) {
|
|
1174
|
+
const small = Object.values(model.variants ?? {})[0] ?? {}
|
|
1175
|
+
if (
|
|
1176
|
+
model.providerID === "openai" ||
|
|
1177
|
+
model.api.npm === "@ai-sdk/openai" ||
|
|
1178
|
+
model.api.npm === "@ai-sdk/github-copilot"
|
|
1179
|
+
) {
|
|
1180
|
+
const base = { store: false }
|
|
1181
|
+
return mergeDeep(base, small)
|
|
1182
|
+
}
|
|
1183
|
+
if (model.providerID === "openrouter" || model.providerID === "llmgateway") {
|
|
1184
|
+
if (Object.keys(small).length === 0 && model.api.id.includes("google")) {
|
|
1185
|
+
return { reasoning: { enabled: false } }
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
if (model.providerID === "venice") {
|
|
1190
|
+
if (Object.keys(small).length > 0) return small
|
|
1191
|
+
return { veniceParameters: { disableThinking: true } }
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
return small
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// Maps model ID prefix to provider slug used in providerOptions.
|
|
1198
|
+
// Example: "amazon/nova-2-lite" → "bedrock"
|
|
1199
|
+
const SLUG_OVERRIDES: Record<string, string> = {
|
|
1200
|
+
amazon: "bedrock",
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
export function providerOptions(model: Provider.Model, options: { [x: string]: any }) {
|
|
1204
|
+
if (model.api.npm === "@ai-sdk/gateway") {
|
|
1205
|
+
// Gateway providerOptions are split across two namespaces:
|
|
1206
|
+
// - `gateway`: gateway-native routing/caching controls (order, only, byok, etc.)
|
|
1207
|
+
// - `<upstream slug>`: provider-specific model options (anthropic/openai/...)
|
|
1208
|
+
// We keep `gateway` as-is and route every other top-level option under the
|
|
1209
|
+
// model-derived upstream slug.
|
|
1210
|
+
const i = model.api.id.indexOf("/")
|
|
1211
|
+
const rawSlug = i > 0 ? model.api.id.slice(0, i) : undefined
|
|
1212
|
+
const slug = rawSlug ? (SLUG_OVERRIDES[rawSlug] ?? rawSlug) : undefined
|
|
1213
|
+
const gateway = options.gateway
|
|
1214
|
+
const rest = Object.fromEntries(Object.entries(options).filter(([k]) => k !== "gateway"))
|
|
1215
|
+
const has = Object.keys(rest).length > 0
|
|
1216
|
+
|
|
1217
|
+
const result: Record<string, any> = {}
|
|
1218
|
+
if (gateway !== undefined) result.gateway = gateway
|
|
1219
|
+
|
|
1220
|
+
if (has) {
|
|
1221
|
+
if (slug) {
|
|
1222
|
+
// Route model-specific options under the provider slug
|
|
1223
|
+
result[slug] = rest
|
|
1224
|
+
} else if (gateway && typeof gateway === "object" && !Array.isArray(gateway)) {
|
|
1225
|
+
result.gateway = { ...gateway, ...rest }
|
|
1226
|
+
} else {
|
|
1227
|
+
result.gateway = rest
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
return result
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// AI SDK packages that resolve providerOptionsName by splitting the
|
|
1235
|
+
// provider name on "." (e.g. "wafer.ai" -> "wafer") need the same
|
|
1236
|
+
// logic here so the key we write matches the key they read.
|
|
1237
|
+
// Other SDKs (xai, mistral, groq, cohere, etc.) use hardcoded keys
|
|
1238
|
+
// like "xai" or "cohere" - applying .split(".")[0] would break those.
|
|
1239
|
+
const usesDotSplitOptions =
|
|
1240
|
+
model.api.npm === "@ai-sdk/openai-compatible" ||
|
|
1241
|
+
model.api.npm === "@ai-sdk/openai" ||
|
|
1242
|
+
model.api.npm === "@ai-sdk/anthropic"
|
|
1243
|
+
const key = sdkKey(model.api.npm) ?? (usesDotSplitOptions ? model.providerID.split(".")[0] : model.providerID)
|
|
1244
|
+
// @ai-sdk/azure delegates to OpenAIChatLanguageModel which reads from
|
|
1245
|
+
// providerOptions["openai"], but OpenAIResponsesLanguageModel checks
|
|
1246
|
+
// "azure" first. Pass both so model options work on either code path.
|
|
1247
|
+
if (model.api.npm === "@ai-sdk/azure") {
|
|
1248
|
+
return { openai: options, azure: options }
|
|
1249
|
+
}
|
|
1250
|
+
return { [key]: options }
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
export function maxOutputTokens(model: Provider.Model, outputTokenMax = OUTPUT_TOKEN_MAX): number {
|
|
1254
|
+
return Math.min(model.limit.output, outputTokenMax) || outputTokenMax
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
export function schema(model: Provider.Model, schema: JSONSchema7): JSONSchema7 {
|
|
1258
|
+
/*
|
|
1259
|
+
if (["openai", "azure"].includes(providerID)) {
|
|
1260
|
+
if (schema.type === "object" && schema.properties) {
|
|
1261
|
+
for (const [key, value] of Object.entries(schema.properties)) {
|
|
1262
|
+
if (schema.required?.includes(key)) continue
|
|
1263
|
+
schema.properties[key] = {
|
|
1264
|
+
anyOf: [
|
|
1265
|
+
value as JSONSchema.JSONSchema,
|
|
1266
|
+
{
|
|
1267
|
+
type: "null",
|
|
1268
|
+
},
|
|
1269
|
+
],
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
*/
|
|
1275
|
+
|
|
1276
|
+
if (model.providerID === "moonshotai" || model.api.id.toLowerCase().includes("kimi")) {
|
|
1277
|
+
const sanitizeMoonshot = (obj: unknown): unknown => {
|
|
1278
|
+
if (obj === null || typeof obj !== "object") return obj
|
|
1279
|
+
if (Array.isArray(obj)) return obj.map(sanitizeMoonshot)
|
|
1280
|
+
// Moonshot expands $ref before validation and rejects sibling keywords like description on the same node.
|
|
1281
|
+
if ("$ref" in obj && typeof obj.$ref === "string") return { $ref: obj.$ref }
|
|
1282
|
+
const result = Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, sanitizeMoonshot(value)]))
|
|
1283
|
+
// MFJS does not support tuple-style `items` arrays; it requires one schema object for all array items.
|
|
1284
|
+
if (Array.isArray(result.items)) result.items = result.items[0] ?? {}
|
|
1285
|
+
return result
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
const sanitized = sanitizeMoonshot(schema)
|
|
1289
|
+
if (typeof sanitized === "object" && sanitized !== null && !Array.isArray(sanitized)) {
|
|
1290
|
+
schema = sanitized
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
// Convert integer enums to string enums for Google/Gemini
|
|
1295
|
+
if (model.providerID === "google" || model.api.id.includes("gemini")) {
|
|
1296
|
+
const isPlainObject = (node: unknown): node is Record<string, any> =>
|
|
1297
|
+
typeof node === "object" && node !== null && !Array.isArray(node)
|
|
1298
|
+
const hasCombiner = (node: unknown) =>
|
|
1299
|
+
isPlainObject(node) && (Array.isArray(node.anyOf) || Array.isArray(node.oneOf) || Array.isArray(node.allOf))
|
|
1300
|
+
const hasSchemaIntent = (node: unknown) => {
|
|
1301
|
+
if (!isPlainObject(node)) return false
|
|
1302
|
+
if (hasCombiner(node)) return true
|
|
1303
|
+
return [
|
|
1304
|
+
"type",
|
|
1305
|
+
"properties",
|
|
1306
|
+
"items",
|
|
1307
|
+
"prefixItems",
|
|
1308
|
+
"enum",
|
|
1309
|
+
"const",
|
|
1310
|
+
"$ref",
|
|
1311
|
+
"additionalProperties",
|
|
1312
|
+
"patternProperties",
|
|
1313
|
+
"required",
|
|
1314
|
+
"not",
|
|
1315
|
+
"if",
|
|
1316
|
+
"then",
|
|
1317
|
+
"else",
|
|
1318
|
+
].some((key) => key in node)
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
const sanitizeGemini = (obj: any): any => {
|
|
1322
|
+
if (obj === null || typeof obj !== "object") {
|
|
1323
|
+
return obj
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
if (Array.isArray(obj)) {
|
|
1327
|
+
return obj.map(sanitizeGemini)
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
const result: any = {}
|
|
1331
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1332
|
+
if (key === "enum" && Array.isArray(value)) {
|
|
1333
|
+
// Convert all enum values to strings
|
|
1334
|
+
result[key] = value.map((v) => String(v))
|
|
1335
|
+
// If we have integer type with enum, change type to string
|
|
1336
|
+
if (result.type === "integer" || result.type === "number") {
|
|
1337
|
+
result.type = "string"
|
|
1338
|
+
}
|
|
1339
|
+
} else if (typeof value === "object" && value !== null) {
|
|
1340
|
+
result[key] = sanitizeGemini(value)
|
|
1341
|
+
} else {
|
|
1342
|
+
result[key] = value
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
// Filter required array to only include fields that exist in properties
|
|
1347
|
+
if (result.type === "object" && result.properties && Array.isArray(result.required)) {
|
|
1348
|
+
result.required = result.required.filter((field: any) => field in result.properties)
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
if (result.type === "array" && !hasCombiner(result)) {
|
|
1352
|
+
if (result.items == null) {
|
|
1353
|
+
result.items = {}
|
|
1354
|
+
}
|
|
1355
|
+
// Ensure items has a type only when it's still schema-empty.
|
|
1356
|
+
if (isPlainObject(result.items) && !hasSchemaIntent(result.items)) {
|
|
1357
|
+
result.items.type = "string"
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// Remove properties/required from non-object types (Gemini rejects these)
|
|
1362
|
+
if (result.type && result.type !== "object" && !hasCombiner(result)) {
|
|
1363
|
+
delete result.properties
|
|
1364
|
+
delete result.required
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
return result
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
schema = sanitizeGemini(schema)
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
return schema
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
export * as ProviderTransform from "./transform"
|