mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-20 06:53:27 -04:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bb86683dab | |||
| 53c3c69830 | |||
| 08f26a2d2e | |||
| a51622a0e7 | |||
| 20ff543ff2 |
@@ -190,7 +190,7 @@
|
||||
"solid-js": "catalog:",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"effect": "4.0.0-beta.107",
|
||||
"effect": "4.0.0-rc.110",
|
||||
"solid-js": ">=1.9.0",
|
||||
},
|
||||
"optionalPeers": [
|
||||
@@ -522,7 +522,7 @@
|
||||
"name": "@opencode-ai/http-recorder",
|
||||
"version": "1.18.15",
|
||||
"dependencies": {
|
||||
"@effect/platform-node-shared": "4.0.0-beta.107",
|
||||
"@effect/platform-node-shared": "4.0.0-rc.110",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@effect/platform-node": "catalog:",
|
||||
@@ -1084,10 +1084,10 @@
|
||||
"catalog": {
|
||||
"@cloudflare/workers-types": "4.20251008.0",
|
||||
"@corvu/drawer": "0.2.4",
|
||||
"@effect/opentelemetry": "4.0.0-beta.107",
|
||||
"@effect/platform-node": "4.0.0-beta.107",
|
||||
"@effect/platform-node-shared": "4.0.0-beta.107",
|
||||
"@effect/sql-sqlite-bun": "4.0.0-beta.107",
|
||||
"@effect/opentelemetry": "4.0.0-rc.110",
|
||||
"@effect/platform-node": "4.0.0-rc.110",
|
||||
"@effect/platform-node-shared": "4.0.0-rc.110",
|
||||
"@effect/sql-sqlite-bun": "4.0.0-rc.110",
|
||||
"@hono/standard-validator": "0.2.0",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"@kobalte/core": "0.13.11",
|
||||
@@ -1124,7 +1124,7 @@
|
||||
"dompurify": "3.3.1",
|
||||
"drizzle-kit": "1.0.0-rc.2",
|
||||
"drizzle-orm": "1.0.0-rc.2",
|
||||
"effect": "4.0.0-beta.107",
|
||||
"effect": "4.0.0-rc.110",
|
||||
"fuzzysort": "3.1.0",
|
||||
"get-east-asian-width": "1.6.0",
|
||||
"hono": "4.10.7",
|
||||
@@ -1571,13 +1571,13 @@
|
||||
|
||||
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="],
|
||||
|
||||
"@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-beta.107", "", { "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <2.0.0", "@opentelemetry/api-logs": ">=0.203.0 <0.300.0", "@opentelemetry/resources": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-base": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-node": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-web": ">=2.0.0 <3.0.0", "@opentelemetry/semantic-conventions": ">=1.33.0 <2.0.0", "effect": "^4.0.0-beta.107" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/api-logs", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-WxR3OEcwVtckNYGxvERA4kiS8cb2B46lSWxQw8P6dCCzW0j0VC7hkWyzryJ16MVXfI/5xQHS3r5j9mud+JVvsg=="],
|
||||
"@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-rc.110", "", { "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <2.0.0", "@opentelemetry/api-logs": ">=0.203.0 <0.300.0", "@opentelemetry/resources": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-base": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-node": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-web": ">=2.0.0 <3.0.0", "@opentelemetry/semantic-conventions": ">=1.33.0 <2.0.0", "effect": "^4.0.0-rc.110" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/api-logs", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-8Uum1ikIAK2DjATeLseS71HZKVI/dduFYypvMhiO68RTli1xq9yaoTI1Y0IThPn2sI0i86ZdpFaKQx1hdw2tWw=="],
|
||||
|
||||
"@effect/platform-node": ["@effect/platform-node@4.0.0-beta.107", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.107", "mime": "^4.1.0", "undici": "^8.7.0" }, "peerDependencies": { "effect": "^4.0.0-beta.107", "ioredis": ">=5.7.0 <6.0.0" } }, "sha512-k+6YNbV4Ck0L6YXtlgkvEnuP5tlxWD8EeWOrpn46PDqbGEwt4ONpRltTwm3tn2cyBXD0i+2P11cUH/6sdFagTA=="],
|
||||
"@effect/platform-node": ["@effect/platform-node@4.0.0-rc.110", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-rc.110", "mime": "^4.1.0", "undici": "^8.7.0" }, "peerDependencies": { "effect": "^4.0.0-rc.110", "redis": ">=5.0.0 <7.0.0" } }, "sha512-poj6VxTc2kRDowUHgjGXAZL2nWHTyGi/18607jK+pV9A9CH/wqZ4NeYj38rij95j5SiZ3U7Y1iOsk2Vfnr4GEw=="],
|
||||
|
||||
"@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.107", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.21.0" }, "peerDependencies": { "effect": "^4.0.0-beta.107" } }, "sha512-y6BqcRi86BfTJv+tvDrob4ozYVHxxlHYcn/zIQqZjXI9CvKnkgD6ng+38G1o45c4f2ucU+6HRI9POCmFdMoVGA=="],
|
||||
"@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-rc.110", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.21.0" }, "peerDependencies": { "effect": "^4.0.0-rc.110" } }, "sha512-P8EZloxS7RtCOSL3VSBPyqSenUm93xLbXf9jkWoy67cibZ2wgZ9nAou9iN8f1i4vzgxUsWtDuy6BEmg67np6Zw=="],
|
||||
|
||||
"@effect/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-beta.107", "", { "peerDependencies": { "effect": "^4.0.0-beta.107" } }, "sha512-BuSSCUoXz6JSR30wT4Y0ukmJKvIFhm/gROwEeA0nLO5QYf33CdTunwX+q35+/MNOOLT8jL3xaDsz5R5aAF1vYQ=="],
|
||||
"@effect/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-rc.110", "", { "peerDependencies": { "effect": "^4.0.0-rc.110" } }, "sha512-Lam3gY1xszjQS/cebdLbWbtR21FtQI4ke7QHqbBQ6AyVJF0CGUtS/ePL9zNq6wHNmJ95FUDGS6W+Qx/l6RPSzA=="],
|
||||
|
||||
"@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="],
|
||||
|
||||
@@ -2503,6 +2503,16 @@
|
||||
|
||||
"@radix-ui/rect": ["@radix-ui/rect@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" } }, "sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ=="],
|
||||
|
||||
"@redis/bloom": ["@redis/bloom@6.2.1", "", { "peerDependencies": { "@redis/client": "^6.2.1" } }, "sha512-huQgNLaCIZfQ9SeLn4q9124uOUd8HbZDYHwwUzNcRgHqCHiHKl2dDxMqJCeWh8cMqZAoWuHR8XnWbDMIf+o7ag=="],
|
||||
|
||||
"@redis/client": ["@redis/client@6.2.1", "", { "dependencies": { "cluster-key-slot": "1.1.2" }, "peerDependencies": { "@node-rs/xxhash": "^1.1.0", "@opentelemetry/api": ">=1 <2" }, "optionalPeers": ["@node-rs/xxhash", "@opentelemetry/api"] }, "sha512-LzxBY7SIBvvJiyCgcaJZZakE3fJrZZ++i24+EDW9fKpCl68D35uJcKFpZZwCfOoG9WZTbyZlMzMeM0gtOAMU9Q=="],
|
||||
|
||||
"@redis/json": ["@redis/json@6.2.1", "", { "peerDependencies": { "@redis/client": "^6.2.1" } }, "sha512-AFIUJ8Gj0DaaSBHYuSt8+O0oYWM+50OK1c0OmodB7XERIA8+BbyV3O4v76f9iccWasd1/7qjfZTpuzexUaZtrQ=="],
|
||||
|
||||
"@redis/search": ["@redis/search@6.2.1", "", { "peerDependencies": { "@redis/client": "^6.2.1" } }, "sha512-2vfOAOyYFE7UUw3sBBlkqqruBtOUS4HRY5MtW4hp83llrwvtrTE4r22CEqXddlV+54zkLxBE4nmsIJ/dpezQrQ=="],
|
||||
|
||||
"@redis/time-series": ["@redis/time-series@6.2.1", "", { "peerDependencies": { "@redis/client": "^6.2.1" } }, "sha512-kiYniph04dJOole+L359B6C9E+jYS2uDP7hca6Onj0xF38ZIpyxARO0Iq0W4ZRn1e8Q6vqW00QFZVSMRA/2Ijw=="],
|
||||
|
||||
"@remix-run/node-fetch-server": ["@remix-run/node-fetch-server@0.8.1", "", {}, "sha512-J1dev372wtJqmqn9U/qbpbZxbJSQrogNN2+Qv1lKlpATpe/WQ9aCZfl/xSb9d2Rgh1IyLSvNxZAXPZxruO6Xig=="],
|
||||
|
||||
"@remix-run/router": ["@remix-run/router@1.9.0", "", {}, "sha512-bV63itrKBC0zdT27qYm6SDZHlkXwFL1xMBuhkn+X7l0+IIhNaH5wuuvZKp6eKhCD4KFhujhfhCT1YxXW6esUIA=="],
|
||||
@@ -3601,7 +3611,7 @@
|
||||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
|
||||
"cluster-key-slot": ["cluster-key-slot@1.1.1", "", {}, "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw=="],
|
||||
"cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
|
||||
|
||||
"cmd-shim": ["cmd-shim@8.0.0", "", {}, "sha512-Jk/BK6NCapZ58BKUxlSI+ouKRbjH1NLZCgJkYoab+vEHUY3f6OzpNBN9u7HFSv9J6TRDGs4PLOHezoKGaFRSCA=="],
|
||||
|
||||
@@ -3879,7 +3889,7 @@
|
||||
|
||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||
|
||||
"effect": ["effect@4.0.0-beta.107", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.9.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.4", "uuid": "^14.0.1" } }, "sha512-OoBAv8eF+yanc+C6xhgEUnWeXUSHA6ynnscYqpkAY9GSnzZWystsIjBowVqCkLpHGlnRtdIqYT3wHwpOY6JDnQ=="],
|
||||
"effect": ["effect@4.0.0-rc.110", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.9.0", "msgpackr": "^2.0.4" } }, "sha512-ega6FTJ8CS2of7tHZiADvgyJyV999Q6tZ9juE56V81O0jw6flRwydaPNtyfvP2a5LL9PZrse8A1jnNUD5sWVHg=="],
|
||||
|
||||
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
|
||||
|
||||
@@ -4541,8 +4551,6 @@
|
||||
|
||||
"klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="],
|
||||
|
||||
"kubernetes-types": ["kubernetes-types@1.30.0", "", {}, "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q=="],
|
||||
|
||||
"lang-map": ["lang-map@0.4.0", "", { "dependencies": { "language-map": "^1.1.0" } }, "sha512-oiSqZIEUnWdFeDNsp4HId4tAxdFbx5iMBOwA3666Fn2L8Khj8NiD9xRvMsGmKXopPVkaDFtSv3CJOmXFUB0Hcg=="],
|
||||
|
||||
"language-map": ["language-map@1.5.0", "", {}, "sha512-n7gFZpe+DwEAX9cXVTw43i3wiudWDDtSn28RmdnS/HCPr284dQI/SztsamWanRr75oSlKSaGbV2nmWCTzGCoVg=="],
|
||||
@@ -5225,6 +5233,8 @@
|
||||
|
||||
"redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="],
|
||||
|
||||
"redis": ["redis@6.2.1", "", { "dependencies": { "@redis/bloom": "6.2.1", "@redis/client": "6.2.1", "@redis/json": "6.2.1", "@redis/search": "6.2.1", "@redis/time-series": "6.2.1" } }, "sha512-Z9VHtgYs48PiQC77X9O2Er8Hj4T+5BtFjT91/vi5Is1D04N72cA946ZslM1ImJw8ZctFBZWAVjM7S5wJNeHMpg=="],
|
||||
|
||||
"redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="],
|
||||
|
||||
"redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="],
|
||||
@@ -6779,6 +6789,8 @@
|
||||
|
||||
"htmlparser2/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||
|
||||
"ioredis/cluster-key-slot": ["cluster-key-slot@1.1.1", "", {}, "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw=="],
|
||||
|
||||
"js-beautify/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
|
||||
|
||||
"js-beautify/nopt": ["nopt@7.2.1", "", { "dependencies": { "abbrev": "^2.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w=="],
|
||||
|
||||
+5
-5
@@ -37,10 +37,10 @@
|
||||
"packages/slack"
|
||||
],
|
||||
"catalog": {
|
||||
"@effect/opentelemetry": "4.0.0-beta.107",
|
||||
"@effect/platform-node": "4.0.0-beta.107",
|
||||
"@effect/platform-node-shared": "4.0.0-beta.107",
|
||||
"@effect/sql-sqlite-bun": "4.0.0-beta.107",
|
||||
"@effect/opentelemetry": "4.0.0-rc.110",
|
||||
"@effect/platform-node": "4.0.0-rc.110",
|
||||
"@effect/platform-node-shared": "4.0.0-rc.110",
|
||||
"@effect/sql-sqlite-bun": "4.0.0-rc.110",
|
||||
"@npmcli/arborist": "9.4.0",
|
||||
"@types/bun": "1.3.13",
|
||||
"@types/cross-spawn": "6.0.6",
|
||||
@@ -71,7 +71,7 @@
|
||||
"dompurify": "3.3.1",
|
||||
"drizzle-kit": "1.0.0-rc.2",
|
||||
"drizzle-orm": "1.0.0-rc.2",
|
||||
"effect": "4.0.0-beta.107",
|
||||
"effect": "4.0.0-rc.110",
|
||||
"ai": "6.0.168",
|
||||
"cross-spawn": "7.0.6",
|
||||
"hono": "4.10.7",
|
||||
|
||||
@@ -159,6 +159,14 @@ export const coreFields = {
|
||||
tools: optionalArray(Tool),
|
||||
tool_choice: Schema.optional(ToolChoice),
|
||||
store: Schema.optional(Schema.Boolean),
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
||||
safety_identifier: Schema.optional(Schema.String),
|
||||
stream_options: Schema.optional(
|
||||
Schema.Struct({
|
||||
include_obfuscation: Schema.optional(Schema.Boolean),
|
||||
}),
|
||||
),
|
||||
top_logprobs: Schema.optional(Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 20 }))),
|
||||
truncation: Schema.optional(OpenResponsesOptions.TruncationSchema),
|
||||
service_tier: Schema.optional(OpenResponsesOptions.ServiceTierSchema),
|
||||
prompt_cache_key: Schema.optional(Schema.String),
|
||||
@@ -179,6 +187,8 @@ export const coreFields = {
|
||||
parallel_tool_calls: Schema.optional(Schema.Boolean),
|
||||
temperature: Schema.optional(Schema.Number),
|
||||
top_p: Schema.optional(Schema.Number),
|
||||
presence_penalty: Schema.optional(Schema.Number),
|
||||
frequency_penalty: Schema.optional(Schema.Number),
|
||||
}
|
||||
|
||||
const OpenResponsesBody = Schema.Struct({
|
||||
@@ -578,6 +588,12 @@ const lowerOptions = (request: LLMRequest) => {
|
||||
return {
|
||||
...(options.instructions ? { instructions: options.instructions } : {}),
|
||||
...(options.store !== undefined ? { store: options.store } : {}),
|
||||
...(options.metadata ? { metadata: options.metadata } : {}),
|
||||
...(options.safetyIdentifier ? { safety_identifier: options.safetyIdentifier } : {}),
|
||||
...(options.streamOptions?.includeObfuscation !== undefined
|
||||
? { stream_options: { include_obfuscation: options.streamOptions.includeObfuscation } }
|
||||
: {}),
|
||||
...(options.topLogprobs !== undefined ? { top_logprobs: options.topLogprobs } : {}),
|
||||
...(request.promptCacheKey ? { prompt_cache_key: request.promptCacheKey } : {}),
|
||||
...(options.include ? { include: options.include } : {}),
|
||||
...(options.reasoningEffort || options.reasoningSummary
|
||||
@@ -627,6 +643,8 @@ export const fromRequestWithExtension = Effect.fn("OpenResponses.fromRequestWith
|
||||
max_output_tokens: generation?.maxTokens,
|
||||
temperature: generation?.temperature,
|
||||
top_p: generation?.topP,
|
||||
presence_penalty: generation?.presencePenalty,
|
||||
frequency_penalty: generation?.frequencyPenalty,
|
||||
...lowerOptions(request),
|
||||
}
|
||||
})
|
||||
|
||||
@@ -47,9 +47,17 @@ export const AllowedTools = Schema.Struct({
|
||||
})
|
||||
export type AllowedTools = typeof AllowedTools.Type
|
||||
|
||||
export const StreamOptions = Schema.Struct({
|
||||
includeObfuscation: Schema.optional(Schema.Boolean),
|
||||
})
|
||||
|
||||
export const Options = Schema.Struct({
|
||||
instructions: Schema.optional(Schema.String),
|
||||
store: Schema.optional(Schema.Boolean),
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
||||
safetyIdentifier: Schema.optional(Schema.String),
|
||||
streamOptions: Schema.optional(StreamOptions),
|
||||
topLogprobs: Schema.optional(Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 20 }))),
|
||||
reasoningEffort: Schema.optional(ReasoningEffort),
|
||||
reasoningSummary: Schema.optional(Schema.Literals(["auto", "concise", "detailed"])),
|
||||
include: Schema.optional(Schema.Array(ResponseIncludableSchema)),
|
||||
|
||||
@@ -126,6 +126,10 @@ describe("Open Responses-compatible route", () => {
|
||||
providerOptions: {
|
||||
reasoningEffort: "low",
|
||||
store: true,
|
||||
metadata: { environment: "test" },
|
||||
safetyIdentifier: "user_123",
|
||||
streamOptions: { includeObfuscation: false },
|
||||
topLogprobs: 3,
|
||||
truncation: "auto",
|
||||
allowedTools: { toolNames: ["lookup"] },
|
||||
maxToolCalls: 2,
|
||||
@@ -136,6 +140,7 @@ describe("Open Responses-compatible route", () => {
|
||||
LLM.request({
|
||||
model,
|
||||
prompt: "Think.",
|
||||
generation: { presencePenalty: 0.2, frequencyPenalty: -0.1 },
|
||||
tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })],
|
||||
}),
|
||||
)
|
||||
@@ -143,6 +148,12 @@ describe("Open Responses-compatible route", () => {
|
||||
expect(prepared.body).toMatchObject({
|
||||
reasoning: { effort: "low" },
|
||||
store: true,
|
||||
metadata: { environment: "test" },
|
||||
safety_identifier: "user_123",
|
||||
stream_options: { include_obfuscation: false },
|
||||
top_logprobs: 3,
|
||||
presence_penalty: 0.2,
|
||||
frequency_penalty: -0.1,
|
||||
truncation: "auto",
|
||||
tool_choice: {
|
||||
type: "allowed_tools",
|
||||
|
||||
@@ -1284,6 +1284,7 @@ describe("OpenAI Responses route", () => {
|
||||
model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).model("gpt-5.2"),
|
||||
prompt: "think",
|
||||
promptCacheKey: "session_123",
|
||||
generation: { presencePenalty: 0.25, frequencyPenalty: -0.25 },
|
||||
tools: [
|
||||
ToolDefinition.make({ name: "read", description: "Read a file", inputSchema: { type: "object" } }),
|
||||
ToolDefinition.make({ name: "grep", description: "Search files", inputSchema: { type: "object" } }),
|
||||
@@ -1293,6 +1294,10 @@ describe("OpenAI Responses route", () => {
|
||||
reasoningEffort: "high",
|
||||
reasoningSummary: "auto",
|
||||
include: ["reasoning.encrypted_content"],
|
||||
metadata: { environment: "test", tenant: "acme" },
|
||||
safetyIdentifier: "user_123",
|
||||
streamOptions: { includeObfuscation: false },
|
||||
topLogprobs: 5,
|
||||
truncation: "disabled",
|
||||
allowedTools: { toolNames: ["read", "grep"], mode: "required" },
|
||||
maxToolCalls: 4,
|
||||
@@ -1306,6 +1311,12 @@ describe("OpenAI Responses route", () => {
|
||||
expect(prepared.body.include).toEqual(["reasoning.encrypted_content"])
|
||||
expect(prepared.body.reasoning).toEqual({ effort: "high", summary: "auto" })
|
||||
expect(prepared.body.text).toEqual({ verbosity: "low" })
|
||||
expect(prepared.body.metadata).toEqual({ environment: "test", tenant: "acme" })
|
||||
expect(prepared.body.safety_identifier).toBe("user_123")
|
||||
expect(prepared.body.stream_options).toEqual({ include_obfuscation: false })
|
||||
expect(prepared.body.top_logprobs).toBe(5)
|
||||
expect(prepared.body.presence_penalty).toBe(0.25)
|
||||
expect(prepared.body.frequency_penalty).toBe(-0.25)
|
||||
expect(prepared.body.truncation).toBe("disabled")
|
||||
expect(prepared.body.tool_choice).toEqual({
|
||||
type: "allowed_tools",
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"@opencode-ai/protocol": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"effect": "4.0.0-beta.107",
|
||||
"effect": "4.0.0-rc.110",
|
||||
"solid-js": ">=1.9.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
|
||||
@@ -111,7 +111,7 @@ test("event.subscribe exposes and decodes the native Effect event stream", async
|
||||
expect(Array.from(events).map((event) => event.type)).toEqual(["server.connected", "session.model.selected"])
|
||||
const durable = events[1]
|
||||
if (durable?.type !== "session.model.selected") throw new Error("Expected model event")
|
||||
expect(DateTime.toEpochMillis(durable.created)).toBe(1_717_171_717_000)
|
||||
expect(durable.created).toBe(1_717_171_717_000)
|
||||
expect(durable.durable).toEqual({ aggregateID: "ses_test", seq: 1, version: 1 })
|
||||
})
|
||||
|
||||
@@ -219,9 +219,7 @@ test("session methods retain decoded Effect inputs and outputs", async () => {
|
||||
expect(logQueries[0]).toEqual({ after: "0" })
|
||||
const logged = Array.from(result.log)
|
||||
expect(logged.map((item) => item.type)).toEqual(["session.model.selected", "log.synced"])
|
||||
expect(logged[0]?.type === "session.model.selected" && DateTime.toEpochMillis(logged[0].created)).toBe(
|
||||
1_717_171_717_000,
|
||||
)
|
||||
expect(logged[0]?.type === "session.model.selected" && logged[0].created).toBe(1_717_171_717_000)
|
||||
expect(logged.at(-1)).toEqual(synced)
|
||||
expect(result.message).toEqual(expect.objectContaining({ id: "msg_model", type: "model-switched" }))
|
||||
})
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Money } from "@opencode-ai/schema/money"
|
||||
import { Effect, Stream } from "effect"
|
||||
import { Config } from "../../config.js"
|
||||
import { Provider } from "../../provider.js"
|
||||
import { VariantPlugin } from "../../plugin/variant.js"
|
||||
|
||||
export const Plugin = define({
|
||||
id: "opencode.config.provider",
|
||||
@@ -34,6 +35,8 @@ export const Plugin = define({
|
||||
})
|
||||
|
||||
yield* ctx.catalog.transform((catalog) => {
|
||||
const fallback = new Map<string, { providerID: string; modelID: string }>()
|
||||
const explicit = new Map<string, { providerID: string; modelID: string }>()
|
||||
const configuredDefault = Config.latest(loaded.entries, "model")
|
||||
if (configuredDefault !== undefined)
|
||||
catalog.model.default.set(configuredDefault.providerID, configuredDefault.model)
|
||||
@@ -48,6 +51,13 @@ export const Plugin = define({
|
||||
if (item.body !== undefined) provider.body = Provider.mergeOverlay(provider.body, item.body)
|
||||
})
|
||||
for (const [id, config] of Object.entries(item.models ?? {})) {
|
||||
const key = `${providerID}\0${id}`
|
||||
if (!catalog.model.get(providerID, id) && config.variants === undefined)
|
||||
fallback.set(key, { providerID, modelID: id })
|
||||
if (config.variants !== undefined) {
|
||||
fallback.delete(key)
|
||||
explicit.set(key, { providerID, modelID: id })
|
||||
}
|
||||
catalog.model.update(providerID, id, (model) => {
|
||||
if (config.family !== undefined) model.family = config.family
|
||||
if (config.name !== undefined) model.name = config.name
|
||||
@@ -67,6 +77,7 @@ export const Plugin = define({
|
||||
}
|
||||
if (config.variants !== undefined) {
|
||||
model.variants ??= []
|
||||
if (config.variants.length === 0) model.variants = []
|
||||
for (const variant of config.variants) {
|
||||
let existing = model.variants.find((item) => item.id === variant.id)
|
||||
if (!existing) {
|
||||
@@ -96,6 +107,15 @@ export const Plugin = define({
|
||||
})
|
||||
}
|
||||
}
|
||||
for (const item of fallback.values()) {
|
||||
const model = catalog.model.get(item.providerID, item.modelID)
|
||||
if (!model || model.variants.length > 0) continue
|
||||
VariantPlugin.markFallback(model)
|
||||
}
|
||||
for (const item of explicit.values()) {
|
||||
const model = catalog.model.get(item.providerID, item.modelID)
|
||||
if (model) VariantPlugin.suppressFallback(model)
|
||||
}
|
||||
})
|
||||
yield* ctx.event.subscribe().pipe(
|
||||
Stream.filter((event) => event.type === "config.updated"),
|
||||
|
||||
@@ -12,7 +12,8 @@ export const Plugin = define({
|
||||
for (const record of catalog.provider.list()) {
|
||||
for (const model of record.models.values()) {
|
||||
catalog.model.update(model.providerID, model.id, (draft) => {
|
||||
const generated = generate(draft, record.provider)
|
||||
if (suppressed.has(draft)) return
|
||||
const generated = fallbacks.has(draft) ? fallback(draft, record.provider) : generate(draft, record.provider)
|
||||
if (generated.length === 0) return
|
||||
|
||||
const variants = draft.variants ?? []
|
||||
@@ -42,3 +43,167 @@ export function generate(
|
||||
settings: { reasoningEffort: id },
|
||||
}))
|
||||
}
|
||||
|
||||
const OPENAI_EFFORTS = ["none", "low", "medium", "high", "xhigh", "max"]
|
||||
const COMMON_EFFORTS = ["low", "medium", "high"]
|
||||
const ENCRYPTED_REASONING = ["reasoning.encrypted_content"]
|
||||
const CLAUDE_MANUAL_THINKING_MAX = { haiku: [4, 5], sonnet: [4, 5], opus: [4, 5] } as const
|
||||
// Config runs immediately before this plugin over the same materialized model objects.
|
||||
// Weak markers retain omitted versus explicit empty variants without exposing provenance publicly.
|
||||
const fallbacks = new WeakSet<object>()
|
||||
const suppressed = new WeakSet<object>()
|
||||
|
||||
export function markFallback(model: object) {
|
||||
suppressed.delete(model)
|
||||
fallbacks.add(model)
|
||||
}
|
||||
|
||||
export function suppressFallback(model: object) {
|
||||
fallbacks.delete(model)
|
||||
suppressed.add(model)
|
||||
}
|
||||
|
||||
export function fallback(
|
||||
model: {
|
||||
readonly modelID: string
|
||||
readonly package?: string
|
||||
readonly settings?: Readonly<Record<string, unknown>>
|
||||
readonly limit: { readonly output: number }
|
||||
},
|
||||
provider?: { readonly package: string },
|
||||
): NonNullable<Model.Info["variants"]> {
|
||||
const packageName = model.package ?? provider?.package
|
||||
if (openAIResponses(packageName, model.settings))
|
||||
return OPENAI_EFFORTS.map((id) => ({
|
||||
id: Model.VariantID.make(id),
|
||||
settings: settings(packageName, {
|
||||
reasoningEffort: id,
|
||||
reasoningSummary: "auto",
|
||||
include: ENCRYPTED_REASONING,
|
||||
}),
|
||||
}))
|
||||
if (openAIChat(packageName, model.settings)) return efforts(packageName, COMMON_EFFORTS)
|
||||
if (google(packageName)) return googleVariants(packageName, model.modelID, model.limit.output)
|
||||
if (anthropic(packageName)) return anthropicVariants(packageName, model.modelID, model.limit.output)
|
||||
return []
|
||||
}
|
||||
|
||||
function openAIResponses(packageName: string | undefined, settings: Readonly<Record<string, unknown>> | undefined) {
|
||||
if (Provider.isAISDK(packageName))
|
||||
return (
|
||||
Provider.packageName(packageName) === "@ai-sdk/openai" ||
|
||||
(Provider.packageName(packageName) === "@ai-sdk/azure" && settings?.useCompletionUrls !== true)
|
||||
)
|
||||
return [
|
||||
"@opencode-ai/ai/providers/openai",
|
||||
"@opencode-ai/ai/providers/openai/responses",
|
||||
"@opencode-ai/ai/providers/azure",
|
||||
"@opencode-ai/ai/providers/azure/responses",
|
||||
"@opencode-ai/ai/providers/google-vertex/responses",
|
||||
].includes(packageName ?? "")
|
||||
}
|
||||
|
||||
function openAIChat(packageName: string | undefined, settings: Readonly<Record<string, unknown>> | undefined) {
|
||||
if (Provider.isAISDK(packageName))
|
||||
return (
|
||||
Provider.packageName(packageName) === "@ai-sdk/openai-compatible" ||
|
||||
(Provider.packageName(packageName) === "@ai-sdk/azure" && settings?.useCompletionUrls === true)
|
||||
)
|
||||
return [
|
||||
"@opencode-ai/ai/providers/openai/chat",
|
||||
"@opencode-ai/ai/providers/openai-compatible",
|
||||
"@opencode-ai/ai/providers/azure/chat",
|
||||
"@opencode-ai/ai/providers/google-vertex/chat",
|
||||
].includes(packageName ?? "")
|
||||
}
|
||||
|
||||
function google(packageName: string | undefined) {
|
||||
if (Provider.isAISDK(packageName))
|
||||
return ["@ai-sdk/google", "@ai-sdk/google-vertex"].includes(Provider.packageName(packageName))
|
||||
return [
|
||||
"@opencode-ai/ai/providers/google",
|
||||
"@opencode-ai/ai/providers/google-vertex",
|
||||
"@opencode-ai/ai/providers/google-vertex/gemini",
|
||||
].includes(packageName ?? "")
|
||||
}
|
||||
|
||||
function anthropic(packageName: string | undefined) {
|
||||
if (Provider.isAISDK(packageName))
|
||||
return ["@ai-sdk/anthropic", "@ai-sdk/google-vertex/anthropic"].includes(Provider.packageName(packageName))
|
||||
return [
|
||||
"@opencode-ai/ai/providers/anthropic",
|
||||
"@opencode-ai/ai/providers/anthropic-compatible",
|
||||
"@opencode-ai/ai/providers/google-vertex/messages",
|
||||
].includes(packageName ?? "")
|
||||
}
|
||||
|
||||
function settings(packageName: string | undefined, value: Readonly<Record<string, unknown>>) {
|
||||
return Provider.isAISDK(packageName) ? value : { providerOptions: value }
|
||||
}
|
||||
|
||||
function efforts(packageName: string | undefined, ids: readonly string[]) {
|
||||
return ids.map((id) => ({ id: Model.VariantID.make(id), settings: settings(packageName, { reasoningEffort: id }) }))
|
||||
}
|
||||
|
||||
function googleVariants(
|
||||
packageName: string | undefined,
|
||||
modelID: string,
|
||||
output: number,
|
||||
): NonNullable<Model.Info["variants"]> {
|
||||
if (!/(?:^|[/.:_-])gemini-2[.-]5(?:[/.:_-]|$)/i.test(modelID))
|
||||
return COMMON_EFFORTS.map((effort) => ({
|
||||
id: Model.VariantID.make(effort),
|
||||
settings: settings(packageName, { thinkingConfig: { includeThoughts: true, thinkingLevel: effort } }),
|
||||
}))
|
||||
const variants = [
|
||||
{ id: "high", budget: 16_000 },
|
||||
{ id: "max", budget: /(?:^|[/.:_-])pro(?:[/.:_-]|$)/i.test(modelID) ? 32_768 : 24_576 },
|
||||
]
|
||||
const maximum = output - 1
|
||||
if (maximum <= 0) return []
|
||||
return variants.map((item) => ({
|
||||
id: Model.VariantID.make(item.id),
|
||||
settings: settings(packageName, {
|
||||
thinkingConfig: { includeThoughts: true, thinkingBudget: Math.min(item.budget, maximum) },
|
||||
}),
|
||||
}))
|
||||
}
|
||||
|
||||
function anthropicVariants(
|
||||
packageName: string | undefined,
|
||||
modelID: string,
|
||||
output: number,
|
||||
): NonNullable<Model.Info["variants"]> {
|
||||
const model = claudeModel(modelID)
|
||||
const version = model && CLAUDE_MANUAL_THINKING_MAX[model.family]
|
||||
const manual = version && (model.major < version[0] || (model.major === version[0] && model.minor <= version[1]))
|
||||
if (!manual) {
|
||||
const ids =
|
||||
!model || model.major > 4 || model.minor >= 7 ? [...COMMON_EFFORTS, "xhigh", "max"] : [...COMMON_EFFORTS, "max"]
|
||||
return ids.map((id) => ({
|
||||
id: Model.VariantID.make(id),
|
||||
settings: settings(packageName, { thinking: { type: "adaptive", display: "summarized" }, effort: id }),
|
||||
}))
|
||||
}
|
||||
const maximum = Math.min(31_999, output - 1)
|
||||
if (maximum <= 0) return []
|
||||
return [
|
||||
{ id: "high", budget: Math.min(16_000, maximum) },
|
||||
{ id: "max", budget: maximum },
|
||||
].map((item) => ({
|
||||
id: Model.VariantID.make(item.id),
|
||||
settings: settings(packageName, { thinking: { type: "enabled", budgetTokens: item.budget } }),
|
||||
}))
|
||||
}
|
||||
|
||||
function claudeModel(modelID: string) {
|
||||
const familyFirst = /(?:^|[/.:_-])(opus|sonnet|haiku)-([1-9]\d*)(?:[.-](\d{1,2}))?(?:[/.:_-]|$)/i.exec(modelID)
|
||||
const versionFirst = /(?:^|[/.:_-])claude-([1-9]\d*)(?:[.-](\d{1,2}))?-(opus|sonnet|haiku)(?:[/.:_-]|$)/i.exec(
|
||||
modelID,
|
||||
)
|
||||
const family = (["haiku", "sonnet", "opus"] as const).find((item) => item === (familyFirst?.[1] ?? versionFirst?.[3]))
|
||||
const major = Number(familyFirst?.[2] ?? versionFirst?.[1])
|
||||
const minor = Number(familyFirst?.[3] ?? versionFirst?.[2] ?? 0)
|
||||
if (!family || !Number.isFinite(major) || !Number.isFinite(minor)) return
|
||||
return { family, major, minor }
|
||||
}
|
||||
|
||||
@@ -399,10 +399,38 @@ describe("PluginSupervisor config", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("lets an explicit empty config array clear generated variants", () =>
|
||||
withLocation(
|
||||
{
|
||||
plugins: [path.join(import.meta.dir, "../plugin/fixtures/variant-source-plugin.ts")],
|
||||
providers: {
|
||||
configured: {
|
||||
models: {
|
||||
"glm-5.2": { variants: [] },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Effect.gen(function* () {
|
||||
yield* ready()
|
||||
const catalog = yield* Catalog.Service
|
||||
expect((yield* catalog.model.get(Provider.ID.make("configured"), Model.ID.make("glm-5.2")))?.variants).toEqual(
|
||||
[],
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("allows variant generation to be disabled", () =>
|
||||
withLocation(
|
||||
{
|
||||
plugins: [path.join(import.meta.dir, "../plugin/fixtures/variant-source-plugin.ts"), "-opencode.variant"],
|
||||
providers: {
|
||||
custom: {
|
||||
package: "aisdk:@ai-sdk/openai",
|
||||
models: { reasoner: {} },
|
||||
},
|
||||
},
|
||||
},
|
||||
Effect.gen(function* () {
|
||||
yield* ready()
|
||||
@@ -413,6 +441,7 @@ describe("PluginSupervisor config", () => {
|
||||
expect((yield* catalog.model.get(Provider.ID.make("configured"), Model.ID.make("glm-5.2")))?.variants).toEqual([
|
||||
expect.objectContaining({ id: "high", headers: { custom: "true" } }),
|
||||
])
|
||||
expect((yield* catalog.model.get(Provider.ID.make("custom"), Model.ID.make("reasoner")))?.variants).toEqual([])
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -9,16 +9,18 @@ import { Integration } from "@opencode-ai/core/integration"
|
||||
import { Model } from "@opencode-ai/core/model"
|
||||
import { Plugin } from "@opencode-ai/core/plugin"
|
||||
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
import { VariantPlugin } from "@opencode-ai/core/plugin/variant"
|
||||
import { Provider } from "@opencode-ai/core/provider"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "../plugin/fixture"
|
||||
|
||||
const it = testEffect(PluginTestLayer)
|
||||
|
||||
const addPlugin = Effect.fn(function* (entries: Entry[]) {
|
||||
const addPlugin = Effect.fn(function* (entries: Entry[], variants = false) {
|
||||
const plugin = yield* Plugin.Service
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
yield* ConfigProviderPlugin.Plugin.effect(host).pipe(Effect.provide(Config.testLayer(entries)))
|
||||
if (variants) yield* VariantPlugin.Plugin.effect(host)
|
||||
})
|
||||
|
||||
function required<T>(value: T | undefined): T {
|
||||
@@ -104,6 +106,175 @@ describe("ConfigProviderPlugin.Plugin", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("adds fallback variants to new configured models when variants are omitted", () =>
|
||||
Effect.gen(function* () {
|
||||
const catalog = yield* Catalog.Service
|
||||
const providerID = Provider.ID.make("custom")
|
||||
const modelID = Model.ID.make("gpt-next")
|
||||
const entries = [
|
||||
new Document({
|
||||
type: "document",
|
||||
info: decode({
|
||||
providers: {
|
||||
custom: {
|
||||
package: "aisdk:@ai-sdk/openai",
|
||||
models: { "gpt-next": {} },
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
]
|
||||
|
||||
yield* addPlugin(entries, true)
|
||||
|
||||
const variants = required(yield* catalog.model.get(providerID, modelID)).variants
|
||||
expect(variants.map((variant) => String(variant.id))).toEqual(["none", "low", "medium", "high", "xhigh", "max"])
|
||||
expect(variants[3]?.settings).toEqual({
|
||||
reasoningEffort: "high",
|
||||
reasoningSummary: "auto",
|
||||
include: ["reasoning.encrypted_content"],
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("keeps explicit empty and custom configured variants", () =>
|
||||
Effect.gen(function* () {
|
||||
const catalog = yield* Catalog.Service
|
||||
const providerID = Provider.ID.make("custom")
|
||||
const entries = [
|
||||
new Document({
|
||||
type: "document",
|
||||
info: decode({
|
||||
providers: {
|
||||
custom: {
|
||||
package: "aisdk:@ai-sdk/openai",
|
||||
models: {
|
||||
disabled: { variants: [] },
|
||||
explicit: { variants: [{ id: "deep", settings: { reasoningEffort: "max" } }] },
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
]
|
||||
|
||||
yield* addPlugin(entries, true)
|
||||
|
||||
expect((yield* catalog.model.get(providerID, Model.ID.make("disabled")))?.variants).toEqual([])
|
||||
expect(
|
||||
(yield* catalog.model.get(providerID, Model.ID.make("explicit")))?.variants.map((variant) => ({
|
||||
...variant,
|
||||
id: String(variant.id),
|
||||
})),
|
||||
).toEqual([{ id: "deep", settings: { reasoningEffort: "max" } }])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("does not add config fallbacks to existing catalog models", () =>
|
||||
Effect.gen(function* () {
|
||||
const catalog = yield* Catalog.Service
|
||||
const providerID = Provider.ID.make("custom")
|
||||
const modelID = Model.ID.make("known")
|
||||
yield* catalog.transform((draft) => {
|
||||
draft.model.update(providerID, modelID, () => {})
|
||||
})
|
||||
const entries = [
|
||||
new Document({
|
||||
type: "document",
|
||||
info: decode({
|
||||
providers: {
|
||||
custom: {
|
||||
package: "aisdk:@ai-sdk/openai",
|
||||
models: { known: { name: "Known" } },
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
]
|
||||
|
||||
yield* addPlugin(entries, true)
|
||||
|
||||
expect((yield* catalog.model.get(providerID, modelID))?.variants).toEqual([])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("lets an explicit empty array clear inherited variants", () =>
|
||||
Effect.gen(function* () {
|
||||
const catalog = yield* Catalog.Service
|
||||
const providerID = Provider.ID.make("custom")
|
||||
const modelID = Model.ID.make("known")
|
||||
yield* catalog.transform((draft) => {
|
||||
draft.model.update(providerID, modelID, (model) => {
|
||||
model.variants = [{ id: Model.VariantID.make("high"), settings: { reasoningEffort: "high" } }]
|
||||
})
|
||||
})
|
||||
const entries = [
|
||||
new Document({
|
||||
type: "document",
|
||||
info: decode({
|
||||
providers: {
|
||||
custom: {
|
||||
package: "aisdk:@ai-sdk/openai",
|
||||
models: { known: { variants: [] } },
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
]
|
||||
|
||||
yield* addPlugin(entries, true)
|
||||
|
||||
expect((yield* catalog.model.get(providerID, modelID))?.variants).toEqual([])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("respects layered variant intent and the final package flavor", () =>
|
||||
Effect.gen(function* () {
|
||||
const catalog = yield* Catalog.Service
|
||||
const providerID = Provider.ID.make("custom")
|
||||
const entries = [
|
||||
new Document({
|
||||
type: "document",
|
||||
info: decode({
|
||||
providers: {
|
||||
custom: {
|
||||
package: "aisdk:@ai-sdk/openai-compatible",
|
||||
models: {
|
||||
cleared: {},
|
||||
disabled: { variants: [] },
|
||||
flavor: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
new Document({
|
||||
type: "document",
|
||||
info: decode({
|
||||
providers: {
|
||||
custom: {
|
||||
package: "aisdk:@ai-sdk/openai",
|
||||
models: {
|
||||
cleared: { variants: [] },
|
||||
disabled: { name: "Disabled" },
|
||||
flavor: { name: "OpenAI" },
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
]
|
||||
|
||||
yield* addPlugin(entries, true)
|
||||
|
||||
expect((yield* catalog.model.get(providerID, Model.ID.make("cleared")))?.variants).toEqual([])
|
||||
expect((yield* catalog.model.get(providerID, Model.ID.make("disabled")))?.variants).toEqual([])
|
||||
expect(
|
||||
(yield* catalog.model.get(providerID, Model.ID.make("flavor")))?.variants.map((variant) => String(variant.id)),
|
||||
).toEqual(["none", "low", "medium", "high", "xhigh", "max"])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("preserves catalog capabilities unless config overrides them", () =>
|
||||
Effect.gen(function* () {
|
||||
const catalog = yield* Catalog.Service
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Integration } from "@opencode-ai/core/integration"
|
||||
import { Compatibility, ID, Info, VariantID } from "@opencode-ai/core/model"
|
||||
import { Provider } from "@opencode-ai/core/provider"
|
||||
import { ModelResolver } from "@opencode-ai/core/model-resolver"
|
||||
import { VariantPlugin } from "@opencode-ai/core/plugin/variant"
|
||||
import { Catalog } from "@opencode-ai/core/catalog"
|
||||
import { AISDK } from "@opencode-ai/core/aisdk"
|
||||
import { Npm } from "@opencode-ai/util/npm"
|
||||
@@ -509,6 +510,34 @@ describe("ModelResolver", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("applies native OpenAI fallback settings to Responses requests", () =>
|
||||
Effect.gen(function* () {
|
||||
const packageName = "@opencode-ai/ai/providers/openai"
|
||||
const base = model(packageName, { modelID: "gpt-next", limit: { context: 100, output: 32_000 } })
|
||||
const catalog = model(packageName, {
|
||||
modelID: "gpt-next",
|
||||
limit: { context: 100, output: 32_000 },
|
||||
variants: VariantPlugin.fallback(base),
|
||||
})
|
||||
const resolved = yield* ModelResolver.resolveModel(
|
||||
catalog,
|
||||
VariantID.make("high"),
|
||||
Credential.Key.make({ type: "key", key: "secret" }),
|
||||
)
|
||||
|
||||
expect(resolved.route.defaults.providerOptions).toMatchObject({
|
||||
reasoningEffort: "high",
|
||||
reasoningSummary: "auto",
|
||||
include: ["reasoning.encrypted_content"],
|
||||
})
|
||||
const prepared = yield* compileRequest(LLM.request({ model: resolved, prompt: "Hello" }))
|
||||
expect(prepared.body).toMatchObject({
|
||||
include: ["reasoning.encrypted_content"],
|
||||
reasoning: { effort: "high", summary: "auto" },
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("overlays selected OpenAI-compatible variant bodies", () =>
|
||||
Effect.gen(function* () {
|
||||
const catalog = model(Provider.aisdk("@ai-sdk/openai-compatible"), {
|
||||
@@ -885,12 +914,7 @@ describe("ModelResolver", () => {
|
||||
{ reasoning: { effort: "high" } },
|
||||
{ reasoning: { effort: "high" } },
|
||||
],
|
||||
[
|
||||
"@ai-sdk/xai",
|
||||
"@opencode-ai/ai/providers/xai",
|
||||
{ reasoningEffort: "high" },
|
||||
{ reasoningEffort: "high" },
|
||||
],
|
||||
["@ai-sdk/xai", "@opencode-ai/ai/providers/xai", { reasoningEffort: "high" }, { reasoningEffort: "high" }],
|
||||
] as const
|
||||
|
||||
yield* Effect.forEach(packages, ([catalogPackage, nativePackage, sourceOptions, providerOptions]) =>
|
||||
@@ -940,11 +964,7 @@ describe("ModelResolver", () => {
|
||||
["@ai-sdk/azure", "@opencode-ai/ai/providers/azure/responses", "api-model"],
|
||||
["@ai-sdk/google", "@opencode-ai/ai/providers/google", "api-model"],
|
||||
["@ai-sdk/google-vertex", "@opencode-ai/ai/providers/google-vertex", "api-model"],
|
||||
[
|
||||
"@ai-sdk/google-vertex/anthropic",
|
||||
"@opencode-ai/ai/providers/google-vertex/messages",
|
||||
"claude-sonnet-4-6",
|
||||
],
|
||||
["@ai-sdk/google-vertex/anthropic", "@opencode-ai/ai/providers/google-vertex/messages", "claude-sonnet-4-6"],
|
||||
["@ai-sdk/openai", "@opencode-ai/ai/providers/openai", "api-model"],
|
||||
["@ai-sdk/openai-compatible", "@opencode-ai/ai/providers/openai-compatible", "api-model"],
|
||||
["@openrouter/ai-sdk-provider", "@opencode-ai/ai/providers/openrouter", "api-model"],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Catalog } from "@opencode-ai/core/catalog"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
||||
@@ -59,3 +59,153 @@ describe("VariantPlugin", () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
describe("VariantPlugin.fallback", () => {
|
||||
const model = (
|
||||
modelID: string,
|
||||
packageName: string,
|
||||
output = 32_000,
|
||||
settings?: Readonly<Record<string, unknown>>,
|
||||
) => ({
|
||||
modelID,
|
||||
package: packageName,
|
||||
settings,
|
||||
limit: { output },
|
||||
})
|
||||
const plain = (variants: Model.Info["variants"]) =>
|
||||
variants.map((variant) => ({ ...variant, id: String(variant.id) }))
|
||||
const settings = (packageName: string, value: Readonly<Record<string, unknown>>) =>
|
||||
Provider.isAISDK(packageName) ? value : { providerOptions: value }
|
||||
|
||||
test.each([
|
||||
Provider.aisdk("@ai-sdk/openai"),
|
||||
Provider.aisdk("@ai-sdk/azure"),
|
||||
"@opencode-ai/ai/providers/openai",
|
||||
"@opencode-ai/ai/providers/openai/responses",
|
||||
"@opencode-ai/ai/providers/azure",
|
||||
"@opencode-ai/ai/providers/azure/responses",
|
||||
"@opencode-ai/ai/providers/google-vertex/responses",
|
||||
])("adds OpenAI Responses variants for %s", (packageName) => {
|
||||
const variants = VariantPlugin.fallback(model("gpt-next", packageName))
|
||||
|
||||
expect(variants.map((variant) => String(variant.id))).toEqual(["none", "low", "medium", "high", "xhigh", "max"])
|
||||
expect(variants[0]?.settings).toEqual(
|
||||
settings(packageName, {
|
||||
reasoningEffort: "none",
|
||||
reasoningSummary: "auto",
|
||||
include: ["reasoning.encrypted_content"],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
test.each([
|
||||
Provider.aisdk("@ai-sdk/openai-compatible"),
|
||||
"@opencode-ai/ai/providers/openai/chat",
|
||||
"@opencode-ai/ai/providers/openai-compatible",
|
||||
"@opencode-ai/ai/providers/azure/chat",
|
||||
"@opencode-ai/ai/providers/google-vertex/chat",
|
||||
])("adds conservative chat variants for %s", (packageName) => {
|
||||
expect(plain(VariantPlugin.fallback(model("reasoner", packageName)))).toEqual([
|
||||
{ id: "low", settings: settings(packageName, { reasoningEffort: "low" }) },
|
||||
{ id: "medium", settings: settings(packageName, { reasoningEffort: "medium" }) },
|
||||
{ id: "high", settings: settings(packageName, { reasoningEffort: "high" }) },
|
||||
])
|
||||
})
|
||||
|
||||
test("uses chat fallbacks for AI SDK Azure completion URLs", () => {
|
||||
const variants = VariantPlugin.fallback(
|
||||
model("deployment", Provider.aisdk("@ai-sdk/azure"), 32_000, { useCompletionUrls: true }),
|
||||
)
|
||||
|
||||
expect(plain(variants)).toEqual([
|
||||
{ id: "low", settings: { reasoningEffort: "low" } },
|
||||
{ id: "medium", settings: { reasoningEffort: "medium" } },
|
||||
{ id: "high", settings: { reasoningEffort: "high" } },
|
||||
])
|
||||
})
|
||||
|
||||
test.each([
|
||||
Provider.aisdk("@ai-sdk/google"),
|
||||
Provider.aisdk("@ai-sdk/google-vertex"),
|
||||
"@opencode-ai/ai/providers/google",
|
||||
"@opencode-ai/ai/providers/google-vertex",
|
||||
"@opencode-ai/ai/providers/google-vertex/gemini",
|
||||
])("adds Google level and legacy budget variants for %s", (packageName) => {
|
||||
expect(plain(VariantPlugin.fallback(model("gemini-next", packageName)))).toEqual([
|
||||
{
|
||||
id: "low",
|
||||
settings: settings(packageName, { thinkingConfig: { includeThoughts: true, thinkingLevel: "low" } }),
|
||||
},
|
||||
{
|
||||
id: "medium",
|
||||
settings: settings(packageName, { thinkingConfig: { includeThoughts: true, thinkingLevel: "medium" } }),
|
||||
},
|
||||
{
|
||||
id: "high",
|
||||
settings: settings(packageName, { thinkingConfig: { includeThoughts: true, thinkingLevel: "high" } }),
|
||||
},
|
||||
])
|
||||
expect(plain(VariantPlugin.fallback(model("gemini-2.5-pro", packageName, 64_000)))).toEqual([
|
||||
{
|
||||
id: "high",
|
||||
settings: settings(packageName, { thinkingConfig: { includeThoughts: true, thinkingBudget: 16_000 } }),
|
||||
},
|
||||
{
|
||||
id: "max",
|
||||
settings: settings(packageName, { thinkingConfig: { includeThoughts: true, thinkingBudget: 32_768 } }),
|
||||
},
|
||||
])
|
||||
expect(VariantPlugin.fallback(model("gemini-12.5-pro", packageName)).map((variant) => String(variant.id))).toEqual([
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
])
|
||||
})
|
||||
|
||||
test.each([
|
||||
Provider.aisdk("@ai-sdk/anthropic"),
|
||||
Provider.aisdk("@ai-sdk/google-vertex/anthropic"),
|
||||
"@opencode-ai/ai/providers/anthropic",
|
||||
"@opencode-ai/ai/providers/anthropic-compatible",
|
||||
"@opencode-ai/ai/providers/google-vertex/messages",
|
||||
])("adds Anthropic adaptive and legacy budget variants for %s", (packageName) => {
|
||||
expect(VariantPlugin.fallback(model("claude-opus-4-7", packageName)).map((variant) => String(variant.id))).toEqual([
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh",
|
||||
"max",
|
||||
])
|
||||
expect(VariantPlugin.fallback(model("claude-opus-4-7", packageName))[0]?.settings).toEqual(
|
||||
settings(packageName, {
|
||||
thinking: { type: "adaptive", display: "summarized" },
|
||||
effort: "low",
|
||||
}),
|
||||
)
|
||||
expect(plain(VariantPlugin.fallback(model("claude-haiku-4-5", packageName, 20_000)))).toEqual([
|
||||
{ id: "high", settings: settings(packageName, { thinking: { type: "enabled", budgetTokens: 16_000 } }) },
|
||||
{ id: "max", settings: settings(packageName, { thinking: { type: "enabled", budgetTokens: 19_999 } }) },
|
||||
])
|
||||
for (const family of ["haiku", "sonnet", "opus"]) {
|
||||
expect(VariantPlugin.fallback(model(`claude-${family}-4-5`, packageName))[0]?.settings).toEqual(
|
||||
settings(packageName, { thinking: { type: "enabled", budgetTokens: 16_000 } }),
|
||||
)
|
||||
expect(VariantPlugin.fallback(model(`claude-${family}-4-6`, packageName))[0]?.settings).toEqual(
|
||||
settings(packageName, {
|
||||
thinking: { type: "adaptive", display: "summarized" },
|
||||
effort: "low",
|
||||
}),
|
||||
)
|
||||
}
|
||||
expect(VariantPlugin.fallback(model("claude-mythos-4-5", packageName))[0]?.settings).toEqual(
|
||||
settings(packageName, {
|
||||
thinking: { type: "adaptive", display: "summarized" },
|
||||
effort: "low",
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
test("does not add fallbacks for unknown packages", () => {
|
||||
expect(VariantPlugin.fallback(model("reasoner", "custom"))).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
"typescript": "catalog:"
|
||||
},
|
||||
"dependencies": {
|
||||
"@effect/platform-node-shared": "4.0.0-beta.107"
|
||||
"@effect/platform-node-shared": "4.0.0-rc.110"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"effect": "catalog:"
|
||||
|
||||
@@ -1376,7 +1376,7 @@ function App(props: { pair?: DialogPairCredentials }) {
|
||||
<StartupLoading ready={plugins.ready} />
|
||||
</Show>
|
||||
<Show when={showReconnecting()}>
|
||||
<Reconnecting />
|
||||
<Reconnecting managed={client.restart !== undefined} />
|
||||
</Show>
|
||||
<MigrationOverlay />
|
||||
<Toast />
|
||||
|
||||
@@ -2,7 +2,7 @@ import { RGBA } from "@opentui/core"
|
||||
import { useTheme } from "../context/theme"
|
||||
import { Spinner } from "./spinner"
|
||||
|
||||
export function Reconnecting() {
|
||||
export function Reconnecting(props: { managed?: boolean }) {
|
||||
const theme = useTheme("elevated")
|
||||
|
||||
return (
|
||||
@@ -28,8 +28,12 @@ export function Reconnecting() {
|
||||
paddingRight={2}
|
||||
gap={1}
|
||||
>
|
||||
<Spinner color={theme.text.default}>Restarting service...</Spinner>
|
||||
<text fg={theme.text.subdued}>Your session will resume automatically.</text>
|
||||
<Spinner color={theme.text.default}>{props.managed ? "Restarting service..." : "Connection lost..."}</Spinner>
|
||||
<text fg={theme.text.subdued}>
|
||||
{props.managed
|
||||
? "Your session will resume automatically."
|
||||
: "Reconnecting to the server automatically."}
|
||||
</text>
|
||||
</box>
|
||||
</box>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user