From 3729fd57068445104ea464a952d41798ed30ea20 Mon Sep 17 00:00:00 2001 From: Simon Klee Date: Sun, 12 Apr 2026 11:33:38 +0200 Subject: [PATCH 001/300] chore(github): vouch simonklee (#22127) --- .github/VOUCHED.td | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/VOUCHED.td b/.github/VOUCHED.td index 5d4f7fa265..733748462b 100644 --- a/.github/VOUCHED.td +++ b/.github/VOUCHED.td @@ -26,6 +26,7 @@ kommander r44vc0rp rekram1-node -robinmordasiewicz +simonklee -spider-yamet clawdbot/llm psychosis, spam pinging the team thdxr -toastythebot From 8b9b9ad31ee715301613f7254424590f0cc8805b Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sun, 12 Apr 2026 12:02:39 -0500 Subject: [PATCH 002/300] fix: ensure images read by agent dont count against quota (#22168) --- .../src/plugin/github-copilot/copilot.ts | 22 ++++++++++++++++--- packages/opencode/src/session/message-v2.ts | 4 +++- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/plugin/github-copilot/copilot.ts b/packages/opencode/src/plugin/github-copilot/copilot.ts index 5f61f013d9..ac685f74da 100644 --- a/packages/opencode/src/plugin/github-copilot/copilot.ts +++ b/packages/opencode/src/plugin/github-copilot/copilot.ts @@ -5,6 +5,7 @@ import { iife } from "@/util/iife" import { Log } from "../../util/log" import { setTimeout as sleep } from "node:timers/promises" import { CopilotModels } from "./models" +import { MessageV2 } from "@/session/message-v2" const log = Log.create({ service: "plugin.copilot" }) @@ -27,6 +28,21 @@ function base(enterpriseUrl?: string) { return enterpriseUrl ? `https://copilot-api.${normalizeDomain(enterpriseUrl)}` : "https://api.githubcopilot.com" } +// Check if a message is a synthetic user msg used to attach an image from a tool call +function imgMsg(msg: any): boolean { + if (msg?.role !== "user") return false + + // Handle the 3 api formats + + const content = msg.content + if (typeof content === "string") return content === MessageV2.SYNTHETIC_ATTACHMENT_PROMPT + if (!Array.isArray(content)) return false + return content.some( + (part: any) => + (part?.type === "text" || part?.type === "input_text") && part.text === MessageV2.SYNTHETIC_ATTACHMENT_PROMPT, + ) +} + function fix(model: Model, url: string): Model { return { ...model, @@ -90,7 +106,7 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise { (msg: any) => Array.isArray(msg.content) && msg.content.some((part: any) => part.type === "image_url"), ), - isAgent: last?.role !== "user", + isAgent: last?.role !== "user" || imgMsg(last), } } @@ -102,7 +118,7 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise { (item: any) => Array.isArray(item?.content) && item.content.some((part: any) => part.type === "input_image"), ), - isAgent: last?.role !== "user", + isAgent: last?.role !== "user" || imgMsg(last), } } @@ -124,7 +140,7 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise { part.content.some((nested: any) => nested?.type === "image")), ), ), - isAgent: !(last?.role === "user" && hasNonToolCalls), + isAgent: !(last?.role === "user" && hasNonToolCalls) || imgMsg(last), } } } catch {} diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index aa42e1c1dc..4c18d1f7e0 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -25,6 +25,8 @@ interface FetchDecompressionError extends Error { } export namespace MessageV2 { + export const SYNTHETIC_ATTACHMENT_PROMPT = "Attached image(s) from tool result:" + export function isMedia(mime: string) { return mime.startsWith("image/") || mime === "application/pdf" } @@ -808,7 +810,7 @@ export namespace MessageV2 { parts: [ { type: "text" as const, - text: "Attached image(s) from tool result:", + text: SYNTHETIC_ATTACHMENT_PROMPT, }, ...media.map((attachment) => ({ type: "file" as const, From 2aa6110c6e72a77e0b8c017091ff26487f69fc67 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sun, 12 Apr 2026 13:14:33 -0400 Subject: [PATCH 003/300] ignore: exploration --- packages/opencode/src/v2/message.ts | 101 ++++++++++++++++++++-------- packages/opencode/src/v2/session.ts | 71 +++++++++++++++++++ 2 files changed, 145 insertions(+), 27 deletions(-) create mode 100644 packages/opencode/src/v2/session.ts diff --git a/packages/opencode/src/v2/message.ts b/packages/opencode/src/v2/message.ts index 008a45a369..868ab82802 100644 --- a/packages/opencode/src/v2/message.ts +++ b/packages/opencode/src/v2/message.ts @@ -10,59 +10,106 @@ export namespace Message { })), ) - export class File extends Schema.Class("Message.File")({ - url: Schema.String, + export class Source extends Schema.Class("Message.Source")({ + start: Schema.Number, + end: Schema.Number, + text: Schema.String, + }) {} + + export class FileAttachment extends Schema.Class("Message.File.Attachment")({ + uri: Schema.String, mime: Schema.String, + name: Schema.String.pipe(Schema.optional), + description: Schema.String.pipe(Schema.optional), + source: Source.pipe(Schema.optional), }) { static create(url: string) { - return new File({ - url, + return new FileAttachment({ + uri: url, mime: "text/plain", }) } } - export class UserContent extends Schema.Class("Message.User.Content")({ - text: Schema.String, - synthetic: Schema.Boolean.pipe(Schema.optional), - agent: Schema.String.pipe(Schema.optional), - files: Schema.Array(File).pipe(Schema.optional), + export class AgentAttachment extends Schema.Class("Message.Agent.Attachment")({ + name: Schema.String, + source: Source.pipe(Schema.optional), }) {} export class User extends Schema.Class("Message.User")({ id: ID, type: Schema.Literal("user"), + text: Schema.String, + files: Schema.Array(FileAttachment).pipe(Schema.optional), + agents: Schema.Array(AgentAttachment).pipe(Schema.optional), time: Schema.Struct({ created: Schema.DateTimeUtc, }), - content: UserContent, }) { - static create(content: Schema.Schema.Type) { + static create(input: { text: User["text"]; files?: User["files"]; agents?: User["agents"] }) { const msg = new User({ id: ID.create(), type: "user", + ...input, time: { created: Effect.runSync(DateTime.now), }, - content, }) return msg } - - static file(url: string) { - return new File({ - url, - mime: "text/plain", - }) - } } - export namespace User {} + export class Synthetic extends Schema.Class("Message.Synthetic")({ + id: ID, + type: Schema.Literal("synthetic"), + text: Schema.String, + time: Schema.Struct({ + created: Schema.DateTimeUtc, + }), + }) {} + + export class Request extends Schema.Class("Message.Request")({ + id: ID, + type: Schema.Literal("start"), + model: Schema.Struct({ + id: Schema.String, + providerID: Schema.String, + variant: Schema.String.pipe(Schema.optional), + }), + time: Schema.Struct({ + created: Schema.DateTimeUtc, + }), + }) {} + + export class Text extends Schema.Class("Message.Text")({ + id: ID, + type: Schema.Literal("text"), + text: Schema.String, + time: Schema.Struct({ + created: Schema.DateTimeUtc, + completed: Schema.DateTimeUtc.pipe(Schema.optional), + }), + }) {} + + export class Complete extends Schema.Class("Message.Complete")({ + id: ID, + type: Schema.Literal("complete"), + time: Schema.Struct({ + created: Schema.DateTimeUtc, + }), + cost: Schema.Number, + tokens: Schema.Struct({ + total: Schema.Number, + input: Schema.Number, + output: Schema.Number, + reasoning: Schema.Number, + cache: Schema.Struct({ + read: Schema.Number, + write: Schema.Number, + }), + }), + }) {} + + export const Info = Schema.Union([User, Text]) + export type Info = Schema.Schema.Type } - -const msg = Message.User.create({ - text: "Hello world", - files: [Message.File.create("file://example.com/file.txt")], -}) - -console.log(JSON.stringify(msg, null, 2)) diff --git a/packages/opencode/src/v2/session.ts b/packages/opencode/src/v2/session.ts new file mode 100644 index 0000000000..4b4fa1978a --- /dev/null +++ b/packages/opencode/src/v2/session.ts @@ -0,0 +1,71 @@ +import { Context, Layer, Schema, Effect } from "effect" +import { Message } from "./message" +import { Struct } from "effect" +import { Identifier } from "@/id/id" +import { withStatics } from "@/util/schema" +import { Session } from "@/session" +import { SessionID } from "@/session/schema" + +export namespace SessionV2 { + export const ID = SessionID + + export type ID = Schema.Schema.Type + + export class PromptInput extends Schema.Class("Session.PromptInput")({ + ...Struct.omit(Message.User.fields, ["time", "type"]), + id: Schema.optionalKey(Message.ID), + sessionID: SessionV2.ID, + }) {} + + export class CreateInput extends Schema.Class("Session.CreateInput")({ + id: Schema.optionalKey(SessionV2.ID), + }) {} + + export class Info extends Schema.Class("Session.Info")({ + id: SessionV2.ID, + model: Schema.Struct({ + id: Schema.String, + providerID: Schema.String, + modelID: Schema.String, + }).pipe(Schema.optional), + }) {} + + export interface Interface { + fromID: (id: SessionV2.ID) => Effect.Effect + create: (input: CreateInput) => Effect.Effect + prompt: (input: PromptInput) => Effect.Effect + } + + export class Service extends Context.Service()("Session.Service") {} + + export const layer = Layer.effect(Service)( + Effect.gen(function* () { + const session = yield* Session.Service + + const create: Interface["create"] = Effect.fn("Session.create")(function* (input) { + throw new Error("Not implemented") + }) + + const prompt: Interface["prompt"] = Effect.fn("Session.prompt")(function* (input) { + throw new Error("Not implemented") + }) + + const fromID: Interface["fromID"] = Effect.fn("Session.fromID")(function* (id) { + const match = yield* session.get(id) + return fromV1(match) + }) + + return Service.of({ + create, + prompt, + fromID, + }) + }), + ) + + function fromV1(input: Session.Info): Info { + return new Info({ + id: SessionV2.ID.make(input.id), + }) + } +} From 8c4d49c2bc7a08248d78552490b6c0ef8b60042b Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sun, 12 Apr 2026 13:16:38 -0400 Subject: [PATCH 004/300] ci: enable signed Windows builds on beta branch Allows beta releases to include properly signed Windows CLI executables, ensuring consistent security verification across all release channels. --- .github/workflows/publish.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 46a6577807..f5030a3c26 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -114,7 +114,7 @@ jobs: - build-cli - version runs-on: blacksmith-4vcpu-windows-2025 - if: github.repository == 'anomalyco/opencode' && github.ref_name != 'beta' + if: github.repository == 'anomalyco/opencode' env: AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} @@ -591,7 +591,6 @@ jobs: path: packages/opencode/dist - uses: actions/download-artifact@v4 - if: github.ref_name != 'beta' with: name: opencode-cli-signed-windows path: packages/opencode/dist From 113304a058d569f00f758e0646fa360cf5b052d5 Mon Sep 17 00:00:00 2001 From: Dax Date: Sun, 12 Apr 2026 13:41:50 -0400 Subject: [PATCH 005/300] fix(snapshot): respect gitignore for previously tracked files (#22171) --- packages/opencode/src/snapshot/index.ts | 67 ++++++++++++++-- .../opencode/test/snapshot/snapshot.test.ts | 77 +++++++++++++++++++ 2 files changed, 137 insertions(+), 7 deletions(-) diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 834cdde252..3b522a03ea 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -177,8 +177,37 @@ export namespace Snapshot { const all = Array.from(new Set([...tracked, ...untracked])) if (!all.length) return + // Filter out files that are now gitignored even if previously tracked + // Files may have been tracked before being gitignored, so we need to check + // against the source project's current gitignore rules + const checkArgs = [ + ...quote, + "--git-dir", + path.join(state.worktree, ".git"), + "--work-tree", + state.worktree, + "check-ignore", + "--", + ...all, + ] + const check = yield* git(checkArgs, { cwd: state.directory }) + const ignored = + check.code === 0 ? new Set(check.text.trim().split("\n").filter(Boolean)) : new Set() + const filtered = all.filter((item) => !ignored.has(item)) + + // Remove newly-ignored files from snapshot index to prevent re-adding + if (ignored.size > 0) { + const ignoredFiles = Array.from(ignored) + log.info("removing gitignored files from snapshot", { count: ignoredFiles.length }) + yield* git([...cfg, ...args(["rm", "--cached", "-f", "--", ...ignoredFiles])], { + cwd: state.directory, + }) + } + + if (!filtered.length) return + const large = (yield* Effect.all( - all.map((item) => + filtered.map((item) => fs .stat(path.join(state.directory, item)) .pipe(Effect.catch(() => Effect.void)) @@ -259,14 +288,38 @@ export namespace Snapshot { log.warn("failed to get diff", { hash, exitCode: result.code }) return { hash, files: [] } } + const files = result.text + .trim() + .split("\n") + .map((x) => x.trim()) + .filter(Boolean) + + // Filter out files that are now gitignored + if (files.length > 0) { + const checkArgs = [ + ...quote, + "--git-dir", + path.join(state.worktree, ".git"), + "--work-tree", + state.worktree, + "check-ignore", + "--", + ...files, + ] + const check = yield* git(checkArgs, { cwd: state.directory }) + if (check.code === 0) { + const ignored = new Set(check.text.trim().split("\n").filter(Boolean)) + const filtered = files.filter((item) => !ignored.has(item)) + return { + hash, + files: filtered.map((x) => path.join(state.worktree, x).replaceAll("\\", "/")), + } + } + } + return { hash, - files: result.text - .trim() - .split("\n") - .map((x) => x.trim()) - .filter(Boolean) - .map((x) => path.join(state.worktree, x).replaceAll("\\", "/")), + files: files.map((x) => path.join(state.worktree, x).replaceAll("\\", "/")), } }), ) diff --git a/packages/opencode/test/snapshot/snapshot.test.ts b/packages/opencode/test/snapshot/snapshot.test.ts index 3cedfb941d..22253ecaba 100644 --- a/packages/opencode/test/snapshot/snapshot.test.ts +++ b/packages/opencode/test/snapshot/snapshot.test.ts @@ -511,6 +511,49 @@ test("circular symlinks", async () => { }) }) +test("source project gitignore is respected - ignored files are not snapshotted", async () => { + await using tmp = await tmpdir({ + git: true, + init: async (dir) => { + // Create gitignore BEFORE any tracking + await Filesystem.write(`${dir}/.gitignore`, "*.ignored\nbuild/\nnode_modules/\n") + await Filesystem.write(`${dir}/tracked.txt`, "tracked content") + await Filesystem.write(`${dir}/ignored.ignored`, "ignored content") + await $`mkdir -p ${dir}/build`.quiet() + await Filesystem.write(`${dir}/build/output.js`, "build output") + await Filesystem.write(`${dir}/normal.js`, "normal js") + await $`git add .`.cwd(dir).quiet() + await $`git commit -m init`.cwd(dir).quiet() + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const before = await Snapshot.track() + expect(before).toBeTruthy() + + // Modify tracked files and create new ones - some ignored, some not + await Filesystem.write(`${tmp.path}/tracked.txt`, "modified tracked") + await Filesystem.write(`${tmp.path}/new.ignored`, "new ignored") + await Filesystem.write(`${tmp.path}/new-tracked.txt`, "new tracked") + await Filesystem.write(`${tmp.path}/build/new-build.js`, "new build file") + + const patch = await Snapshot.patch(before!) + + // Modified and new tracked files should be in snapshot + expect(patch.files).toContain(fwd(tmp.path, "new-tracked.txt")) + expect(patch.files).toContain(fwd(tmp.path, "tracked.txt")) + + // Ignored files should NOT be in snapshot + expect(patch.files).not.toContain(fwd(tmp.path, "new.ignored")) + expect(patch.files).not.toContain(fwd(tmp.path, "ignored.ignored")) + expect(patch.files).not.toContain(fwd(tmp.path, "build/output.js")) + expect(patch.files).not.toContain(fwd(tmp.path, "build/new-build.js")) + }, + }) +}) + test("gitignore changes", async () => { await using tmp = await bootstrap() await Instance.provide({ @@ -535,6 +578,40 @@ test("gitignore changes", async () => { }) }) +test("files tracked in snapshot but now gitignored are filtered out", async () => { + await using tmp = await bootstrap() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + // First, create a file and snapshot it + await Filesystem.write(`${tmp.path}/later-ignored.txt`, "initial content") + const before = await Snapshot.track() + expect(before).toBeTruthy() + + // Modify the file (so it appears in diff-files) + await Filesystem.write(`${tmp.path}/later-ignored.txt`, "modified content") + + // Now add gitignore that would exclude this file + await Filesystem.write(`${tmp.path}/.gitignore`, "later-ignored.txt\n") + + // Also create another tracked file + await Filesystem.write(`${tmp.path}/still-tracked.txt`, "new tracked file") + + const patch = await Snapshot.patch(before!) + + // The file that is now gitignored should NOT appear, even though it was + // previously tracked and modified + expect(patch.files).not.toContain(fwd(tmp.path, "later-ignored.txt")) + + // The gitignore file itself should appear + expect(patch.files).toContain(fwd(tmp.path, ".gitignore")) + + // Other tracked files should appear + expect(patch.files).toContain(fwd(tmp.path, "still-tracked.txt")) + }, + }) +}) + test("git info exclude changes", async () => { await using tmp = await bootstrap() await Instance.provide({ From fa2c69f09c031c175d0872bd6406e18241ff2d78 Mon Sep 17 00:00:00 2001 From: shafdev <96260000+shafdev@users.noreply.github.com> Date: Sun, 12 Apr 2026 23:19:24 +0530 Subject: [PATCH 006/300] fix(opencode): remove spurious scripts and randomField from package.json (#22160) --- packages/opencode/package.json | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 18feb46757..60d63f8403 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -14,18 +14,11 @@ "fix-node-pty": "bun run script/fix-node-pty.ts", "upgrade-opentui": "bun run script/upgrade-opentui.ts", "dev": "bun run --conditions=browser ./src/index.ts", - "random": "echo 'Random script updated at $(date)' && echo 'Change queued successfully' && echo 'Another change made' && echo 'Yet another change' && echo 'One more change' && echo 'Final change' && echo 'Another final change' && echo 'Yet another final change'", - "clean": "echo 'Cleaning up...' && rm -rf node_modules dist", - "lint": "echo 'Running lint checks...' && bun test --coverage", - "format": "echo 'Formatting code...' && bun run --prettier --write src/**/*.ts", - "docs": "echo 'Generating documentation...' && find src -name '*.ts' -exec echo 'Processing: {}' \\;", - "deploy": "echo 'Deploying application...' && bun run build && echo 'Deployment completed successfully'", "db": "bun drizzle-kit" }, "bin": { "opencode": "./bin/opencode" }, - "randomField": "this-is-a-random-value-12345", "exports": { "./*": "./src/*.ts" }, From 264418c0cdda37e214c01688dca66c0dcfd3e0b0 Mon Sep 17 00:00:00 2001 From: Dax Date: Sun, 12 Apr 2026 14:05:46 -0400 Subject: [PATCH 007/300] fix(snapshot): complete gitignore respect for previously tracked files (#22172) --- packages/opencode/src/snapshot/index.ts | 27 ++++++++++++++ .../opencode/test/snapshot/snapshot.test.ts | 35 +++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 3b522a03ea..995e8d3fda 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -180,6 +180,7 @@ export namespace Snapshot { // Filter out files that are now gitignored even if previously tracked // Files may have been tracked before being gitignored, so we need to check // against the source project's current gitignore rules + // Use --no-index to check purely against patterns (ignoring whether file is tracked) const checkArgs = [ ...quote, "--git-dir", @@ -187,6 +188,7 @@ export namespace Snapshot { "--work-tree", state.worktree, "check-ignore", + "--no-index", "--", ...all, ] @@ -303,6 +305,7 @@ export namespace Snapshot { "--work-tree", state.worktree, "check-ignore", + "--no-index", "--", ...files, ] @@ -669,6 +672,30 @@ export namespace Snapshot { } satisfies Row, ] }) + + // Filter out files that are now gitignored + if (rows.length > 0) { + const files = rows.map((r) => r.file) + const checkArgs = [ + ...quote, + "--git-dir", + path.join(state.worktree, ".git"), + "--work-tree", + state.worktree, + "check-ignore", + "--no-index", + "--", + ...files, + ] + const check = yield* git(checkArgs, { cwd: state.directory }) + if (check.code === 0) { + const ignored = new Set(check.text.trim().split("\n").filter(Boolean)) + const filtered = rows.filter((r) => !ignored.has(r.file)) + rows.length = 0 + rows.push(...filtered) + } + } + const step = 100 const patch = (file: string, before: string, after: string) => formatPatch(structuredPatch(file, file, before, after, "", "", { context: Number.MAX_SAFE_INTEGER })) diff --git a/packages/opencode/test/snapshot/snapshot.test.ts b/packages/opencode/test/snapshot/snapshot.test.ts index 22253ecaba..971d053bd6 100644 --- a/packages/opencode/test/snapshot/snapshot.test.ts +++ b/packages/opencode/test/snapshot/snapshot.test.ts @@ -612,6 +612,41 @@ test("files tracked in snapshot but now gitignored are filtered out", async () = }) }) +test("gitignore updated between track calls filters from diff", async () => { + await using tmp = await bootstrap() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + // a.txt is already committed from bootstrap - track it in snapshot + const before = await Snapshot.track() + expect(before).toBeTruthy() + + // Modify a.txt (so it appears in diff-files) + await Filesystem.write(`${tmp.path}/a.txt`, "modified content") + + // Now add gitignore that would exclude a.txt + await Filesystem.write(`${tmp.path}/.gitignore`, "a.txt\n") + + // Also modify b.txt which is not gitignored + await Filesystem.write(`${tmp.path}/b.txt`, "also modified") + + // Second track - should not include a.txt even though it changed + const after = await Snapshot.track() + expect(after).toBeTruthy() + + // Verify a.txt is NOT in the diff between snapshots + const diffs = await Snapshot.diffFull(before!, after!) + expect(diffs.some((x) => x.file === "a.txt")).toBe(false) + + // But .gitignore should be in the diff + expect(diffs.some((x) => x.file === ".gitignore")).toBe(true) + + // b.txt should be in the diff (not gitignored) + expect(diffs.some((x) => x.file === "b.txt")).toBe(true) + }, + }) +}) + test("git info exclude changes", async () => { await using tmp = await bootstrap() await Instance.provide({ From 3c0ad706537189c6215d63e88ddba5cdbe00c81b Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sun, 12 Apr 2026 14:40:24 -0400 Subject: [PATCH 008/300] ci: enable beta branch releases with auto-update support --- .github/workflows/publish.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f5030a3c26..af008f6b17 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -213,7 +213,6 @@ jobs: needs: - build-cli - version - if: github.ref_name != 'beta' continue-on-error: false env: AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} @@ -390,7 +389,7 @@ jobs: needs: - build-cli - version - if: github.repository == 'anomalyco/opencode' && github.ref_name != 'beta' + if: github.repository == 'anomalyco/opencode' continue-on-error: false env: AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} @@ -596,7 +595,7 @@ jobs: path: packages/opencode/dist - uses: actions/download-artifact@v4 - if: needs.version.outputs.release && github.ref_name != 'beta' + if: needs.version.outputs.release with: pattern: latest-yml-* path: /tmp/latest-yml From 8ffadde85c89230bdca6b26a6e5b957cbfae7281 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sun, 12 Apr 2026 15:52:55 -0500 Subject: [PATCH 009/300] chore: rm git ignored files (#22200) --- .../src/provider/models-snapshot.d.ts | 2 - .../opencode/src/provider/models-snapshot.js | 66216 ---------------- 2 files changed, 66218 deletions(-) delete mode 100644 packages/opencode/src/provider/models-snapshot.d.ts delete mode 100644 packages/opencode/src/provider/models-snapshot.js diff --git a/packages/opencode/src/provider/models-snapshot.d.ts b/packages/opencode/src/provider/models-snapshot.d.ts deleted file mode 100644 index 839eba6b7d..0000000000 --- a/packages/opencode/src/provider/models-snapshot.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Auto-generated by build.ts - do not edit -export declare const snapshot: Record diff --git a/packages/opencode/src/provider/models-snapshot.js b/packages/opencode/src/provider/models-snapshot.js deleted file mode 100644 index f6a6d09afa..0000000000 --- a/packages/opencode/src/provider/models-snapshot.js +++ /dev/null @@ -1,66216 +0,0 @@ -// @ts-nocheck -// Auto-generated by build.ts - do not edit -export const snapshot = { - "302ai": { - id: "302ai", - env: ["302AI_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.302.ai/v1", - name: "302.AI", - doc: "https://doc.302.ai", - models: { - "qwen3-235b-a22b": { - id: "qwen3-235b-a22b", - name: "Qwen3-235B-A22B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04-29", - last_updated: "2025-04-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.29, output: 2.86 }, - limit: { context: 128000, output: 16384 }, - }, - "grok-4.1": { - id: "grok-4.1", - name: "grok-4.1", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-11-18", - last_updated: "2025-11-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 10 }, - limit: { context: 200000, output: 64000 }, - }, - "MiniMax-M2": { - id: "MiniMax-M2", - name: "MiniMax-M2", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-10-26", - last_updated: "2025-10-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.33, output: 1.32 }, - limit: { context: 1000000, output: 128000 }, - }, - "grok-4-1-fast-reasoning": { - id: "grok-4-1-fast-reasoning", - name: "grok-4-1-fast-reasoning", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-11-20", - last_updated: "2025-11-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5 }, - limit: { context: 2000000, output: 30000 }, - }, - "gemini-2.5-flash-nothink": { - id: "gemini-2.5-flash-nothink", - name: "gemini-2.5-flash-nothink", - family: "gemini-flash", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-06-24", - last_updated: "2025-06-24", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5 }, - limit: { context: 1000000, output: 65536 }, - }, - "kimi-k2-0905-preview": { - id: "kimi-k2-0905-preview", - name: "kimi-k2-0905-preview", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.632, output: 2.53 }, - limit: { context: 262144, output: 262144 }, - }, - "claude-opus-4-5-20251101": { - id: "claude-opus-4-5-20251101", - name: "claude-opus-4-5-20251101", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-03", - release_date: "2025-11-25", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25 }, - limit: { context: 200000, output: 64000 }, - }, - "gemini-2.5-flash-lite-preview-09-2025": { - id: "gemini-2.5-flash-lite-preview-09-2025", - name: "gemini-2.5-flash-lite-preview-09-2025", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-09-26", - last_updated: "2025-09-26", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 1000000, output: 65536 }, - }, - "qwen3-235b-a22b-instruct-2507": { - id: "qwen3-235b-a22b-instruct-2507", - name: "qwen3-235b-a22b-instruct-2507", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-30", - last_updated: "2025-07-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.29, output: 1.143 }, - limit: { context: 128000, output: 65536 }, - }, - "mistral-large-2512": { - id: "mistral-large-2512", - name: "mistral-large-2512", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-12-16", - last_updated: "2025-12-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 3.3 }, - limit: { context: 128000, output: 262144 }, - }, - "glm-4.7": { - id: "glm-4.7", - name: "glm-4.7", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.286, output: 1.142 }, - limit: { context: 200000, output: 131072 }, - }, - "doubao-seed-1-8-251215": { - id: "doubao-seed-1-8-251215", - name: "doubao-seed-1-8-251215", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-12-18", - last_updated: "2025-12-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.114, output: 0.286 }, - limit: { context: 224000, output: 64000 }, - }, - "chatgpt-4o-latest": { - id: "chatgpt-4o-latest", - name: "chatgpt-4o-latest", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-09", - release_date: "2024-08-08", - last_updated: "2024-08-08", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 15 }, - limit: { context: 128000, output: 16384 }, - }, - "deepseek-chat": { - id: "deepseek-chat", - name: "Deepseek-Chat", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2024-11-29", - last_updated: "2024-11-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.29, output: 0.43 }, - limit: { context: 128000, output: 8192 }, - }, - "deepseek-v3.2-thinking": { - id: "deepseek-v3.2-thinking", - name: "DeepSeek-V3.2-Thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.29, output: 0.43 }, - limit: { context: 128000, output: 128000 }, - }, - "gpt-5-thinking": { - id: "gpt-5-thinking", - name: "gpt-5-thinking", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-08-08", - last_updated: "2025-08-08", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 400000, output: 128000 }, - }, - "gemini-3-flash-preview": { - id: "gemini-3-flash-preview", - name: "gemini-3-flash-preview", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-12-18", - last_updated: "2025-12-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 3 }, - limit: { context: 1000000, output: 65536 }, - }, - "qwen-plus": { - id: "qwen-plus", - name: "Qwen-Plus", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.12, output: 1.2 }, - limit: { context: 1000000, output: 32768 }, - }, - "gpt-5-mini": { - id: "gpt-5-mini", - name: "gpt-5-mini", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-08-08", - last_updated: "2025-08-08", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2 }, - limit: { context: 400000, output: 128000 }, - }, - "gemini-3-pro-preview": { - id: "gemini-3-pro-preview", - name: "gemini-3-pro-preview", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-11-19", - last_updated: "2025-11-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12 }, - limit: { context: 1000000, output: 64000 }, - }, - "qwen3-max-2025-09-23": { - id: "qwen3-max-2025-09-23", - name: "qwen3-max-2025-09-23", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-24", - last_updated: "2025-09-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.86, output: 3.43 }, - limit: { context: 258048, output: 65536 }, - }, - "claude-sonnet-4-5-20250929": { - id: "claude-sonnet-4-5-20250929", - name: "claude-sonnet-4-5-20250929", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-03", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 200000, output: 64000 }, - }, - "qwen-flash": { - id: "qwen-flash", - name: "Qwen-Flash", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.022, output: 0.22 }, - limit: { context: 1000000, output: 32768 }, - }, - "gemini-2.5-pro": { - id: "gemini-2.5-pro", - name: "gemini-2.5-pro", - family: "gemini-pro", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 1000000, output: 65536 }, - }, - "grok-4-1-fast-non-reasoning": { - id: "grok-4-1-fast-non-reasoning", - name: "grok-4-1-fast-non-reasoning", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-11-20", - last_updated: "2025-11-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5 }, - limit: { context: 2000000, output: 30000 }, - }, - "claude-opus-4-5-20251101-thinking": { - id: "claude-opus-4-5-20251101-thinking", - name: "claude-opus-4-5-20251101-thinking", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03", - release_date: "2025-11-25", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25 }, - limit: { context: 200000, output: 64000 }, - }, - "gpt-5.2": { - id: "gpt-5.2", - name: "gpt-5.2", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-12-12", - last_updated: "2025-12-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14 }, - limit: { context: 400000, output: 128000 }, - }, - "gemini-3-pro-image-preview": { - id: "gemini-3-pro-image-preview", - name: "gemini-3-pro-image-preview", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-06", - release_date: "2025-11-20", - last_updated: "2025-11-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 120 }, - limit: { context: 32768, output: 64000 }, - }, - "qwen-max-latest": { - id: "qwen-max-latest", - name: "Qwen-Max-Latest", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2024-04-03", - last_updated: "2025-01-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.343, output: 1.372 }, - limit: { context: 131072, output: 8192 }, - }, - "gemini-2.5-flash-image": { - id: "gemini-2.5-flash-image", - name: "gemini-2.5-flash-image", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-01", - release_date: "2025-10-08", - last_updated: "2025-10-08", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 30 }, - limit: { context: 32768, output: 32768 }, - }, - "glm-4.5": { - id: "glm-4.5", - name: "GLM-4.5", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-07-29", - last_updated: "2025-07-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.286, output: 1.142 }, - limit: { context: 128000, output: 98304 }, - }, - "gemini-2.5-flash": { - id: "gemini-2.5-flash", - name: "gemini-2.5-flash", - family: "gemini-flash", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5 }, - limit: { context: 1000000, output: 65536 }, - }, - "gpt-5.2-chat-latest": { - id: "gpt-5.2-chat-latest", - name: "gpt-5.2-chat-latest", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-12-12", - last_updated: "2025-12-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14 }, - limit: { context: 128000, output: 16384 }, - }, - "doubao-seed-1-6-vision-250815": { - id: "doubao-seed-1-6-vision-250815", - name: "doubao-seed-1-6-vision-250815", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.114, output: 1.143 }, - limit: { context: 256000, output: 32000 }, - }, - "MiniMax-M2.1": { - id: "MiniMax-M2.1", - name: "MiniMax-M2.1", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-12-19", - last_updated: "2025-12-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 1000000, output: 131072 }, - }, - "gpt-5.1": { - id: "gpt-5.1", - name: "gpt-5.1", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-11-14", - last_updated: "2025-11-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 400000, output: 128000 }, - }, - "kimi-k2-thinking-turbo": { - id: "kimi-k2-thinking-turbo", - name: "kimi-k2-thinking-turbo", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.265, output: 9.119 }, - limit: { context: 262144, output: 262144 }, - }, - "deepseek-reasoner": { - id: "deepseek-reasoner", - name: "Deepseek-Reasoner", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.29, output: 0.43 }, - limit: { context: 128000, output: 128000 }, - }, - "grok-4-fast-reasoning": { - id: "grok-4-fast-reasoning", - name: "grok-4-fast-reasoning", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-09-23", - last_updated: "2025-09-23", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5 }, - limit: { context: 2000000, output: 30000 }, - }, - "claude-opus-4-1-20250805-thinking": { - id: "claude-opus-4-1-20250805-thinking", - name: "claude-opus-4-1-20250805-thinking", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03", - release_date: "2025-05-27", - last_updated: "2025-05-27", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75 }, - limit: { context: 200000, output: 32000 }, - }, - "qwen3-30b-a3b": { - id: "qwen3-30b-a3b", - name: "Qwen3-30B-A3B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04-29", - last_updated: "2025-04-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.11, output: 1.08 }, - limit: { context: 128000, output: 8192 }, - }, - "glm-4.5v": { - id: "glm-4.5v", - name: "GLM-4.5V", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-07-29", - last_updated: "2025-07-29", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.29, output: 0.86 }, - limit: { context: 64000, output: 16384 }, - }, - "glm-4.6": { - id: "glm-4.6", - name: "glm-4.6", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-03", - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.286, output: 1.142 }, - limit: { context: 200000, output: 131072 }, - }, - "gemini-2.5-flash-preview-09-2025": { - id: "gemini-2.5-flash-preview-09-2025", - name: "gemini-2.5-flash-preview-09-2025", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-09-26", - last_updated: "2025-09-26", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5 }, - limit: { context: 1000000, output: 65536 }, - }, - "glm-4.6v": { - id: "glm-4.6v", - name: "GLM-4.6V", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-03", - release_date: "2025-12-08", - last_updated: "2025-12-08", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.145, output: 0.43 }, - limit: { context: 128000, output: 32768 }, - }, - "claude-opus-4-1-20250805": { - id: "claude-opus-4-1-20250805", - name: "claude-opus-4-1-20250805", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-03", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75 }, - limit: { context: 200000, output: 32000 }, - }, - "gpt-5.1-chat-latest": { - id: "gpt-5.1-chat-latest", - name: "gpt-5.1-chat-latest", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-11-14", - last_updated: "2025-11-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 128000, output: 16384 }, - }, - "claude-haiku-4-5-20251001": { - id: "claude-haiku-4-5-20251001", - name: "claude-haiku-4-5-20251001", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-03", - release_date: "2025-10-16", - last_updated: "2025-10-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5 }, - limit: { context: 200000, output: 64000 }, - }, - "MiniMax-M1": { - id: "MiniMax-M1", - name: "MiniMax-M1", - family: "minimax", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-06-16", - last_updated: "2025-06-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.132, output: 1.254 }, - limit: { context: 1000000, output: 128000 }, - }, - "qwen3-coder-480b-a35b-instruct": { - id: "qwen3-coder-480b-a35b-instruct", - name: "qwen3-coder-480b-a35b-instruct", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.86, output: 3.43 }, - limit: { context: 262144, output: 65536 }, - }, - "doubao-seed-code-preview-251028": { - id: "doubao-seed-code-preview-251028", - name: "doubao-seed-code-preview-251028", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-11-11", - last_updated: "2025-11-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.17, output: 1.14 }, - limit: { context: 256000, output: 32000 }, - }, - "gpt-4.1-nano": { - id: "gpt-4.1-nano", - name: "gpt-4.1-nano", - family: "gpt-nano", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 1000000, output: 32768 }, - }, - "deepseek-v3.2": { - id: "deepseek-v3.2", - name: "deepseek-v3.2", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.29, output: 0.43 }, - limit: { context: 128000, output: 8192 }, - }, - "gpt-5-pro": { - id: "gpt-5-pro", - name: "gpt-5-pro", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-10-08", - last_updated: "2025-10-08", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 120 }, - limit: { context: 400000, output: 272000 }, - }, - "gpt-4o": { - id: "gpt-4o", - name: "gpt-4o", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-09", - release_date: "2024-05-13", - last_updated: "2024-05-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10 }, - limit: { context: 128000, output: 16384 }, - }, - "gpt-5": { - id: "gpt-5", - name: "gpt-5", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-08-08", - last_updated: "2025-08-08", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 400000, output: 128000 }, - }, - "claude-sonnet-4-5-20250929-thinking": { - id: "claude-sonnet-4-5-20250929-thinking", - name: "claude-sonnet-4-5-20250929-thinking", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03", - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 200000, output: 64000 }, - }, - "gpt-4.1": { - id: "gpt-4.1", - name: "gpt-4.1", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8 }, - limit: { context: 1000000, output: 32768 }, - }, - "kimi-k2-thinking": { - id: "kimi-k2-thinking", - name: "kimi-k2-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.575, output: 2.3 }, - limit: { context: 262144, output: 262144 }, - }, - "gemini-2.0-flash-lite": { - id: "gemini-2.0-flash-lite", - name: "gemini-2.0-flash-lite", - family: "gemini-flash-lite", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-11", - release_date: "2025-06-16", - last_updated: "2025-06-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.075, output: 0.3 }, - limit: { context: 2000000, output: 8192 }, - }, - "gpt-4.1-mini": { - id: "gpt-4.1-mini", - name: "gpt-4.1-mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.6 }, - limit: { context: 1000000, output: 32768 }, - }, - "grok-4-fast-non-reasoning": { - id: "grok-4-fast-non-reasoning", - name: "grok-4-fast-non-reasoning", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-09-23", - last_updated: "2025-09-23", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5 }, - limit: { context: 2000000, output: 30000 }, - }, - "doubao-seed-1-6-thinking-250715": { - id: "doubao-seed-1-6-thinking-250715", - name: "doubao-seed-1-6-thinking-250715", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-07-15", - last_updated: "2025-07-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.121, output: 1.21 }, - limit: { context: 256000, output: 16000 }, - }, - "ministral-14b-2512": { - id: "ministral-14b-2512", - name: "ministral-14b-2512", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-12-16", - last_updated: "2025-12-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.33, output: 0.33 }, - limit: { context: 128000, output: 128000 }, - }, - }, - }, - alibaba: { - id: "alibaba", - env: ["DASHSCOPE_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", - name: "Alibaba", - doc: "https://www.alibabacloud.com/help/en/model-studio/models", - models: { - "qwen3-235b-a22b": { - id: "qwen3-235b-a22b", - name: "Qwen3 235B-A22B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.7, output: 2.8, reasoning: 8.4 }, - limit: { context: 131072, output: 16384 }, - }, - "qwen3-coder-plus": { - id: "qwen3-coder-plus", - name: "Qwen3 Coder Plus", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 5 }, - limit: { context: 1048576, output: 65536 }, - }, - "qwen-vl-ocr": { - id: "qwen-vl-ocr", - name: "Qwen-VL OCR", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-04", - release_date: "2024-10-28", - last_updated: "2025-04-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.72, output: 0.72 }, - limit: { context: 34096, output: 4096 }, - }, - "qwen-omni-turbo-realtime": { - id: "qwen-omni-turbo-realtime", - name: "Qwen-Omni Turbo Realtime", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-05-08", - last_updated: "2025-05-08", - modalities: { input: ["text", "image", "audio"], output: ["text", "audio"] }, - open_weights: false, - cost: { input: 0.27, output: 1.07, input_audio: 4.44, output_audio: 8.89 }, - limit: { context: 32768, output: 2048 }, - }, - "qwen3-8b": { - id: "qwen3-8b", - name: "Qwen3 8B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.18, output: 0.7, reasoning: 2.1 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen3.5-397b-a17b": { - id: "qwen3.5-397b-a17b", - name: "Qwen3.5 397B-A17B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02-16", - last_updated: "2026-02-16", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3.6, reasoning: 3.6 }, - limit: { context: 262144, output: 65536 }, - }, - "qwq-plus": { - id: "qwq-plus", - name: "QwQ Plus", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-03-05", - last_updated: "2025-03-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 2.4 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen-vl-plus": { - id: "qwen-vl-plus", - name: "Qwen-VL Plus", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-01-25", - last_updated: "2025-08-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.21, output: 0.63 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen3-livetranslate-flash-realtime": { - id: "qwen3-livetranslate-flash-realtime", - name: "Qwen3-LiveTranslate Flash Realtime", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-04", - release_date: "2025-09-22", - last_updated: "2025-09-22", - modalities: { input: ["text", "image", "audio", "video"], output: ["text", "audio"] }, - open_weights: false, - cost: { input: 10, output: 10, input_audio: 10, output_audio: 38 }, - limit: { context: 53248, output: 4096 }, - }, - "qwen3-32b": { - id: "qwen3-32b", - name: "Qwen3 32B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.7, output: 2.8, reasoning: 8.4 }, - limit: { context: 131072, output: 16384 }, - }, - "qwen-max": { - id: "qwen-max", - name: "Qwen Max", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-04-03", - last_updated: "2025-01-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.6, output: 6.4 }, - limit: { context: 32768, output: 8192 }, - }, - "qwen-plus": { - id: "qwen-plus", - name: "Qwen Plus", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-01-25", - last_updated: "2025-09-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.2, reasoning: 4 }, - limit: { context: 1000000, output: 32768 }, - }, - "qwen-omni-turbo": { - id: "qwen-omni-turbo", - name: "Qwen-Omni Turbo", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-01-19", - last_updated: "2025-03-26", - modalities: { input: ["text", "image", "audio", "video"], output: ["text", "audio"] }, - open_weights: false, - cost: { input: 0.07, output: 0.27, input_audio: 4.44, output_audio: 8.89 }, - limit: { context: 32768, output: 2048 }, - }, - "qwen-flash": { - id: "qwen-flash", - name: "Qwen Flash", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.4 }, - limit: { context: 1000000, output: 32768 }, - }, - "qwen2-5-vl-7b-instruct": { - id: "qwen2-5-vl-7b-instruct", - name: "Qwen2.5-VL 7B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-09", - last_updated: "2024-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.35, output: 1.05 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen3.6-plus": { - id: "qwen3.6-plus", - name: "Qwen3.6 Plus", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-04-02", - last_updated: "2026-04-02", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.276, output: 1.651, cache_read: 0.028, cache_write: 0.344 }, - limit: { context: 1000000, output: 65536 }, - }, - "qwen3-max": { - id: "qwen3-max", - name: "Qwen3 Max", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-23", - last_updated: "2025-09-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.2, output: 6 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen3-omni-flash": { - id: "qwen3-omni-flash", - name: "Qwen3-Omni Flash", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-09-15", - last_updated: "2025-09-15", - modalities: { input: ["text", "image", "audio", "video"], output: ["text", "audio"] }, - open_weights: false, - cost: { input: 0.43, output: 1.66, input_audio: 3.81, output_audio: 15.11 }, - limit: { context: 65536, output: 16384 }, - }, - "qwen2-5-72b-instruct": { - id: "qwen2-5-72b-instruct", - name: "Qwen2.5 72B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-09", - last_updated: "2024-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.4, output: 5.6 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen3-vl-235b-a22b": { - id: "qwen3-vl-235b-a22b", - name: "Qwen3-VL 235B-A22B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.7, output: 2.8, reasoning: 8.4 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen3-asr-flash": { - id: "qwen3-asr-flash", - name: "Qwen3-ASR Flash", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2024-04", - release_date: "2025-09-08", - last_updated: "2025-09-08", - modalities: { input: ["audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.035, output: 0.035 }, - limit: { context: 53248, output: 4096 }, - }, - "qwen3-next-80b-a3b-thinking": { - id: "qwen3-next-80b-a3b-thinking", - name: "Qwen3-Next 80B-A3B (Thinking)", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09", - last_updated: "2025-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 6 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen-mt-plus": { - id: "qwen-mt-plus", - name: "Qwen-MT Plus", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-04", - release_date: "2025-01", - last_updated: "2025-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.46, output: 7.37 }, - limit: { context: 16384, output: 8192 }, - }, - "qwen-vl-max": { - id: "qwen-vl-max", - name: "Qwen-VL Max", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-04-08", - last_updated: "2025-08-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 3.2 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen3-coder-flash": { - id: "qwen3-coder-flash", - name: "Qwen3 Coder Flash", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.5 }, - limit: { context: 1000000, output: 65536 }, - }, - "qwen2-5-7b-instruct": { - id: "qwen2-5-7b-instruct", - name: "Qwen2.5 7B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-09", - last_updated: "2024-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.175, output: 0.7 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen2-5-14b-instruct": { - id: "qwen2-5-14b-instruct", - name: "Qwen2.5 14B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-09", - last_updated: "2024-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.35, output: 1.4 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen2-5-32b-instruct": { - id: "qwen2-5-32b-instruct", - name: "Qwen2.5 32B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-09", - last_updated: "2024-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.7, output: 2.8 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen3-next-80b-a3b-instruct": { - id: "qwen3-next-80b-a3b-instruct", - name: "Qwen3-Next 80B-A3B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09", - last_updated: "2025-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 2 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen-plus-character-ja": { - id: "qwen-plus-character-ja", - name: "Qwen Plus Character (Japanese)", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-01", - last_updated: "2024-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 1.4 }, - limit: { context: 8192, output: 512 }, - }, - "qwen3-omni-flash-realtime": { - id: "qwen3-omni-flash-realtime", - name: "Qwen3-Omni Flash Realtime", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-09-15", - last_updated: "2025-09-15", - modalities: { input: ["text", "image", "audio", "video"], output: ["text", "audio"] }, - open_weights: false, - cost: { input: 0.52, output: 1.99, input_audio: 4.57, output_audio: 18.13 }, - limit: { context: 65536, output: 16384 }, - }, - "qwen3-vl-30b-a3b": { - id: "qwen3-vl-30b-a3b", - name: "Qwen3-VL 30B-A3B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.8, reasoning: 2.4 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen3-vl-plus": { - id: "qwen3-vl-plus", - name: "Qwen3-VL Plus", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-23", - last_updated: "2025-09-23", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.6, reasoning: 4.8 }, - limit: { context: 262144, output: 32768 }, - }, - "qwen3-coder-480b-a35b-instruct": { - id: "qwen3-coder-480b-a35b-instruct", - name: "Qwen3-Coder 480B-A35B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.5, output: 7.5 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen3-coder-30b-a3b-instruct": { - id: "qwen3-coder-30b-a3b-instruct", - name: "Qwen3-Coder 30B-A3B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.45, output: 2.25 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen-turbo": { - id: "qwen-turbo", - name: "Qwen Turbo", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-11-01", - last_updated: "2025-04-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.2, reasoning: 0.5 }, - limit: { context: 1000000, output: 16384 }, - }, - "qwen-mt-turbo": { - id: "qwen-mt-turbo", - name: "Qwen-MT Turbo", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-04", - release_date: "2025-01", - last_updated: "2025-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.16, output: 0.49 }, - limit: { context: 16384, output: 8192 }, - }, - "qwen2-5-omni-7b": { - id: "qwen2-5-omni-7b", - name: "Qwen2.5-Omni 7B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-12", - last_updated: "2024-12", - modalities: { input: ["text", "image", "audio", "video"], output: ["text", "audio"] }, - open_weights: true, - cost: { input: 0.1, output: 0.4, input_audio: 6.76 }, - limit: { context: 32768, output: 2048 }, - }, - "qwen3.5-plus": { - id: "qwen3.5-plus", - name: "Qwen3.5 Plus", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02-16", - last_updated: "2026-02-16", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2.4, reasoning: 2.4 }, - limit: { context: 1000000, output: 65536 }, - }, - "qwen2-5-vl-72b-instruct": { - id: "qwen2-5-vl-72b-instruct", - name: "Qwen2.5-VL 72B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-09", - last_updated: "2024-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 2.8, output: 8.4 }, - limit: { context: 131072, output: 8192 }, - }, - "qvq-max": { - id: "qvq-max", - name: "QVQ Max", - family: "qvq", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-03-25", - last_updated: "2025-03-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.2, output: 4.8 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen3-14b": { - id: "qwen3-14b", - name: "Qwen3 14B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.35, output: 1.4, reasoning: 4.2 }, - limit: { context: 131072, output: 8192 }, - }, - }, - }, - scaleway: { - id: "scaleway", - env: ["SCALEWAY_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.scaleway.ai/v1", - name: "Scaleway", - doc: "https://www.scaleway.com/en/docs/generative-apis/", - models: { - "qwen3-embedding-8b": { - id: "qwen3-embedding-8b", - name: "Qwen3 Embedding 8B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-25-11", - last_updated: "2026-03-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0 }, - limit: { context: 32768, output: 4096 }, - }, - "qwen3-235b-a22b-instruct-2507": { - id: "qwen3-235b-a22b-instruct-2507", - name: "Qwen3 235B A22B Instruct 2507", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-07-01", - last_updated: "2026-03-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.75, output: 2.25 }, - limit: { context: 260000, output: 16384 }, - }, - "llama-3.3-70b-instruct": { - id: "llama-3.3-70b-instruct", - name: "Llama-3.3-70B-Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-12-06", - last_updated: "2026-03-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.9, output: 0.9 }, - limit: { context: 100000, output: 16384 }, - }, - "qwen3.5-397b-a17b": { - id: "qwen3.5-397b-a17b", - name: "Qwen3.5 397B A17B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3.6 }, - limit: { context: 256000, output: 16384 }, - }, - "devstral-2-123b-instruct-2512": { - id: "devstral-2-123b-instruct-2512", - name: "Devstral 2 123B Instruct (2512)", - family: "devstral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2026-01-07", - last_updated: "2026-03-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 2 }, - limit: { context: 256000, output: 16384 }, - }, - "deepseek-r1-distill-llama-70b": { - id: "deepseek-r1-distill-llama-70b", - name: "DeepSeek R1 Distill Llama 70B", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-01-20", - last_updated: "2026-03-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.9, output: 0.9 }, - limit: { context: 32000, output: 8196 }, - }, - "pixtral-12b-2409": { - id: "pixtral-12b-2409", - name: "Pixtral 12B 2409", - family: "pixtral", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-09-25", - last_updated: "2026-03-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 128000, output: 4096 }, - }, - "whisper-large-v3": { - id: "whisper-large-v3", - name: "Whisper Large v3", - family: "whisper", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2023-09", - release_date: "2023-09-01", - last_updated: "2026-03-17", - modalities: { input: ["audio"], output: ["text"] }, - open_weights: true, - cost: { input: 0.003, output: 0 }, - limit: { context: 0, output: 8192 }, - }, - "voxtral-small-24b-2507": { - id: "voxtral-small-24b-2507", - name: "Voxtral Small 24B 2507", - family: "voxtral", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-07-01", - last_updated: "2026-03-17", - modalities: { input: ["text", "audio"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.35 }, - limit: { context: 32000, output: 16384 }, - }, - "gemma-3-27b-it": { - id: "gemma-3-27b-it", - name: "Gemma-3-27B-IT", - family: "gemma", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2024-12-01", - last_updated: "2026-03-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 0.5 }, - limit: { context: 40000, output: 8192 }, - }, - "bge-multilingual-gemma2": { - id: "bge-multilingual-gemma2", - name: "BGE Multilingual Gemma2", - family: "gemma", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-07-26", - last_updated: "2025-06-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0 }, - limit: { context: 8191, output: 3072 }, - }, - "qwen3-coder-30b-a3b-instruct": { - id: "qwen3-coder-30b-a3b-instruct", - name: "Qwen3-Coder 30B-A3B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2026-03-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.8 }, - limit: { context: 128000, output: 32768 }, - }, - "mistral-small-3.2-24b-instruct-2506": { - id: "mistral-small-3.2-24b-instruct-2506", - name: "Mistral Small 3.2 24B Instruct (2506)", - family: "mistral-small", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-06-20", - last_updated: "2026-03-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.35 }, - limit: { context: 128000, output: 32768 }, - }, - "gpt-oss-120b": { - id: "gpt-oss-120b", - name: "GPT-OSS 120B", - family: "gpt-oss", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-01-01", - last_updated: "2026-03-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 128000, output: 32768 }, - }, - "mistral-nemo-instruct-2407": { - id: "mistral-nemo-instruct-2407", - name: "Mistral Nemo Instruct 2407", - family: "mistral-nemo", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-07-25", - last_updated: "2026-03-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 128000, output: 8192 }, - }, - "llama-3.1-8b-instruct": { - id: "llama-3.1-8b-instruct", - name: "Llama 3.1 8B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2025-01-01", - last_updated: "2026-03-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 128000, output: 16384 }, - }, - }, - }, - "nano-gpt": { - id: "nano-gpt", - env: ["NANO_GPT_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://nano-gpt.com/api/v1", - name: "NanoGPT", - doc: "https://docs.nano-gpt.com", - models: { - "glm-4-flash": { - id: "glm-4-flash", - name: "GLM-4 Flash", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-08-01", - last_updated: "2024-08-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1003, output: 0.1003 }, - limit: { context: 128000, input: 128000, output: 4096 }, - }, - "Meta-Llama-3-1-8B-Instruct-FP8": { - id: "Meta-Llama-3-1-8B-Instruct-FP8", - name: "Llama 3.1 8B (decentralized)", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.02, output: 0.03 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "claude-opus-4-thinking:32000": { - id: "claude-opus-4-thinking:32000", - name: "Claude 4 Opus Thinking (32K)", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 14.994, output: 75.004 }, - limit: { context: 200000, input: 200000, output: 32000 }, - }, - "gemini-2.5-pro-preview-05-06": { - id: "gemini-2.5-pro-preview-05-06", - name: "Gemini 2.5 Pro Preview 0506", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-05-06", - last_updated: "2025-05-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10 }, - limit: { context: 1048756, input: 1048756, output: 65536 }, - }, - "grok-3-mini-fast-beta": { - id: "grok-3-mini-fast-beta", - name: "Grok 3 Mini Fast Beta", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 4 }, - limit: { context: 131072, input: 131072, output: 131072 }, - }, - "MiniMax-M2": { - id: "MiniMax-M2", - name: "MiniMax M2", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-10-25", - last_updated: "2025-10-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.17, output: 1.53 }, - limit: { context: 200000, input: 200000, output: 131072 }, - }, - "command-a-reasoning-08-2025": { - id: "command-a-reasoning-08-2025", - name: "Cohere Command A (08/2025)", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-08-22", - last_updated: "2025-08-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10 }, - limit: { context: 256000, input: 256000, output: 8192 }, - }, - brave: { - id: "brave", - name: "Brave (Answers)", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2023-03-02", - last_updated: "2024-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 5 }, - limit: { context: 8192, input: 8192, output: 8192 }, - }, - "exa-research": { - id: "exa-research", - name: "Exa (Research)", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-06-04", - last_updated: "2025-06-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 2.5 }, - limit: { context: 8192, input: 8192, output: 8192 }, - }, - "Llama-3.3-70B-Nova": { - id: "Llama-3.3-70B-Nova", - name: "Llama 3.3 70B Nova", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "gemini-exp-1206": { - id: "gemini-exp-1206", - name: "Gemini 2.0 Pro 1206", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.258, output: 4.998 }, - limit: { context: 2097152, input: 2097152, output: 8192 }, - }, - "claude-opus-4-5-20251101": { - id: "claude-opus-4-5-20251101", - name: "Claude 4.5 Opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-11-01", - last_updated: "2025-11-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 4.998, output: 25.007 }, - limit: { context: 200000, input: 200000, output: 32000 }, - }, - "auto-model-basic": { - id: "auto-model-basic", - name: "Auto model (Basic)", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-06-01", - last_updated: "2024-06-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 9.996, output: 19.992 }, - limit: { context: 1000000, input: 1000000, output: 1000000 }, - }, - "jamba-mini": { - id: "jamba-mini", - name: "Jamba Mini", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1989, output: 0.408 }, - limit: { context: 256000, input: 256000, output: 4096 }, - }, - "gemini-2.5-flash-lite-preview-09-2025": { - id: "gemini-2.5-flash-lite-preview-09-2025", - name: "Gemini 2.5 Flash Lite Preview (09/2025)", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 1048756, input: 1048756, output: 65536 }, - }, - "yi-large": { - id: "yi-large", - name: "Yi Large", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-05-13", - last_updated: "2024-05-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3.196, output: 3.196 }, - limit: { context: 32000, input: 32000, output: 4096 }, - }, - "auto-model-premium": { - id: "auto-model-premium", - name: "Auto model (Premium)", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-06-01", - last_updated: "2024-06-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 9.996, output: 19.992 }, - limit: { context: 1000000, input: 1000000, output: 1000000 }, - }, - "azure-gpt-4o": { - id: "azure-gpt-4o", - name: "Azure gpt-4o", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2024-05-13", - last_updated: "2024-05-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.499, output: 9.996 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "deepseek-v3-0324": { - id: "deepseek-v3-0324", - name: "DeepSeek Chat 0324", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-03-24", - last_updated: "2025-03-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 0.7 }, - limit: { context: 128000, input: 128000, output: 8192 }, - }, - "claude-3-5-haiku-20241022": { - id: "claude-3-5-haiku-20241022", - name: "Claude 3.5 Haiku", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 4 }, - limit: { context: 200000, input: 200000, output: 8192 }, - }, - "doubao-seed-1-8-251215": { - id: "doubao-seed-1-8-251215", - name: "Doubao Seed 1.8", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-15", - last_updated: "2025-12-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.612, output: 6.12 }, - limit: { context: 128000, input: 128000, output: 8192 }, - }, - "doubao-seed-1-6-250615": { - id: "doubao-seed-1-6-250615", - name: "Doubao Seed 1.6", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-06-15", - last_updated: "2025-06-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.204, output: 0.51 }, - limit: { context: 256000, input: 256000, output: 16384 }, - }, - "ernie-x1.1-preview": { - id: "ernie-x1.1-preview", - name: "ERNIE X1.1", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-09-10", - last_updated: "2025-09-10", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 64000, input: 64000, output: 8192 }, - }, - "ernie-5.0-thinking-preview": { - id: "ernie-5.0-thinking-preview", - name: "Ernie 5.0 Thinking Preview", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-11-18", - last_updated: "2025-11-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 2 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "glm-4-air-0111": { - id: "glm-4-air-0111", - name: "GLM 4 Air 0111", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-01-11", - last_updated: "2025-01-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1394, output: 0.1394 }, - limit: { context: 128000, input: 128000, output: 4096 }, - }, - fastgpt: { - id: "fastgpt", - name: "Web Answer", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2023-08-01", - last_updated: "2024-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 7.5, output: 7.5 }, - limit: { context: 32768, input: 32768, output: 32768 }, - }, - "doubao-seed-1-6-thinking-250615": { - id: "doubao-seed-1-6-thinking-250615", - name: "Doubao Seed 1.6 Thinking", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-06-15", - last_updated: "2025-06-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.204, output: 2.04 }, - limit: { context: 256000, input: 256000, output: 16384 }, - }, - "gemini-2.0-flash-001": { - id: "gemini-2.0-flash-001", - name: "Gemini 2.0 Flash", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1003, output: 0.408 }, - limit: { context: 1000000, input: 1000000, output: 8192 }, - }, - "claude-opus-4-1-thinking:32000": { - id: "claude-opus-4-1-thinking:32000", - name: "Claude 4.1 Opus Thinking (32K)", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 14.994, output: 75.004 }, - limit: { context: 200000, input: 200000, output: 32000 }, - }, - "Llama-3.3-70B-RAWMAW": { - id: "Llama-3.3-70B-RAWMAW", - name: "Llama 3.3 70B RAWMAW", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "GLM-4.5-Air-Derestricted-Steam": { - id: "GLM-4.5-Air-Derestricted-Steam", - name: "GLM 4.5 Air Derestricted Steam", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 220600, input: 220600, output: 65536 }, - }, - "claude-3-5-sonnet-20241022": { - id: "claude-3-5-sonnet-20241022", - name: "Claude 3.5 Sonnet", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-08-26", - last_updated: "2025-08-26", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.992, output: 14.994 }, - limit: { context: 200000, input: 200000, output: 8192 }, - }, - "yi-medium-200k": { - id: "yi-medium-200k", - name: "Yi Medium 200k", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-03-01", - last_updated: "2024-03-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.499, output: 2.499 }, - limit: { context: 200000, input: 200000, output: 4096 }, - }, - "Gemma-3-27B-ArliAI-RPMax-v3": { - id: "Gemma-3-27B-ArliAI-RPMax-v3", - name: "Gemma 3 27B RPMax v3", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-03", - last_updated: "2025-07-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "phi-4-mini-instruct": { - id: "phi-4-mini-instruct", - name: "Phi 4 Mini", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-26", - last_updated: "2025-07-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.17, output: 0.68 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "Llama-3.3+(3v3.3)-70B-TenyxChat-DaybreakStorywriter": { - id: "Llama-3.3+(3v3.3)-70B-TenyxChat-DaybreakStorywriter", - name: "Llama 3.3+ 70B TenyxChat DaybreakStorywriter", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "ernie-x1-32k": { - id: "ernie-x1-32k", - name: "Ernie X1 32k", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-05-08", - last_updated: "2025-05-08", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.33, output: 1.32 }, - limit: { context: 32000, input: 32000, output: 16384 }, - }, - "deepseek-chat": { - id: "deepseek-chat", - name: "DeepSeek V3/Deepseek Chat", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-02-27", - last_updated: "2025-02-27", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 0.7 }, - limit: { context: 128000, input: 128000, output: 8192 }, - }, - "glm-z1-air": { - id: "glm-z1-air", - name: "GLM Z1 Air", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-04-15", - last_updated: "2025-04-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.07, output: 0.07 }, - limit: { context: 32000, input: 32000, output: 16384 }, - }, - "claude-3-7-sonnet-thinking:128000": { - id: "claude-3-7-sonnet-thinking:128000", - name: "Claude 3.7 Sonnet Thinking (128K)", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-02-24", - last_updated: "2025-02-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.992, output: 14.994 }, - limit: { context: 200000, input: 200000, output: 64000 }, - }, - "glm-4-air": { - id: "glm-4-air", - name: "GLM-4 Air", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-06-05", - last_updated: "2024-06-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2006, output: 0.2006 }, - limit: { context: 128000, input: 128000, output: 4096 }, - }, - "Llama-3.3-70B-MiraiFanfare": { - id: "Llama-3.3-70B-MiraiFanfare", - name: "Llama 3.3 70b Mirai Fanfare", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-26", - last_updated: "2025-07-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.493, output: 0.493 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "gemini-2.0-flash-thinking-exp-01-21": { - id: "gemini-2.0-flash-thinking-exp-01-21", - name: "Gemini 2.0 Flash Thinking 0121", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-01-21", - last_updated: "2025-01-21", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 1.003 }, - limit: { context: 1000000, input: 1000000, output: 8192 }, - }, - "Magistral-Small-2506": { - id: "Magistral-Small-2506", - name: "Magistral Small 2506", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.4 }, - limit: { context: 32768, input: 32768, output: 32768 }, - }, - "doubao-1.5-pro-32k": { - id: "doubao-1.5-pro-32k", - name: "Doubao 1.5 Pro 32k", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-01-22", - last_updated: "2025-01-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1343, output: 0.3349 }, - limit: { context: 32000, input: 32000, output: 8192 }, - }, - "venice-uncensored:web": { - id: "venice-uncensored:web", - name: "Venice Uncensored Web", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-05-01", - last_updated: "2024-05-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 0.4 }, - limit: { context: 80000, input: 80000, output: 16384 }, - }, - "glm-4": { - id: "glm-4", - name: "GLM-4", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-01-16", - last_updated: "2024-01-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 14.994, output: 14.994 }, - limit: { context: 128000, input: 128000, output: 4096 }, - }, - "qwen-max": { - id: "qwen-max", - name: "Qwen 2.5 Max", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-04-03", - last_updated: "2024-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.5997, output: 6.392 }, - limit: { context: 32000, input: 32000, output: 8192 }, - }, - "qwen3-vl-235b-a22b-instruct-original": { - id: "qwen3-vl-235b-a22b-instruct-original", - name: "Qwen3 VL 235B A22B Instruct Original", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 1.2 }, - limit: { context: 32768, input: 32768, output: 32768 }, - }, - "jamba-large-1.6": { - id: "jamba-large-1.6", - name: "Jamba Large 1.6", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-03-12", - last_updated: "2025-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.989, output: 7.99 }, - limit: { context: 256000, input: 256000, output: 4096 }, - }, - "qwen-plus": { - id: "qwen-plus", - name: "Qwen Plus", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2024-01-25", - last_updated: "2024-01-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3995, output: 1.2002 }, - limit: { context: 995904, input: 995904, output: 32768 }, - }, - "qwen25-vl-72b-instruct": { - id: "qwen25-vl-72b-instruct", - name: "Qwen25 VL 72b", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-05-10", - last_updated: "2025-05-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.69989, output: 0.69989 }, - limit: { context: 32000, input: 32000, output: 32768 }, - }, - "claude-sonnet-4-thinking:64000": { - id: "claude-sonnet-4-thinking:64000", - name: "Claude 4 Sonnet Thinking (64K)", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.992, output: 14.994 }, - limit: { context: 1000000, input: 1000000, output: 64000 }, - }, - "gemini-3-pro-preview": { - id: "gemini-3-pro-preview", - name: "Gemini 3 Pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-11-18", - last_updated: "2025-11-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12 }, - limit: { context: 1048756, input: 1048756, output: 65536 }, - }, - "Llama-3.3+(3.1v3.3)-70B-New-Dawn-v1.1": { - id: "Llama-3.3+(3.1v3.3)-70B-New-Dawn-v1.1", - name: "Llama 3.3+ 70B New Dawn v1.1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "GLM-4.5-Air-Derestricted-Iceblink-ReExtract": { - id: "GLM-4.5-Air-Derestricted-Iceblink-ReExtract", - name: "GLM 4.5 Air Derestricted Iceblink ReExtract", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-12", - last_updated: "2025-12-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 131072, input: 131072, output: 98304 }, - }, - "universal-summarizer": { - id: "universal-summarizer", - name: "Universal Summarizer", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2023-05-01", - last_updated: "2024-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 30, output: 30 }, - limit: { context: 32768, input: 32768, output: 32768 }, - }, - "claude-sonnet-4-thinking:32768": { - id: "claude-sonnet-4-thinking:32768", - name: "Claude 4 Sonnet Thinking (32K)", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.992, output: 14.994 }, - limit: { context: 1000000, input: 1000000, output: 64000 }, - }, - "sarvan-medium": { - id: "sarvan-medium", - name: "Sarvam Medium", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 0.75 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "claude-3-7-sonnet-thinking:8192": { - id: "claude-3-7-sonnet-thinking:8192", - name: "Claude 3.7 Sonnet Thinking (8K)", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-02-24", - last_updated: "2025-02-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.992, output: 14.994 }, - limit: { context: 200000, input: 200000, output: 64000 }, - }, - "gemini-2.5-flash-preview-05-20": { - id: "gemini-2.5-flash-preview-05-20", - name: "Gemini 2.5 Flash 0520", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-05-20", - last_updated: "2025-05-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 1048000, input: 1048000, output: 65536 }, - }, - "GLM-4.5-Air-Derestricted-Iceblink-v2-ReExtract": { - id: "GLM-4.5-Air-Derestricted-Iceblink-v2-ReExtract", - name: "GLM 4.5 Air Derestricted Iceblink v2 ReExtract", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-12", - last_updated: "2025-12-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 131072, input: 131072, output: 65536 }, - }, - "Llama-3.3-70B-Fallen-v1": { - id: "Llama-3.3-70B-Fallen-v1", - name: "Llama 3.3 70B Fallen v1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "qwen3-vl-235b-a22b-thinking": { - id: "qwen3-vl-235b-a22b-thinking", - name: "Qwen3 VL 235B A22B Thinking", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-08-26", - last_updated: "2025-08-26", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 6 }, - limit: { context: 32768, input: 32768, output: 32768 }, - }, - "claude-3-7-sonnet-thinking:32768": { - id: "claude-3-7-sonnet-thinking:32768", - name: "Claude 3.7 Sonnet Thinking (32K)", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-07-15", - last_updated: "2025-07-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.992, output: 14.994 }, - limit: { context: 200000, input: 200000, output: 64000 }, - }, - "claude-3-7-sonnet-thinking:1024": { - id: "claude-3-7-sonnet-thinking:1024", - name: "Claude 3.7 Sonnet Thinking (1K)", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-02-24", - last_updated: "2025-02-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.992, output: 14.994 }, - limit: { context: 200000, input: 200000, output: 64000 }, - }, - "claude-sonnet-4-5-20250929": { - id: "claude-sonnet-4-5-20250929", - name: "Claude Sonnet 4.5", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.992, output: 14.994 }, - limit: { context: 1000000, input: 1000000, output: 64000 }, - }, - "Llama-3.3-70B-Vulpecula-R1": { - id: "Llama-3.3-70B-Vulpecula-R1", - name: "Llama 3.3 70B Vulpecula R1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "claude-sonnet-4-thinking:8192": { - id: "claude-sonnet-4-thinking:8192", - name: "Claude 4 Sonnet Thinking (8K)", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.992, output: 14.994 }, - limit: { context: 1000000, input: 1000000, output: 64000 }, - }, - "gemini-2.5-pro": { - id: "gemini-2.5-pro", - name: "Gemini 2.5 Pro", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-06-05", - last_updated: "2025-06-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10 }, - limit: { context: 1048756, input: 1048756, output: 65536 }, - }, - "Llama-3.3-70B-Ignition-v0.1": { - id: "Llama-3.3-70B-Ignition-v0.1", - name: "Llama 3.3 70B Ignition v0.1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "glm-4-plus-0111": { - id: "glm-4-plus-0111", - name: "GLM 4 Plus 0111", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-19", - last_updated: "2025-02-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 9.996, output: 9.996 }, - limit: { context: 128000, input: 128000, output: 4096 }, - }, - "KAT-Coder-Air-V1": { - id: "KAT-Coder-Air-V1", - name: "KAT Coder Air V1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-10-28", - last_updated: "2025-10-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.2 }, - limit: { context: 128000, input: 128000, output: 32768 }, - }, - "deepseek-r1-sambanova": { - id: "deepseek-r1-sambanova", - name: "DeepSeek R1 Fast", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-20", - last_updated: "2025-02-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 4.998, output: 6.987 }, - limit: { context: 128000, input: 128000, output: 4096 }, - }, - "deepseek-r1": { - id: "deepseek-r1", - name: "DeepSeek R1", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.7 }, - limit: { context: 128000, input: 128000, output: 8192 }, - }, - "doubao-1-5-thinking-pro-250415": { - id: "doubao-1-5-thinking-pro-250415", - name: "Doubao 1.5 Thinking Pro", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-04-17", - last_updated: "2025-04-17", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 2.4 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "sonar-pro": { - id: "sonar-pro", - name: "Perplexity Pro", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-19", - last_updated: "2025-02-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.992, output: 14.994 }, - limit: { context: 200000, input: 200000, output: 128000 }, - }, - "Gemma-3-27B-it-Abliterated": { - id: "Gemma-3-27B-it-Abliterated", - name: "Gemma 3 27B IT Abliterated", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-03", - last_updated: "2025-07-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.42, output: 0.42 }, - limit: { context: 32768, input: 32768, output: 96000 }, - }, - "deepseek-chat-cheaper": { - id: "deepseek-chat-cheaper", - name: "DeepSeek V3/Chat Cheaper", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-04-15", - last_updated: "2025-04-15", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 0.7 }, - limit: { context: 128000, input: 128000, output: 8192 }, - }, - "gemini-2.0-pro-exp-02-05": { - id: "gemini-2.0-pro-exp-02-05", - name: "Gemini 2.0 Pro 0205", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-05", - last_updated: "2025-02-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.989, output: 7.956 }, - limit: { context: 2097152, input: 2097152, output: 8192 }, - }, - "azure-gpt-4o-mini": { - id: "azure-gpt-4o-mini", - name: "Azure gpt-4o-mini", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1496, output: 0.595 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "Llama-3.3-70B-MS-Nevoria": { - id: "Llama-3.3-70B-MS-Nevoria", - name: "Llama 3.3 70B MS Nevoria", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "claude-opus-4-thinking": { - id: "claude-opus-4-thinking", - name: "Claude 4 Opus Thinking", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-07-15", - last_updated: "2025-07-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 14.994, output: 75.004 }, - limit: { context: 200000, input: 200000, output: 32000 }, - }, - "Llama-3.3-70B-Sapphira-0.1": { - id: "Llama-3.3-70B-Sapphira-0.1", - name: "Llama 3.3 70B Sapphira 0.1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "doubao-seed-code-preview-latest": { - id: "doubao-seed-code-preview-latest", - name: "Doubao Seed Code Preview", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 256000, input: 256000, output: 16384 }, - }, - "Llama-3.3-70B-ArliAI-RPMax-v1.4": { - id: "Llama-3.3-70B-ArliAI-RPMax-v1.4", - name: "Llama 3.3 70B RPMax v1.4", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "mistral-small-31-24b-instruct": { - id: "mistral-small-31-24b-instruct", - name: "Mistral Small 31 24b Instruct", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-04-15", - last_updated: "2025-04-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 128000, input: 128000, output: 131072 }, - }, - "glm-4.1v-thinking-flashx": { - id: "glm-4.1v-thinking-flashx", - name: "GLM 4.1V Thinking FlashX", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.3 }, - limit: { context: 64000, input: 64000, output: 8192 }, - }, - "hunyuan-t1-latest": { - id: "hunyuan-t1-latest", - name: "Hunyuan T1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-03-22", - last_updated: "2025-03-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.17, output: 0.66 }, - limit: { context: 256000, input: 256000, output: 16384 }, - }, - "doubao-1-5-thinking-vision-pro-250428": { - id: "doubao-1-5-thinking-vision-pro-250428", - name: "Doubao 1.5 Thinking Vision Pro", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-05-15", - last_updated: "2025-05-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.55, output: 1.43 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "asi1-mini": { - id: "asi1-mini", - name: "ASI1 Mini", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-03-25", - last_updated: "2025-03-25", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 1 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "ernie-5.0-thinking-latest": { - id: "ernie-5.0-thinking-latest", - name: "Ernie 5.0 Thinking", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-11-18", - last_updated: "2025-11-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 2 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "Llama-3.3-70B-Incandescent-Malevolence": { - id: "Llama-3.3-70B-Incandescent-Malevolence", - name: "Llama 3.3 70B Incandescent Malevolence", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "Llama-3.3-70B-Damascus-R1": { - id: "Llama-3.3-70B-Damascus-R1", - name: "Damascus R1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "Gemma-3-27B-Nidum-Uncensored": { - id: "Gemma-3-27B-Nidum-Uncensored", - name: "Gemma 3 27B Nidum Uncensored", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-08-08", - last_updated: "2025-08-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 96000 }, - }, - "gemini-2.5-flash-lite-preview-09-2025-thinking": { - id: "gemini-2.5-flash-lite-preview-09-2025-thinking", - name: "Gemini 2.5 Flash Lite Preview (09/2025) – Thinking", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 1048756, input: 1048756, output: 65536 }, - }, - "doubao-seed-2-0-pro-260215": { - id: "doubao-seed-2-0-pro-260215", - name: "Doubao Seed 2.0 Pro", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2026-02-14", - last_updated: "2026-02-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.782, output: 3.876 }, - limit: { context: 256000, input: 256000, output: 128000 }, - }, - "gemini-3-pro-image-preview": { - id: "gemini-3-pro-image-preview", - name: "Gemini 3 Pro Image", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-11-18", - last_updated: "2025-11-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12 }, - limit: { context: 1048756, input: 1048756, output: 65536 }, - }, - "Gemma-3-27B-CardProjector-v4": { - id: "Gemma-3-27B-CardProjector-v4", - name: "Gemma 3 27B CardProjector v4", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-03-10", - last_updated: "2025-03-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "jamba-mini-1.7": { - id: "jamba-mini-1.7", - name: "Jamba Mini 1.7", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1989, output: 0.408 }, - limit: { context: 256000, input: 256000, output: 4096 }, - }, - "Llama-3.3-70B-Forgotten-Safeword-3.6": { - id: "Llama-3.3-70B-Forgotten-Safeword-3.6", - name: "Llama 3.3 70B Forgotten Safeword 3.6", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "doubao-1-5-thinking-pro-vision-250415": { - id: "doubao-1-5-thinking-pro-vision-250415", - name: "Doubao 1.5 Thinking Pro Vision", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-04-15", - last_updated: "2025-04-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 2.4 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "gemini-2.5-pro-preview-06-05": { - id: "gemini-2.5-pro-preview-06-05", - name: "Gemini 2.5 Pro Preview 0605", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-06-05", - last_updated: "2025-06-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10 }, - limit: { context: 1048756, input: 1048756, output: 65536 }, - }, - "gemini-2.0-pro-reasoner": { - id: "gemini-2.0-pro-reasoner", - name: "Gemini 2.0 Pro Reasoner", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-05", - last_updated: "2025-02-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.292, output: 4.998 }, - limit: { context: 128000, input: 128000, output: 65536 }, - }, - "doubao-seed-2-0-lite-260215": { - id: "doubao-seed-2-0-lite-260215", - name: "Doubao Seed 2.0 Lite", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2026-02-14", - last_updated: "2026-02-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1462, output: 0.8738 }, - limit: { context: 256000, input: 256000, output: 32000 }, - }, - "gemini-2.5-flash-lite-preview-06-17": { - id: "gemini-2.5-flash-lite-preview-06-17", - name: "Gemini 2.5 Flash Lite Preview", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 1048756, input: 1048756, output: 65536 }, - }, - "sonar-deep-research": { - id: "sonar-deep-research", - name: "Perplexity Deep Research", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-25", - last_updated: "2025-02-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3.4, output: 13.6 }, - limit: { context: 60000, input: 60000, output: 128000 }, - }, - "Gemma-3-27B-it": { - id: "Gemma-3-27B-it", - name: "Gemma 3 27B IT", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-03-10", - last_updated: "2025-03-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "Llama-3.3-70B-GeneticLemonade-Unleashed-v3": { - id: "Llama-3.3-70B-GeneticLemonade-Unleashed-v3", - name: "Llama 3.3 70B GeneticLemonade Unleashed v3", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "Gemma-3-27B-Glitter": { - id: "Gemma-3-27B-Glitter", - name: "Gemma 3 27B Glitter", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-03-10", - last_updated: "2025-03-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "Llama-3.3-70B-The-Omega-Directive-Unslop-v2.1": { - id: "Llama-3.3-70B-The-Omega-Directive-Unslop-v2.1", - name: "Llama 3.3 70B Omega Directive Unslop v2.1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "qwen3-30b-a3b-instruct-2507": { - id: "qwen3-30b-a3b-instruct-2507", - name: "Qwen3 30B A3B Instruct 2507", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-20", - last_updated: "2025-02-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5 }, - limit: { context: 256000, input: 256000, output: 32768 }, - }, - "gemini-2.5-flash-preview-09-2025-thinking": { - id: "gemini-2.5-flash-preview-09-2025-thinking", - name: "Gemini 2.5 Flash Preview (09/2025) – Thinking", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5 }, - limit: { context: 1048756, input: 1048756, output: 65536 }, - }, - "gemini-2.5-flash": { - id: "gemini-2.5-flash", - name: "Gemini 2.5 Flash", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-06-05", - last_updated: "2025-06-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5 }, - limit: { context: 1048756, input: 1048756, output: 65536 }, - }, - deepclaude: { - id: "deepclaude", - name: "DeepClaude", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-01", - last_updated: "2025-02-01", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 128000, input: 128000, output: 8192 }, - }, - "ernie-4.5-8k-preview": { - id: "ernie-4.5-8k-preview", - name: "Ernie 4.5 8k Preview", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-03-25", - last_updated: "2025-03-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.66, output: 2.6 }, - limit: { context: 8000, input: 8000, output: 16384 }, - }, - "doubao-seed-2-0-mini-260215": { - id: "doubao-seed-2-0-mini-260215", - name: "Doubao Seed 2.0 Mini", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2026-02-14", - last_updated: "2026-02-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.0493, output: 0.4845 }, - limit: { context: 256000, input: 256000, output: 32000 }, - }, - "gemini-3-pro-preview-thinking": { - id: "gemini-3-pro-preview-thinking", - name: "Gemini 3 Pro Thinking", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-11-18", - last_updated: "2025-11-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12 }, - limit: { context: 1048756, input: 1048756, output: 65536 }, - }, - "Llama-3.3-70B-GeneticLemonade-Opus": { - id: "Llama-3.3-70B-GeneticLemonade-Opus", - name: "Llama 3.3 70B GeneticLemonade Opus", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "v0-1.5-lg": { - id: "v0-1.5-lg", - name: "v0 1.5 LG", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-04", - last_updated: "2025-07-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75 }, - limit: { context: 1000000, input: 1000000, output: 64000 }, - }, - "ernie-4.5-turbo-128k": { - id: "ernie-4.5-turbo-128k", - name: "Ernie 4.5 Turbo 128k", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-05-08", - last_updated: "2025-05-08", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.132, output: 0.55 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "KAT-Coder-Pro-V1": { - id: "KAT-Coder-Pro-V1", - name: "KAT Coder Pro V1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-10-28", - last_updated: "2025-10-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.5, output: 6 }, - limit: { context: 256000, input: 256000, output: 32768 }, - }, - "claude-3-5-sonnet-20240620": { - id: "claude-3-5-sonnet-20240620", - name: "Claude 3.5 Sonnet Old", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2024-06-20", - last_updated: "2024-06-20", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.992, output: 14.994 }, - limit: { context: 200000, input: 200000, output: 8192 }, - }, - "claude-opus-4-1-thinking:8192": { - id: "claude-opus-4-1-thinking:8192", - name: "Claude 4.1 Opus Thinking (8K)", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 14.994, output: 75.004 }, - limit: { context: 200000, input: 200000, output: 32000 }, - }, - "gemini-2.0-flash-exp-image-generation": { - id: "gemini-2.0-flash-exp-image-generation", - name: "Gemini Text + Image", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-19", - last_updated: "2025-02-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.8 }, - limit: { context: 32767, input: 32767, output: 8192 }, - }, - "Llama-3.3-70B-Magnum-v4-SE": { - id: "Llama-3.3-70B-Magnum-v4-SE", - name: "Llama 3.3 70B Magnum v4 SE", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "glm-zero-preview": { - id: "glm-zero-preview", - name: "GLM Zero Preview", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.802, output: 1.802 }, - limit: { context: 8000, input: 8000, output: 4096 }, - }, - "study_gpt-chatgpt-4o-latest": { - id: "study_gpt-chatgpt-4o-latest", - name: "Study Mode", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-05-13", - last_updated: "2024-05-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 4.998, output: 14.994 }, - limit: { context: 200000, input: 200000, output: 16384 }, - }, - "glm-4-airx": { - id: "glm-4-airx", - name: "GLM-4 AirX", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-06-05", - last_updated: "2024-06-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.006, output: 2.006 }, - limit: { context: 8000, input: 8000, output: 4096 }, - }, - "step-2-mini": { - id: "step-2-mini", - name: "Step-2 Mini", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-05", - last_updated: "2024-07-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2006, output: 0.408 }, - limit: { context: 8000, input: 8000, output: 4096 }, - }, - "gemini-2.5-flash-preview-04-17:thinking": { - id: "gemini-2.5-flash-preview-04-17:thinking", - name: "Gemini 2.5 Flash Preview Thinking", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-04-17", - last_updated: "2025-04-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 3.5 }, - limit: { context: 1048756, input: 1048756, output: 65536 }, - }, - "Llama-3.3-70B-Mokume-Gane-R1": { - id: "Llama-3.3-70B-Mokume-Gane-R1", - name: "Llama 3.3 70B Mokume Gane R1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "deepseek-reasoner": { - id: "deepseek-reasoner", - name: "DeepSeek Reasoner", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.7 }, - limit: { context: 64000, input: 64000, output: 65536 }, - }, - "glm-z1-airx": { - id: "glm-z1-airx", - name: "GLM Z1 AirX", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-04-15", - last_updated: "2025-04-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.7, output: 0.7 }, - limit: { context: 32000, input: 32000, output: 16384 }, - }, - "jamba-mini-1.6": { - id: "jamba-mini-1.6", - name: "Jamba Mini 1.6", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-03-01", - last_updated: "2025-03-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1989, output: 0.408 }, - limit: { context: 256000, input: 256000, output: 4096 }, - }, - "claude-opus-4-1-thinking": { - id: "claude-opus-4-1-thinking", - name: "Claude 4.1 Opus Thinking", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 14.994, output: 75.004 }, - limit: { context: 200000, input: 200000, output: 32000 }, - }, - "grok-3-beta": { - id: "grok-3-beta", - name: "Grok 3 Beta", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 131072, input: 131072, output: 131072 }, - }, - "Llama-3.3-70B-Legion-V2.1": { - id: "Llama-3.3-70B-Legion-V2.1", - name: "Llama 3.3 70B Legion V2.1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - sonar: { - id: "sonar", - name: "Perplexity Simple", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-19", - last_updated: "2025-02-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.003, output: 1.003 }, - limit: { context: 127000, input: 127000, output: 128000 }, - }, - "z-image-turbo": { - id: "z-image-turbo", - name: "Z Image Turbo", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-11-27", - last_updated: "2025-11-27", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 0, output: 0 }, - }, - "GLM-4.5-Air-Derestricted-Iceblink-v2": { - id: "GLM-4.5-Air-Derestricted-Iceblink-v2", - name: "GLM 4.5 Air Derestricted Iceblink v2", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 158600, input: 158600, output: 65536 }, - }, - "jamba-large": { - id: "jamba-large", - name: "Jamba Large", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.989, output: 7.99 }, - limit: { context: 256000, input: 256000, output: 4096 }, - }, - "claude-3-7-sonnet-reasoner": { - id: "claude-3-7-sonnet-reasoner", - name: "Claude 3.7 Sonnet Reasoner", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-03-29", - last_updated: "2025-03-29", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 128000, input: 128000, output: 8192 }, - }, - "ernie-4.5-turbo-vl-32k": { - id: "ernie-4.5-turbo-vl-32k", - name: "Ernie 4.5 Turbo VL 32k", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-05-08", - last_updated: "2025-05-08", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.495, output: 1.43 }, - limit: { context: 32000, input: 32000, output: 16384 }, - }, - "Mistral-Nemo-12B-Instruct-2407": { - id: "Mistral-Nemo-12B-Instruct-2407", - name: "Mistral Nemo 12B Instruct 2407", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.01, output: 0.01 }, - limit: { context: 16384, input: 16384, output: 16384 }, - }, - "doubao-seed-1-6-flash-250615": { - id: "doubao-seed-1-6-flash-250615", - name: "Doubao Seed 1.6 Flash", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-06-15", - last_updated: "2025-06-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.0374, output: 0.374 }, - limit: { context: 256000, input: 256000, output: 16384 }, - }, - "qwq-32b": { - id: "qwq-32b", - name: "Qwen: QwQ 32B", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-04-15", - last_updated: "2025-04-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25599999, output: 0.30499999 }, - limit: { context: 128000, input: 128000, output: 32768 }, - }, - "Llama-3.3-70B-Strawberrylemonade-v1.2": { - id: "Llama-3.3-70B-Strawberrylemonade-v1.2", - name: "Llama 3.3 70B StrawberryLemonade v1.2", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "gemini-2.5-flash-preview-04-17": { - id: "gemini-2.5-flash-preview-04-17", - name: "Gemini 2.5 Flash Preview", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-04-17", - last_updated: "2025-04-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 1048756, input: 1048756, output: 65536 }, - }, - "ernie-x1-turbo-32k": { - id: "ernie-x1-turbo-32k", - name: "Ernie X1 Turbo 32k", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-05-08", - last_updated: "2025-05-08", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.165, output: 0.66 }, - limit: { context: 32000, input: 32000, output: 16384 }, - }, - "deepseek-math-v2": { - id: "deepseek-math-v2", - name: "DeepSeek Math V2", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-03", - last_updated: "2025-12-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 2.2 }, - limit: { context: 128000, input: 128000, output: 65536 }, - }, - "Llama-3.3-70B-Electranova-v1.0": { - id: "Llama-3.3-70B-Electranova-v1.0", - name: "Llama 3.3 70B Electranova v1.0", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "Llama-3.3-70B-ArliAI-RPMax-v2": { - id: "Llama-3.3-70B-ArliAI-RPMax-v2", - name: "Llama 3.3 70B ArliAI RPMax v2", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-08-08", - last_updated: "2025-08-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "qwen-image": { - id: "qwen-image", - name: "Qwen Image", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["image"] }, - open_weights: false, - limit: { context: 0, output: 0 }, - }, - "Llama-3.3-70B-Cu-Mai-R1": { - id: "Llama-3.3-70B-Cu-Mai-R1", - name: "Llama 3.3 70B Cu Mai R1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "GLM-4.5-Air-Derestricted-Iceblink": { - id: "GLM-4.5-Air-Derestricted-Iceblink", - name: "GLM 4.5 Air Derestricted Iceblink", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 131072, input: 131072, output: 98304 }, - }, - "Llama-3.3-70B-Bigger-Body": { - id: "Llama-3.3-70B-Bigger-Body", - name: "Llama 3.3 70B Bigger Body", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "Llama-3.3+(3.1v3.3)-70B-Hanami-x1": { - id: "Llama-3.3+(3.1v3.3)-70B-Hanami-x1", - name: "Llama 3.3+ 70B Hanami x1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "hunyuan-turbos-20250226": { - id: "hunyuan-turbos-20250226", - name: "Hunyuan Turbo S", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-27", - last_updated: "2025-02-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.187, output: 0.374 }, - limit: { context: 24000, input: 24000, output: 8192 }, - }, - "gemini-2.5-flash-preview-09-2025": { - id: "gemini-2.5-flash-preview-09-2025", - name: "Gemini 2.5 Flash Preview (09/2025)", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5 }, - limit: { context: 1048756, input: 1048756, output: 65536 }, - }, - "GLM-4.6-Derestricted-v5": { - id: "GLM-4.6-Derestricted-v5", - name: "GLM 4.6 Derestricted v5", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.5 }, - limit: { context: 131072, input: 131072, output: 8192 }, - }, - "glm-4-plus": { - id: "glm-4-plus", - name: "GLM-4 Plus", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-08-01", - last_updated: "2024-08-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 7.497, output: 7.497 }, - limit: { context: 128000, input: 128000, output: 4096 }, - }, - "Gemma-3-27B-Big-Tiger-v3": { - id: "Gemma-3-27B-Big-Tiger-v3", - name: "Gemma 3 27B Big Tiger v3", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-08-08", - last_updated: "2025-08-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "brave-research": { - id: "brave-research", - name: "Brave (Research)", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2023-03-02", - last_updated: "2024-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 5 }, - limit: { context: 16384, input: 16384, output: 16384 }, - }, - hidream: { - id: "hidream", - name: "Hidream", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2024-01-01", - last_updated: "2024-01-01", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 0, output: 0 }, - }, - "qwen3-max-2026-01-23": { - id: "qwen3-max-2026-01-23", - name: "Qwen3 Max 2026-01-23", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2026-01-26", - last_updated: "2026-01-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.2002, output: 6.001 }, - limit: { context: 256000, input: 256000, output: 32768 }, - }, - "claude-opus-4-1-20250805": { - id: "claude-opus-4-1-20250805", - name: "Claude 4.1 Opus", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 14.994, output: 75.004 }, - limit: { context: 200000, input: 200000, output: 32000 }, - }, - "claude-haiku-4-5-20251001": { - id: "claude-haiku-4-5-20251001", - name: "Claude Haiku 4.5", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5 }, - limit: { context: 200000, input: 200000, output: 64000 }, - }, - "MiniMax-M1": { - id: "MiniMax-M1", - name: "MiniMax M1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-06-16", - last_updated: "2025-06-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1394, output: 1.3328 }, - limit: { context: 1000000, input: 1000000, output: 131072 }, - }, - "gemini-2.5-flash-nothinking": { - id: "gemini-2.5-flash-nothinking", - name: "Gemini 2.5 Flash (No Thinking)", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-06-05", - last_updated: "2025-06-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5 }, - limit: { context: 1048756, input: 1048756, output: 65536 }, - }, - "exa-research-pro": { - id: "exa-research-pro", - name: "Exa (Research Pro)", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-06-04", - last_updated: "2025-06-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 2.5 }, - limit: { context: 16384, input: 16384, output: 16384 }, - }, - "grok-3-fast-beta": { - id: "grok-3-fast-beta", - name: "Grok 3 Fast Beta", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25 }, - limit: { context: 131072, input: 131072, output: 131072 }, - }, - "claude-opus-4-5-20251101:thinking": { - id: "claude-opus-4-5-20251101:thinking", - name: "Claude 4.5 Opus Thinking", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-11-01", - last_updated: "2025-11-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 4.998, output: 25.007 }, - limit: { context: 200000, input: 200000, output: 32000 }, - }, - "gemini-2.5-pro-exp-03-25": { - id: "gemini-2.5-pro-exp-03-25", - name: "Gemini 2.5 Pro Experimental 0325", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-03-25", - last_updated: "2025-03-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10 }, - limit: { context: 1048756, input: 1048756, output: 65536 }, - }, - "claude-3-7-sonnet-thinking": { - id: "claude-3-7-sonnet-thinking", - name: "Claude 3.7 Sonnet Thinking", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-02-24", - last_updated: "2025-02-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.992, output: 14.994 }, - limit: { context: 200000, input: 200000, output: 16000 }, - }, - "claude-opus-4-thinking:8192": { - id: "claude-opus-4-thinking:8192", - name: "Claude 4 Opus Thinking (8K)", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 14.994, output: 75.004 }, - limit: { context: 200000, input: 200000, output: 32000 }, - }, - "claude-sonnet-4-thinking:1024": { - id: "claude-sonnet-4-thinking:1024", - name: "Claude 4 Sonnet Thinking (1K)", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.992, output: 14.994 }, - limit: { context: 1000000, input: 1000000, output: 64000 }, - }, - "Llama-3.3-70B-Magnum-v4-SE-Cirrus-x1-SLERP": { - id: "Llama-3.3-70B-Magnum-v4-SE-Cirrus-x1-SLERP", - name: "Llama 3.3 70B Magnum v4 SE Cirrus x1 SLERP", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-26", - last_updated: "2025-07-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "step-r1-v-mini": { - id: "step-r1-v-mini", - name: "Step R1 V Mini", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-04-08", - last_updated: "2025-04-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 11 }, - limit: { context: 128000, input: 128000, output: 65536 }, - }, - "ernie-x1-32k-preview": { - id: "ernie-x1-32k-preview", - name: "Ernie X1 32k", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.33, output: 1.32 }, - limit: { context: 32000, input: 32000, output: 16384 }, - }, - "Llama-3.3-70B-StrawberryLemonade-v1.0": { - id: "Llama-3.3-70B-StrawberryLemonade-v1.0", - name: "Llama 3.3 70B StrawberryLemonade v1.0", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "KAT-Coder-Exp-72B-1010": { - id: "KAT-Coder-Exp-72B-1010", - name: "KAT Coder Exp 72B 1010", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-10-28", - last_updated: "2025-10-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.2 }, - limit: { context: 128000, input: 128000, output: 32768 }, - }, - "gemini-2.5-pro-preview-03-25": { - id: "gemini-2.5-pro-preview-03-25", - name: "Gemini 2.5 Pro Preview 0325", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-03-25", - last_updated: "2025-03-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10 }, - limit: { context: 1048756, input: 1048756, output: 65536 }, - }, - "claude-opus-4-thinking:1024": { - id: "claude-opus-4-thinking:1024", - name: "Claude 4 Opus Thinking (1K)", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 14.994, output: 75.004 }, - limit: { context: 200000, input: 200000, output: 32000 }, - }, - "claude-sonnet-4-20250514": { - id: "claude-sonnet-4-20250514", - name: "Claude 4 Sonnet", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.992, output: 14.994 }, - limit: { context: 200000, input: 200000, output: 64000 }, - }, - "Llama-3.3-70B-Progenitor-V3.3": { - id: "Llama-3.3-70B-Progenitor-V3.3", - name: "Llama 3.3 70B Progenitor V3.3", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-26", - last_updated: "2025-07-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "Qwen2.5-32B-EVA-v0.2": { - id: "Qwen2.5-32B-EVA-v0.2", - name: "Qwen 2.5 32b EVA", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-09-01", - last_updated: "2024-09-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.493, output: 0.493 }, - limit: { context: 24576, input: 24576, output: 8192 }, - }, - "brave-pro": { - id: "brave-pro", - name: "Brave (Pro)", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2023-03-02", - last_updated: "2024-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 5 }, - limit: { context: 8192, input: 8192, output: 8192 }, - }, - "step-2-16k-exp": { - id: "step-2-16k-exp", - name: "Step-2 16k Exp", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-05", - last_updated: "2024-07-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 7.004, output: 19.992 }, - limit: { context: 16000, input: 16000, output: 8192 }, - }, - "Llama-3.3-70B-Fallen-R1-v1": { - id: "Llama-3.3-70B-Fallen-R1-v1", - name: "Llama 3.3 70B Fallen R1 v1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "claude-sonnet-4-thinking": { - id: "claude-sonnet-4-thinking", - name: "Claude 4 Sonnet Thinking", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-02-24", - last_updated: "2025-02-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.992, output: 14.994 }, - limit: { context: 1000000, input: 1000000, output: 64000 }, - }, - "doubao-1.5-pro-256k": { - id: "doubao-1.5-pro-256k", - name: "Doubao 1.5 Pro 256k", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-03-12", - last_updated: "2025-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.799, output: 1.445 }, - limit: { context: 256000, input: 256000, output: 16384 }, - }, - "claude-3-7-sonnet-20250219": { - id: "claude-3-7-sonnet-20250219", - name: "Claude 3.7 Sonnet", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-02-19", - last_updated: "2025-02-19", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.992, output: 14.994 }, - limit: { context: 200000, input: 200000, output: 16000 }, - }, - "learnlm-1.5-pro-experimental": { - id: "learnlm-1.5-pro-experimental", - name: "Gemini LearnLM Experimental", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-05-14", - last_updated: "2024-05-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3.502, output: 10.506 }, - limit: { context: 32767, input: 32767, output: 8192 }, - }, - "qwen3-coder-30b-a3b-instruct": { - id: "qwen3-coder-30b-a3b-instruct", - name: "Qwen3 Coder 30B A3B Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 128000, input: 128000, output: 65536 }, - }, - chroma: { - id: "chroma", - name: "Chroma", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-08-12", - last_updated: "2025-08-12", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 0, output: 0 }, - }, - "Llama-3.3-70B-Predatorial-Extasy": { - id: "Llama-3.3-70B-Predatorial-Extasy", - name: "Llama 3.3 70B Predatorial Extasy", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "Llama-3.3-70B-Aurora-Borealis": { - id: "Llama-3.3-70B-Aurora-Borealis", - name: "Llama 3.3 70B Aurora Borealis", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "Llama-3.3-70B-ArliAI-RPMax-v3": { - id: "Llama-3.3-70B-ArliAI-RPMax-v3", - name: "Llama 3.3 70B ArliAI RPMax v3", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "venice-uncensored": { - id: "venice-uncensored", - name: "Venice Uncensored", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-24", - last_updated: "2025-02-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 0.4 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "step-3": { - id: "step-3", - name: "Step-3", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-31", - last_updated: "2025-07-31", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2499, output: 0.6494 }, - limit: { context: 65536, input: 65536, output: 8192 }, - }, - "Llama-3.3-70B-The-Omega-Directive-Unslop-v2.0": { - id: "Llama-3.3-70B-The-Omega-Directive-Unslop-v2.0", - name: "Llama 3.3 70B Omega Directive Unslop v2.0", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "auto-model": { - id: "auto-model", - name: "Auto model", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-06-01", - last_updated: "2024-06-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 1000000, input: 1000000, output: 1000000 }, - }, - "claude-opus-4-1-thinking:32768": { - id: "claude-opus-4-1-thinking:32768", - name: "Claude 4.1 Opus Thinking (32K)", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 14.994, output: 75.004 }, - limit: { context: 200000, input: 200000, output: 32000 }, - }, - "Llama-3.3-70B-Shakudo": { - id: "Llama-3.3-70B-Shakudo", - name: "Llama 3.3 70B Shakudo", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "Baichuan4-Air": { - id: "Baichuan4-Air", - name: "Baichuan 4 Air", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-08-19", - last_updated: "2025-08-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.157, output: 0.157 }, - limit: { context: 32768, input: 32768, output: 32768 }, - }, - "kimi-thinking-preview": { - id: "kimi-thinking-preview", - name: "Kimi Thinking Preview", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-05-07", - last_updated: "2025-05-07", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 31.46, output: 31.46 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "qwen-turbo": { - id: "qwen-turbo", - name: "Qwen Turbo", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-11-01", - last_updated: "2024-11-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.04998, output: 0.2006 }, - limit: { context: 1000000, input: 1000000, output: 8192 }, - }, - "Llama-3.3-70B-Mhnnn-x1": { - id: "Llama-3.3-70B-Mhnnn-x1", - name: "Llama 3.3 70B Mhnnn x1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "claude-opus-4-thinking:32768": { - id: "claude-opus-4-thinking:32768", - name: "Claude 4 Opus Thinking (32K)", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 14.994, output: 75.004 }, - limit: { context: 200000, input: 200000, output: 32000 }, - }, - "Llama-3.3-70B-Argunaut-1-SFT": { - id: "Llama-3.3-70B-Argunaut-1-SFT", - name: "Llama 3.3 70B Argunaut 1 SFT", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "claude-opus-4-1-thinking:1024": { - id: "claude-opus-4-1-thinking:1024", - name: "Claude 4.1 Opus Thinking (1K)", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 14.994, output: 75.004 }, - limit: { context: 200000, input: 200000, output: 32000 }, - }, - "gemini-2.5-flash-lite": { - id: "gemini-2.5-flash-lite", - name: "Gemini 2.5 Flash Lite", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 1048756, input: 1048756, output: 65536 }, - }, - "phi-4-multimodal-instruct": { - id: "phi-4-multimodal-instruct", - name: "Phi 4 Multimodal", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-26", - last_updated: "2025-07-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.07, output: 0.11 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "doubao-seed-2-0-code-preview-260215": { - id: "doubao-seed-2-0-code-preview-260215", - name: "Doubao Seed 2.0 Code Preview", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2026-02-14", - last_updated: "2026-02-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.782, output: 3.893 }, - limit: { context: 256000, input: 256000, output: 128000 }, - }, - "deepseek-reasoner-cheaper": { - id: "deepseek-reasoner-cheaper", - name: "Deepseek R1 Cheaper", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.7 }, - limit: { context: 128000, input: 128000, output: 65536 }, - }, - "exa-answer": { - id: "exa-answer", - name: "Exa (Answer)", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-06-04", - last_updated: "2025-06-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 2.5 }, - limit: { context: 4096, input: 4096, output: 4096 }, - }, - "v0-1.0-md": { - id: "v0-1.0-md", - name: "v0 1.0 MD", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-04", - last_updated: "2025-07-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 200000, input: 200000, output: 64000 }, - }, - "glm-4.1v-thinking-flash": { - id: "glm-4.1v-thinking-flash", - name: "GLM 4.1V Thinking Flash", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.3 }, - limit: { context: 64000, input: 64000, output: 8192 }, - }, - "azure-o1": { - id: "azure-o1", - name: "Azure o1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-17", - last_updated: "2024-12-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 14.994, output: 59.993 }, - limit: { context: 200000, input: 200000, output: 100000 }, - }, - "GLM-4.5-Air-Derestricted": { - id: "GLM-4.5-Air-Derestricted", - name: "GLM 4.5 Air Derestricted", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 202600, input: 202600, output: 98304 }, - }, - "azure-o3-mini": { - id: "azure-o3-mini", - name: "Azure o3-mini", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-01-31", - last_updated: "2025-01-31", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.088, output: 4.3996 }, - limit: { context: 200000, input: 200000, output: 65536 }, - }, - "Llama-3.3-70B-Sapphira-0.2": { - id: "Llama-3.3-70B-Sapphira-0.2", - name: "Llama 3.3 70B Sapphira 0.2", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "Llama-3.3-70B-Anthrobomination": { - id: "Llama-3.3-70B-Anthrobomination", - name: "Llama 3.3 70B Anthrobomination", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "QwQ-32B-ArliAI-RpR-v1": { - id: "QwQ-32B-ArliAI-RpR-v1", - name: "QwQ 32b Arli V1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 32768, input: 32768, output: 32768 }, - }, - "claude-opus-4-20250514": { - id: "claude-opus-4-20250514", - name: "Claude 4 Opus", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-05-14", - last_updated: "2025-05-14", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 14.994, output: 75.004 }, - limit: { context: 200000, input: 200000, output: 32000 }, - }, - "yi-lightning": { - id: "yi-lightning", - name: "Yi Lightning", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-10-16", - last_updated: "2024-10-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2006, output: 0.2006 }, - limit: { context: 12000, input: 12000, output: 4096 }, - }, - "Llama-3.3-70B-Electra-R1": { - id: "Llama-3.3-70B-Electra-R1", - name: "Llama 3.3 70B Electra R1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "Llama-3.3-70B-Forgotten-Abomination-v5.0": { - id: "Llama-3.3-70B-Forgotten-Abomination-v5.0", - name: "Llama 3.3 70B Forgotten Abomination v5.0", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "Llama-3.3-70B-Cirrus-x1": { - id: "Llama-3.3-70B-Cirrus-x1", - name: "Llama 3.3 70B Cirrus x1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "grok-3-mini-beta": { - id: "grok-3-mini-beta", - name: "Grok 3 Mini Beta", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.5 }, - limit: { context: 131072, input: 131072, output: 131072 }, - }, - "auto-model-standard": { - id: "auto-model-standard", - name: "Auto model (Standard)", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-06-01", - last_updated: "2024-06-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 9.996, output: 19.992 }, - limit: { context: 1000000, input: 1000000, output: 1000000 }, - }, - "claude-sonnet-4-5-20250929-thinking": { - id: "claude-sonnet-4-5-20250929-thinking", - name: "Claude Sonnet 4.5 Thinking", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.992, output: 14.994 }, - limit: { context: 1000000, input: 1000000, output: 64000 }, - }, - "v0-1.5-md": { - id: "v0-1.5-md", - name: "v0 1.5 MD", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-04", - last_updated: "2025-07-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 200000, input: 200000, output: 64000 }, - }, - "kimi-k2-instruct-fast": { - id: "kimi-k2-instruct-fast", - name: "Kimi K2 0711 Fast", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-15", - last_updated: "2025-07-15", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 2 }, - limit: { context: 131072, input: 131072, output: 16384 }, - }, - "glm-4-long": { - id: "glm-4-long", - name: "GLM-4 Long", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-08-01", - last_updated: "2024-08-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2006, output: 0.2006 }, - limit: { context: 1000000, input: 1000000, output: 4096 }, - }, - "mercury-coder-small": { - id: "mercury-coder-small", - name: "Mercury Coder Small", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-26", - last_updated: "2025-02-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "jamba-large-1.7": { - id: "jamba-large-1.7", - name: "Jamba Large 1.7", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.989, output: 7.99 }, - limit: { context: 256000, input: 256000, output: 4096 }, - }, - "qvq-max": { - id: "qvq-max", - name: "Qwen: QvQ Max", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-03-28", - last_updated: "2025-03-28", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.4, output: 5.3 }, - limit: { context: 128000, input: 128000, output: 8192 }, - }, - "gemini-2.0-flash-thinking-exp-1219": { - id: "gemini-2.0-flash-thinking-exp-1219", - name: "Gemini 2.0 Flash Thinking 1219", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-19", - last_updated: "2024-12-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1003, output: 0.408 }, - limit: { context: 32767, input: 32767, output: 8192 }, - }, - "gemini-2.0-flash-lite": { - id: "gemini-2.0-flash-lite", - name: "Gemini 2.0 Flash Lite", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.0748, output: 0.306 }, - limit: { context: 1000000, input: 1000000, output: 8192 }, - }, - "azure-gpt-4-turbo": { - id: "azure-gpt-4-turbo", - name: "Azure gpt-4-turbo", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2023-11-06", - last_updated: "2024-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 9.996, output: 30.005 }, - limit: { context: 128000, input: 128000, output: 4096 }, - }, - "Baichuan-M2": { - id: "Baichuan-M2", - name: "Baichuan M2 32B Medical", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-08-19", - last_updated: "2025-08-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 15.73, output: 15.73 }, - limit: { context: 32768, input: 32768, output: 32768 }, - }, - "qwen-long": { - id: "qwen-long", - name: "Qwen Long 10M", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-01-25", - last_updated: "2025-01-25", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1003, output: 0.408 }, - limit: { context: 10000000, input: 10000000, output: 8192 }, - }, - "sonar-reasoning-pro": { - id: "sonar-reasoning-pro", - name: "Perplexity Reasoning Pro", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-02-19", - last_updated: "2025-02-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.006, output: 7.9985 }, - limit: { context: 127000, input: 127000, output: 128000 }, - }, - "gemini-2.5-flash-preview-05-20:thinking": { - id: "gemini-2.5-flash-preview-05-20:thinking", - name: "Gemini 2.5 Flash 0520 Thinking", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-05-20", - last_updated: "2025-05-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 3.5 }, - limit: { context: 1048000, input: 1048000, output: 65536 }, - }, - "GLM-4.5-Air-Derestricted-Steam-ReExtract": { - id: "GLM-4.5-Air-Derestricted-Steam-ReExtract", - name: "GLM 4.5 Air Derestricted Steam ReExtract", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-12", - last_updated: "2025-12-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 131072, input: 131072, output: 65536 }, - }, - "Llama-3.3-70B-Dark-Ages-v0.1": { - id: "Llama-3.3-70B-Dark-Ages-v0.1", - name: "Llama 3.3 70B Dark Ages v0.1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "Baichuan4-Turbo": { - id: "Baichuan4-Turbo", - name: "Baichuan 4 Turbo", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-08-19", - last_updated: "2025-08-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.42, output: 2.42 }, - limit: { context: 128000, input: 128000, output: 32768 }, - }, - "doubao-1.5-vision-pro-32k": { - id: "doubao-1.5-vision-pro-32k", - name: "Doubao 1.5 Vision Pro 32k", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-01-22", - last_updated: "2025-01-22", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.459, output: 1.377 }, - limit: { context: 32000, input: 32000, output: 8192 }, - }, - "inflection/inflection-3-pi": { - id: "inflection/inflection-3-pi", - name: "Inflection 3 Pi", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-10-11", - last_updated: "2024-10-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.499, output: 9.996 }, - limit: { context: 8000, input: 8000, output: 4096 }, - }, - "inflection/inflection-3-productivity": { - id: "inflection/inflection-3-productivity", - name: "Inflection 3 Productivity", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-10-11", - last_updated: "2024-10-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.499, output: 9.996 }, - limit: { context: 8000, input: 8000, output: 4096 }, - }, - "essentialai/rnj-1-instruct": { - id: "essentialai/rnj-1-instruct", - name: "RNJ-1 Instruct 8B", - family: "rnj", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-13", - last_updated: "2025-12-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.15 }, - limit: { context: 128000, input: 128000, output: 8192 }, - }, - "LLM360/K2-Think": { - id: "LLM360/K2-Think", - name: "K2-Think", - family: "kimi-thinking", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-26", - last_updated: "2025-07-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.17, output: 0.68 }, - limit: { context: 128000, input: 128000, output: 32768 }, - }, - "TEE/kimi-k2.5": { - id: "TEE/kimi-k2.5", - name: "Kimi K2.5 TEE", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2026-01-29", - last_updated: "2026-01-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.9 }, - limit: { context: 128000, input: 128000, output: 65535 }, - }, - "TEE/glm-4.7": { - id: "TEE/glm-4.7", - name: "GLM 4.7 TEE", - family: "glm", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2026-01-29", - last_updated: "2026-01-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.85, output: 3.3 }, - limit: { context: 131000, input: 131000, output: 65535 }, - }, - "TEE/qwen3.5-397b-a17b": { - id: "TEE/qwen3.5-397b-a17b", - name: "Qwen3.5 397B A17B TEE", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2026-02-28", - last_updated: "2026-02-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 3.6 }, - limit: { context: 258048, input: 258048, output: 65536 }, - }, - "TEE/glm-5": { - id: "TEE/glm-5", - name: "GLM 5 TEE", - family: "glm", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.2, output: 3.5 }, - limit: { context: 203000, input: 203000, output: 65535 }, - }, - "TEE/qwen2.5-vl-72b-instruct": { - id: "TEE/qwen2.5-vl-72b-instruct", - name: "Qwen2.5 VL 72B TEE", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-01", - last_updated: "2025-02-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.7, output: 0.7 }, - limit: { context: 65536, input: 65536, output: 8192 }, - }, - "TEE/minimax-m2.1": { - id: "TEE/minimax-m2.1", - name: "MiniMax M2.1 TEE", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 200000, input: 200000, output: 131072 }, - }, - "TEE/qwen3-30b-a3b-instruct-2507": { - id: "TEE/qwen3-30b-a3b-instruct-2507", - name: "Qwen3 30B A3B Instruct 2507 TEE", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-29", - last_updated: "2025-07-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.44999999999999996 }, - limit: { context: 262000, input: 262000, output: 32768 }, - }, - "TEE/deepseek-v3.1": { - id: "TEE/deepseek-v3.1", - name: "DeepSeek V3.1 TEE", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-08-21", - last_updated: "2025-08-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 2.5 }, - limit: { context: 164000, input: 164000, output: 8192 }, - }, - "TEE/llama3-3-70b": { - id: "TEE/llama3-3-70b", - name: "Llama 3.3 70B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-03", - last_updated: "2025-07-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 2 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "TEE/glm-4.6": { - id: "TEE/glm-4.6", - name: "GLM 4.6 TEE", - family: "glm", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.75, output: 2 }, - limit: { context: 203000, input: 203000, output: 65535 }, - }, - "TEE/kimi-k2.5-thinking": { - id: "TEE/kimi-k2.5-thinking", - name: "Kimi K2.5 Thinking TEE", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2026-01-29", - last_updated: "2026-01-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.9 }, - limit: { context: 128000, input: 128000, output: 65535 }, - }, - "TEE/gemma-3-27b-it": { - id: "TEE/gemma-3-27b-it", - name: "Gemma 3 27B TEE", - family: "gemma", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-03-10", - last_updated: "2025-03-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.8 }, - limit: { context: 131072, input: 131072, output: 8192 }, - }, - "TEE/deepseek-v3.2": { - id: "TEE/deepseek-v3.2", - name: "DeepSeek V3.2 TEE", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 1 }, - limit: { context: 164000, input: 164000, output: 65536 }, - }, - "TEE/gpt-oss-20b": { - id: "TEE/gpt-oss-20b", - name: "GPT-OSS 20B TEE", - family: "gpt-oss", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.8 }, - limit: { context: 131072, input: 131072, output: 8192 }, - }, - "TEE/qwen3-coder": { - id: "TEE/qwen3-coder", - name: "Qwen3 Coder 480B TEE", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.5, output: 2 }, - limit: { context: 128000, input: 128000, output: 32768 }, - }, - "TEE/glm-4.7-flash": { - id: "TEE/glm-4.7-flash", - name: "GLM 4.7 Flash TEE", - family: "glm-flash", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2026-01-19", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.5 }, - limit: { context: 203000, input: 203000, output: 65535 }, - }, - "TEE/gpt-oss-120b": { - id: "TEE/gpt-oss-120b", - name: "GPT-OSS 120B TEE", - family: "gpt-oss", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 2 }, - limit: { context: 131072, input: 131072, output: 16384 }, - }, - "TEE/deepseek-r1-0528": { - id: "TEE/deepseek-r1-0528", - name: "DeepSeek R1 0528 TEE", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-05-28", - last_updated: "2025-05-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 2 }, - limit: { context: 128000, input: 128000, output: 65536 }, - }, - "TEE/kimi-k2-thinking": { - id: "TEE/kimi-k2-thinking", - name: "Kimi K2 Thinking TEE", - family: "kimi-thinking", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-11-06", - last_updated: "2025-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 2 }, - limit: { context: 128000, input: 128000, output: 65535 }, - }, - "CrucibleLab/L3.3-70B-Loki-V2.0": { - id: "CrucibleLab/L3.3-70B-Loki-V2.0", - name: "L3.3 70B Loki v2.0", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2026-01-22", - last_updated: "2026-01-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.49299999999999994, output: 0.49299999999999994 }, - limit: { context: 16384, input: 16384, output: 16384 }, - }, - "deepseek/deepseek-v3.2:thinking": { - id: "deepseek/deepseek-v3.2:thinking", - name: "DeepSeek V3.2 Thinking", - family: "deepseek", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27999999999999997, output: 0.42000000000000004 }, - limit: { context: 163000, input: 163000, output: 65536 }, - }, - "deepseek/deepseek-prover-v2-671b": { - id: "deepseek/deepseek-prover-v2-671b", - name: "DeepSeek Prover v2 671B", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-04-30", - last_updated: "2025-04-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 2.5 }, - limit: { context: 160000, input: 160000, output: 16384 }, - }, - "deepseek/deepseek-v3.2-speciale": { - id: "deepseek/deepseek-v3.2-speciale", - name: "DeepSeek V3.2 Speciale", - family: "deepseek", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-12-02", - last_updated: "2025-12-02", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27999999999999997, output: 0.42000000000000004 }, - limit: { context: 163000, input: 163000, output: 65536 }, - }, - "deepseek/deepseek-v3.2": { - id: "deepseek/deepseek-v3.2", - name: "DeepSeek V3.2", - family: "deepseek", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27999999999999997, output: 0.42000000000000004 }, - limit: { context: 163000, input: 163000, output: 65536 }, - }, - "Doctor-Shotgun/MS3.2-24B-Magnum-Diamond": { - id: "Doctor-Shotgun/MS3.2-24B-Magnum-Diamond", - name: "MS3.2 24B Magnum Diamond", - family: "mistral", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-11-24", - last_updated: "2025-11-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.49299999999999994, output: 0.49299999999999994 }, - limit: { context: 16384, input: 16384, output: 32768 }, - }, - "NeverSleep/Llama-3-Lumimaid-70B-v0.1": { - id: "NeverSleep/Llama-3-Lumimaid-70B-v0.1", - name: "Lumimaid 70b", - family: "llama", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-01", - last_updated: "2024-07-01", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.006, output: 2.006 }, - limit: { context: 16384, input: 16384, output: 8192 }, - }, - "NeverSleep/Lumimaid-v0.2-70B": { - id: "NeverSleep/Lumimaid-v0.2-70B", - name: "Lumimaid v0.2", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-01", - last_updated: "2024-07-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 1.5 }, - limit: { context: 16384, input: 16384, output: 8192 }, - }, - "Steelskull/L3.3-Cu-Mai-R1-70b": { - id: "Steelskull/L3.3-Cu-Mai-R1-70b", - name: "Llama 3.3 70B Cu Mai", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.49299999999999994, output: 0.49299999999999994 }, - limit: { context: 16384, input: 16384, output: 16384 }, - }, - "Steelskull/L3.3-Nevoria-R1-70b": { - id: "Steelskull/L3.3-Nevoria-R1-70b", - name: "Steelskull Nevoria R1 70b", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.49299999999999994, output: 0.49299999999999994 }, - limit: { context: 16384, input: 16384, output: 16384 }, - }, - "Steelskull/L3.3-MS-Evayale-70B": { - id: "Steelskull/L3.3-MS-Evayale-70B", - name: "Evayale 70b ", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.49299999999999994, output: 0.49299999999999994 }, - limit: { context: 16384, input: 16384, output: 16384 }, - }, - "Steelskull/L3.3-Electra-R1-70b": { - id: "Steelskull/L3.3-Electra-R1-70b", - name: "Steelskull Electra R1 70b", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.69989, output: 0.69989 }, - limit: { context: 16384, input: 16384, output: 16384 }, - }, - "Steelskull/L3.3-MS-Nevoria-70b": { - id: "Steelskull/L3.3-MS-Nevoria-70b", - name: "Steelskull Nevoria 70b", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.49299999999999994, output: 0.49299999999999994 }, - limit: { context: 16384, input: 16384, output: 16384 }, - }, - "Steelskull/L3.3-MS-Evalebis-70b": { - id: "Steelskull/L3.3-MS-Evalebis-70b", - name: "MS Evalebis 70b", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.49299999999999994, output: 0.49299999999999994 }, - limit: { context: 16384, input: 16384, output: 16384 }, - }, - "miromind-ai/mirothinker-v1.5-235b": { - id: "miromind-ai/mirothinker-v1.5-235b", - name: "MiroThinker v1.5 235B", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2026-01-07", - last_updated: "2026-01-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 32768, input: 32768, output: 4000 }, - }, - "pamanseau/OpenReasoning-Nemotron-32B": { - id: "pamanseau/OpenReasoning-Nemotron-32B", - name: "OpenReasoning Nemotron 32B", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-08-21", - last_updated: "2025-08-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 32768, input: 32768, output: 65536 }, - }, - "arcee-ai/trinity-mini": { - id: "arcee-ai/trinity-mini", - name: "Trinity Mini", - family: "trinity-mini", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.045000000000000005, output: 0.15 }, - limit: { context: 131072, input: 131072, output: 8192 }, - }, - "arcee-ai/trinity-large": { - id: "arcee-ai/trinity-large", - name: "Trinity Large", - family: "trinity", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1 }, - limit: { context: 131072, input: 131072, output: 8192 }, - }, - "cognitivecomputations/dolphin-2.9.2-qwen2-72b": { - id: "cognitivecomputations/dolphin-2.9.2-qwen2-72b", - name: "Dolphin 72b", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-27", - last_updated: "2025-02-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 8192, input: 8192, output: 4096 }, - }, - "deepcogito/cogito-v1-preview-qwen-32B": { - id: "deepcogito/cogito-v1-preview-qwen-32B", - name: "Cogito v1 Preview Qwen 32B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-05-10", - last_updated: "2025-05-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.7999999999999998, output: 1.7999999999999998 }, - limit: { context: 128000, input: 128000, output: 32768 }, - }, - "deepcogito/cogito-v2.1-671b": { - id: "deepcogito/cogito-v2.1-671b", - name: "Cogito v2.1 671B MoE", - family: "cogito", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-11-19", - last_updated: "2025-11-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 1.25 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "Salesforce/Llama-xLAM-2-70b-fc-r": { - id: "Salesforce/Llama-xLAM-2-70b-fc-r", - name: "Llama-xLAM-2 70B fc-r", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-04-13", - last_updated: "2025-04-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 2.5 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "NousResearch 2/hermes-4-405b:thinking": { - id: "NousResearch 2/hermes-4-405b:thinking", - name: "Hermes 4 Large (Thinking)", - family: "nousresearch", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 128000, input: 128000, output: 8192 }, - }, - "NousResearch 2/DeepHermes-3-Mistral-24B-Preview": { - id: "NousResearch 2/DeepHermes-3-Mistral-24B-Preview", - name: "DeepHermes-3 Mistral 24B (Preview)", - family: "nousresearch", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-05-10", - last_updated: "2025-05-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.3 }, - limit: { context: 128000, input: 128000, output: 32768 }, - }, - "NousResearch 2/Hermes-4-70B:thinking": { - id: "NousResearch 2/Hermes-4-70B:thinking", - name: "Hermes 4 (Thinking)", - family: "nousresearch", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-09-17", - last_updated: "2025-09-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2006, output: 0.39949999999999997 }, - limit: { context: 128000, input: 128000, output: 8192 }, - }, - "NousResearch 2/hermes-4-405b": { - id: "NousResearch 2/hermes-4-405b", - name: "Hermes 4 Large", - family: "nousresearch", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-08-26", - last_updated: "2025-08-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 128000, input: 128000, output: 8192 }, - }, - "NousResearch 2/hermes-3-llama-3.1-70b": { - id: "NousResearch 2/hermes-3-llama-3.1-70b", - name: "Hermes 3 70B", - family: "nousresearch", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2026-01-07", - last_updated: "2026-01-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.408, output: 0.408 }, - limit: { context: 65536, input: 65536, output: 8192 }, - }, - "NousResearch 2/hermes-4-70b": { - id: "NousResearch 2/hermes-4-70b", - name: "Hermes 4 Medium", - family: "nousresearch", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-03", - last_updated: "2025-07-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2006, output: 0.39949999999999997 }, - limit: { context: 128000, input: 128000, output: 8192 }, - }, - "soob3123/Veiled-Calla-12B": { - id: "soob3123/Veiled-Calla-12B", - name: "Veiled Calla 12B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-04-13", - last_updated: "2025-04-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.3 }, - limit: { context: 32768, input: 32768, output: 8192 }, - }, - "soob3123/GrayLine-Qwen3-8B": { - id: "soob3123/GrayLine-Qwen3-8B", - name: "Grayline Qwen3 8B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.3 }, - limit: { context: 16384, input: 16384, output: 32768 }, - }, - "soob3123/amoral-gemma3-27B-v2": { - id: "soob3123/amoral-gemma3-27B-v2", - name: "Amoral Gemma3 27B v2", - family: "gemma", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-05-23", - last_updated: "2025-05-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.3 }, - limit: { context: 32768, input: 32768, output: 8192 }, - }, - "nex-agi/deepseek-v3.1-nex-n1": { - id: "nex-agi/deepseek-v3.1-nex-n1", - name: "DeepSeek V3.1 Nex N1", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-10", - last_updated: "2025-12-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27999999999999997, output: 0.42000000000000004 }, - limit: { context: 128000, input: 128000, output: 8192 }, - }, - "Envoid/Llama-3.05-NT-Storybreaker-Ministral-70B": { - id: "Envoid/Llama-3.05-NT-Storybreaker-Ministral-70B", - name: "Llama 3.05 Storybreaker Ministral 70b", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.49299999999999994, output: 0.49299999999999994 }, - limit: { context: 16384, input: 16384, output: 8192 }, - }, - "Envoid/Llama-3.05-Nemotron-Tenyxchat-Storybreaker-70B": { - id: "Envoid/Llama-3.05-Nemotron-Tenyxchat-Storybreaker-70B", - name: "Nemotron Tenyxchat Storybreaker 70b", - family: "nemotron", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.49299999999999994, output: 0.49299999999999994 }, - limit: { context: 16384, input: 16384, output: 8192 }, - }, - "anthracite-org/magnum-v4-72b": { - id: "anthracite-org/magnum-v4-72b", - name: "Magnum v4 72B", - family: "llama", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.006, output: 2.992 }, - limit: { context: 16384, input: 16384, output: 8192 }, - }, - "anthracite-org/magnum-v2-72b": { - id: "anthracite-org/magnum-v2-72b", - name: "Magnum V2 72B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-01", - last_updated: "2024-07-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.006, output: 2.992 }, - limit: { context: 16384, input: 16384, output: 8192 }, - }, - "ReadyArt/MS3.2-The-Omega-Directive-24B-Unslop-v2.0": { - id: "ReadyArt/MS3.2-The-Omega-Directive-24B-Unslop-v2.0", - name: "Omega Directive 24B Unslop v2.0", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-08", - last_updated: "2025-12-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 0.5 }, - limit: { context: 16384, input: 16384, output: 32768 }, - }, - "ReadyArt/The-Omega-Abomination-L-70B-v1.0": { - id: "ReadyArt/The-Omega-Abomination-L-70B-v1.0", - name: "The Omega Abomination V1", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.7, output: 0.95 }, - limit: { context: 16384, input: 16384, output: 16384 }, - }, - "undi95/remm-slerp-l2-13b": { - id: "undi95/remm-slerp-l2-13b", - name: "ReMM SLERP 13B", - family: "llama", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.7989999999999999, output: 1.2069999999999999 }, - limit: { context: 6144, input: 6144, output: 4096 }, - }, - "MarinaraSpaghetti/NemoMix-Unleashed-12B": { - id: "MarinaraSpaghetti/NemoMix-Unleashed-12B", - name: "NemoMix 12B Unleashed", - family: "mistral-nemo", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-01", - last_updated: "2024-07-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.49299999999999994, output: 0.49299999999999994 }, - limit: { context: 32768, input: 32768, output: 8192 }, - }, - "allenai/molmo-2-8b": { - id: "allenai/molmo-2-8b", - name: "Molmo 2 8B", - family: "allenai", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2026-02-14", - last_updated: "2026-02-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 36864, input: 36864, output: 36864 }, - }, - "allenai/olmo-3.1-32b-instruct": { - id: "allenai/olmo-3.1-32b-instruct", - name: "Olmo 3.1 32B Instruct", - family: "allenai", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2026-01-25", - last_updated: "2026-01-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.6 }, - limit: { context: 65536, input: 65536, output: 8192 }, - }, - "allenai/olmo-3.1-32b-think": { - id: "allenai/olmo-3.1-32b-think", - name: "Olmo 3.1 32B Think", - family: "allenai", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2026-01-25", - last_updated: "2026-01-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.5 }, - limit: { context: 65536, input: 65536, output: 8192 }, - }, - "allenai/olmo-3-32b-think": { - id: "allenai/olmo-3-32b-think", - name: "Olmo 3 32B Think", - family: "allenai", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-11-01", - last_updated: "2025-11-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.44999999999999996 }, - limit: { context: 128000, input: 128000, output: 8192 }, - }, - "stepfun-ai/step-3.5-flash:thinking": { - id: "stepfun-ai/step-3.5-flash:thinking", - name: "Step 3.5 Flash Thinking", - family: "step", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2026-02-02", - last_updated: "2026-02-02", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5 }, - limit: { context: 256000, input: 256000, output: 256000 }, - }, - "stepfun-ai/step-3.5-flash": { - id: "stepfun-ai/step-3.5-flash", - name: "Step 3.5 Flash", - family: "step", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2026-02-02", - last_updated: "2026-02-02", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5 }, - limit: { context: 256000, input: 256000, output: 256000 }, - }, - "zai-org/glm-4.7": { - id: "zai-org/glm-4.7", - name: "GLM 4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2026-01-29", - last_updated: "2026-01-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.8 }, - limit: { context: 200000, input: 200000, output: 128000 }, - }, - "zai-org/glm-5": { - id: "zai-org/glm-5", - name: "GLM 5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 2.55 }, - limit: { context: 200000, input: 200000, output: 128000 }, - }, - "zai-org/glm-5.1": { - id: "zai-org/glm-5.1", - name: "GLM 5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2026-03-27", - last_updated: "2026-03-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 2.55 }, - limit: { context: 200000, input: 200000, output: 131072 }, - }, - "zai-org/glm-5.1:thinking": { - id: "zai-org/glm-5.1:thinking", - name: "GLM 5.1 Thinking", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2026-03-27", - last_updated: "2026-03-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 2.55 }, - limit: { context: 200000, input: 200000, output: 131072 }, - }, - "zai-org/glm-5:thinking": { - id: "zai-org/glm-5:thinking", - name: "GLM 5 Thinking", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 2.55 }, - limit: { context: 200000, input: 200000, output: 128000 }, - }, - "zai-org/glm-4.7-flash": { - id: "zai-org/glm-4.7-flash", - name: "GLM 4.7 Flash", - family: "glm-flash", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2026-01-19", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.4 }, - limit: { context: 200000, input: 200000, output: 128000 }, - }, - "featherless-ai/Qwerky-72B": { - id: "featherless-ai/Qwerky-72B", - name: "Qwerky 72B", - family: "qwerky", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-03-20", - last_updated: "2025-03-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 0.5 }, - limit: { context: 32000, input: 32000, output: 8192 }, - }, - "mlabonne/NeuralDaredevil-8B-abliterated": { - id: "mlabonne/NeuralDaredevil-8B-abliterated", - name: "Neural Daredevil 8B abliterated", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.44, output: 0.44 }, - limit: { context: 8192, input: 8192, output: 8192 }, - }, - "raifle/sorcererlm-8x22b": { - id: "raifle/sorcererlm-8x22b", - name: "SorcererLM 8x22B", - family: "mixtral", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 4.505, output: 4.505 }, - limit: { context: 16000, input: 16000, output: 8192 }, - }, - "mistralai/mixtral-8x7b-instruct-v0.1": { - id: "mistralai/mixtral-8x7b-instruct-v0.1", - name: "Mixtral 8x7B", - family: "mixtral", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 0.27 }, - limit: { context: 32768, input: 32768, output: 32768 }, - }, - "mistralai/mistral-saba": { - id: "mistralai/mistral-saba", - name: "Mistral Saba", - family: "mistral", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1989, output: 0.595 }, - limit: { context: 32000, input: 32000, output: 32768 }, - }, - "mistralai/mistral-large-3-675b-instruct-2512": { - id: "mistralai/mistral-large-3-675b-instruct-2512", - name: "Mistral Large 3 675B", - family: "mistral-large", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-02", - last_updated: "2025-12-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 3 }, - limit: { context: 262144, input: 262144, output: 256000 }, - }, - "mistralai/devstral-2-123b-instruct-2512": { - id: "mistralai/devstral-2-123b-instruct-2512", - name: "Devstral 2 123B", - family: "devstral", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-09", - last_updated: "2025-12-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.4 }, - limit: { context: 262144, input: 262144, output: 65536 }, - }, - "mistralai/codestral-2508": { - id: "mistralai/codestral-2508", - name: "Codestral 2508", - family: "codestral", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-08-01", - last_updated: "2025-08-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.8999999999999999 }, - limit: { context: 256000, input: 256000, output: 32768 }, - }, - "mistralai/ministral-14b-instruct-2512": { - id: "mistralai/ministral-14b-instruct-2512", - name: "Ministral 3 14B", - family: "ministral", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-02", - last_updated: "2025-12-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 262144, input: 262144, output: 32768 }, - }, - "mistralai/mistral-tiny": { - id: "mistralai/mistral-tiny", - name: "Mistral Tiny", - family: "mistral", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2023-12-11", - last_updated: "2024-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25499999999999995, output: 0.25499999999999995 }, - limit: { context: 32000, input: 32000, output: 8192 }, - }, - "mistralai/ministral-8b-2512": { - id: "mistralai/ministral-8b-2512", - name: "Ministral 8B", - family: "ministral", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-04", - last_updated: "2025-12-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.15 }, - limit: { context: 262144, input: 262144, output: 32768 }, - }, - "mistralai/mixtral-8x22b-instruct-v0.1": { - id: "mistralai/mixtral-8x22b-instruct-v0.1", - name: "Mixtral 8x22B", - family: "mixtral", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8999999999999999, output: 0.8999999999999999 }, - limit: { context: 65536, input: 65536, output: 32768 }, - }, - "mistralai/mistral-medium-3.1": { - id: "mistralai/mistral-medium-3.1", - name: "Mistral Medium 3.1", - family: "mistral-medium", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2 }, - limit: { context: 131072, input: 131072, output: 32768 }, - }, - "mistralai/ministral-3b-2512": { - id: "mistralai/ministral-3b-2512", - name: "Ministral 3B", - family: "ministral", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-04", - last_updated: "2025-12-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 131072, input: 131072, output: 32768 }, - }, - "mistralai/Mistral-Nemo-Instruct-2407": { - id: "mistralai/Mistral-Nemo-Instruct-2407", - name: "Mistral Nemo", - family: "mistral-nemo", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1003, output: 0.1207 }, - limit: { context: 16384, input: 16384, output: 8192 }, - }, - "mistralai/mistral-medium-3": { - id: "mistralai/mistral-medium-3", - name: "Mistral Medium 3", - family: "mistral-medium", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2 }, - limit: { context: 131072, input: 131072, output: 32768 }, - }, - "mistralai/mistral-7b-instruct": { - id: "mistralai/mistral-7b-instruct", - name: "Mistral 7B Instruct", - family: "mistral", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-05-27", - last_updated: "2024-05-27", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.0544, output: 0.0544 }, - limit: { context: 32768, input: 32768, output: 8192 }, - }, - "mistralai/Devstral-Small-2505": { - id: "mistralai/Devstral-Small-2505", - name: "Mistral Devstral Small 2505", - family: "devstral", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-08-02", - last_updated: "2025-08-02", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.060000000000000005, output: 0.060000000000000005 }, - limit: { context: 32768, input: 32768, output: 8192 }, - }, - "mistralai/mistral-small-creative": { - id: "mistralai/mistral-small-creative", - name: "Mistral Small Creative", - family: "mistral-small", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-12-16", - last_updated: "2025-12-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 32768, input: 32768, output: 32768 }, - }, - "mistralai/mistral-large": { - id: "mistralai/mistral-large", - name: "Mistral Large 2411", - family: "mistral-large", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-02-26", - last_updated: "2024-02-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.006, output: 6.001 }, - limit: { context: 128000, input: 128000, output: 256000 }, - }, - "mistralai/ministral-14b-2512": { - id: "mistralai/ministral-14b-2512", - name: "Ministral 14B", - family: "ministral", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-04", - last_updated: "2025-12-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 262144, input: 262144, output: 32768 }, - }, - "shisa-ai/shisa-v2.1-llama3.3-70b": { - id: "shisa-ai/shisa-v2.1-llama3.3-70b", - name: "Shisa V2.1 Llama 3.3 70B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 0.5 }, - limit: { context: 32768, input: 32768, output: 4096 }, - }, - "shisa-ai/shisa-v2-llama3.3-70b": { - id: "shisa-ai/shisa-v2-llama3.3-70b", - name: "Shisa V2 Llama 3.3 70B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-26", - last_updated: "2025-07-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 0.5 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "meta-llama/llama-3.3-70b-instruct": { - id: "meta-llama/llama-3.3-70b-instruct", - name: "Llama 3.3 70b Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-02-27", - last_updated: "2025-02-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.23 }, - limit: { context: 131072, input: 131072, output: 16384 }, - }, - "meta-llama/llama-4-scout": { - id: "meta-llama/llama-4-scout", - name: "Llama 4 Scout", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.085, output: 0.46 }, - limit: { context: 328000, input: 328000, output: 65536 }, - }, - "meta-llama/llama-4-maverick": { - id: "meta-llama/llama-4-maverick", - name: "Llama 4 Maverick", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.18000000000000002, output: 0.8 }, - limit: { context: 1048576, input: 1048576, output: 65536 }, - }, - "meta-llama/llama-3.2-90b-vision-instruct": { - id: "meta-llama/llama-3.2-90b-vision-instruct", - name: "Llama 3.2 Medium", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.9009999999999999, output: 0.9009999999999999 }, - limit: { context: 131072, input: 131072, output: 16384 }, - }, - "meta-llama/llama-3.2-3b-instruct": { - id: "meta-llama/llama-3.2-3b-instruct", - name: "Llama 3.2 3b Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-09-25", - last_updated: "2024-09-25", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.0306, output: 0.0493 }, - limit: { context: 131072, input: 131072, output: 8192 }, - }, - "meta-llama/llama-3.1-8b-instruct": { - id: "meta-llama/llama-3.1-8b-instruct", - name: "Llama 3.1 8b Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.0544, output: 0.0544 }, - limit: { context: 131072, input: 131072, output: 16384 }, - }, - "GalrionSoftworks/MN-LooseCannon-12B-v1": { - id: "GalrionSoftworks/MN-LooseCannon-12B-v1", - name: "MN-LooseCannon-12B-v1", - family: "mistral-nemo", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-01", - last_updated: "2024-07-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.49299999999999994, output: 0.49299999999999994 }, - limit: { context: 16384, input: 16384, output: 8192 }, - }, - "baseten/Kimi-K2-Instruct-FP4": { - id: "baseten/Kimi-K2-Instruct-FP4", - name: "Kimi K2 0711 Instruct FP4", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-11", - last_updated: "2025-07-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 2 }, - limit: { context: 128000, input: 128000, output: 131072 }, - }, - "Gryphe/MythoMax-L2-13b": { - id: "Gryphe/MythoMax-L2-13b", - name: "MythoMax 13B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-08-08", - last_updated: "2025-08-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1003, output: 0.1003 }, - limit: { context: 4000, input: 4000, output: 4096 }, - }, - "x-ai/grok-4-fast:thinking": { - id: "x-ai/grok-4-fast:thinking", - name: "Grok 4 Fast Thinking", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5 }, - limit: { context: 2000000, input: 2000000, output: 131072 }, - }, - "x-ai/grok-4-07-09": { - id: "x-ai/grok-4-07-09", - name: "Grok 4", - family: "grok", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 256000, input: 256000, output: 131072 }, - }, - "x-ai/grok-4-fast": { - id: "x-ai/grok-4-fast", - name: "Grok 4 Fast", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-09-20", - last_updated: "2025-09-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5 }, - limit: { context: 2000000, input: 2000000, output: 131072 }, - }, - "x-ai/grok-code-fast-1": { - id: "x-ai/grok-code-fast-1", - name: "Grok Code Fast 1", - family: "grok", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-08-28", - last_updated: "2025-08-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.5 }, - limit: { context: 256000, input: 256000, output: 131072 }, - }, - "x-ai/grok-4.1-fast": { - id: "x-ai/grok-4.1-fast", - name: "Grok 4.1 Fast", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-11-20", - last_updated: "2025-11-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5 }, - limit: { context: 2000000, input: 2000000, output: 131072 }, - }, - "x-ai/grok-4.1-fast-reasoning": { - id: "x-ai/grok-4.1-fast-reasoning", - name: "Grok 4.1 Fast Reasoning", - family: "grok", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-11-20", - last_updated: "2025-11-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5 }, - limit: { context: 2000000, input: 2000000, output: 131072 }, - }, - "tencent/Hunyuan-MT-7B": { - id: "tencent/Hunyuan-MT-7B", - name: "Hunyuan MT 7B", - family: "hunyuan", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-09-18", - last_updated: "2025-09-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 10, output: 20 }, - limit: { context: 8192, input: 8192, output: 8192 }, - }, - "microsoft/wizardlm-2-8x22b": { - id: "microsoft/wizardlm-2-8x22b", - name: "WizardLM-2 8x22B", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-04-15", - last_updated: "2025-04-15", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.49299999999999994, output: 0.49299999999999994 }, - limit: { context: 65536, input: 65536, output: 8192 }, - }, - "microsoft/MAI-DS-R1-FP8": { - id: "microsoft/MAI-DS-R1-FP8", - name: "Microsoft DeepSeek R1", - family: "deepseek", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.3 }, - limit: { context: 128000, input: 128000, output: 8192 }, - }, - "cohere/command-r": { - id: "cohere/command-r", - name: "Cohere: Command R", - family: "command-r", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-03-11", - last_updated: "2024-03-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.476, output: 1.428 }, - limit: { context: 128000, input: 128000, output: 4096 }, - }, - "cohere/command-r-plus-08-2024": { - id: "cohere/command-r-plus-08-2024", - name: "Cohere: Command R+", - family: "command-r", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - release_date: "2024-08-30", - last_updated: "2024-08-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.856, output: 14.246 }, - limit: { context: 128000, input: 128000, output: 4096 }, - }, - "chutesai/Mistral-Small-3.2-24B-Instruct-2506": { - id: "chutesai/Mistral-Small-3.2-24B-Instruct-2506", - name: "Mistral Small 3.2 24b Instruct", - family: "chutesai", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-04-15", - last_updated: "2025-04-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.4 }, - limit: { context: 128000, input: 128000, output: 131072 }, - }, - "nvidia/Llama-3.1-Nemotron-Ultra-253B-v1": { - id: "nvidia/Llama-3.1-Nemotron-Ultra-253B-v1", - name: "Nvidia Nemotron Ultra 253B", - family: "nemotron", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-03", - last_updated: "2025-07-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 0.8 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "nvidia/nemotron-3-nano-30b-a3b": { - id: "nvidia/nemotron-3-nano-30b-a3b", - name: "Nvidia Nemotron 3 Nano 30B", - family: "nemotron", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-15", - last_updated: "2025-12-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.17, output: 0.68 }, - limit: { context: 256000, input: 256000, output: 262144 }, - }, - "nvidia/nvidia-nemotron-nano-9b-v2": { - id: "nvidia/nvidia-nemotron-nano-9b-v2", - name: "Nvidia Nemotron Nano 9B v2", - family: "nemotron", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-08-18", - last_updated: "2025-08-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.17, output: 0.68 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "nvidia/Llama-3.1-Nemotron-70B-Instruct-HF": { - id: "nvidia/Llama-3.1-Nemotron-70B-Instruct-HF", - name: "Nvidia Nemotron 70b", - family: "nemotron", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-04-15", - last_updated: "2025-04-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.357, output: 0.408 }, - limit: { context: 16384, input: 16384, output: 8192 }, - }, - "nvidia/Llama-3.3-Nemotron-Super-49B-v1": { - id: "nvidia/Llama-3.3-Nemotron-Super-49B-v1", - name: "Nvidia Nemotron Super 49B", - family: "nemotron", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-08-08", - last_updated: "2025-08-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.15 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "nvidia/Llama-3_3-Nemotron-Super-49B-v1_5": { - id: "nvidia/Llama-3_3-Nemotron-Super-49B-v1_5", - name: "Nvidia Nemotron Super 49B v1.5", - family: "nemotron", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-08-08", - last_updated: "2025-08-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.25 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "TheDrummer 2/Anubis-70B-v1": { - id: "TheDrummer 2/Anubis-70B-v1", - name: "Anubis 70B v1", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-01", - last_updated: "2024-07-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.31, output: 0.31 }, - limit: { context: 65536, input: 65536, output: 16384 }, - }, - "TheDrummer 2/Cydonia-24B-v4.3": { - id: "TheDrummer 2/Cydonia-24B-v4.3", - name: "The Drummer Cydonia 24B v4.3", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-25", - last_updated: "2025-12-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1003, output: 0.1207 }, - limit: { context: 32768, input: 32768, output: 32768 }, - }, - "TheDrummer 2/Magidonia-24B-v4.3": { - id: "TheDrummer 2/Magidonia-24B-v4.3", - name: "The Drummer Magidonia 24B v4.3", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-25", - last_updated: "2025-12-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1003, output: 0.1207 }, - limit: { context: 32768, input: 32768, output: 32768 }, - }, - "TheDrummer 2/Cydonia-24B-v4": { - id: "TheDrummer 2/Cydonia-24B-v4", - name: "The Drummer Cydonia 24B v4", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-22", - last_updated: "2025-07-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2006, output: 0.2414 }, - limit: { context: 16384, input: 16384, output: 32768 }, - }, - "TheDrummer 2/Anubis-70B-v1.1": { - id: "TheDrummer 2/Anubis-70B-v1.1", - name: "Anubis 70B v1.1", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-01", - last_updated: "2024-07-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.31, output: 0.31 }, - limit: { context: 131072, input: 131072, output: 16384 }, - }, - "TheDrummer 2/Rocinante-12B-v1.1": { - id: "TheDrummer 2/Rocinante-12B-v1.1", - name: "Rocinante 12b", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-01", - last_updated: "2024-07-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.408, output: 0.595 }, - limit: { context: 16384, input: 16384, output: 8192 }, - }, - "TheDrummer 2/Cydonia-24B-v4.1": { - id: "TheDrummer 2/Cydonia-24B-v4.1", - name: "The Drummer Cydonia 24B v4.1", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-08-19", - last_updated: "2025-08-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1003, output: 0.1207 }, - limit: { context: 16384, input: 16384, output: 32768 }, - }, - "TheDrummer 2/UnslopNemo-12B-v4.1": { - id: "TheDrummer 2/UnslopNemo-12B-v4.1", - name: "UnslopNemo 12b v4", - family: "llama", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-01", - last_updated: "2024-07-01", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.49299999999999994, output: 0.49299999999999994 }, - limit: { context: 32768, input: 32768, output: 8192 }, - }, - "TheDrummer 2/Cydonia-24B-v2": { - id: "TheDrummer 2/Cydonia-24B-v2", - name: "The Drummer Cydonia 24B v2", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1003, output: 0.1207 }, - limit: { context: 16384, input: 16384, output: 32768 }, - }, - "TheDrummer 2/skyfall-36b-v2": { - id: "TheDrummer 2/skyfall-36b-v2", - name: "TheDrummer Skyfall 36B V2", - family: "llama", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-03-10", - last_updated: "2025-03-10", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.49299999999999994, output: 0.49299999999999994 }, - limit: { context: 64000, input: 64000, output: 32768 }, - }, - "deepseek-ai/DeepSeek-V3.1:thinking": { - id: "deepseek-ai/DeepSeek-V3.1:thinking", - name: "DeepSeek V3.1 Thinking", - family: "deepseek-thinking", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-08-21", - last_updated: "2025-08-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.7 }, - limit: { context: 128000, input: 128000, output: 65536 }, - }, - "deepseek-ai/DeepSeek-V3.1": { - id: "deepseek-ai/DeepSeek-V3.1", - name: "DeepSeek V3.1", - family: "deepseek", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-26", - last_updated: "2025-07-26", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.7 }, - limit: { context: 128000, input: 128000, output: 65536 }, - }, - "deepseek-ai/DeepSeek-V3.1-Terminus:thinking": { - id: "deepseek-ai/DeepSeek-V3.1-Terminus:thinking", - name: "DeepSeek V3.1 Terminus (Thinking)", - family: "deepseek-thinking", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-09-22", - last_updated: "2025-09-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 0.7 }, - limit: { context: 128000, input: 128000, output: 65536 }, - }, - "deepseek-ai/deepseek-v3.2-exp-thinking": { - id: "deepseek-ai/deepseek-v3.2-exp-thinking", - name: "DeepSeek V3.2 Exp Thinking", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27999999999999997, output: 0.42000000000000004 }, - limit: { context: 163840, input: 163840, output: 65536 }, - }, - "deepseek-ai/deepseek-v3.2-exp": { - id: "deepseek-ai/deepseek-v3.2-exp", - name: "DeepSeek V3.2 Exp", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27999999999999997, output: 0.42000000000000004 }, - limit: { context: 163840, input: 163840, output: 65536 }, - }, - "deepseek-ai/DeepSeek-R1-0528": { - id: "deepseek-ai/DeepSeek-R1-0528", - name: "DeepSeek R1 0528", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-05-28", - last_updated: "2025-05-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.7 }, - limit: { context: 128000, input: 128000, output: 163840 }, - }, - "deepseek-ai/DeepSeek-V3.1-Terminus": { - id: "deepseek-ai/DeepSeek-V3.1-Terminus", - name: "DeepSeek V3.1 Terminus", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-08-02", - last_updated: "2025-08-02", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 0.7 }, - limit: { context: 128000, input: 128000, output: 65536 }, - }, - "openai/gpt-5.1-codex-max": { - id: "openai/gpt-5.1-codex-max", - name: "GPT 5.1 Codex Max", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 20 }, - limit: { context: 400000, input: 400000, output: 128000 }, - }, - "openai/gpt-5.2-chat": { - id: "openai/gpt-5.2-chat", - name: "GPT 5.2 Chat", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2026-01-01", - last_updated: "2026-01-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14 }, - limit: { context: 400000, input: 400000, output: 16384 }, - }, - "openai/gpt-4o-mini-search-preview": { - id: "openai/gpt-4o-mini-search-preview", - name: "GPT-4o mini Search Preview", - family: "gpt-mini", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.088, output: 0.35 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "openai/chatgpt-4o-latest": { - id: "openai/chatgpt-4o-latest", - name: "ChatGPT 4o", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2024-05-13", - last_updated: "2024-05-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 4.998, output: 14.993999999999998 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "openai/gpt-5.2-pro": { - id: "openai/gpt-5.2-pro", - name: "GPT 5.2 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2026-01-01", - last_updated: "2026-01-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 21, output: 168 }, - limit: { context: 400000, input: 400000, output: 128000 }, - }, - "openai/gpt-5-mini": { - id: "openai/gpt-5-mini", - name: "GPT 5 Mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2 }, - limit: { context: 400000, input: 400000, output: 128000 }, - }, - "openai/gpt-5-nano": { - id: "openai/gpt-5-nano", - name: "GPT 5 Nano", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.4 }, - limit: { context: 400000, input: 400000, output: 128000 }, - }, - "openai/gpt-4-turbo": { - id: "openai/gpt-4-turbo", - name: "GPT-4 Turbo", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2023-11-06", - last_updated: "2024-01-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 10, output: 30 }, - limit: { context: 128000, input: 128000, output: 4096 }, - }, - "openai/gpt-5.2": { - id: "openai/gpt-5.2", - name: "GPT 5.2", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2026-01-01", - last_updated: "2026-01-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14 }, - limit: { context: 400000, input: 400000, output: 128000 }, - }, - "openai/o3-mini-high": { - id: "openai/o3-mini-high", - name: "OpenAI o3-mini (High)", - family: "o-mini", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-01-31", - last_updated: "2025-01-31", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.64, output: 2.588 }, - limit: { context: 200000, input: 200000, output: 100000 }, - }, - "openai/gpt-4o-mini": { - id: "openai/gpt-4o-mini", - name: "GPT-4o mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1496, output: 0.595 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "openai/o4-mini-deep-research": { - id: "openai/o4-mini-deep-research", - name: "OpenAI o4-mini Deep Research", - family: "o-mini", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 9.996, output: 19.992 }, - limit: { context: 200000, input: 200000, output: 100000 }, - }, - "openai/gpt-5.1-chat": { - id: "openai/gpt-5.1-chat", - name: "GPT 5.1 Chat", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 400000, input: 400000, output: 128000 }, - }, - "openai/o4-mini": { - id: "openai/o4-mini", - name: "OpenAI o4-mini", - family: "o-mini", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4 }, - limit: { context: 200000, input: 200000, output: 100000 }, - }, - "openai/gpt-5.2-codex": { - id: "openai/gpt-5.2-codex", - name: "GPT 5.2 Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2026-01-14", - last_updated: "2026-01-14", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14 }, - limit: { context: 400000, input: 400000, output: 128000 }, - }, - "openai/gpt-5.1-codex-mini": { - id: "openai/gpt-5.1-codex-mini", - name: "GPT 5.1 Codex Mini", - family: "gpt-codex-mini", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2 }, - limit: { context: 400000, input: 400000, output: 128000 }, - }, - "openai/o1-preview": { - id: "openai/o1-preview", - name: "OpenAI o1-preview", - family: "o", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2024-09-12", - last_updated: "2024-09-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 14.993999999999998, output: 59.993 }, - limit: { context: 128000, input: 128000, output: 32768 }, - }, - "openai/gpt-4o-2024-08-06": { - id: "openai/gpt-4o-2024-08-06", - name: "GPT-4o (2024-08-06)", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-08-06", - last_updated: "2024-08-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.499, output: 9.996 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "openai/gpt-5.1": { - id: "openai/gpt-5.1", - name: "GPT 5.1", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 400000, input: 400000, output: 128000 }, - }, - "openai/o1": { - id: "openai/o1", - name: "OpenAI o1", - family: "o", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2024-12-17", - last_updated: "2024-12-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 14.993999999999998, output: 59.993 }, - limit: { context: 200000, input: 200000, output: 100000 }, - }, - "openai/gpt-3.5-turbo": { - id: "openai/gpt-3.5-turbo", - name: "GPT-3.5 Turbo", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2022-11-30", - last_updated: "2024-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 1.5 }, - limit: { context: 16385, input: 16385, output: 4096 }, - }, - "openai/o3-deep-research": { - id: "openai/o3-deep-research", - name: "OpenAI o3 Deep Research", - family: "o", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 9.996, output: 19.992 }, - limit: { context: 200000, input: 200000, output: 100000 }, - }, - "openai/o3-mini": { - id: "openai/o3-mini", - name: "OpenAI o3-mini", - family: "o-mini", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-01-31", - last_updated: "2025-01-31", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4 }, - limit: { context: 200000, input: 200000, output: 100000 }, - }, - "openai/gpt-4-turbo-preview": { - id: "openai/gpt-4-turbo-preview", - name: "GPT-4 Turbo Preview", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2023-11-06", - last_updated: "2024-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 9.996, output: 30.004999999999995 }, - limit: { context: 128000, input: 128000, output: 4096 }, - }, - "openai/o1-pro": { - id: "openai/o1-pro", - name: "OpenAI o1 Pro", - family: "o-pro", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-01-25", - last_updated: "2025-01-25", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 150, output: 600 }, - limit: { context: 200000, input: 200000, output: 100000 }, - }, - "openai/gpt-5-codex": { - id: "openai/gpt-5-codex", - name: "GPT-5 Codex", - family: "gpt-codex", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-09-15", - last_updated: "2025-09-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 9.996, output: 19.992 }, - limit: { context: 256000, input: 256000, output: 32768 }, - }, - "openai/gpt-5.1-chat-latest": { - id: "openai/gpt-5.1-chat-latest", - name: "GPT 5.1 Chat (Latest)", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 400000, input: 400000, output: 16384 }, - }, - "openai/gpt-4o-search-preview": { - id: "openai/gpt-4o-search-preview", - name: "GPT-4o Search Preview", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-05-13", - last_updated: "2024-05-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.47, output: 5.88 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "openai/gpt-4.1-nano": { - id: "openai/gpt-4.1-nano", - name: "GPT 4.1 Nano", - family: "gpt-nano", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 1047576, input: 1047576, output: 32768 }, - }, - "openai/o4-mini-high": { - id: "openai/o4-mini-high", - name: "OpenAI o4-mini high", - family: "o-mini", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4 }, - limit: { context: 200000, input: 200000, output: 100000 }, - }, - "openai/o3": { - id: "openai/o3", - name: "OpenAI o3", - family: "o", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8 }, - limit: { context: 200000, input: 200000, output: 100000 }, - }, - "openai/gpt-oss-20b": { - id: "openai/gpt-oss-20b", - name: "GPT OSS 20B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.04, output: 0.15 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "openai/gpt-5-pro": { - id: "openai/gpt-5-pro", - name: "GPT 5 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 120 }, - limit: { context: 400000, input: 400000, output: 128000 }, - }, - "openai/gpt-5.1-2025-11-13": { - id: "openai/gpt-5.1-2025-11-13", - name: "GPT-5.1 (2025-11-13)", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 1000000, input: 1000000, output: 32768 }, - }, - "openai/gpt-4o": { - id: "openai/gpt-4o", - name: "GPT-4o", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-05-13", - last_updated: "2024-05-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.499, output: 9.996 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "openai/o3-mini-low": { - id: "openai/o3-mini-low", - name: "OpenAI o3-mini (Low)", - family: "o-mini", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-01-31", - last_updated: "2025-01-31", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 9.996, output: 19.992 }, - limit: { context: 200000, input: 200000, output: 100000 }, - }, - "openai/gpt-5": { - id: "openai/gpt-5", - name: "GPT 5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 400000, input: 400000, output: 128000 }, - }, - "openai/gpt-oss-safeguard-20b": { - id: "openai/gpt-oss-safeguard-20b", - name: "GPT OSS Safeguard 20B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-10-29", - last_updated: "2025-10-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.075, output: 0.3 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "openai/o3-pro-2025-06-10": { - id: "openai/o3-pro-2025-06-10", - name: "OpenAI o3-pro (2025-06-10)", - family: "o-pro", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-06-10", - last_updated: "2025-06-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 9.996, output: 19.992 }, - limit: { context: 200000, input: 200000, output: 100000 }, - }, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "GPT OSS 120B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.25 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "openai/gpt-5-chat-latest": { - id: "openai/gpt-5-chat-latest", - name: "GPT 5 Chat", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 400000, input: 400000, output: 128000 }, - }, - "openai/gpt-4.1": { - id: "openai/gpt-4.1", - name: "GPT 4.1", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-09-10", - last_updated: "2025-09-10", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8 }, - limit: { context: 1047576, input: 1047576, output: 32768 }, - }, - "openai/gpt-4.1-mini": { - id: "openai/gpt-4.1-mini", - name: "GPT 4.1 Mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.6 }, - limit: { context: 1047576, input: 1047576, output: 32768 }, - }, - "openai/gpt-5.1-codex": { - id: "openai/gpt-5.1-codex", - name: "GPT 5.1 Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 400000, input: 400000, output: 128000 }, - }, - "openai/gpt-4o-2024-11-20": { - id: "openai/gpt-4o-2024-11-20", - name: "GPT-4o (2024-11-20)", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-11-20", - last_updated: "2024-11-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "VongolaChouko/Starcannon-Unleashed-12B-v1.0": { - id: "VongolaChouko/Starcannon-Unleashed-12B-v1.0", - name: "Mistral Nemo Starcannon 12b v1", - family: "mistral-nemo", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-01", - last_updated: "2024-07-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.49299999999999994, output: 0.49299999999999994 }, - limit: { context: 16384, input: 16384, output: 8192 }, - }, - "amazon/nova-lite-v1": { - id: "amazon/nova-lite-v1", - name: "Amazon Nova Lite 1.0", - family: "nova-lite", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-03", - last_updated: "2024-12-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.0595, output: 0.238 }, - limit: { context: 300000, input: 300000, output: 5120 }, - }, - "amazon/nova-pro-v1": { - id: "amazon/nova-pro-v1", - name: "Amazon Nova Pro 1.0", - family: "nova-pro", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-03", - last_updated: "2024-12-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.7989999999999999, output: 3.1959999999999997 }, - limit: { context: 300000, input: 300000, output: 32000 }, - }, - "amazon/nova-2-lite-v1": { - id: "amazon/nova-2-lite-v1", - name: "Amazon Nova 2 Lite", - family: "nova", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-03", - last_updated: "2024-12-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5099999999999999, output: 4.25 }, - limit: { context: 1000000, input: 1000000, output: 65535 }, - }, - "amazon/nova-micro-v1": { - id: "amazon/nova-micro-v1", - name: "Amazon Nova Micro 1.0", - family: "nova-micro", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-03", - last_updated: "2024-12-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.0357, output: 0.1394 }, - limit: { context: 128000, input: 128000, output: 5120 }, - }, - "Sao10K/L3.3-70B-Euryale-v2.3": { - id: "Sao10K/L3.3-70B-Euryale-v2.3", - name: "Llama 3.3 70B Euryale", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.49299999999999994, output: 0.49299999999999994 }, - limit: { context: 20480, input: 20480, output: 16384 }, - }, - "Sao10K/L3.1-70B-Euryale-v2.2": { - id: "Sao10K/L3.1-70B-Euryale-v2.2", - name: "Llama 3.1 70B Euryale", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.357 }, - limit: { context: 20480, input: 20480, output: 16384 }, - }, - "Sao10K/L3.1-70B-Hanami-x1": { - id: "Sao10K/L3.1-70B-Hanami-x1", - name: "Llama 3.1 70B Hanami", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.49299999999999994, output: 0.49299999999999994 }, - limit: { context: 16384, input: 16384, output: 16384 }, - }, - "Sao10K/L3-8B-Stheno-v3.2": { - id: "Sao10K/L3-8B-Stheno-v3.2", - name: "Sao10K Stheno 8b", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-11-29", - last_updated: "2024-11-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2006, output: 0.2006 }, - limit: { context: 16384, input: 16384, output: 8192 }, - }, - "LatitudeGames/Wayfarer-Large-70B-Llama-3.3": { - id: "LatitudeGames/Wayfarer-Large-70B-Llama-3.3", - name: "Llama 3.3 70B Wayfarer", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-20", - last_updated: "2025-02-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.700000007, output: 0.700000007 }, - limit: { context: 16384, input: 16384, output: 16384 }, - }, - "z-ai/glm-4.6:thinking": { - id: "z-ai/glm-4.6:thinking", - name: "GLM 4.6 Thinking", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.5 }, - limit: { context: 200000, input: 200000, output: 65535 }, - }, - "z-ai/glm-4.5v": { - id: "z-ai/glm-4.5v", - name: "GLM 4.5V", - family: "glmv", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-11-22", - last_updated: "2025-11-22", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 1.7999999999999998 }, - limit: { context: 64000, input: 64000, output: 96000 }, - }, - "z-ai/glm-4.6": { - id: "z-ai/glm-4.6", - name: "GLM 4.6", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.5 }, - limit: { context: 200000, input: 200000, output: 65535 }, - }, - "z-ai/glm-4.5v:thinking": { - id: "z-ai/glm-4.5v:thinking", - name: "GLM 4.5V Thinking", - family: "glmv", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-11-22", - last_updated: "2025-11-22", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 1.7999999999999998 }, - limit: { context: 64000, input: 64000, output: 96000 }, - }, - "baidu/ernie-4.5-vl-28b-a3b": { - id: "baidu/ernie-4.5-vl-28b-a3b", - name: "ERNIE 4.5 VL 28B", - family: "ernie", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-06-30", - last_updated: "2025-06-30", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.13999999999999999, output: 0.5599999999999999 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "baidu/ernie-4.5-300b-a47b": { - id: "baidu/ernie-4.5-300b-a47b", - name: "ERNIE 4.5 300B", - family: "ernie", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-06-30", - last_updated: "2025-06-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.35, output: 1.15 }, - limit: { context: 131072, input: 131072, output: 16384 }, - }, - "dmind/dmind-1": { - id: "dmind/dmind-1", - name: "DMind-1", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-06-01", - last_updated: "2025-06-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.6 }, - limit: { context: 32768, input: 32768, output: 8192 }, - }, - "dmind/dmind-1-mini": { - id: "dmind/dmind-1-mini", - name: "DMind-1-Mini", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-06-01", - last_updated: "2025-06-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.4 }, - limit: { context: 32768, input: 32768, output: 8192 }, - }, - "Infermatic/MN-12B-Inferor-v0.0": { - id: "Infermatic/MN-12B-Inferor-v0.0", - name: "Mistral Nemo Inferor 12B", - family: "mistral-nemo", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-01", - last_updated: "2024-07-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25499999999999995, output: 0.49299999999999994 }, - limit: { context: 16384, input: 16384, output: 8192 }, - }, - "meituan-longcat/LongCat-Flash-Chat-FP8": { - id: "meituan-longcat/LongCat-Flash-Chat-FP8", - name: "LongCat Flash", - family: "longcat", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-08-31", - last_updated: "2025-08-31", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.7 }, - limit: { context: 128000, input: 128000, output: 32768 }, - }, - "meganova-ai/manta-mini-1.0": { - id: "meganova-ai/manta-mini-1.0", - name: "Manta Mini 1.0", - family: "nova", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-20", - last_updated: "2025-12-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.02, output: 0.16 }, - limit: { context: 8192, input: 8192, output: 8192 }, - }, - "meganova-ai/manta-pro-1.0": { - id: "meganova-ai/manta-pro-1.0", - name: "Manta Pro 1.0", - family: "nova", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-20", - last_updated: "2025-12-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.060000000000000005, output: 0.5 }, - limit: { context: 32768, input: 32768, output: 32768 }, - }, - "meganova-ai/manta-flash-1.0": { - id: "meganova-ai/manta-flash-1.0", - name: "Manta Flash 1.0", - family: "nova", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-20", - last_updated: "2025-12-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.02, output: 0.16 }, - limit: { context: 16384, input: 16384, output: 16384 }, - }, - "minimax/minimax-m2.7": { - id: "minimax/minimax-m2.7", - name: "MiniMax M2.7", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 204800, input: 204800, output: 131072 }, - }, - "minimax/minimax-01": { - id: "minimax/minimax-01", - name: "MiniMax 01", - family: "minimax", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-01-15", - last_updated: "2025-01-15", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1394, output: 1.1219999999999999 }, - limit: { context: 1000192, input: 1000192, output: 16384 }, - }, - "minimax/minimax-m2.1": { - id: "minimax/minimax-m2.1", - name: "MiniMax M2.1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-12-19", - last_updated: "2025-12-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.33, output: 1.32 }, - limit: { context: 200000, input: 200000, output: 131072 }, - }, - "minimax/minimax-m2-her": { - id: "minimax/minimax-m2-her", - name: "MiniMax M2-her", - family: "minimax", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2026-01-24", - last_updated: "2026-01-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.30200000000000005, output: 1.2069999999999999 }, - limit: { context: 65532, input: 65532, output: 2048 }, - }, - "minimax/minimax-m2.5": { - id: "minimax/minimax-m2.5", - name: "MiniMax M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 204800, input: 204800, output: 131072 }, - }, - "qwen/qwen3.5-397b-a17b": { - id: "qwen/qwen3.5-397b-a17b", - name: "Qwen3.5 397B A17B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2026-02-16", - last_updated: "2026-02-16", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3.6 }, - limit: { context: 258048, input: 258048, output: 65536 }, - }, - "unsloth/gemma-3-1b-it": { - id: "unsloth/gemma-3-1b-it", - name: "Gemma 3 1B IT", - family: "unsloth", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-03-10", - last_updated: "2025-03-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1003, output: 0.1003 }, - limit: { context: 128000, input: 128000, output: 8192 }, - }, - "unsloth/gemma-3-12b-it": { - id: "unsloth/gemma-3-12b-it", - name: "Gemma 3 12B IT", - family: "unsloth", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-03-10", - last_updated: "2025-03-10", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.272, output: 0.272 }, - limit: { context: 128000, input: 128000, output: 131072 }, - }, - "unsloth/gemma-3-4b-it": { - id: "unsloth/gemma-3-4b-it", - name: "Gemma 3 4B IT", - family: "unsloth", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-03-10", - last_updated: "2025-03-10", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2006, output: 0.2006 }, - limit: { context: 128000, input: 128000, output: 8192 }, - }, - "unsloth/gemma-3-27b-it": { - id: "unsloth/gemma-3-27b-it", - name: "Gemma 3 27B IT", - family: "unsloth", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-03-10", - last_updated: "2025-03-10", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2992, output: 0.2992 }, - limit: { context: 128000, input: 128000, output: 96000 }, - }, - "THUDM/GLM-Z1-9B-0414": { - id: "THUDM/GLM-Z1-9B-0414", - name: "GLM Z1 9B 0414", - family: "glm-z", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 32000, input: 32000, output: 8000 }, - }, - "THUDM/GLM-4-9B-0414": { - id: "THUDM/GLM-4-9B-0414", - name: "GLM 4 9B 0414", - family: "glm", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 32000, input: 32000, output: 8000 }, - }, - "THUDM/GLM-Z1-Rumination-32B-0414": { - id: "THUDM/GLM-Z1-Rumination-32B-0414", - name: "GLM Z1 Rumination 32B 0414", - family: "glm-z", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-04-15", - last_updated: "2025-04-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 32000, input: 32000, output: 65536 }, - }, - "THUDM/GLM-4-32B-0414": { - id: "THUDM/GLM-4-32B-0414", - name: "GLM 4 32B 0414", - family: "glm", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 128000, input: 128000, output: 65536 }, - }, - "THUDM/GLM-Z1-32B-0414": { - id: "THUDM/GLM-Z1-32B-0414", - name: "GLM Z1 32B 0414", - family: "glm-z", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-04-15", - last_updated: "2025-04-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 128000, input: 128000, output: 65536 }, - }, - "google/gemini-3-flash-preview": { - id: "google/gemini-3-flash-preview", - name: "Gemini 3 Flash (Preview)", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 3 }, - limit: { context: 1048756, input: 1048756, output: 65536 }, - }, - "google/gemini-flash-1.5": { - id: "google/gemini-flash-1.5", - name: "Gemini 1.5 Flash", - family: "gemini-flash", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-05-14", - last_updated: "2024-05-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.0748, output: 0.306 }, - limit: { context: 2000000, input: 2000000, output: 8192 }, - }, - "google/gemini-3-flash-preview-thinking": { - id: "google/gemini-3-flash-preview-thinking", - name: "Gemini 3 Flash Thinking", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 3 }, - limit: { context: 1048756, input: 1048756, output: 65536 }, - }, - "moonshotai/kimi-k2.5": { - id: "moonshotai/kimi-k2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - release_date: "2026-01-26", - last_updated: "2026-01-26", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.9 }, - limit: { context: 256000, input: 256000, output: 65536 }, - }, - "moonshotai/kimi-k2-instruct": { - id: "moonshotai/kimi-k2-instruct", - name: "Kimi K2 Instruct", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-07-01", - last_updated: "2025-07-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 2 }, - limit: { context: 256000, input: 256000, output: 8192 }, - }, - "moonshotai/kimi-k2-thinking-original": { - id: "moonshotai/kimi-k2-thinking-original", - name: "Kimi K2 Thinking Original", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-11-06", - last_updated: "2025-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 2.5 }, - limit: { context: 256000, input: 256000, output: 16384 }, - }, - "moonshotai/kimi-k2-instruct-0711": { - id: "moonshotai/kimi-k2-instruct-0711", - name: "Kimi K2 0711", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-07-11", - last_updated: "2025-07-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 2 }, - limit: { context: 128000, input: 128000, output: 8192 }, - }, - "moonshotai/Kimi-Dev-72B": { - id: "moonshotai/Kimi-Dev-72B", - name: "Kimi Dev 72B", - family: "kimi", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-04-15", - last_updated: "2025-04-15", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 0.4 }, - limit: { context: 128000, input: 128000, output: 131072 }, - }, - "moonshotai/kimi-k2-thinking-turbo-original": { - id: "moonshotai/kimi-k2-thinking-turbo-original", - name: "Kimi K2 Thinking Turbo Original", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-11-06", - last_updated: "2025-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.15, output: 8 }, - limit: { context: 256000, input: 256000, output: 16384 }, - }, - "moonshotai/Kimi-K2-Instruct-0905": { - id: "moonshotai/Kimi-K2-Instruct-0905", - name: "Kimi K2 0905", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2 }, - limit: { context: 256000, input: 256000, output: 262144 }, - }, - "moonshotai/kimi-k2-thinking": { - id: "moonshotai/kimi-k2-thinking", - name: "Kimi K2 Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-11-06", - last_updated: "2025-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 256000, input: 256000, output: 262144 }, - }, - "moonshotai/kimi-k2.5:thinking": { - id: "moonshotai/kimi-k2.5:thinking", - name: "Kimi K2.5 Thinking", - family: "kimi-thinking", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - release_date: "2026-01-26", - last_updated: "2026-01-26", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.9 }, - limit: { context: 256000, input: 256000, output: 65536 }, - }, - "Tongyi-Zhiwen/QwenLong-L1-32B": { - id: "Tongyi-Zhiwen/QwenLong-L1-32B", - name: "QwenLong L1 32B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-01-25", - last_updated: "2025-01-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.13999999999999999, output: 0.6 }, - limit: { context: 128000, input: 128000, output: 40960 }, - }, - "nothingiisreal/L3.1-70B-Celeste-V0.1-BF16": { - id: "nothingiisreal/L3.1-70B-Celeste-V0.1-BF16", - name: "Llama 3.1 70B Celeste v0.1", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.49299999999999994, output: 0.49299999999999994 }, - limit: { context: 16384, input: 16384, output: 16384 }, - }, - "aion-labs/aion-1.0": { - id: "aion-labs/aion-1.0", - name: "Aion 1.0", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-01", - last_updated: "2025-02-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3.995, output: 7.99 }, - limit: { context: 65536, input: 65536, output: 8192 }, - }, - "aion-labs/aion-rp-llama-3.1-8b": { - id: "aion-labs/aion-rp-llama-3.1-8b", - name: "Llama 3.1 8b (uncensored)", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2006, output: 0.2006 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "aion-labs/aion-1.0-mini": { - id: "aion-labs/aion-1.0-mini", - name: "Aion 1.0 mini (DeepSeek)", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-20", - last_updated: "2025-02-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.7989999999999999, output: 1.394 }, - limit: { context: 131072, input: 131072, output: 8192 }, - }, - "Alibaba-NLP/Tongyi-DeepResearch-30B-A3B": { - id: "Alibaba-NLP/Tongyi-DeepResearch-30B-A3B", - name: "Tongyi DeepResearch 30B A3B", - family: "yi", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-08-26", - last_updated: "2025-08-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.08, output: 0.24000000000000002 }, - limit: { context: 128000, input: 128000, output: 65536 }, - }, - "MiniMaxAI/MiniMax-M1-80k": { - id: "MiniMaxAI/MiniMax-M1-80k", - name: "MiniMax M1 80K", - family: "minimax", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-06-16", - last_updated: "2025-06-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6052, output: 2.4225000000000003 }, - limit: { context: 1000000, input: 1000000, output: 131072 }, - }, - "anthropic/claude-opus-4.6:thinking:low": { - id: "anthropic/claude-opus-4.6:thinking:low", - name: "Claude 4.6 Opus Thinking Low", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 4.998, output: 25.007 }, - limit: { context: 1000000, input: 1000000, output: 128000 }, - }, - "anthropic/claude-opus-4.6": { - id: "anthropic/claude-opus-4.6", - name: "Claude 4.6 Opus", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 4.998, output: 25.007 }, - limit: { context: 1000000, input: 1000000, output: 128000 }, - }, - "anthropic/claude-sonnet-4.6:thinking": { - id: "anthropic/claude-sonnet-4.6:thinking", - name: "Claude Sonnet 4.6 Thinking", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2026-02-17", - last_updated: "2026-02-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.992, output: 14.993999999999998 }, - limit: { context: 1000000, input: 1000000, output: 128000 }, - }, - "anthropic/claude-opus-4.6:thinking:max": { - id: "anthropic/claude-opus-4.6:thinking:max", - name: "Claude 4.6 Opus Thinking Max", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 4.998, output: 25.007 }, - limit: { context: 1000000, input: 1000000, output: 128000 }, - }, - "anthropic/claude-opus-4.6:thinking:medium": { - id: "anthropic/claude-opus-4.6:thinking:medium", - name: "Claude 4.6 Opus Thinking Medium", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 4.998, output: 25.007 }, - limit: { context: 1000000, input: 1000000, output: 128000 }, - }, - "anthropic/claude-sonnet-4.6": { - id: "anthropic/claude-sonnet-4.6", - name: "Claude Sonnet 4.6", - family: "claude-sonnet", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2026-02-17", - last_updated: "2026-02-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.992, output: 14.993999999999998 }, - limit: { context: 1000000, input: 1000000, output: 128000 }, - }, - "anthropic/claude-opus-4.6:thinking": { - id: "anthropic/claude-opus-4.6:thinking", - name: "Claude 4.6 Opus Thinking", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 4.998, output: 25.007 }, - limit: { context: 1000000, input: 1000000, output: 128000 }, - }, - "abacusai/Dracarys-72B-Instruct": { - id: "abacusai/Dracarys-72B-Instruct", - name: "Llama 3.1 70B Dracarys 2", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-08-02", - last_updated: "2025-08-02", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.49299999999999994, output: 0.49299999999999994 }, - limit: { context: 16384, input: 16384, output: 8192 }, - }, - "EVA-UNIT-01/EVA-LLaMA-3.33-70B-v0.0": { - id: "EVA-UNIT-01/EVA-LLaMA-3.33-70B-v0.0", - name: "EVA Llama 3.33 70B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-26", - last_updated: "2025-07-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.006, output: 2.006 }, - limit: { context: 16384, input: 16384, output: 16384 }, - }, - "EVA-UNIT-01/EVA-Qwen2.5-72B-v0.2": { - id: "EVA-UNIT-01/EVA-Qwen2.5-72B-v0.2", - name: "EVA-Qwen2.5-72B-v0.2", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.7989999999999999, output: 0.7989999999999999 }, - limit: { context: 16384, input: 16384, output: 8192 }, - }, - "EVA-UNIT-01/EVA-LLaMA-3.33-70B-v0.1": { - id: "EVA-UNIT-01/EVA-LLaMA-3.33-70B-v0.1", - name: "EVA-LLaMA-3.33-70B-v0.1", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.006, output: 2.006 }, - limit: { context: 16384, input: 16384, output: 16384 }, - }, - "EVA-UNIT-01/EVA-Qwen2.5-32B-v0.2": { - id: "EVA-UNIT-01/EVA-Qwen2.5-32B-v0.2", - name: "EVA-Qwen2.5-32B-v0.2", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-26", - last_updated: "2025-07-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.7989999999999999, output: 0.7989999999999999 }, - limit: { context: 16384, input: 16384, output: 8192 }, - }, - "huihui-ai/DeepSeek-R1-Distill-Qwen-32B-abliterated": { - id: "huihui-ai/DeepSeek-R1-Distill-Qwen-32B-abliterated", - name: "DeepSeek R1 Qwen Abliterated", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.4, output: 1.4 }, - limit: { context: 16384, input: 16384, output: 8192 }, - }, - "huihui-ai/DeepSeek-R1-Distill-Llama-70B-abliterated": { - id: "huihui-ai/DeepSeek-R1-Distill-Llama-70B-abliterated", - name: "DeepSeek R1 Llama 70B Abliterated", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.7, output: 0.7 }, - limit: { context: 16384, input: 16384, output: 8192 }, - }, - "huihui-ai/Llama-3.3-70B-Instruct-abliterated": { - id: "huihui-ai/Llama-3.3-70B-Instruct-abliterated", - name: "Llama 3.3 70B Instruct abliterated", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-08-08", - last_updated: "2025-08-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.7, output: 0.7 }, - limit: { context: 16384, input: 16384, output: 16384 }, - }, - "huihui-ai/Qwen2.5-32B-Instruct-abliterated": { - id: "huihui-ai/Qwen2.5-32B-Instruct-abliterated", - name: "Qwen 2.5 32B Abliterated", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-01-06", - last_updated: "2025-01-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.7, output: 0.7 }, - limit: { context: 32768, input: 32768, output: 8192 }, - }, - "huihui-ai/Llama-3.1-Nemotron-70B-Instruct-HF-abliterated": { - id: "huihui-ai/Llama-3.1-Nemotron-70B-Instruct-HF-abliterated", - name: "Nemotron 3.1 70B abliterated", - family: "nemotron", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.7, output: 0.7 }, - limit: { context: 16384, input: 16384, output: 16384 }, - }, - "xiaomi/mimo-v2-flash-thinking-original": { - id: "xiaomi/mimo-v2-flash-thinking-original", - name: "MiMo V2 Flash (Thinking) Original", - family: "mimo", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.102, output: 0.306 }, - limit: { context: 256000, input: 256000, output: 32768 }, - }, - "xiaomi/mimo-v2-flash-thinking": { - id: "xiaomi/mimo-v2-flash-thinking", - name: "MiMo V2 Flash (Thinking)", - family: "mimo", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.102, output: 0.306 }, - limit: { context: 256000, input: 256000, output: 32768 }, - }, - "xiaomi/mimo-v2-flash": { - id: "xiaomi/mimo-v2-flash", - name: "MiMo V2 Flash", - family: "mimo", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.102, output: 0.306 }, - limit: { context: 256000, input: 256000, output: 32768 }, - }, - "xiaomi/mimo-v2-flash-original": { - id: "xiaomi/mimo-v2-flash-original", - name: "MiMo V2 Flash Original", - family: "mimo", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.102, output: 0.306 }, - limit: { context: 256000, input: 256000, output: 32768 }, - }, - "tngtech/DeepSeek-TNG-R1T2-Chimera": { - id: "tngtech/DeepSeek-TNG-R1T2-Chimera", - name: "DeepSeek TNG R1T2 Chimera", - family: "tngtech", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.31, output: 0.31 }, - limit: { context: 128000, input: 128000, output: 8192 }, - }, - "tngtech/tng-r1t-chimera": { - id: "tngtech/tng-r1t-chimera", - name: "TNG R1T Chimera", - family: "tngtech", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-11-26", - last_updated: "2025-11-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 128000, input: 128000, output: 65536 }, - }, - "inflatebot/MN-12B-Mag-Mell-R1": { - id: "inflatebot/MN-12B-Mag-Mell-R1", - name: "Mag Mell R1", - family: "mistral-nemo", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-01", - last_updated: "2024-07-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.49299999999999994, output: 0.49299999999999994 }, - limit: { context: 16384, input: 16384, output: 8192 }, - }, - "failspy/Meta-Llama-3-70B-Instruct-abliterated-v3.5": { - id: "failspy/Meta-Llama-3-70B-Instruct-abliterated-v3.5", - name: "Llama 3 70B abliterated", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-26", - last_updated: "2025-07-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.7, output: 0.7 }, - limit: { context: 8192, input: 8192, output: 8192 }, - }, - }, - }, - abacus: { - id: "abacus", - env: ["ABACUS_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://routellm.abacus.ai/v1", - name: "Abacus", - doc: "https://abacus.ai/help/api", - models: { - "gpt-5.1-codex-max": { - id: "gpt-5.1-codex-max", - name: "GPT-5.1 Codex Max", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "claude-opus-4-5-20251101": { - id: "claude-opus-4-5-20251101", - name: "Claude Opus 4.5", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-01", - last_updated: "2025-11-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25 }, - limit: { context: 200000, output: 64000 }, - }, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3 }, - limit: { context: 262144, output: 32768 }, - }, - "gemini-3.1-flash-lite-preview": { - id: "gemini-3.1-flash-lite-preview", - name: "Gemini 3.1 Flash Lite Preview", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-03-01", - last_updated: "2026-03-01", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1.5, cache_read: 0.025, cache_write: 1 }, - limit: { context: 1048576, output: 65536 }, - }, - "claude-sonnet-4-6": { - id: "claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08", - release_date: "2026-02-17", - last_updated: "2026-02-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 200000, output: 64000 }, - }, - "gemini-3.1-pro-preview": { - id: "gemini-3.1-pro-preview", - name: "Gemini 3.1 Pro Preview", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-02-19", - last_updated: "2026-02-19", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12 }, - limit: { context: 1048576, output: 65536 }, - }, - "gpt-5.3-chat-latest": { - id: "gpt-5.3-chat-latest", - name: "GPT-5.3 Chat Latest", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-01", - last_updated: "2026-03-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14 }, - limit: { context: 400000, output: 128000 }, - }, - "gemini-3-flash-preview": { - id: "gemini-3-flash-preview", - name: "Gemini 3 Flash Preview", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 3 }, - limit: { context: 1048576, output: 65536 }, - }, - "llama-3.3-70b-versatile": { - id: "llama-3.3-70b-versatile", - name: "Llama 3.3 70B Versatile", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.59, output: 0.79 }, - limit: { context: 128000, output: 32768 }, - }, - "gpt-5-mini": { - id: "gpt-5-mini", - name: "GPT-5 Mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2 }, - limit: { context: 400000, output: 128000 }, - }, - "gpt-5-nano": { - id: "gpt-5-nano", - name: "GPT-5 Nano", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.4 }, - limit: { context: 400000, output: 128000 }, - }, - "gpt-5.3-codex": { - id: "gpt-5.3-codex", - name: "GPT-5.3 Codex", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "claude-sonnet-4-5-20250929": { - id: "claude-sonnet-4-5-20250929", - name: "Claude Sonnet 4.5", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 200000, output: 64000 }, - }, - "gemini-2.5-pro": { - id: "gemini-2.5-pro", - name: "Gemini 2.5 Pro", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-03-25", - last_updated: "2025-03-25", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 1048576, output: 65536 }, - }, - "grok-4-1-fast-non-reasoning": { - id: "grok-4-1-fast-non-reasoning", - name: "Grok 4.1 Fast (Non-Reasoning)", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-11-17", - last_updated: "2025-11-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5 }, - limit: { context: 2000000, output: 16384 }, - }, - "gpt-5.2": { - id: "gpt-5.2", - name: "GPT-5.2", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14 }, - limit: { context: 400000, output: 128000 }, - }, - "o3-pro": { - id: "o3-pro", - name: "o3-pro", - family: "o-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05", - release_date: "2025-06-10", - last_updated: "2025-06-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 20, output: 40 }, - limit: { context: 200000, output: 100000 }, - }, - "gpt-4o-mini": { - id: "gpt-4o-mini", - name: "GPT-4o Mini", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 128000, output: 16384 }, - }, - "qwen3-max": { - id: "qwen3-max", - name: "Qwen3 Max", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-05-28", - last_updated: "2025-05-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.2, output: 6 }, - limit: { context: 131072, output: 16384 }, - }, - "o4-mini": { - id: "o4-mini", - name: "o4-mini", - family: "o-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05", - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4 }, - limit: { context: 200000, output: 100000 }, - }, - "gpt-5.2-codex": { - id: "gpt-5.2-codex", - name: "GPT-5.2 Codex", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gemini-2.5-flash": { - id: "gemini-2.5-flash", - name: "Gemini 2.5 Flash", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-03-20", - last_updated: "2025-06-05", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5 }, - limit: { context: 1048576, output: 65536 }, - }, - "gpt-5.2-chat-latest": { - id: "gpt-5.2-chat-latest", - name: "GPT-5.2 Chat Latest", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-09-30", - release_date: "2026-01-01", - last_updated: "2026-01-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14 }, - limit: { context: 400000, output: 128000 }, - }, - "gpt-5.3-codex-xhigh": { - id: "gpt-5.3-codex-xhigh", - name: "GPT-5.3 Codex XHigh", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "grok-code-fast-1": { - id: "grok-code-fast-1", - name: "Grok Code Fast 1", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-09-01", - last_updated: "2025-09-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.5 }, - limit: { context: 256000, output: 16384 }, - }, - "gpt-5.1": { - id: "gpt-5.1", - name: "GPT-5.1", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 400000, output: 128000 }, - }, - "o3-mini": { - id: "o3-mini", - name: "o3-mini", - family: "o-mini", - attachment: false, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05", - release_date: "2024-12-20", - last_updated: "2025-01-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4 }, - limit: { context: 200000, output: 100000 }, - }, - "grok-4-0709": { - id: "grok-4-0709", - name: "Grok 4", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 256000, output: 16384 }, - }, - "route-llm": { - id: "route-llm", - name: "Route LLM", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-01-01", - last_updated: "2024-01-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 128000, output: 16384 }, - }, - "qwen-2.5-coder-32b": { - id: "qwen-2.5-coder-32b", - name: "Qwen 2.5 Coder 32B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-11-11", - last_updated: "2024-11-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.79, output: 0.79 }, - limit: { context: 128000, output: 8192 }, - }, - "gpt-5-codex": { - id: "gpt-5-codex", - name: "GPT-5 Codex", - family: "gpt", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-09-15", - last_updated: "2025-09-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "claude-opus-4-1-20250805": { - id: "claude-opus-4-1-20250805", - name: "Claude Opus 4.1", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75 }, - limit: { context: 200000, output: 32000 }, - }, - "gpt-5.4": { - id: "gpt-5.4", - name: "GPT-5.4", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 15 }, - limit: { context: 1050000, input: 922000, output: 128000 }, - }, - "gpt-5.1-chat-latest": { - id: "gpt-5.1-chat-latest", - name: "GPT-5.1 Chat Latest", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 400000, output: 128000 }, - }, - "claude-haiku-4-5-20251001": { - id: "claude-haiku-4-5-20251001", - name: "Claude Haiku 4.5", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5 }, - limit: { context: 200000, output: 64000 }, - }, - "claude-sonnet-4-20250514": { - id: "claude-sonnet-4-20250514", - name: "Claude Sonnet 4", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-05-14", - last_updated: "2025-05-14", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 200000, output: 64000 }, - }, - "kimi-k2-turbo-preview": { - id: "kimi-k2-turbo-preview", - name: "Kimi K2 Turbo Preview", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-07-08", - last_updated: "2025-07-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 8 }, - limit: { context: 256000, output: 8192 }, - }, - "claude-opus-4-6": { - id: "claude-opus-4-6", - name: "Claude Opus 4.6", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25 }, - limit: { context: 200000, output: 128000 }, - }, - "gpt-4.1-nano": { - id: "gpt-4.1-nano", - name: "GPT-4.1 Nano", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 1047576, output: 32768 }, - }, - "claude-3-7-sonnet-20250219": { - id: "claude-3-7-sonnet-20250219", - name: "Claude Sonnet 3.7", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10-31", - release_date: "2025-02-19", - last_updated: "2025-02-19", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 200000, output: 64000 }, - }, - o3: { - id: "o3", - name: "o3", - family: "o", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05", - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8 }, - limit: { context: 200000, output: 100000 }, - }, - "gpt-5": { - id: "gpt-5", - name: "GPT-5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 400000, output: 128000 }, - }, - "claude-opus-4-20250514": { - id: "claude-opus-4-20250514", - name: "Claude Opus 4", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-05-14", - last_updated: "2025-05-14", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75 }, - limit: { context: 200000, output: 32000 }, - }, - "gpt-4.1": { - id: "gpt-4.1", - name: "GPT-4.1", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8 }, - limit: { context: 1047576, output: 32768 }, - }, - "gpt-4.1-mini": { - id: "gpt-4.1-mini", - name: "GPT-4.1 Mini", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.6 }, - limit: { context: 1047576, output: 32768 }, - }, - "gpt-5.1-codex": { - id: "gpt-5.1-codex", - name: "GPT-5.1 Codex", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gpt-4o-2024-11-20": { - id: "gpt-4o-2024-11-20", - name: "GPT-4o (2024-11-20)", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-11-20", - last_updated: "2024-11-20", - modalities: { input: ["text", "image", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10 }, - limit: { context: 128000, output: 16384 }, - }, - "grok-4-fast-non-reasoning": { - id: "grok-4-fast-non-reasoning", - name: "Grok 4 Fast (Non-Reasoning)", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5 }, - limit: { context: 2000000, output: 16384 }, - }, - "deepseek/deepseek-v3.1": { - id: "deepseek/deepseek-v3.1", - name: "DeepSeek V3.1", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 1.66 }, - limit: { context: 128000, output: 8192 }, - }, - "Qwen/QwQ-32B": { - id: "Qwen/QwQ-32B", - name: "QwQ 32B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2024-11-28", - last_updated: "2024-11-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 0.4 }, - limit: { context: 32768, output: 32768 }, - }, - "Qwen/Qwen3-235B-A22B-Instruct-2507": { - id: "Qwen/Qwen3-235B-A22B-Instruct-2507", - name: "Qwen3 235B A22B Instruct", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-07-01", - last_updated: "2025-07-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.13, output: 0.6 }, - limit: { context: 262144, output: 8192 }, - }, - "Qwen/Qwen3-32B": { - id: "Qwen/Qwen3-32B", - name: "Qwen3 32B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-04-29", - last_updated: "2025-04-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.09, output: 0.29 }, - limit: { context: 128000, output: 8192 }, - }, - "Qwen/qwen3-coder-480b-a35b-instruct": { - id: "Qwen/qwen3-coder-480b-a35b-instruct", - name: "Qwen3 Coder 480B A35B Instruct", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-07-22", - last_updated: "2025-07-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.29, output: 1.2 }, - limit: { context: 262144, output: 65536 }, - }, - "Qwen/Qwen2.5-72B-Instruct": { - id: "Qwen/Qwen2.5-72B-Instruct", - name: "Qwen 2.5 72B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-09-19", - last_updated: "2024-09-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.11, output: 0.38 }, - limit: { context: 128000, output: 8192 }, - }, - "zai-org/glm-4.7": { - id: "zai-org/glm-4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-06-01", - last_updated: "2025-06-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2 }, - limit: { context: 128000, output: 8192 }, - }, - "zai-org/glm-5": { - id: "zai-org/glm-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3.2 }, - limit: { context: 204800, output: 131072 }, - }, - "zai-org/glm-4.5": { - id: "zai-org/glm-4.5", - name: "GLM-4.5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2 }, - limit: { context: 128000, output: 8192 }, - }, - "zai-org/glm-4.6": { - id: "zai-org/glm-4.6", - name: "GLM-4.6", - family: "glm", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-03-01", - last_updated: "2025-03-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2 }, - limit: { context: 128000, output: 8192 }, - }, - "meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo": { - id: "meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo", - name: "Llama 3.1 405B Instruct Turbo", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 3.5, output: 3.5 }, - limit: { context: 128000, output: 4096 }, - }, - "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8": { - id: "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8", - name: "Llama 4 Maverick 17B 128E Instruct FP8", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.14, output: 0.59 }, - limit: { context: 1000000, output: 32768 }, - }, - "meta-llama/Meta-Llama-3.1-8B-Instruct": { - id: "meta-llama/Meta-Llama-3.1-8B-Instruct", - name: "Llama 3.1 8B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0.05 }, - limit: { context: 128000, output: 4096 }, - }, - "deepseek-ai/DeepSeek-R1": { - id: "deepseek-ai/DeepSeek-R1", - name: "DeepSeek R1", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 3, output: 7 }, - limit: { context: 128000, output: 8192 }, - }, - "deepseek-ai/DeepSeek-V3.2": { - id: "deepseek-ai/DeepSeek-V3.2", - name: "DeepSeek V3.2", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-06-15", - last_updated: "2025-06-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 0.4 }, - limit: { context: 128000, output: 8192 }, - }, - "deepseek-ai/DeepSeek-V3.1-Terminus": { - id: "deepseek-ai/DeepSeek-V3.1-Terminus", - name: "DeepSeek V3.1 Terminus", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-06-01", - last_updated: "2025-06-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 1 }, - limit: { context: 128000, output: 8192 }, - }, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "GPT-OSS 120B", - family: "gpt-oss", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.08, output: 0.44 }, - limit: { context: 128000, output: 32768 }, - }, - }, - }, - "perplexity-agent": { - id: "perplexity-agent", - env: ["PERPLEXITY_API_KEY"], - npm: "@ai-sdk/openai", - api: "https://api.perplexity.ai/v1", - name: "Perplexity Agent", - doc: "https://docs.perplexity.ai/docs/agent-api/models", - models: { - "perplexity/sonar": { - id: "perplexity/sonar", - name: "Sonar", - family: "sonar", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-09-01", - release_date: "2024-01-01", - last_updated: "2025-09-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2.5, cache_read: 0.0625 }, - limit: { context: 128000, output: 8192 }, - }, - "xai/grok-4-1-fast-non-reasoning": { - id: "xai/grok-4-1-fast-non-reasoning", - name: "Grok 4.1 Fast (Non-Reasoning)", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-11-19", - last_updated: "2025-11-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - "nvidia/nemotron-3-super-120b-a12b": { - id: "nvidia/nemotron-3-super-120b-a12b", - name: "Nemotron 3 Super 120B", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2026-02", - release_date: "2026-03-11", - last_updated: "2026-03-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 2.5 }, - limit: { context: 1000000, output: 32000 }, - }, - "openai/gpt-5-mini": { - id: "openai/gpt-5-mini", - name: "GPT-5 Mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.025 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "openai/gpt-5.2": { - id: "openai/gpt-5.2", - name: "GPT-5.2", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "openai/gpt-5.1": { - id: "openai/gpt-5.1", - name: "GPT-5.1", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "openai/gpt-5.4": { - id: "openai/gpt-5.4", - name: "GPT-5.4", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 15, cache_read: 0.25 }, - limit: { context: 1050000, input: 922000, output: 128000 }, - }, - "google/gemini-3.1-pro-preview": { - id: "google/gemini-3.1-pro-preview", - name: "Gemini 3.1 Pro Preview", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-02-19", - last_updated: "2026-02-19", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2, context_over_200k: { input: 4, output: 18, cache_read: 0.4 } }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-3-flash-preview": { - id: "google/gemini-3-flash-preview", - name: "Gemini 3 Flash Preview", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 0.5, - output: 3, - cache_read: 0.05, - context_over_200k: { input: 0.5, output: 3, cache_read: 0.05 }, - }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-2.5-pro": { - id: "google/gemini-2.5-pro", - name: "Gemini 2.5 Pro", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-03-20", - last_updated: "2025-06-05", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 1.25, - output: 10, - cache_read: 0.125, - context_over_200k: { input: 2.5, output: 15, cache_read: 0.25 }, - }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-2.5-flash": { - id: "google/gemini-2.5-flash", - name: "Gemini 2.5 Flash", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-03-20", - last_updated: "2025-06-05", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5, cache_read: 0.03 }, - limit: { context: 1048576, output: 65536 }, - }, - "anthropic/claude-haiku-4-5": { - id: "anthropic/claude-haiku-4-5", - name: "Claude Haiku 4.5", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-sonnet-4-6": { - id: "anthropic/claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08", - release_date: "2026-02-17", - last_updated: "2026-02-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-opus-4-5": { - id: "anthropic/claude-opus-4-5", - name: "Claude Opus 4.5", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-24", - last_updated: "2025-11-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-opus-4-6": { - id: "anthropic/claude-opus-4-6", - name: "Claude Opus 4.6", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5 }, - limit: { context: 200000, output: 128000 }, - }, - "anthropic/claude-sonnet-4-5": { - id: "anthropic/claude-sonnet-4-5", - name: "Claude Sonnet 4.5", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3 }, - limit: { context: 200000, output: 64000 }, - }, - }, - }, - "siliconflow-cn": { - id: "siliconflow-cn", - env: ["SILICONFLOW_CN_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.siliconflow.cn/v1", - name: "SiliconFlow (China)", - doc: "https://cloud.siliconflow.com/models", - models: { - "Kwaipilot/KAT-Dev": { - id: "Kwaipilot/KAT-Dev", - name: "Kwaipilot/KAT-Dev", - family: "kat-coder", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-27", - last_updated: "2026-01-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.6 }, - limit: { context: 128000, output: 128000 }, - }, - "Qwen/Qwen3.5-397B-A17B": { - id: "Qwen/Qwen3.5-397B-A17B", - name: "Qwen/Qwen3.5-397B-A17B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02-16", - last_updated: "2026-02-16", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.29, output: 1.74 }, - limit: { context: 262144, output: 65536 }, - }, - "Qwen/Qwen3.5-35B-A3B": { - id: "Qwen/Qwen3.5-35B-A3B", - name: "Qwen/Qwen3.5-35B-A3B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02-25", - last_updated: "2026-02-25", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.23, output: 1.86 }, - limit: { context: 262144, output: 65536 }, - }, - "Qwen/Qwen3.5-122B-A10B": { - id: "Qwen/Qwen3.5-122B-A10B", - name: "Qwen/Qwen3.5-122B-A10B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02-26", - last_updated: "2026-02-26", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.29, output: 2.32 }, - limit: { context: 262144, output: 65536 }, - }, - "Qwen/Qwen3.5-9B": { - id: "Qwen/Qwen3.5-9B", - name: "Qwen/Qwen3.5-9B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-03-03", - last_updated: "2026-03-03", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.22, output: 1.74 }, - limit: { context: 262144, output: 65536 }, - }, - "Qwen/Qwen3.5-27B": { - id: "Qwen/Qwen3.5-27B", - name: "Qwen/Qwen3.5-27B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02-25", - last_updated: "2026-02-25", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.26, output: 2.09 }, - limit: { context: 262144, output: 65536 }, - }, - "Qwen/Qwen3.5-4B": { - id: "Qwen/Qwen3.5-4B", - name: "Qwen/Qwen3.5-4B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-03-03", - last_updated: "2026-03-03", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 65536 }, - }, - "Qwen/Qwen2.5-72B-Instruct": { - id: "Qwen/Qwen2.5-72B-Instruct", - name: "Qwen/Qwen2.5-72B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-09-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.59, output: 0.59 }, - limit: { context: 33000, output: 4000 }, - }, - "Qwen/Qwen3-Coder-480B-A35B-Instruct": { - id: "Qwen/Qwen3-Coder-480B-A35B-Instruct", - name: "Qwen/Qwen3-Coder-480B-A35B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-31", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen3-VL-8B-Instruct": { - id: "Qwen/Qwen3-VL-8B-Instruct", - name: "Qwen/Qwen3-VL-8B-Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-15", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.18, output: 0.68 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen3-VL-32B-Instruct": { - id: "Qwen/Qwen3-VL-32B-Instruct", - name: "Qwen/Qwen3-VL-32B-Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-21", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.6 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen3-VL-30B-A3B-Thinking": { - id: "Qwen/Qwen3-VL-30B-A3B-Thinking", - name: "Qwen/Qwen3-VL-30B-A3B-Thinking", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-11", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.29, output: 1 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen2.5-14B-Instruct": { - id: "Qwen/Qwen2.5-14B-Instruct", - name: "Qwen/Qwen2.5-14B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-09-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 33000, output: 4000 }, - }, - "Qwen/Qwen3-VL-235B-A22B-Instruct": { - id: "Qwen/Qwen3-VL-235B-A22B-Instruct", - name: "Qwen/Qwen3-VL-235B-A22B-Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-04", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.5 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen3-Next-80B-A3B-Thinking": { - id: "Qwen/Qwen3-Next-80B-A3B-Thinking", - name: "Qwen/Qwen3-Next-80B-A3B-Thinking", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-25", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.57 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen2.5-VL-32B-Instruct": { - id: "Qwen/Qwen2.5-VL-32B-Instruct", - name: "Qwen/Qwen2.5-VL-32B-Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-03-24", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 0.27 }, - limit: { context: 131000, output: 131000 }, - }, - "Qwen/Qwen3-Omni-30B-A3B-Thinking": { - id: "Qwen/Qwen3-Omni-30B-A3B-Thinking", - name: "Qwen/Qwen3-Omni-30B-A3B-Thinking", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-04", - last_updated: "2025-11-25", - modalities: { input: ["text", "image", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 66000, output: 66000 }, - }, - "Qwen/Qwen3-235B-A22B-Thinking-2507": { - id: "Qwen/Qwen3-235B-A22B-Thinking-2507", - name: "Qwen/Qwen3-235B-A22B-Thinking-2507", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-28", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.13, output: 0.6 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen2.5-32B-Instruct": { - id: "Qwen/Qwen2.5-32B-Instruct", - name: "Qwen/Qwen2.5-32B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-09-19", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.18, output: 0.18 }, - limit: { context: 33000, output: 4000 }, - }, - "Qwen/Qwen2.5-72B-Instruct-128K": { - id: "Qwen/Qwen2.5-72B-Instruct-128K", - name: "Qwen/Qwen2.5-72B-Instruct-128K", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-09-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.59, output: 0.59 }, - limit: { context: 131000, output: 4000 }, - }, - "Qwen/Qwen3-14B": { - id: "Qwen/Qwen3-14B", - name: "Qwen/Qwen3-14B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-30", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.07, output: 0.28 }, - limit: { context: 131000, output: 131000 }, - }, - "Qwen/Qwen3-Omni-30B-A3B-Instruct": { - id: "Qwen/Qwen3-Omni-30B-A3B-Instruct", - name: "Qwen/Qwen3-Omni-30B-A3B-Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-04", - last_updated: "2025-11-25", - modalities: { input: ["text", "image", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 66000, output: 66000 }, - }, - "Qwen/Qwen3-Coder-30B-A3B-Instruct": { - id: "Qwen/Qwen3-Coder-30B-A3B-Instruct", - name: "Qwen/Qwen3-Coder-30B-A3B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-01", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.07, output: 0.28 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen3-32B": { - id: "Qwen/Qwen3-32B", - name: "Qwen/Qwen3-32B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-30", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.57 }, - limit: { context: 131000, output: 131000 }, - }, - "Qwen/Qwen3-235B-A22B-Instruct-2507": { - id: "Qwen/Qwen3-235B-A22B-Instruct-2507", - name: "Qwen/Qwen3-235B-A22B-Instruct-2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-23", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.09, output: 0.6 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen3-30B-A3B-Instruct-2507": { - id: "Qwen/Qwen3-30B-A3B-Instruct-2507", - name: "Qwen/Qwen3-30B-A3B-Instruct-2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-30", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.09, output: 0.3 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen3-8B": { - id: "Qwen/Qwen3-8B", - name: "Qwen/Qwen3-8B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-30", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.06, output: 0.06 }, - limit: { context: 131000, output: 131000 }, - }, - "Qwen/Qwen3-Next-80B-A3B-Instruct": { - id: "Qwen/Qwen3-Next-80B-A3B-Instruct", - name: "Qwen/Qwen3-Next-80B-A3B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 1.4 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen3-VL-8B-Thinking": { - id: "Qwen/Qwen3-VL-8B-Thinking", - name: "Qwen/Qwen3-VL-8B-Thinking", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-15", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.18, output: 2 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen3-Omni-30B-A3B-Captioner": { - id: "Qwen/Qwen3-Omni-30B-A3B-Captioner", - name: "Qwen/Qwen3-Omni-30B-A3B-Captioner", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-04", - last_updated: "2025-11-25", - modalities: { input: ["audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 66000, output: 66000 }, - }, - "Qwen/QwQ-32B": { - id: "Qwen/QwQ-32B", - name: "Qwen/QwQ-32B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-03-06", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.58 }, - limit: { context: 131000, output: 131000 }, - }, - "Qwen/Qwen3-VL-30B-A3B-Instruct": { - id: "Qwen/Qwen3-VL-30B-A3B-Instruct", - name: "Qwen/Qwen3-VL-30B-A3B-Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-05", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.29, output: 1 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen2.5-Coder-32B-Instruct": { - id: "Qwen/Qwen2.5-Coder-32B-Instruct", - name: "Qwen/Qwen2.5-Coder-32B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-11-11", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.18, output: 0.18 }, - limit: { context: 33000, output: 4000 }, - }, - "Qwen/Qwen2.5-7B-Instruct": { - id: "Qwen/Qwen2.5-7B-Instruct", - name: "Qwen/Qwen2.5-7B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-09-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.05 }, - limit: { context: 33000, output: 4000 }, - }, - "Qwen/Qwen3-VL-235B-A22B-Thinking": { - id: "Qwen/Qwen3-VL-235B-A22B-Thinking", - name: "Qwen/Qwen3-VL-235B-A22B-Thinking", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-04", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.45, output: 3.5 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen3-30B-A3B-Thinking-2507": { - id: "Qwen/Qwen3-30B-A3B-Thinking-2507", - name: "Qwen/Qwen3-30B-A3B-Thinking-2507", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-31", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.09, output: 0.3 }, - limit: { context: 262000, output: 131000 }, - }, - "Qwen/Qwen3-VL-32B-Thinking": { - id: "Qwen/Qwen3-VL-32B-Thinking", - name: "Qwen/Qwen3-VL-32B-Thinking", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-21", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.5 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen2.5-VL-72B-Instruct": { - id: "Qwen/Qwen2.5-VL-72B-Instruct", - name: "Qwen/Qwen2.5-VL-72B-Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-01-28", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.59, output: 0.59 }, - limit: { context: 131000, output: 4000 }, - }, - "stepfun-ai/Step-3.5-Flash": { - id: "stepfun-ai/Step-3.5-Flash", - name: "stepfun-ai/Step-3.5-Flash", - family: "step", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 262000, output: 262000 }, - }, - "zai-org/GLM-4.5V": { - id: "zai-org/GLM-4.5V", - name: "zai-org/GLM-4.5V", - family: "glm", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-13", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.86 }, - limit: { context: 66000, output: 66000 }, - }, - "zai-org/GLM-4.6": { - id: "zai-org/GLM-4.6", - name: "zai-org/GLM-4.6", - family: "glm", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-04", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 1.9 }, - limit: { context: 205000, output: 205000 }, - }, - "zai-org/GLM-4.6V": { - id: "zai-org/GLM-4.6V", - name: "zai-org/GLM-4.6V", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-12-07", - last_updated: "2025-12-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.9 }, - limit: { context: 131000, output: 131000 }, - }, - "zai-org/GLM-4.5-Air": { - id: "zai-org/GLM-4.5-Air", - name: "zai-org/GLM-4.5-Air", - family: "glm-air", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-28", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.86 }, - limit: { context: 131000, output: 131000 }, - }, - "inclusionAI/Ling-flash-2.0": { - id: "inclusionAI/Ling-flash-2.0", - name: "inclusionAI/Ling-flash-2.0", - family: "ling", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.57 }, - limit: { context: 131000, output: 131000 }, - }, - "inclusionAI/Ling-mini-2.0": { - id: "inclusionAI/Ling-mini-2.0", - name: "inclusionAI/Ling-mini-2.0", - family: "ling", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-10", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.07, output: 0.28 }, - limit: { context: 131000, output: 131000 }, - }, - "inclusionAI/Ring-flash-2.0": { - id: "inclusionAI/Ring-flash-2.0", - name: "inclusionAI/Ring-flash-2.0", - family: "ring", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-29", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.57 }, - limit: { context: 131000, output: 131000 }, - }, - "ascend-tribe/pangu-pro-moe": { - id: "ascend-tribe/pangu-pro-moe", - name: "ascend-tribe/pangu-pro-moe", - family: "pangu", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-07-02", - last_updated: "2026-01-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.6 }, - limit: { context: 128000, output: 128000 }, - }, - "tencent/Hunyuan-MT-7B": { - id: "tencent/Hunyuan-MT-7B", - name: "tencent/Hunyuan-MT-7B", - family: "hunyuan", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 33000, output: 33000 }, - }, - "tencent/Hunyuan-A13B-Instruct": { - id: "tencent/Hunyuan-A13B-Instruct", - name: "tencent/Hunyuan-A13B-Instruct", - family: "hunyuan", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-06-30", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.57 }, - limit: { context: 131000, output: 131000 }, - }, - "Pro/zai-org/GLM-4.7": { - id: "Pro/zai-org/GLM-4.7", - name: "Pro/zai-org/GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 2.2 }, - limit: { context: 205000, output: 205000 }, - }, - "Pro/zai-org/GLM-5.1": { - id: "Pro/zai-org/GLM-5.1", - name: "Pro/zai-org/GLM-5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-04-08", - last_updated: "2026-04-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.4, output: 4.4, cache_write: 0 }, - limit: { context: 205000, output: 205000 }, - }, - "Pro/zai-org/GLM-5": { - id: "Pro/zai-org/GLM-5", - name: "Pro/zai-org/GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3.2 }, - limit: { context: 205000, output: 205000 }, - }, - "Pro/deepseek-ai/DeepSeek-V3": { - id: "Pro/deepseek-ai/DeepSeek-V3", - name: "Pro/deepseek-ai/DeepSeek-V3", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-12-26", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1 }, - limit: { context: 164000, output: 164000 }, - }, - "Pro/deepseek-ai/DeepSeek-R1": { - id: "Pro/deepseek-ai/DeepSeek-R1", - name: "Pro/deepseek-ai/DeepSeek-R1", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-05-28", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 2.18 }, - limit: { context: 164000, output: 164000 }, - }, - "Pro/deepseek-ai/DeepSeek-V3.2": { - id: "Pro/deepseek-ai/DeepSeek-V3.2", - name: "Pro/deepseek-ai/DeepSeek-V3.2", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-03", - last_updated: "2025-12-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 0.42 }, - limit: { context: 164000, output: 164000 }, - }, - "Pro/deepseek-ai/DeepSeek-V3.1-Terminus": { - id: "Pro/deepseek-ai/DeepSeek-V3.1-Terminus", - name: "Pro/deepseek-ai/DeepSeek-V3.1-Terminus", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-29", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 1 }, - limit: { context: 164000, output: 164000 }, - }, - "Pro/moonshotai/Kimi-K2-Thinking": { - id: "Pro/moonshotai/Kimi-K2-Thinking", - name: "Pro/moonshotai/Kimi-K2-Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-11-07", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.55, output: 2.5 }, - limit: { context: 262000, output: 262000 }, - }, - "Pro/moonshotai/Kimi-K2-Instruct-0905": { - id: "Pro/moonshotai/Kimi-K2-Instruct-0905", - name: "Pro/moonshotai/Kimi-K2-Instruct-0905", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-08", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2 }, - limit: { context: 262000, output: 262000 }, - }, - "Pro/moonshotai/Kimi-K2.5": { - id: "Pro/moonshotai/Kimi-K2.5", - name: "Pro/moonshotai/Kimi-K2.5", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.55, output: 3 }, - limit: { context: 262000, output: 262000 }, - }, - "Pro/MiniMaxAI/MiniMax-M2.5": { - id: "Pro/MiniMaxAI/MiniMax-M2.5", - name: "Pro/MiniMaxAI/MiniMax-M2.5", - family: "minimax", - attachment: false, - reasoning: false, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-02-13", - last_updated: "2026-02-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.22 }, - limit: { context: 192000, output: 131000 }, - }, - "Pro/MiniMaxAI/MiniMax-M2.1": { - id: "Pro/MiniMaxAI/MiniMax-M2.1", - name: "Pro/MiniMaxAI/MiniMax-M2.1", - family: "minimax", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 197000, output: 131000 }, - }, - "PaddlePaddle/PaddleOCR-VL": { - id: "PaddlePaddle/PaddleOCR-VL", - name: "PaddlePaddle/PaddleOCR-VL", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-10-16", - last_updated: "2025-10-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 16384, output: 16384 }, - }, - "PaddlePaddle/PaddleOCR-VL-1.5": { - id: "PaddlePaddle/PaddleOCR-VL-1.5", - name: "PaddlePaddle/PaddleOCR-VL-1.5", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2026-01-29", - last_updated: "2026-01-29", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 16384, output: 16384 }, - }, - "deepseek-ai/DeepSeek-OCR": { - id: "deepseek-ai/DeepSeek-OCR", - name: "deepseek-ai/DeepSeek-OCR", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-10-20", - last_updated: "2025-10-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 8192, output: 8192 }, - }, - "deepseek-ai/DeepSeek-V3.1-Terminus": { - id: "deepseek-ai/DeepSeek-V3.1-Terminus", - name: "deepseek-ai/DeepSeek-V3.1-Terminus", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-29", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 1 }, - limit: { context: 164000, output: 164000 }, - }, - "deepseek-ai/DeepSeek-V3.2": { - id: "deepseek-ai/DeepSeek-V3.2", - name: "deepseek-ai/DeepSeek-V3.2", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-03", - last_updated: "2025-12-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 0.42 }, - limit: { context: 164000, output: 164000 }, - }, - "deepseek-ai/DeepSeek-R1-Distill-Qwen-14B": { - id: "deepseek-ai/DeepSeek-R1-Distill-Qwen-14B", - name: "deepseek-ai/DeepSeek-R1-Distill-Qwen-14B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-01-20", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 131000, output: 131000 }, - }, - "deepseek-ai/DeepSeek-R1": { - id: "deepseek-ai/DeepSeek-R1", - name: "deepseek-ai/DeepSeek-R1", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-05-28", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 2.18 }, - limit: { context: 164000, output: 164000 }, - }, - "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B": { - id: "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B", - name: "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-01-20", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.18, output: 0.18 }, - limit: { context: 131000, output: 131000 }, - }, - "deepseek-ai/DeepSeek-V3": { - id: "deepseek-ai/DeepSeek-V3", - name: "deepseek-ai/DeepSeek-V3", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-12-26", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1 }, - limit: { context: 164000, output: 164000 }, - }, - "deepseek-ai/deepseek-vl2": { - id: "deepseek-ai/deepseek-vl2", - name: "deepseek-ai/deepseek-vl2", - family: "deepseek", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-12-13", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.15 }, - limit: { context: 4000, output: 4000 }, - }, - "baidu/ERNIE-4.5-300B-A47B": { - id: "baidu/ERNIE-4.5-300B-A47B", - name: "baidu/ERNIE-4.5-300B-A47B", - family: "ernie", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-02", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.28, output: 1.1 }, - limit: { context: 131000, output: 131000 }, - }, - "THUDM/GLM-Z1-32B-0414": { - id: "THUDM/GLM-Z1-32B-0414", - name: "THUDM/GLM-Z1-32B-0414", - family: "glm-z", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.57 }, - limit: { context: 131000, output: 131000 }, - }, - "THUDM/GLM-4-32B-0414": { - id: "THUDM/GLM-4-32B-0414", - name: "THUDM/GLM-4-32B-0414", - family: "glm", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 0.27 }, - limit: { context: 33000, output: 33000 }, - }, - "THUDM/GLM-4-9B-0414": { - id: "THUDM/GLM-4-9B-0414", - name: "THUDM/GLM-4-9B-0414", - family: "glm", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.086, output: 0.086 }, - limit: { context: 33000, output: 33000 }, - }, - "THUDM/GLM-Z1-9B-0414": { - id: "THUDM/GLM-Z1-9B-0414", - name: "THUDM/GLM-Z1-9B-0414", - family: "glm-z", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.086, output: 0.086 }, - limit: { context: 131000, output: 131000 }, - }, - "moonshotai/Kimi-K2-Instruct-0905": { - id: "moonshotai/Kimi-K2-Instruct-0905", - name: "moonshotai/Kimi-K2-Instruct-0905", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-08", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2 }, - limit: { context: 262000, output: 262000 }, - }, - "moonshotai/Kimi-K2-Thinking": { - id: "moonshotai/Kimi-K2-Thinking", - name: "moonshotai/Kimi-K2-Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-11-07", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.55, output: 2.5 }, - limit: { context: 262000, output: 262000 }, - }, - "ByteDance-Seed/Seed-OSS-36B-Instruct": { - id: "ByteDance-Seed/Seed-OSS-36B-Instruct", - name: "ByteDance-Seed/Seed-OSS-36B-Instruct", - family: "seed", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-04", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.21, output: 0.57 }, - limit: { context: 262000, output: 262000 }, - }, - }, - }, - submodel: { - id: "submodel", - env: ["SUBMODEL_INSTAGEN_ACCESS_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://llm.submodel.ai/v1", - name: "submodel", - doc: "https://submodel.gitbook.io", - models: { - "Qwen/Qwen3-235B-A22B-Instruct-2507": { - id: "Qwen/Qwen3-235B-A22B-Instruct-2507", - name: "Qwen3 235B A22B Instruct 2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-08-23", - last_updated: "2025-08-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.3 }, - limit: { context: 262144, output: 131072 }, - }, - "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8": { - id: "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8", - name: "Qwen3 Coder 480B A35B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-08-23", - last_updated: "2025-08-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.8 }, - limit: { context: 262144, output: 262144 }, - }, - "Qwen/Qwen3-235B-A22B-Thinking-2507": { - id: "Qwen/Qwen3-235B-A22B-Thinking-2507", - name: "Qwen3 235B A22B Thinking 2507", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-23", - last_updated: "2025-08-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.6 }, - limit: { context: 262144, output: 131072 }, - }, - "zai-org/GLM-4.5-Air": { - id: "zai-org/GLM-4.5-Air", - name: "GLM 4.5 Air", - family: "glm-air", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.5 }, - limit: { context: 131072, output: 131072 }, - }, - "zai-org/GLM-4.5-FP8": { - id: "zai-org/GLM-4.5-FP8", - name: "GLM 4.5 FP8", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.8 }, - limit: { context: 131072, output: 131072 }, - }, - "deepseek-ai/DeepSeek-V3.1": { - id: "deepseek-ai/DeepSeek-V3.1", - name: "DeepSeek V3.1", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-23", - last_updated: "2025-08-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.8 }, - limit: { context: 75000, output: 163840 }, - }, - "deepseek-ai/DeepSeek-V3-0324": { - id: "deepseek-ai/DeepSeek-V3-0324", - name: "DeepSeek V3 0324", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-08-23", - last_updated: "2025-08-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.8 }, - limit: { context: 75000, output: 163840 }, - }, - "deepseek-ai/DeepSeek-R1-0528": { - id: "deepseek-ai/DeepSeek-R1-0528", - name: "DeepSeek R1 0528", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-23", - last_updated: "2025-08-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 2.15 }, - limit: { context: 75000, output: 163840 }, - }, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "GPT OSS 120B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-23", - last_updated: "2025-08-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.5 }, - limit: { context: 131072, output: 32768 }, - }, - }, - }, - "minimax-coding-plan": { - id: "minimax-coding-plan", - env: ["MINIMAX_API_KEY"], - npm: "@ai-sdk/anthropic", - api: "https://api.minimax.io/anthropic/v1", - name: "MiniMax Coding Plan (minimax.io)", - doc: "https://platform.minimax.io/docs/coding-plan/intro", - models: { - "MiniMax-M2": { - id: "MiniMax-M2", - name: "MiniMax-M2", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-10-27", - last_updated: "2025-10-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 196608, output: 128000 }, - }, - "MiniMax-M2.5": { - id: "MiniMax-M2.5", - name: "MiniMax-M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "MiniMax-M2.7": { - id: "MiniMax-M2.7", - name: "MiniMax-M2.7", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "MiniMax-M2.7-highspeed": { - id: "MiniMax-M2.7-highspeed", - name: "MiniMax-M2.7-highspeed", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "MiniMax-M2.1": { - id: "MiniMax-M2.1", - name: "MiniMax-M2.1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "MiniMax-M2.5-highspeed": { - id: "MiniMax-M2.5-highspeed", - name: "MiniMax-M2.5-highspeed", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-13", - last_updated: "2026-02-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - }, - }, - perplexity: { - id: "perplexity", - env: ["PERPLEXITY_API_KEY"], - npm: "@ai-sdk/perplexity", - name: "Perplexity", - doc: "https://docs.perplexity.ai", - models: { - "sonar-pro": { - id: "sonar-pro", - name: "Sonar Pro", - family: "sonar-pro", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-09-01", - release_date: "2024-01-01", - last_updated: "2025-09-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 200000, output: 8192 }, - }, - "sonar-deep-research": { - id: "sonar-deep-research", - name: "Perplexity Sonar Deep Research", - attachment: false, - reasoning: true, - tool_call: false, - temperature: false, - knowledge: "2025-01", - release_date: "2025-02-01", - last_updated: "2025-09-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, reasoning: 3 }, - limit: { context: 128000, output: 32768 }, - }, - sonar: { - id: "sonar", - name: "Sonar", - family: "sonar", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-09-01", - release_date: "2024-01-01", - last_updated: "2025-09-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 1 }, - limit: { context: 128000, output: 4096 }, - }, - "sonar-reasoning-pro": { - id: "sonar-reasoning-pro", - name: "Sonar Reasoning Pro", - family: "sonar-reasoning", - attachment: true, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2025-09-01", - release_date: "2024-01-01", - last_updated: "2025-09-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8 }, - limit: { context: 128000, output: 4096 }, - }, - }, - }, - deepseek: { - id: "deepseek", - env: ["DEEPSEEK_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.deepseek.com", - name: "DeepSeek", - doc: "https://api-docs.deepseek.com/quick_start/pricing", - models: { - "deepseek-chat": { - id: "deepseek-chat", - name: "DeepSeek Chat", - family: "deepseek", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-09", - release_date: "2025-12-01", - last_updated: "2026-02-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.28, output: 0.42, cache_read: 0.028 }, - limit: { context: 131072, output: 8192 }, - }, - "deepseek-reasoner": { - id: "deepseek-reasoner", - name: "DeepSeek Reasoner", - family: "deepseek-thinking", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-09", - release_date: "2025-12-01", - last_updated: "2026-02-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.28, output: 0.42, cache_read: 0.028 }, - limit: { context: 128000, output: 64000 }, - }, - }, - }, - llama: { - id: "llama", - env: ["LLAMA_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.llama.com/compat/v1/", - name: "Llama", - doc: "https://llama.developer.meta.com/docs/models", - models: { - "llama-3.3-70b-instruct": { - id: "llama-3.3-70b-instruct", - name: "Llama-3.3-70B-Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "cerebras-llama-4-maverick-17b-128e-instruct": { - id: "cerebras-llama-4-maverick-17b-128e-instruct", - name: "Cerebras-Llama-4-Maverick-17B-128E-Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "llama-3.3-8b-instruct": { - id: "llama-3.3-8b-instruct", - name: "Llama-3.3-8B-Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "cerebras-llama-4-scout-17b-16e-instruct": { - id: "cerebras-llama-4-scout-17b-16e-instruct", - name: "Cerebras-Llama-4-Scout-17B-16E-Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "groq-llama-4-maverick-17b-128e-instruct": { - id: "groq-llama-4-maverick-17b-128e-instruct", - name: "Groq-Llama-4-Maverick-17B-128E-Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "llama-4-scout-17b-16e-instruct-fp8": { - id: "llama-4-scout-17b-16e-instruct-fp8", - name: "Llama-4-Scout-17B-16E-Instruct-FP8", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "llama-4-maverick-17b-128e-instruct-fp8": { - id: "llama-4-maverick-17b-128e-instruct-fp8", - name: "Llama-4-Maverick-17B-128E-Instruct-FP8", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - }, - }, - openrouter: { - id: "openrouter", - env: ["OPENROUTER_API_KEY"], - npm: "@openrouter/ai-sdk-provider", - api: "https://openrouter.ai/api/v1", - name: "OpenRouter", - doc: "https://openrouter.ai/models", - models: { - "liquid/lfm-2.5-1.2b-instruct:free": { - id: "liquid/lfm-2.5-1.2b-instruct:free", - name: "LFM2.5-1.2B-Instruct (free)", - family: "liquid", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-06", - release_date: "2026-01-20", - last_updated: "2026-01-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 32768 }, - }, - "liquid/lfm-2.5-1.2b-thinking:free": { - id: "liquid/lfm-2.5-1.2b-thinking:free", - name: "LFM2.5-1.2B-Thinking (free)", - family: "liquid", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2025-06", - release_date: "2026-01-20", - last_updated: "2026-01-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 32768 }, - }, - "deepseek/deepseek-chat-v3.1": { - id: "deepseek/deepseek-chat-v3.1", - name: "DeepSeek-V3.1", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-08-21", - last_updated: "2025-08-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.8 }, - limit: { context: 163840, output: 163840 }, - }, - "deepseek/deepseek-r1-distill-llama-70b": { - id: "deepseek/deepseek-r1-distill-llama-70b", - name: "DeepSeek R1 Distill Llama 70B", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-01-23", - last_updated: "2025-01-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 8192, output: 8192 }, - }, - "deepseek/deepseek-v3.2-speciale": { - id: "deepseek/deepseek-v3.2-speciale", - name: "DeepSeek V3.2 Speciale", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 0.41 }, - limit: { context: 163840, output: 65536 }, - }, - "deepseek/deepseek-v3.2": { - id: "deepseek/deepseek-v3.2", - name: "DeepSeek V3.2", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.28, output: 0.4 }, - limit: { context: 163840, output: 65536 }, - }, - "deepseek/deepseek-v3.1-terminus:exacto": { - id: "deepseek/deepseek-v3.1-terminus:exacto", - name: "DeepSeek V3.1 Terminus (exacto)", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-09-22", - last_updated: "2025-09-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 1 }, - limit: { context: 131072, output: 65536 }, - }, - "deepseek/deepseek-chat-v3-0324": { - id: "deepseek/deepseek-chat-v3-0324", - name: "DeepSeek V3 0324", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-03-24", - last_updated: "2025-03-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 16384, output: 8192 }, - }, - "deepseek/deepseek-v3.1-terminus": { - id: "deepseek/deepseek-v3.1-terminus", - name: "DeepSeek V3.1 Terminus", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-09-22", - last_updated: "2025-09-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 1 }, - limit: { context: 131072, output: 65536 }, - }, - "openrouter/free": { - id: "openrouter/free", - name: "Free Models Router", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-01", - last_updated: "2026-02-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 200000, input: 200000, output: 8000 }, - }, - "arcee-ai/trinity-mini:free": { - id: "arcee-ai/trinity-mini:free", - name: "Trinity Mini", - family: "trinity-mini", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-06", - release_date: "2026-01-28", - last_updated: "2026-01-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 131072 }, - }, - "arcee-ai/trinity-large-thinking": { - id: "arcee-ai/trinity-large-thinking", - name: "Trinity Large Thinking", - family: "trinity", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-01", - last_updated: "2026-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.22, output: 0.85 }, - limit: { context: 262144, output: 80000 }, - }, - "arcee-ai/trinity-large-preview:free": { - id: "arcee-ai/trinity-large-preview:free", - name: "Trinity Large Preview", - family: "trinity", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-06", - release_date: "2026-01-28", - last_updated: "2026-01-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 131072 }, - }, - "cognitivecomputations/dolphin-mistral-24b-venice-edition:free": { - id: "cognitivecomputations/dolphin-mistral-24b-venice-edition:free", - name: "Uncensored (free)", - family: "mistral", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-07-09", - last_updated: "2026-01-31", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 32768, output: 32768 }, - }, - "bytedance-seed/seedream-4.5": { - id: "bytedance-seed/seedream-4.5", - name: "Seedream 4.5", - family: "seed", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-06", - release_date: "2025-12-23", - last_updated: "2026-01-31", - modalities: { input: ["image", "text"], output: ["image"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 4096, output: 4096 }, - }, - "black-forest-labs/flux.2-max": { - id: "black-forest-labs/flux.2-max", - name: "FLUX.2 Max", - family: "flux", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-06", - release_date: "2025-12-16", - last_updated: "2026-01-31", - modalities: { input: ["image", "text"], output: ["image"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 46864, output: 46864 }, - }, - "black-forest-labs/flux.2-flex": { - id: "black-forest-labs/flux.2-flex", - name: "FLUX.2 Flex", - family: "flux", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-06", - release_date: "2025-11-25", - last_updated: "2026-01-31", - modalities: { input: ["image", "text"], output: ["image"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 67344, output: 67344 }, - }, - "black-forest-labs/flux.2-pro": { - id: "black-forest-labs/flux.2-pro", - name: "FLUX.2 Pro", - family: "flux", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-06", - release_date: "2025-11-25", - last_updated: "2026-01-31", - modalities: { input: ["image", "text"], output: ["image"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 46864, output: 46864 }, - }, - "black-forest-labs/flux.2-klein-4b": { - id: "black-forest-labs/flux.2-klein-4b", - name: "FLUX.2 Klein 4B", - family: "flux", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-06", - release_date: "2026-01-14", - last_updated: "2026-01-31", - modalities: { input: ["image", "text"], output: ["image"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 40960, output: 40960 }, - }, - "nousresearch/hermes-3-llama-3.1-405b:free": { - id: "nousresearch/hermes-3-llama-3.1-405b:free", - name: "Hermes 3 405B Instruct (free)", - family: "hermes", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2023-12", - release_date: "2024-08-16", - last_updated: "2024-08-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 131072 }, - }, - "nousresearch/hermes-4-405b": { - id: "nousresearch/hermes-4-405b", - name: "Hermes 4 405B", - family: "hermes", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2025-08-25", - last_updated: "2025-08-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3 }, - limit: { context: 131072, output: 131072 }, - }, - "nousresearch/hermes-4-70b": { - id: "nousresearch/hermes-4-70b", - name: "Hermes 4 70B", - family: "hermes", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-12", - release_date: "2025-08-25", - last_updated: "2025-08-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.13, output: 0.4 }, - limit: { context: 131072, output: 131072 }, - }, - "stepfun/step-3.5-flash:free": { - id: "stepfun/step-3.5-flash:free", - name: "Step 3.5 Flash (free)", - family: "step", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-01-29", - last_updated: "2026-01-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 256000, output: 256000 }, - }, - "stepfun/step-3.5-flash": { - id: "stepfun/step-3.5-flash", - name: "Step 3.5 Flash", - family: "step", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-01-29", - last_updated: "2026-01-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3, cache_read: 0.02 }, - limit: { context: 256000, output: 256000 }, - }, - "mistralai/mistral-small-3.1-24b-instruct": { - id: "mistralai/mistral-small-3.1-24b-instruct", - name: "Mistral Small 3.1 24B Instruct", - family: "mistral-small", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-03-17", - last_updated: "2025-03-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 8192 }, - }, - "mistralai/devstral-2512": { - id: "mistralai/devstral-2512", - name: "Devstral 2 2512", - family: "devstral", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-12", - release_date: "2025-09-12", - last_updated: "2025-09-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 262144, output: 262144 }, - }, - "mistralai/codestral-2508": { - id: "mistralai/codestral-2508", - name: "Codestral 2508", - family: "codestral", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2025-08-01", - last_updated: "2025-08-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.9 }, - limit: { context: 256000, output: 256000 }, - }, - "mistralai/mistral-medium-3.1": { - id: "mistralai/mistral-medium-3.1", - name: "Mistral Medium 3.1", - family: "mistral-medium", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2025-08-12", - last_updated: "2025-08-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2 }, - limit: { context: 262144, output: 262144 }, - }, - "mistralai/mistral-small-2603": { - id: "mistralai/mistral-small-2603", - name: "Mistral Small 4", - family: "mistral-small", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2026-03-16", - last_updated: "2026-03-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 262144, output: 262144 }, - }, - "mistralai/mistral-medium-3": { - id: "mistralai/mistral-medium-3", - name: "Mistral Medium 3", - family: "mistral-medium", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2025-05-07", - last_updated: "2025-05-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2 }, - limit: { context: 131072, output: 131072 }, - }, - "mistralai/devstral-small-2505": { - id: "mistralai/devstral-small-2505", - name: "Devstral Small", - family: "devstral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-05", - release_date: "2025-05-07", - last_updated: "2025-05-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.06, output: 0.12 }, - limit: { context: 128000, output: 128000 }, - }, - "mistralai/mistral-small-3.2-24b-instruct": { - id: "mistralai/mistral-small-3.2-24b-instruct", - name: "Mistral Small 3.2 24B Instruct", - family: "mistral-small", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-06-20", - last_updated: "2025-06-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 96000, output: 8192 }, - }, - "mistralai/devstral-medium-2507": { - id: "mistralai/devstral-medium-2507", - name: "Devstral Medium", - family: "devstral", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2025-07-10", - last_updated: "2025-07-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 2 }, - limit: { context: 131072, output: 131072 }, - }, - "mistralai/devstral-small-2507": { - id: "mistralai/devstral-small-2507", - name: "Devstral Small 1.1", - family: "devstral", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2025-07-10", - last_updated: "2025-07-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 131072, output: 131072 }, - }, - "meta-llama/llama-3.2-11b-vision-instruct": { - id: "meta-llama/llama-3.2-11b-vision-instruct", - name: "Llama 3.2 11B Vision Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-12", - release_date: "2024-09-25", - last_updated: "2024-09-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 8192 }, - }, - "meta-llama/llama-3.2-3b-instruct:free": { - id: "meta-llama/llama-3.2-3b-instruct:free", - name: "Llama 3.2 3B Instruct (free)", - family: "llama", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-12", - release_date: "2024-09-25", - last_updated: "2024-09-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 131072 }, - }, - "meta-llama/llama-3.3-70b-instruct:free": { - id: "meta-llama/llama-3.3-70b-instruct:free", - name: "Llama 3.3 70B Instruct (free)", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-12", - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 131072 }, - }, - "x-ai/grok-4.20-multi-agent-beta": { - id: "x-ai/grok-4.20-multi-agent-beta", - name: "Grok 4.20 Multi - Agent Beta", - family: "grok", - attachment: true, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2026-03-12", - last_updated: "2026-03-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6, cache_read: 0.2, context_over_200k: { input: 4, output: 12 } }, - limit: { context: 2000000, output: 30000 }, - status: "beta", - }, - "x-ai/grok-4-fast": { - id: "x-ai/grok-4-fast", - name: "Grok 4 Fast", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-08-19", - last_updated: "2025-08-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05, cache_write: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - "x-ai/grok-code-fast-1": { - id: "x-ai/grok-code-fast-1", - name: "Grok Code Fast 1", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-08", - release_date: "2025-08-26", - last_updated: "2025-08-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.5, cache_read: 0.02 }, - limit: { context: 256000, output: 10000 }, - }, - "x-ai/grok-3-beta": { - id: "x-ai/grok-3-beta", - name: "Grok 3 Beta", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.75, cache_write: 15 }, - limit: { context: 131072, output: 8192 }, - }, - "x-ai/grok-4": { - id: "x-ai/grok-4", - name: "Grok 4", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.75, cache_write: 15 }, - limit: { context: 256000, output: 64000 }, - }, - "x-ai/grok-3-mini": { - id: "x-ai/grok-3-mini", - name: "Grok 3 Mini", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.5, cache_read: 0.075, cache_write: 0.5 }, - limit: { context: 131072, output: 8192 }, - }, - "x-ai/grok-4.1-fast": { - id: "x-ai/grok-4.1-fast", - name: "Grok 4.1 Fast", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-11-19", - last_updated: "2025-11-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05, cache_write: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - "x-ai/grok-4.20-beta": { - id: "x-ai/grok-4.20-beta", - name: "Grok 4.20 Beta", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-12", - last_updated: "2026-03-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6, cache_read: 0.2, context_over_200k: { input: 4, output: 12 } }, - limit: { context: 2000000, output: 30000 }, - status: "beta", - }, - "x-ai/grok-3-mini-beta": { - id: "x-ai/grok-3-mini-beta", - name: "Grok 3 Mini Beta", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.5, cache_read: 0.075, cache_write: 0.5 }, - limit: { context: 131072, output: 8192 }, - }, - "x-ai/grok-3": { - id: "x-ai/grok-3", - name: "Grok 3", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.75, cache_write: 15 }, - limit: { context: 131072, output: 8192 }, - }, - "prime-intellect/intellect-3": { - id: "prime-intellect/intellect-3", - name: "Intellect 3", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-01-15", - last_updated: "2025-01-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 1.1 }, - limit: { context: 131072, output: 8192 }, - }, - "nvidia/nemotron-3-super-120b-a12b": { - id: "nvidia/nemotron-3-super-120b-a12b", - name: "Nemotron 3 Super", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2026-03-11", - last_updated: "2026-03-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.5 }, - limit: { context: 262144, output: 262144 }, - }, - "nvidia/nemotron-3-nano-30b-a3b:free": { - id: "nvidia/nemotron-3-nano-30b-a3b:free", - name: "Nemotron 3 Nano 30B A3B (free)", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-11", - release_date: "2025-12-14", - last_updated: "2026-01-31", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 256000, output: 256000 }, - }, - "nvidia/nemotron-nano-9b-v2:free": { - id: "nvidia/nemotron-nano-9b-v2:free", - name: "Nemotron Nano 9B V2 (free)", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-09", - release_date: "2025-09-05", - last_updated: "2025-08-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 128000 }, - }, - "nvidia/nemotron-3-super-120b-a12b:free": { - id: "nvidia/nemotron-3-super-120b-a12b:free", - name: "Nemotron 3 Super (free)", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2026-03-11", - last_updated: "2026-03-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 262144 }, - }, - "nvidia/nemotron-nano-9b-v2": { - id: "nvidia/nemotron-nano-9b-v2", - name: "nvidia-nemotron-nano-9b-v2", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-09", - release_date: "2025-08-18", - last_updated: "2025-08-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.04, output: 0.16 }, - limit: { context: 131072, output: 131072 }, - }, - "nvidia/nemotron-nano-12b-v2-vl:free": { - id: "nvidia/nemotron-nano-12b-v2-vl:free", - name: "Nemotron Nano 12B 2 VL (free)", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-11", - release_date: "2025-10-28", - last_updated: "2026-01-31", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 128000 }, - }, - "inception/mercury-2": { - id: "inception/mercury-2", - name: "Mercury 2", - family: "mercury", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-03-04", - last_updated: "2026-03-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 0.75, cache_read: 0.025 }, - limit: { context: 128000, output: 50000 }, - }, - "inception/mercury": { - id: "inception/mercury", - name: "Mercury", - family: "mercury", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-06-26", - last_updated: "2025-06-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 0.75, cache_read: 0.025 }, - limit: { context: 128000, output: 32000 }, - }, - "inception/mercury-coder": { - id: "inception/mercury-coder", - name: "Mercury Coder", - family: "mercury", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-30", - last_updated: "2025-04-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 0.75, cache_read: 0.025 }, - limit: { context: 128000, output: 32000 }, - }, - "openai/gpt-5.1-codex-max": { - id: "openai/gpt-5.1-codex-max", - name: "GPT-5.1-Codex-Max", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 9, cache_read: 0.11 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.2-chat": { - id: "openai/gpt-5.2-chat", - name: "GPT-5.2 Chat", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-oss-120b:exacto": { - id: "openai/gpt-oss-120b:exacto", - name: "GPT OSS 120B (exacto)", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.24 }, - limit: { context: 131072, output: 32768 }, - }, - "openai/gpt-5-chat": { - id: "openai/gpt-5-chat", - name: "GPT-5 Chat (latest)", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: true, - temperature: true, - knowledge: "2024-09-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.2-pro": { - id: "openai/gpt-5.2-pro", - name: "GPT-5.2 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 21, output: 168 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5-mini": { - id: "openai/gpt-5-mini", - name: "GPT-5 Mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-10-01", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5-nano": { - id: "openai/gpt-5-nano", - name: "GPT-5 Nano", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-10-01", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.4 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.3-codex": { - id: "openai/gpt-5.3-codex", - name: "GPT-5.3-Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-02-24", - last_updated: "2026-02-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.2": { - id: "openai/gpt-5.2", - name: "GPT-5.2", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-oss-20b:free": { - id: "openai/gpt-oss-20b:free", - name: "gpt-oss-20b (free)", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2026-01-31", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 32768 }, - }, - "openai/gpt-4o-mini": { - id: "openai/gpt-4o-mini", - name: "GPT-4o-mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6, cache_read: 0.08 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-5.4-mini": { - id: "openai/gpt-5.4-mini", - name: "GPT-5.4 Mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 7.5e-7, output: 0.0000045, cache_read: 7.5e-8 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.1-chat": { - id: "openai/gpt-5.1-chat", - name: "GPT-5.1 Chat", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/o4-mini": { - id: "openai/o4-mini", - name: "o4 Mini", - family: "o-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-06", - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.28 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-5.4-nano": { - id: "openai/gpt-5.4-nano", - name: "GPT-5.4 Nano", - family: "gpt-nano", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2e-7, output: 0.00000125, cache_read: 2e-8 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.2-codex": { - id: "openai/gpt-5.2-codex", - name: "GPT-5.2-Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-01-14", - last_updated: "2026-01-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.1-codex-mini": { - id: "openai/gpt-5.1-codex-mini", - name: "GPT-5.1-Codex-Mini", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.025 }, - limit: { context: 400000, output: 100000 }, - }, - "openai/gpt-5-image": { - id: "openai/gpt-5-image", - name: "GPT-5 Image", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-10-01", - release_date: "2025-10-14", - last_updated: "2025-10-14", - modalities: { input: ["text", "image", "pdf"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 5, output: 10, cache_read: 1.25 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.1": { - id: "openai/gpt-5.1", - name: "GPT-5.1", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.4-pro": { - id: "openai/gpt-5.4-pro", - name: "GPT-5.4 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 30, output: 180, cache_read: 30 }, - limit: { context: 1050000, input: 922000, output: 128000 }, - }, - "openai/gpt-5-codex": { - id: "openai/gpt-5-codex", - name: "GPT-5 Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-10-01", - release_date: "2025-09-15", - last_updated: "2025-09-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.4": { - id: "openai/gpt-5.4", - name: "GPT-5.4", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 2.5, - output: 15, - cache_read: 0.25, - context_over_200k: { input: 5, output: 22.5, cache_read: 0.5 }, - }, - limit: { context: 1050000, input: 922000, output: 128000 }, - }, - "openai/gpt-oss-20b": { - id: "openai/gpt-oss-20b", - name: "GPT OSS 20B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.2 }, - limit: { context: 131072, output: 32768 }, - }, - "openai/gpt-5-pro": { - id: "openai/gpt-5-pro", - name: "GPT-5 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-10-06", - last_updated: "2025-10-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 120 }, - limit: { context: 400000, output: 272000 }, - }, - "openai/gpt-oss-120b:free": { - id: "openai/gpt-oss-120b:free", - name: "gpt-oss-120b (free)", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 32768 }, - }, - "openai/gpt-5": { - id: "openai/gpt-5", - name: "GPT-5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-10-01", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-oss-safeguard-20b": { - id: "openai/gpt-oss-safeguard-20b", - name: "GPT OSS Safeguard 20B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-10-29", - last_updated: "2025-10-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.075, output: 0.3 }, - limit: { context: 131072, output: 65536 }, - }, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "GPT OSS 120B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.072, output: 0.28 }, - limit: { context: 131072, output: 32768 }, - }, - "openai/gpt-4.1": { - id: "openai/gpt-4.1", - name: "GPT-4.1", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 1047576, output: 32768 }, - }, - "openai/gpt-4.1-mini": { - id: "openai/gpt-4.1-mini", - name: "GPT-4.1 Mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.6, cache_read: 0.1 }, - limit: { context: 1047576, output: 32768 }, - }, - "openai/gpt-5.1-codex": { - id: "openai/gpt-5.1-codex", - name: "GPT-5.1-Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "z-ai/glm-4.7": { - id: "z-ai/glm-4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_details" }, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2, cache_read: 0.11 }, - limit: { context: 204800, output: 131072 }, - }, - "z-ai/glm-4.5-air:free": { - id: "z-ai/glm-4.5-air:free", - name: "GLM 4.5 Air (free)", - family: "glm-air", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 96000 }, - }, - "z-ai/glm-5": { - id: "z-ai/glm-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3.2, cache_read: 0.2 }, - limit: { context: 202752, output: 131000 }, - }, - "z-ai/glm-5.1": { - id: "z-ai/glm-5.1", - name: "GLM-5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-04-07", - last_updated: "2026-04-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.4, output: 4.4, cache_read: 0.26 }, - limit: { context: 202752, output: 131072 }, - }, - "z-ai/glm-4.5": { - id: "z-ai/glm-4.5", - name: "GLM 4.5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2 }, - limit: { context: 128000, output: 96000 }, - }, - "z-ai/glm-4.6:exacto": { - id: "z-ai/glm-4.6:exacto", - name: "GLM 4.6 (exacto)", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-09", - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 1.9, cache_read: 0.11 }, - limit: { context: 200000, output: 128000 }, - }, - "z-ai/glm-4.5-air": { - id: "z-ai/glm-4.5-air", - name: "GLM 4.5 Air", - family: "glm-air", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 1.1 }, - limit: { context: 128000, output: 96000 }, - }, - "z-ai/glm-5-turbo": { - id: "z-ai/glm-5-turbo", - name: "GLM-5-Turbo", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-03-16", - last_updated: "2026-03-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.96, output: 3.2, cache_read: 0.192, cache_write: 0 }, - limit: { context: 202752, output: 131072 }, - }, - "z-ai/glm-4.5v": { - id: "z-ai/glm-4.5v", - name: "GLM 4.5V", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-08-11", - last_updated: "2025-08-11", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 1.8 }, - limit: { context: 64000, output: 16384 }, - }, - "z-ai/glm-4.6": { - id: "z-ai/glm-4.6", - name: "GLM 4.6", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-09", - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2, cache_read: 0.11 }, - limit: { context: 200000, output: 128000 }, - }, - "z-ai/glm-4.7-flash": { - id: "z-ai/glm-4.7-flash", - name: "GLM-4.7-Flash", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_details" }, - structured_output: true, - temperature: true, - release_date: "2026-01-19", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.4 }, - limit: { context: 200000, output: 65535 }, - }, - "sourceful/riverflow-v2-standard-preview": { - id: "sourceful/riverflow-v2-standard-preview", - name: "Riverflow V2 Standard Preview", - family: "sourceful", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-06", - release_date: "2025-12-08", - last_updated: "2026-01-28", - modalities: { input: ["text", "image"], output: ["image"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 8192, output: 8192 }, - }, - "sourceful/riverflow-v2-fast-preview": { - id: "sourceful/riverflow-v2-fast-preview", - name: "Riverflow V2 Fast Preview", - family: "sourceful", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-06", - release_date: "2025-12-08", - last_updated: "2026-01-28", - modalities: { input: ["text", "image"], output: ["image"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 8192, output: 8192 }, - }, - "sourceful/riverflow-v2-max-preview": { - id: "sourceful/riverflow-v2-max-preview", - name: "Riverflow V2 Max Preview", - family: "sourceful", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-06", - release_date: "2025-12-08", - last_updated: "2026-01-28", - modalities: { input: ["text", "image"], output: ["image"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 8192, output: 8192 }, - }, - "minimax/minimax-m2.7": { - id: "minimax/minimax-m2.7", - name: "MiniMax M2.7", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.06, cache_write: 0.375 }, - limit: { context: 204800, output: 131072 }, - }, - "minimax/minimax-m2": { - id: "minimax/minimax-m2", - name: "MiniMax M2", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_details" }, - structured_output: true, - temperature: true, - release_date: "2025-10-23", - last_updated: "2025-10-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.28, output: 1.15, cache_read: 0.28, cache_write: 1.15 }, - limit: { context: 196600, output: 118000 }, - }, - "minimax/minimax-01": { - id: "minimax/minimax-01", - name: "MiniMax-01", - family: "minimax", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-01-15", - last_updated: "2025-01-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 1.1 }, - limit: { context: 1000000, output: 1000000 }, - }, - "minimax/minimax-m2.1": { - id: "minimax/minimax-m2.1", - name: "MiniMax M2.1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_details" }, - structured_output: true, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 204800, output: 131072 }, - }, - "minimax/minimax-m2.5:free": { - id: "minimax/minimax-m2.5:free", - name: "MiniMax M2.5 (free)", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_details" }, - structured_output: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "minimax/minimax-m1": { - id: "minimax/minimax-m1", - name: "MiniMax M1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 2.2 }, - limit: { context: 1000000, output: 40000 }, - }, - "minimax/minimax-m2.5": { - id: "minimax/minimax-m2.5", - name: "MiniMax M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_details" }, - structured_output: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.03 }, - limit: { context: 204800, output: 131072 }, - }, - "qwen/qwen3.5-397b-a17b": { - id: "qwen/qwen3.5-397b-a17b", - name: "Qwen3.5 397B A17B", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02-16", - last_updated: "2026-02-16", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3.6 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen/qwen2.5-vl-72b-instruct": { - id: "qwen/qwen2.5-vl-72b-instruct", - name: "Qwen2.5 VL 72B Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-02-01", - last_updated: "2025-02-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 32768, output: 8192 }, - }, - "qwen/qwen3-coder:free": { - id: "qwen/qwen3-coder:free", - name: "Qwen3 Coder 480B A35B Instruct (free)", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 66536 }, - }, - "qwen/qwen3.5-flash-02-23": { - id: "qwen/qwen3.5-flash-02-23", - name: "Qwen: Qwen3.5-Flash", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-25", - last_updated: "2026-02-25", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.065, output: 0.26 }, - limit: { context: 1000000, output: 65536 }, - }, - "qwen/qwen3.6-plus": { - id: "qwen/qwen3.6-plus", - name: "Qwen3.6 Plus", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-04-02", - last_updated: "2026-04-02", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.325, output: 1.95 }, - limit: { context: 1000000, output: 65536 }, - }, - "qwen/qwen3-max": { - id: "qwen/qwen3-max", - name: "Qwen3 Max", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.2, output: 6 }, - limit: { context: 262144, output: 32768 }, - }, - "qwen/qwen3-coder:exacto": { - id: "qwen/qwen3-coder:exacto", - name: "Qwen3 Coder (exacto)", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.38, output: 1.53 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen/qwen3-30b-a3b-instruct-2507": { - id: "qwen/qwen3-30b-a3b-instruct-2507", - name: "Qwen3 30B A3B Instruct 2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-29", - last_updated: "2025-07-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.8 }, - limit: { context: 262000, output: 262000 }, - }, - "qwen/qwen3-235b-a22b-thinking-2507": { - id: "qwen/qwen3-235b-a22b-thinking-2507", - name: "Qwen3 235B A22B Thinking 2507", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-25", - last_updated: "2025-07-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.078, output: 0.312 }, - limit: { context: 262144, output: 81920 }, - }, - "qwen/qwen3-next-80b-a3b-thinking": { - id: "qwen/qwen3-next-80b-a3b-thinking", - name: "Qwen3 Next 80B A3B Thinking", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-11", - last_updated: "2025-09-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.14, output: 1.4 }, - limit: { context: 262144, output: 262144 }, - }, - "qwen/qwen3-30b-a3b-thinking-2507": { - id: "qwen/qwen3-30b-a3b-thinking-2507", - name: "Qwen3 30B A3B Thinking 2507", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-29", - last_updated: "2025-07-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.8 }, - limit: { context: 262000, output: 262000 }, - }, - "qwen/qwen3-4b:free": { - id: "qwen/qwen3-4b:free", - name: "Qwen3 4B (free)", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04-30", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 40960, output: 40960 }, - }, - "qwen/qwen3-next-80b-a3b-instruct:free": { - id: "qwen/qwen3-next-80b-a3b-instruct:free", - name: "Qwen3 Next 80B A3B Instruct (free)", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-11", - last_updated: "2025-09-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 262144 }, - }, - "qwen/qwen3-coder-flash": { - id: "qwen/qwen3-coder-flash", - name: "Qwen3 Coder Flash", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.5 }, - limit: { context: 128000, output: 66536 }, - }, - "qwen/qwen3-next-80b-a3b-instruct": { - id: "qwen/qwen3-next-80b-a3b-instruct", - name: "Qwen3 Next 80B A3B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-11", - last_updated: "2025-09-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.14, output: 1.4 }, - limit: { context: 262144, output: 262144 }, - }, - "qwen/qwen-2.5-coder-32b-instruct": { - id: "qwen/qwen-2.5-coder-32b-instruct", - name: "Qwen2.5 Coder 32B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-11-11", - last_updated: "2024-11-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 32768, output: 8192 }, - }, - "qwen/qwen3-coder-30b-a3b-instruct": { - id: "qwen/qwen3-coder-30b-a3b-instruct", - name: "Qwen3 Coder 30B A3B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-31", - last_updated: "2025-07-31", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.27 }, - limit: { context: 160000, output: 65536 }, - }, - "qwen/qwen3-coder": { - id: "qwen/qwen3-coder", - name: "Qwen3 Coder", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 262144, output: 66536 }, - }, - "qwen/qwen3.5-plus-02-15": { - id: "qwen/qwen3.5-plus-02-15", - name: "Qwen3.5 Plus 2026-02-15", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02-16", - last_updated: "2026-02-16", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2.4 }, - limit: { context: 1000000, output: 65536 }, - }, - "qwen/qwen3-235b-a22b-07-25": { - id: "qwen/qwen3-235b-a22b-07-25", - name: "Qwen3 235B A22B Instruct 2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04-28", - last_updated: "2025-07-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.85 }, - limit: { context: 262144, output: 131072 }, - }, - "google/gemini-2.5-pro-preview-05-06": { - id: "google/gemini-2.5-pro-preview-05-06", - name: "Gemini 2.5 Pro Preview 05-06", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-05-06", - last_updated: "2025-05-06", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.31 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-3.1-pro-preview-customtools": { - id: "google/gemini-3.1-pro-preview-customtools", - name: "Gemini 3.1 Pro Preview Custom Tools", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_details" }, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-02-19", - last_updated: "2026-02-19", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, reasoning: 12, context_over_200k: { input: 4, output: 18, cache_read: 0.4 } }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemma-3-4b-it:free": { - id: "google/gemma-3-4b-it:free", - name: "Gemma 3 4B (free)", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2025-03-13", - last_updated: "2025-03-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 32768, output: 8192 }, - }, - "google/gemini-2.5-flash-lite-preview-09-2025": { - id: "google/gemini-2.5-flash-lite-preview-09-2025", - name: "Gemini 2.5 Flash Lite Preview 09-25", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.025 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-2.0-flash-001": { - id: "google/gemini-2.0-flash-001", - name: "Gemini 2.0 Flash", - family: "gemini-flash", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-06", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.025 }, - limit: { context: 1048576, output: 8192 }, - }, - "google/gemma-3n-e4b-it": { - id: "google/gemma-3n-e4b-it", - name: "Gemma 3n 4B", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-06", - release_date: "2025-05-20", - last_updated: "2025-05-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0.04 }, - limit: { context: 32768, output: 32768 }, - }, - "google/gemini-3.1-flash-lite-preview": { - id: "google/gemini-3.1-flash-lite-preview", - name: "Gemini 3.1 Flash Lite Preview", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-03-03", - last_updated: "2026-03-03", - modalities: { input: ["text", "image", "video", "pdf", "audio"], output: ["text"] }, - open_weights: false, - cost: { - input: 0.25, - output: 1.5, - reasoning: 1.5, - cache_read: 0.025, - cache_write: 0.083, - input_audio: 0.5, - output_audio: 0.5, - }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemma-3n-e4b-it:free": { - id: "google/gemma-3n-e4b-it:free", - name: "Gemma 3n 4B (free)", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-06", - release_date: "2025-05-20", - last_updated: "2025-05-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 8192, output: 2000 }, - }, - "google/gemini-3.1-pro-preview": { - id: "google/gemini-3.1-pro-preview", - name: "Gemini 3.1 Pro Preview", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_details" }, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-02-19", - last_updated: "2026-02-19", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, reasoning: 12, context_over_200k: { input: 4, output: 18, cache_read: 0.4 } }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-3-flash-preview": { - id: "google/gemini-3-flash-preview", - name: "Gemini 3 Flash Preview", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_details" }, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 3, cache_read: 0.05 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-3-pro-preview": { - id: "google/gemini-3-pro-preview", - name: "Gemini 3 Pro Preview", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_details" }, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-11-18", - last_updated: "2025-11", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12 }, - limit: { context: 1050000, output: 66000 }, - }, - "google/gemma-3n-e2b-it:free": { - id: "google/gemma-3n-e2b-it:free", - name: "Gemma 3n 2B (free)", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-06", - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 8192, output: 2000 }, - }, - "google/gemini-2.5-pro": { - id: "google/gemini-2.5-pro", - name: "Gemini 2.5 Pro", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-03-20", - last_updated: "2025-06-05", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.31 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemma-2-9b-it": { - id: "google/gemma-2-9b-it", - name: "Gemma 2 9B", - family: "gemma", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-06", - release_date: "2024-06-28", - last_updated: "2024-06-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.09 }, - limit: { context: 8192, output: 8192 }, - }, - "google/gemma-4-31b-it": { - id: "google/gemma-4-31b-it", - name: "Gemma 4 31B", - family: "gemma", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-04-02", - last_updated: "2026-04-02", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.14, output: 0.4 }, - limit: { context: 262144, output: 262144 }, - }, - "google/gemini-2.5-pro-preview-06-05": { - id: "google/gemini-2.5-pro-preview-06-05", - name: "Gemini 2.5 Pro Preview 06-05", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-06-05", - last_updated: "2025-06-05", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.31 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemma-3-12b-it": { - id: "google/gemma-3-12b-it", - name: "Gemma 3 12B", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-03-13", - last_updated: "2025-03-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.1 }, - limit: { context: 131072, output: 131072 }, - }, - "google/gemma-3-27b-it:free": { - id: "google/gemma-3-27b-it:free", - name: "Gemma 3 27B (free)", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-03-12", - last_updated: "2025-03-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 8192 }, - }, - "google/gemini-2.5-flash": { - id: "google/gemini-2.5-flash", - name: "Gemini 2.5 Flash", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-07-17", - last_updated: "2025-07-17", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5, cache_read: 0.0375 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemma-4-31b-it:free": { - id: "google/gemma-4-31b-it:free", - name: "Gemma 4 31B (free)", - family: "gemma", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-04-02", - last_updated: "2026-04-02", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 32768 }, - }, - "google/gemma-3-12b-it:free": { - id: "google/gemma-3-12b-it:free", - name: "Gemma 3 12B (free)", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2025-03-13", - last_updated: "2025-03-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 32768, output: 8192 }, - }, - "google/gemma-3-4b-it": { - id: "google/gemma-3-4b-it", - name: "Gemma 3 4B", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2025-03-13", - last_updated: "2025-03-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.01703, output: 0.06815 }, - limit: { context: 96000, output: 96000 }, - }, - "google/gemini-2.5-flash-preview-09-2025": { - id: "google/gemini-2.5-flash-preview-09-2025", - name: "Gemini 2.5 Flash Preview 09-25", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5, cache_read: 0.031 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemma-3-27b-it": { - id: "google/gemma-3-27b-it", - name: "Gemma 3 27B", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-03-12", - last_updated: "2025-03-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.04, output: 0.15 }, - limit: { context: 96000, output: 96000 }, - }, - "google/gemma-4-26b-a4b-it": { - id: "google/gemma-4-26b-a4b-it", - name: "Gemma 4 26B A4B", - family: "gemma", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-04-03", - last_updated: "2026-04-03", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.13, output: 0.4 }, - limit: { context: 262144, output: 262144 }, - }, - "google/gemma-4-26b-a4b-it:free": { - id: "google/gemma-4-26b-a4b-it:free", - name: "Gemma 4 26B A4B (free)", - family: "gemma", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-04-03", - last_updated: "2026-04-03", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 32768 }, - }, - "google/gemini-2.5-flash-lite": { - id: "google/gemini-2.5-flash-lite", - name: "Gemini 2.5 Flash Lite", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.025 }, - limit: { context: 1048576, output: 65536 }, - }, - "moonshotai/kimi-k2.5": { - id: "moonshotai/kimi-k2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_details" }, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3, cache_read: 0.1 }, - limit: { context: 262144, output: 262144 }, - }, - "moonshotai/kimi-k2-0905": { - id: "moonshotai/kimi-k2-0905", - name: "Kimi K2 Instruct 0905", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.5 }, - limit: { context: 262144, output: 16384 }, - }, - "moonshotai/kimi-k2-0905:exacto": { - id: "moonshotai/kimi-k2-0905:exacto", - name: "Kimi K2 Instruct 0905 (exacto)", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.5 }, - limit: { context: 262144, output: 16384 }, - }, - "moonshotai/kimi-k2": { - id: "moonshotai/kimi-k2", - name: "Kimi K2", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-07-11", - last_updated: "2025-07-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 2.2 }, - limit: { context: 131072, output: 32768 }, - }, - "moonshotai/kimi-k2:free": { - id: "moonshotai/kimi-k2:free", - name: "Kimi K2 (free)", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-11", - last_updated: "2025-07-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 32800, output: 32800 }, - }, - "moonshotai/kimi-k2-thinking": { - id: "moonshotai/kimi-k2-thinking", - name: "Kimi K2 Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_details" }, - structured_output: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-11-06", - last_updated: "2025-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.5, cache_read: 0.15 }, - limit: { context: 262144, output: 262144 }, - }, - "anthropic/claude-opus-4.1": { - id: "anthropic/claude-opus-4.1", - name: "Claude Opus 4.1", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "anthropic/claude-3.7-sonnet": { - id: "anthropic/claude-3.7-sonnet", - name: "Claude Sonnet 3.7", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-01", - release_date: "2025-02-19", - last_updated: "2025-02-19", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 128000 }, - }, - "anthropic/claude-opus-4.6": { - id: "anthropic/claude-opus-4.6", - name: "Claude Opus 4.6", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-05-30", - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 5, - output: 25, - cache_read: 0.5, - cache_write: 6.25, - context_over_200k: { input: 10, output: 37.5, cache_read: 1, cache_write: 12.5 }, - }, - limit: { context: 1000000, output: 128000 }, - }, - "anthropic/claude-sonnet-4": { - id: "anthropic/claude-sonnet-4", - name: "Claude Sonnet 4", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 3, - output: 15, - cache_read: 0.3, - cache_write: 3.75, - context_over_200k: { input: 6, output: 22.5, cache_read: 0.6, cache_write: 7.5 }, - }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-sonnet-4.5": { - id: "anthropic/claude-sonnet-4.5", - name: "Claude Sonnet 4.5", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 3, - output: 15, - cache_read: 0.3, - cache_write: 3.75, - context_over_200k: { input: 6, output: 22.5, cache_read: 0.6, cache_write: 7.5 }, - }, - limit: { context: 1000000, output: 64000 }, - }, - "anthropic/claude-opus-4.5": { - id: "anthropic/claude-opus-4.5", - name: "Claude Opus 4.5", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-05-30", - release_date: "2025-11-24", - last_updated: "2025-11-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 32000 }, - }, - "anthropic/claude-opus-4": { - id: "anthropic/claude-opus-4", - name: "Claude Opus 4", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "anthropic/claude-3.5-haiku": { - id: "anthropic/claude-3.5-haiku", - name: "Claude Haiku 3.5", - family: "claude-haiku", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07-31", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 4, cache_read: 0.08, cache_write: 1 }, - limit: { context: 200000, output: 8192 }, - }, - "anthropic/claude-haiku-4.5": { - id: "anthropic/claude-haiku-4.5", - name: "Claude Haiku 4.5", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-sonnet-4.6": { - id: "anthropic/claude-sonnet-4.6", - name: "Claude Sonnet 4.6", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-17", - last_updated: "2026-02-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { - input: 3, - output: 15, - cache_read: 0.3, - cache_write: 3.75, - context_over_200k: { input: 6, output: 22.5, cache_read: 0.6, cache_write: 7.5 }, - }, - limit: { context: 1000000, output: 128000 }, - }, - "xiaomi/mimo-v2-omni": { - id: "xiaomi/mimo-v2-omni", - name: "MiMo-V2-Omni", - family: "mimo", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_details" }, - structured_output: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 2, cache_read: 0.08 }, - limit: { context: 262144, output: 65536 }, - }, - "xiaomi/mimo-v2-pro": { - id: "xiaomi/mimo-v2-pro", - name: "MiMo-V2-Pro", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_details" }, - structured_output: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3, cache_read: 0.2 }, - limit: { context: 1048576, output: 65536 }, - }, - "xiaomi/mimo-v2-flash": { - id: "xiaomi/mimo-v2-flash", - name: "MiMo-V2-Flash", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-12-14", - last_updated: "2025-12-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3, cache_read: 0.01 }, - limit: { context: 262144, output: 65536 }, - }, - }, - }, - "fireworks-ai": { - id: "fireworks-ai", - env: ["FIREWORKS_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.fireworks.ai/inference/v1/", - name: "Fireworks AI", - doc: "https://fireworks.ai/docs/", - models: { - "accounts/fireworks/models/glm-5p1": { - id: "accounts/fireworks/models/glm-5p1", - name: "GLM 5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-04-01", - last_updated: "2026-04-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.4, output: 4.4, cache_read: 0.26 }, - limit: { context: 202800, output: 131072 }, - }, - "accounts/fireworks/models/deepseek-v3p2": { - id: "accounts/fireworks/models/deepseek-v3p2", - name: "DeepSeek V3.2", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-09", - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.56, output: 1.68, cache_read: 0.28 }, - limit: { context: 160000, output: 160000 }, - }, - "accounts/fireworks/models/minimax-m2p5": { - id: "accounts/fireworks/models/minimax-m2p5", - name: "MiniMax-M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.03 }, - limit: { context: 196608, output: 196608 }, - }, - "accounts/fireworks/models/glm-4p5-air": { - id: "accounts/fireworks/models/glm-4p5-air", - name: "GLM 4.5 Air", - family: "glm-air", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-08-01", - last_updated: "2025-08-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.22, output: 0.88 }, - limit: { context: 131072, output: 131072 }, - }, - "accounts/fireworks/models/glm-5": { - id: "accounts/fireworks/models/glm-5", - name: "GLM 5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3.2, cache_read: 0.5 }, - limit: { context: 202752, output: 131072 }, - }, - "accounts/fireworks/models/deepseek-v3p1": { - id: "accounts/fireworks/models/deepseek-v3p1", - name: "DeepSeek V3.1", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-08-21", - last_updated: "2025-08-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.56, output: 1.68 }, - limit: { context: 163840, output: 163840 }, - }, - "accounts/fireworks/models/kimi-k2-instruct": { - id: "accounts/fireworks/models/kimi-k2-instruct", - name: "Kimi K2 Instruct", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-07-11", - last_updated: "2025-07-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3 }, - limit: { context: 128000, output: 16384 }, - }, - "accounts/fireworks/models/qwen3p6-plus": { - id: "accounts/fireworks/models/qwen3p6-plus", - name: "Qwen 3.6 Plus", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-04", - last_updated: "2026-04-04", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 3, cache_read: 0.1 }, - limit: { context: 128000, output: 8192 }, - }, - "accounts/fireworks/models/minimax-m2p1": { - id: "accounts/fireworks/models/minimax-m2p1", - name: "MiniMax-M2.1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.03 }, - limit: { context: 200000, output: 200000 }, - }, - "accounts/fireworks/models/glm-4p7": { - id: "accounts/fireworks/models/glm-4p7", - name: "GLM 4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2, cache_read: 0.3 }, - limit: { context: 198000, output: 198000 }, - }, - "accounts/fireworks/models/glm-4p5": { - id: "accounts/fireworks/models/glm-4p5", - name: "GLM 4.5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-29", - last_updated: "2025-07-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 2.19 }, - limit: { context: 131072, output: 131072 }, - }, - "accounts/fireworks/models/kimi-k2p5": { - id: "accounts/fireworks/models/kimi-k2p5", - name: "Kimi K2.5", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-01", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3, cache_read: 0.1 }, - limit: { context: 256000, output: 256000 }, - }, - "accounts/fireworks/models/gpt-oss-20b": { - id: "accounts/fireworks/models/gpt-oss-20b", - name: "GPT OSS 20B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.2 }, - limit: { context: 131072, output: 32768 }, - }, - "accounts/fireworks/models/gpt-oss-120b": { - id: "accounts/fireworks/models/gpt-oss-120b", - name: "GPT OSS 120B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 131072, output: 32768 }, - }, - "accounts/fireworks/models/kimi-k2-thinking": { - id: "accounts/fireworks/models/kimi-k2-thinking", - name: "Kimi K2 Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2025-11-06", - last_updated: "2025-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.5, cache_read: 0.3 }, - limit: { context: 256000, output: 256000 }, - }, - "accounts/fireworks/routers/kimi-k2p5-turbo": { - id: "accounts/fireworks/routers/kimi-k2p5-turbo", - name: "Kimi K2.5 Turbo (firepass)", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-01", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0 }, - limit: { context: 256000, output: 256000 }, - }, - }, - }, - "kimi-for-coding": { - id: "kimi-for-coding", - env: ["KIMI_API_KEY"], - npm: "@ai-sdk/anthropic", - api: "https://api.kimi.com/coding/v1", - name: "Kimi For Coding", - doc: "https://www.kimi.com/coding/docs/en/third-party-agents.html", - models: { - k2p5: { - id: "k2p5", - name: "Kimi K2.5", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 262144, output: 32768 }, - }, - "kimi-k2-thinking": { - id: "kimi-k2-thinking", - name: "Kimi K2 Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-11", - last_updated: "2025-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 262144, output: 32768 }, - }, - }, - }, - moark: { - id: "moark", - env: ["MOARK_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://moark.com/v1", - name: "Moark", - doc: "https://moark.com/docs/openapi/v1#tag/%E6%96%87%E6%9C%AC%E7%94%9F%E6%88%90", - models: { - "GLM-4.7": { - id: "GLM-4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 3.5, output: 14 }, - limit: { context: 204800, output: 131072 }, - }, - "MiniMax-M2.1": { - id: "MiniMax-M2.1", - name: "MiniMax-M2.1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2.1, output: 8.4 }, - limit: { context: 204800, output: 131072 }, - }, - }, - }, - "opencode-go": { - id: "opencode-go", - env: ["OPENCODE_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://opencode.ai/zen/go/v1", - name: "OpenCode Go", - doc: "https://opencode.ai/docs/zen", - models: { - "minimax-m2.7": { - id: "minimax-m2.7", - name: "MiniMax M2.7", - family: "minimax-m2.7", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.06 }, - limit: { context: 204800, output: 131072 }, - provider: { npm: "@ai-sdk/anthropic" }, - }, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-10", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3, cache_read: 0.1 }, - limit: { context: 262144, output: 65536 }, - }, - "glm-5": { - id: "glm-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3.2, cache_read: 0.2 }, - limit: { context: 204800, output: 131072 }, - }, - "mimo-v2-omni": { - id: "mimo-v2-omni", - name: "MiMo V2 Omni", - family: "mimo-omni", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text", "image", "audio", "pdf"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 2, cache_read: 0.08 }, - limit: { context: 262144, output: 64000 }, - }, - "glm-5.1": { - id: "glm-5.1", - name: "GLM-5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2026-04-07", - last_updated: "2026-04-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.4, output: 4.4, cache_read: 0.26 }, - limit: { context: 204800, output: 131072 }, - }, - "minimax-m2.5": { - id: "minimax-m2.5", - name: "MiniMax M2.5", - family: "minimax-m2.5", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.03 }, - limit: { context: 204800, output: 131072 }, - provider: { npm: "@ai-sdk/anthropic" }, - }, - "mimo-v2-pro": { - id: "mimo-v2-pro", - name: "MiMo V2 Pro", - family: "mimo-pro", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3, cache_read: 0.2, context_over_200k: { input: 2, output: 6, cache_read: 0.4 } }, - limit: { context: 1048576, output: 64000 }, - }, - }, - }, - "io-net": { - id: "io-net", - env: ["IOINTELLIGENCE_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.intelligence.io.solutions/api/v1", - name: "IO.NET", - doc: "https://io.net/docs/guides/intelligence/io-intelligence", - models: { - "Intel/Qwen3-Coder-480B-A35B-Instruct-int4-mixed-ar": { - id: "Intel/Qwen3-Coder-480B-A35B-Instruct-int4-mixed-ar", - name: "Qwen 3 Coder 480B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-01-15", - last_updated: "2025-01-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.22, output: 0.95, cache_read: 0.11, cache_write: 0.44 }, - limit: { context: 106000, output: 4096 }, - }, - "Qwen/Qwen3-Next-80B-A3B-Instruct": { - id: "Qwen/Qwen3-Next-80B-A3B-Instruct", - name: "Qwen 3 Next 80B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-01-10", - last_updated: "2025-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.8, cache_read: 0.05, cache_write: 0.2 }, - limit: { context: 262144, output: 4096 }, - }, - "Qwen/Qwen3-235B-A22B-Thinking-2507": { - id: "Qwen/Qwen3-235B-A22B-Thinking-2507", - name: "Qwen 3 235B Thinking", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-07-01", - last_updated: "2025-07-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.11, output: 0.6, cache_read: 0.055, cache_write: 0.22 }, - limit: { context: 262144, output: 4096 }, - }, - "Qwen/Qwen2.5-VL-32B-Instruct": { - id: "Qwen/Qwen2.5-VL-32B-Instruct", - name: "Qwen 2.5 VL 32B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-09", - release_date: "2024-11-01", - last_updated: "2024-11-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.22, cache_read: 0.025, cache_write: 0.1 }, - limit: { context: 32000, output: 4096 }, - }, - "zai-org/GLM-4.6": { - id: "zai-org/GLM-4.6", - name: "GLM 4.6", - family: "glm", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-11-15", - last_updated: "2024-11-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.75, cache_read: 0.2, cache_write: 0.8 }, - limit: { context: 200000, output: 4096 }, - }, - "mistralai/Magistral-Small-2506": { - id: "mistralai/Magistral-Small-2506", - name: "Magistral Small 2506", - family: "magistral-small", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-06-01", - last_updated: "2025-06-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 1.5, cache_read: 0.25, cache_write: 1 }, - limit: { context: 128000, output: 4096 }, - }, - "mistralai/Mistral-Large-Instruct-2411": { - id: "mistralai/Mistral-Large-Instruct-2411", - name: "Mistral Large Instruct 2411", - family: "mistral-large", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-11-01", - last_updated: "2024-11-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6, cache_read: 1, cache_write: 4 }, - limit: { context: 128000, output: 4096 }, - }, - "mistralai/Mistral-Nemo-Instruct-2407": { - id: "mistralai/Mistral-Nemo-Instruct-2407", - name: "Mistral Nemo Instruct 2407", - family: "mistral-nemo", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-05", - release_date: "2024-07-01", - last_updated: "2024-07-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0.04, cache_read: 0.01, cache_write: 0.04 }, - limit: { context: 128000, output: 4096 }, - }, - "mistralai/Devstral-Small-2505": { - id: "mistralai/Devstral-Small-2505", - name: "Devstral Small 2505", - family: "devstral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-05-01", - last_updated: "2025-05-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.22, cache_read: 0.025, cache_write: 0.1 }, - limit: { context: 128000, output: 4096 }, - }, - "meta-llama/Llama-3.3-70B-Instruct": { - id: "meta-llama/Llama-3.3-70B-Instruct", - name: "Llama 3.3 70B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.13, output: 0.38, cache_read: 0.065, cache_write: 0.26 }, - limit: { context: 128000, output: 4096 }, - }, - "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8": { - id: "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8", - name: "Llama 4 Maverick 17B 128E Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-01-15", - last_updated: "2025-01-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6, cache_read: 0.075, cache_write: 0.3 }, - limit: { context: 430000, output: 4096 }, - }, - "meta-llama/Llama-3.2-90B-Vision-Instruct": { - id: "meta-llama/Llama-3.2-90B-Vision-Instruct", - name: "Llama 3.2 90B Vision Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-09-25", - last_updated: "2024-09-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.35, output: 0.4, cache_read: 0.175, cache_write: 0.7 }, - limit: { context: 16000, output: 4096 }, - }, - "deepseek-ai/DeepSeek-R1-0528": { - id: "deepseek-ai/DeepSeek-R1-0528", - name: "DeepSeek R1", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-01-20", - last_updated: "2025-05-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2, output: 8.75, cache_read: 1, cache_write: 4 }, - limit: { context: 128000, output: 4096 }, - }, - "openai/gpt-oss-20b": { - id: "openai/gpt-oss-20b", - name: "GPT-OSS 20B", - family: "gpt-oss", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.14, cache_read: 0.015, cache_write: 0.06 }, - limit: { context: 64000, output: 4096 }, - }, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "GPT-OSS 120B", - family: "gpt-oss", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.04, output: 0.4, cache_read: 0.02, cache_write: 0.08 }, - limit: { context: 131072, output: 4096 }, - }, - "moonshotai/Kimi-K2-Thinking": { - id: "moonshotai/Kimi-K2-Thinking", - name: "Kimi K2 Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2024-11-01", - last_updated: "2024-11-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.55, output: 2.25, cache_read: 0.275, cache_write: 1.1 }, - limit: { context: 32768, output: 4096 }, - }, - "moonshotai/Kimi-K2-Instruct-0905": { - id: "moonshotai/Kimi-K2-Instruct-0905", - name: "Kimi K2 Instruct", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2024-09-05", - last_updated: "2024-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.39, output: 1.9, cache_read: 0.195, cache_write: 0.78 }, - limit: { context: 32768, output: 4096 }, - }, - }, - }, - "alibaba-cn": { - id: "alibaba-cn", - env: ["DASHSCOPE_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://dashscope.aliyuncs.com/compatible-mode/v1", - name: "Alibaba (China)", - doc: "https://www.alibabacloud.com/help/en/model-studio/models", - models: { - "qwen3-235b-a22b": { - id: "qwen3-235b-a22b", - name: "Qwen3 235B-A22B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.287, output: 1.147, reasoning: 2.868 }, - limit: { context: 131072, output: 16384 }, - }, - "qwen-plus-character": { - id: "qwen-plus-character", - name: "Qwen Plus Character", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-01", - last_updated: "2024-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.115, output: 0.287 }, - limit: { context: 32768, output: 4096 }, - }, - "qwen2-5-math-7b-instruct": { - id: "qwen2-5-math-7b-instruct", - name: "Qwen2.5-Math 7B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-09", - last_updated: "2024-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.144, output: 0.287 }, - limit: { context: 4096, output: 3072 }, - }, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Moonshot Kimi K2.5", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: false, - temperature: true, - knowledge: "2025-01", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.574, output: 2.411 }, - limit: { context: 262144, output: 32768 }, - }, - "qwen-doc-turbo": { - id: "qwen-doc-turbo", - name: "Qwen Doc Turbo", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-01", - last_updated: "2024-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.087, output: 0.144 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen-vl-ocr": { - id: "qwen-vl-ocr", - name: "Qwen-VL OCR", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-04", - release_date: "2024-10-28", - last_updated: "2025-04-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.717, output: 0.717 }, - limit: { context: 34096, output: 4096 }, - }, - "qwen-omni-turbo-realtime": { - id: "qwen-omni-turbo-realtime", - name: "Qwen-Omni Turbo Realtime", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-05-08", - last_updated: "2025-05-08", - modalities: { input: ["text", "image", "audio"], output: ["text", "audio"] }, - open_weights: false, - cost: { input: 0.23, output: 0.918, input_audio: 3.584, output_audio: 7.168 }, - limit: { context: 32768, output: 2048 }, - }, - "qwen3-8b": { - id: "qwen3-8b", - name: "Qwen3 8B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.072, output: 0.287, reasoning: 0.717 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen3.5-397b-a17b": { - id: "qwen3.5-397b-a17b", - name: "Qwen3.5 397B-A17B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02-16", - last_updated: "2026-02-16", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.43, output: 2.58, reasoning: 2.58 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen-math-turbo": { - id: "qwen-math-turbo", - name: "Qwen Math Turbo", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-09-19", - last_updated: "2024-09-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.287, output: 0.861 }, - limit: { context: 4096, output: 3072 }, - }, - "qwq-plus": { - id: "qwq-plus", - name: "QwQ Plus", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-03-05", - last_updated: "2025-03-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.23, output: 0.574 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen-vl-plus": { - id: "qwen-vl-plus", - name: "Qwen-VL Plus", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-01-25", - last_updated: "2025-08-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.115, output: 0.287 }, - limit: { context: 131072, output: 8192 }, - }, - "glm-5": { - id: "glm-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.86, output: 3.15 }, - limit: { context: 202752, output: 16384 }, - }, - "deepseek-r1-distill-llama-70b": { - id: "deepseek-r1-distill-llama-70b", - name: "DeepSeek R1 Distill Llama 70B", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.287, output: 0.861 }, - limit: { context: 32768, output: 16384 }, - }, - "qwen3-32b": { - id: "qwen3-32b", - name: "Qwen3 32B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.287, output: 1.147, reasoning: 2.868 }, - limit: { context: 131072, output: 16384 }, - }, - "MiniMax-M2.5": { - id: "MiniMax-M2.5", - name: "MiniMax-M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 204800, output: 131072 }, - }, - "qwen-max": { - id: "qwen-max", - name: "Qwen Max", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-04-03", - last_updated: "2025-01-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.345, output: 1.377 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen-plus": { - id: "qwen-plus", - name: "Qwen Plus", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-01-25", - last_updated: "2025-09-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.115, output: 0.287, reasoning: 1.147 }, - limit: { context: 1000000, output: 32768 }, - }, - "qwen-omni-turbo": { - id: "qwen-omni-turbo", - name: "Qwen-Omni Turbo", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-01-19", - last_updated: "2025-03-26", - modalities: { input: ["text", "image", "audio", "video"], output: ["text", "audio"] }, - open_weights: false, - cost: { input: 0.058, output: 0.23, input_audio: 3.584, output_audio: 7.168 }, - limit: { context: 32768, output: 2048 }, - }, - "qwen-flash": { - id: "qwen-flash", - name: "Qwen Flash", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.022, output: 0.216 }, - limit: { context: 1000000, output: 32768 }, - }, - "qwen2-5-vl-7b-instruct": { - id: "qwen2-5-vl-7b-instruct", - name: "Qwen2.5-VL 7B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-09", - last_updated: "2024-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.287, output: 0.717 }, - limit: { context: 131072, output: 8192 }, - }, - "deepseek-r1": { - id: "deepseek-r1", - name: "DeepSeek R1", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.574, output: 2.294 }, - limit: { context: 131072, output: 16384 }, - }, - "qwen3.5-flash": { - id: "qwen3.5-flash", - name: "Qwen3.5 Flash", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02-23", - last_updated: "2026-02-23", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.172, output: 1.72, reasoning: 1.72 }, - limit: { context: 1000000, output: 65536 }, - }, - "qwen3.6-plus": { - id: "qwen3.6-plus", - name: "Qwen3.6 Plus", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-04-02", - last_updated: "2026-04-02", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.276, output: 1.651, cache_read: 0.028, cache_write: 0.344 }, - limit: { context: 1000000, output: 65536 }, - }, - "qwen3-max": { - id: "qwen3-max", - name: "Qwen3 Max", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-23", - last_updated: "2025-09-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.861, output: 3.441 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen3-omni-flash": { - id: "qwen3-omni-flash", - name: "Qwen3-Omni Flash", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-09-15", - last_updated: "2025-09-15", - modalities: { input: ["text", "image", "audio", "video"], output: ["text", "audio"] }, - open_weights: false, - cost: { input: 0.058, output: 0.23, input_audio: 3.584, output_audio: 7.168 }, - limit: { context: 65536, output: 16384 }, - }, - "deepseek-v3-1": { - id: "deepseek-v3-1", - name: "DeepSeek V3.1", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.574, output: 1.721 }, - limit: { context: 131072, output: 65536 }, - }, - "qwen2-5-72b-instruct": { - id: "qwen2-5-72b-instruct", - name: "Qwen2.5 72B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-09", - last_updated: "2024-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.574, output: 1.721 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen3-vl-235b-a22b": { - id: "qwen3-vl-235b-a22b", - name: "Qwen3-VL 235B-A22B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.286705, output: 1.14682, reasoning: 2.867051 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen-math-plus": { - id: "qwen-math-plus", - name: "Qwen Math Plus", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-08-16", - last_updated: "2024-09-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.574, output: 1.721 }, - limit: { context: 4096, output: 3072 }, - }, - "qwen2-5-coder-32b-instruct": { - id: "qwen2-5-coder-32b-instruct", - name: "Qwen2.5-Coder 32B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-11", - last_updated: "2024-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.287, output: 0.861 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen3-asr-flash": { - id: "qwen3-asr-flash", - name: "Qwen3-ASR Flash", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2024-04", - release_date: "2025-09-08", - last_updated: "2025-09-08", - modalities: { input: ["audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.032, output: 0.032 }, - limit: { context: 53248, output: 4096 }, - }, - "qwen-deep-research": { - id: "qwen-deep-research", - name: "Qwen Deep Research", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-01", - last_updated: "2024-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 7.742, output: 23.367 }, - limit: { context: 1000000, output: 32768 }, - }, - "qwen3-next-80b-a3b-thinking": { - id: "qwen3-next-80b-a3b-thinking", - name: "Qwen3-Next 80B-A3B (Thinking)", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09", - last_updated: "2025-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.144, output: 1.434 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen-mt-plus": { - id: "qwen-mt-plus", - name: "Qwen-MT Plus", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-04", - release_date: "2025-01", - last_updated: "2025-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.259, output: 0.775 }, - limit: { context: 16384, output: 8192 }, - }, - "deepseek-r1-distill-qwen-32b": { - id: "deepseek-r1-distill-qwen-32b", - name: "DeepSeek R1 Distill Qwen 32B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.287, output: 0.861 }, - limit: { context: 32768, output: 16384 }, - }, - "qwen-vl-max": { - id: "qwen-vl-max", - name: "Qwen-VL Max", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-04-08", - last_updated: "2025-08-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.23, output: 0.574 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen3-coder-flash": { - id: "qwen3-coder-flash", - name: "Qwen3 Coder Flash", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.144, output: 0.574 }, - limit: { context: 1000000, output: 65536 }, - }, - "deepseek-r1-distill-qwen-7b": { - id: "deepseek-r1-distill-qwen-7b", - name: "DeepSeek R1 Distill Qwen 7B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.072, output: 0.144 }, - limit: { context: 32768, output: 16384 }, - }, - "qwen2-5-7b-instruct": { - id: "qwen2-5-7b-instruct", - name: "Qwen2.5 7B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-09", - last_updated: "2024-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.072, output: 0.144 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen2-5-14b-instruct": { - id: "qwen2-5-14b-instruct", - name: "Qwen2.5 14B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-09", - last_updated: "2024-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.144, output: 0.431 }, - limit: { context: 131072, output: 8192 }, - }, - "tongyi-intent-detect-v3": { - id: "tongyi-intent-detect-v3", - name: "Tongyi Intent Detect V3", - family: "yi", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-04", - release_date: "2024-01", - last_updated: "2024-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.058, output: 0.144 }, - limit: { context: 8192, output: 1024 }, - }, - "qwq-32b": { - id: "qwq-32b", - name: "QwQ 32B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-12", - last_updated: "2024-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.287, output: 0.861 }, - limit: { context: 131072, output: 8192 }, - }, - "moonshot-kimi-k2-instruct": { - id: "moonshot-kimi-k2-instruct", - name: "Moonshot Kimi K2 Instruct", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.574, output: 2.294 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen2-5-32b-instruct": { - id: "qwen2-5-32b-instruct", - name: "Qwen2.5 32B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-09", - last_updated: "2024-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.287, output: 0.861 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen3-next-80b-a3b-instruct": { - id: "qwen3-next-80b-a3b-instruct", - name: "Qwen3-Next 80B-A3B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09", - last_updated: "2025-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.144, output: 0.574 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen3-omni-flash-realtime": { - id: "qwen3-omni-flash-realtime", - name: "Qwen3-Omni Flash Realtime", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-09-15", - last_updated: "2025-09-15", - modalities: { input: ["text", "image", "audio"], output: ["text", "audio"] }, - open_weights: false, - cost: { input: 0.23, output: 0.918, input_audio: 3.584, output_audio: 7.168 }, - limit: { context: 65536, output: 16384 }, - }, - "qwen3-vl-30b-a3b": { - id: "qwen3-vl-30b-a3b", - name: "Qwen3-VL 30B-A3B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.108, output: 0.431, reasoning: 1.076 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen3-vl-plus": { - id: "qwen3-vl-plus", - name: "Qwen3-VL Plus", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-23", - last_updated: "2025-09-23", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.143353, output: 1.433525, reasoning: 4.300576 }, - limit: { context: 262144, output: 32768 }, - }, - "deepseek-v3-2-exp": { - id: "deepseek-v3-2-exp", - name: "DeepSeek V3.2 Exp", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.287, output: 0.431 }, - limit: { context: 131072, output: 65536 }, - }, - "qwen3-coder-480b-a35b-instruct": { - id: "qwen3-coder-480b-a35b-instruct", - name: "Qwen3-Coder 480B-A35B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.861, output: 3.441 }, - limit: { context: 262144, output: 65536 }, - }, - "deepseek-r1-distill-qwen-1-5b": { - id: "deepseek-r1-distill-qwen-1-5b", - name: "DeepSeek R1 Distill Qwen 1.5B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 32768, output: 16384 }, - }, - "qwen3-coder-30b-a3b-instruct": { - id: "qwen3-coder-30b-a3b-instruct", - name: "Qwen3-Coder 30B-A3B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.216, output: 0.861 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen2-5-coder-7b-instruct": { - id: "qwen2-5-coder-7b-instruct", - name: "Qwen2.5-Coder 7B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-11", - last_updated: "2024-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.144, output: 0.287 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen-turbo": { - id: "qwen-turbo", - name: "Qwen Turbo", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-11-01", - last_updated: "2025-07-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.044, output: 0.087, reasoning: 0.431 }, - limit: { context: 1000000, output: 16384 }, - }, - "qwen-mt-turbo": { - id: "qwen-mt-turbo", - name: "Qwen-MT Turbo", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-04", - release_date: "2025-01", - last_updated: "2025-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.101, output: 0.28 }, - limit: { context: 16384, output: 8192 }, - }, - "qwen2-5-math-72b-instruct": { - id: "qwen2-5-math-72b-instruct", - name: "Qwen2.5-Math 72B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-09", - last_updated: "2024-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.574, output: 1.721 }, - limit: { context: 4096, output: 3072 }, - }, - "qwen2-5-omni-7b": { - id: "qwen2-5-omni-7b", - name: "Qwen2.5-Omni 7B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-12", - last_updated: "2024-12", - modalities: { input: ["text", "image", "audio", "video"], output: ["text", "audio"] }, - open_weights: true, - cost: { input: 0.087, output: 0.345, input_audio: 5.448 }, - limit: { context: 32768, output: 2048 }, - }, - "qwen3.5-plus": { - id: "qwen3.5-plus", - name: "Qwen3.5 Plus", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02-16", - last_updated: "2026-02-16", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.573, output: 3.44, reasoning: 3.44 }, - limit: { context: 1000000, output: 65536 }, - }, - "deepseek-r1-distill-qwen-14b": { - id: "deepseek-r1-distill-qwen-14b", - name: "DeepSeek R1 Distill Qwen 14B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.144, output: 0.431 }, - limit: { context: 32768, output: 16384 }, - }, - "qwen2-5-vl-72b-instruct": { - id: "qwen2-5-vl-72b-instruct", - name: "Qwen2.5-VL 72B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-09", - last_updated: "2024-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 2.294, output: 6.881 }, - limit: { context: 131072, output: 8192 }, - }, - "deepseek-v3": { - id: "deepseek-v3", - name: "DeepSeek V3", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.287, output: 1.147 }, - limit: { context: 65536, output: 8192 }, - }, - "deepseek-r1-0528": { - id: "deepseek-r1-0528", - name: "DeepSeek R1 0528", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-05-28", - last_updated: "2025-05-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.574, output: 2.294 }, - limit: { context: 131072, output: 16384 }, - }, - "qvq-max": { - id: "qvq-max", - name: "QVQ Max", - family: "qvq", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-03-25", - last_updated: "2025-03-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.147, output: 4.588 }, - limit: { context: 131072, output: 8192 }, - }, - "kimi-k2-thinking": { - id: "kimi-k2-thinking", - name: "Moonshot Kimi K2 Thinking", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-11-06", - last_updated: "2025-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.574, output: 2.294 }, - limit: { context: 262144, output: 16384 }, - }, - "qwen3-14b": { - id: "qwen3-14b", - name: "Qwen3 14B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.144, output: 0.574, reasoning: 1.434 }, - limit: { context: 131072, output: 8192 }, - }, - "deepseek-r1-distill-llama-8b": { - id: "deepseek-r1-distill-llama-8b", - name: "DeepSeek R1 Distill Llama 8B", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 32768, output: 16384 }, - }, - "qwen-long": { - id: "qwen-long", - name: "Qwen Long", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-01-25", - last_updated: "2025-01-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.072, output: 0.287 }, - limit: { context: 10000000, output: 8192 }, - }, - "kimi/kimi-k2.5": { - id: "kimi/kimi-k2.5", - name: "kimi/kimi-k2.5", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: false, - knowledge: "2025-01", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3, cache_read: 0.1 }, - limit: { context: 262144, output: 262144 }, - }, - "MiniMax/MiniMax-M2.7": { - id: "MiniMax/MiniMax-M2.7", - name: "MiniMax-M2.7", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.06, cache_write: 0.375 }, - limit: { context: 204800, output: 131072 }, - }, - "siliconflow/deepseek-v3-0324": { - id: "siliconflow/deepseek-v3-0324", - name: "siliconflow/deepseek-v3-0324", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-12-26", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1 }, - limit: { context: 163840, output: 163840 }, - }, - "siliconflow/deepseek-v3.2": { - id: "siliconflow/deepseek-v3.2", - name: "siliconflow/deepseek-v3.2", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-03", - last_updated: "2025-12-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 0.42 }, - limit: { context: 163840, output: 65536 }, - }, - "siliconflow/deepseek-r1-0528": { - id: "siliconflow/deepseek-r1-0528", - name: "siliconflow/deepseek-r1-0528", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-05-28", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 2.18 }, - limit: { context: 163840, output: 32768 }, - }, - "siliconflow/deepseek-v3.1-terminus": { - id: "siliconflow/deepseek-v3.1-terminus", - name: "siliconflow/deepseek-v3.1-terminus", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-29", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 1 }, - limit: { context: 163840, output: 65536 }, - }, - "qwen3-coder-plus": { - id: "qwen3-coder-plus", - name: "Qwen3 Coder Plus", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 5 }, - limit: { context: 1048576, output: 65536 }, - }, - }, - }, - "minimax-cn-coding-plan": { - id: "minimax-cn-coding-plan", - env: ["MINIMAX_API_KEY"], - npm: "@ai-sdk/anthropic", - api: "https://api.minimaxi.com/anthropic/v1", - name: "MiniMax Coding Plan (minimaxi.com)", - doc: "https://platform.minimaxi.com/docs/coding-plan/intro", - models: { - "MiniMax-M2": { - id: "MiniMax-M2", - name: "MiniMax-M2", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-10-27", - last_updated: "2025-10-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 196608, output: 128000 }, - }, - "MiniMax-M2.5": { - id: "MiniMax-M2.5", - name: "MiniMax-M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "MiniMax-M2.7": { - id: "MiniMax-M2.7", - name: "MiniMax-M2.7", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "MiniMax-M2.7-highspeed": { - id: "MiniMax-M2.7-highspeed", - name: "MiniMax-M2.7-highspeed", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "MiniMax-M2.1": { - id: "MiniMax-M2.1", - name: "MiniMax-M2.1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "MiniMax-M2.5-highspeed": { - id: "MiniMax-M2.5-highspeed", - name: "MiniMax-M2.5-highspeed", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-13", - last_updated: "2026-02-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - }, - }, - jiekou: { - id: "jiekou", - env: ["JIEKOU_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.jiekou.ai/openai", - name: "Jiekou.AI", - doc: "https://docs.jiekou.ai/docs/support/quickstart?utm_source=github_models.dev", - models: { - "gpt-5.1-codex-max": { - id: "gpt-5.1-codex-max", - name: "gpt-5.1-codex-max", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.125, output: 9 }, - limit: { context: 400000, output: 128000 }, - }, - "grok-4-1-fast-reasoning": { - id: "grok-4-1-fast-reasoning", - name: "grok-4-1-fast-reasoning", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.18, output: 0.45 }, - limit: { context: 2000000, output: 2000000 }, - }, - "claude-opus-4-5-20251101": { - id: "claude-opus-4-5-20251101", - name: "claude-opus-4-5-20251101", - family: "claude-opus", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 4.5, output: 22.5 }, - limit: { context: 200000, output: 65536 }, - }, - "gemini-2.5-flash-lite-preview-09-2025": { - id: "gemini-2.5-flash-lite-preview-09-2025", - name: "gemini-2.5-flash-lite-preview-09-2025", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.09, output: 0.36 }, - limit: { context: 1048576, output: 65536 }, - }, - "gpt-5.2-pro": { - id: "gpt-5.2-pro", - name: "gpt-5.2-pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 18.9, output: 151.2 }, - limit: { context: 400000, output: 128000 }, - }, - "gemini-3-flash-preview": { - id: "gemini-3-flash-preview", - name: "gemini-3-flash-preview", - family: "gemini-flash", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 3 }, - limit: { context: 1048576, output: 65536 }, - }, - "gpt-5-mini": { - id: "gpt-5-mini", - name: "gpt-5-mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.225, output: 1.8 }, - limit: { context: 400000, output: 128000 }, - }, - "gpt-5-nano": { - id: "gpt-5-nano", - name: "gpt-5-nano", - family: "gpt-nano", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.045, output: 0.36 }, - limit: { context: 400000, output: 128000 }, - }, - "gemini-3-pro-preview": { - id: "gemini-3-pro-preview", - name: "gemini-3-pro-preview", - family: "gemini-pro", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 1.8, output: 10.8 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-2.5-flash-preview-05-20": { - id: "gemini-2.5-flash-preview-05-20", - name: "gemini-2.5-flash-preview-05-20", - family: "gemini-flash", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.135, output: 3.15 }, - limit: { context: 1048576, output: 200000 }, - }, - "claude-sonnet-4-5-20250929": { - id: "claude-sonnet-4-5-20250929", - name: "claude-sonnet-4-5-20250929", - family: "claude-sonnet", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.7, output: 13.5 }, - limit: { context: 200000, output: 64000 }, - }, - "gemini-2.5-pro": { - id: "gemini-2.5-pro", - name: "gemini-2.5-pro", - family: "gemini-pro", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 1.125, output: 9 }, - limit: { context: 1048576, output: 65535 }, - }, - "grok-4-1-fast-non-reasoning": { - id: "grok-4-1-fast-non-reasoning", - name: "grok-4-1-fast-non-reasoning", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.18, output: 0.45 }, - limit: { context: 2000000, output: 2000000 }, - }, - "gpt-5.2": { - id: "gpt-5.2", - name: "gpt-5.2", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.575, output: 12.6 }, - limit: { context: 400000, output: 128000 }, - }, - "o4-mini": { - id: "o4-mini", - name: "o4-mini", - family: "o", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4 }, - limit: { context: 200000, output: 100000 }, - }, - "gemini-2.5-pro-preview-06-05": { - id: "gemini-2.5-pro-preview-06-05", - name: "gemini-2.5-pro-preview-06-05", - family: "gemini-pro", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 1.125, output: 9 }, - limit: { context: 1048576, output: 200000 }, - }, - "gemini-2.5-flash-lite-preview-06-17": { - id: "gemini-2.5-flash-lite-preview-06-17", - name: "gemini-2.5-flash-lite-preview-06-17", - family: "gemini-flash-lite", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "video", "image", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.09, output: 0.36 }, - limit: { context: 1048576, output: 65535 }, - }, - "gpt-5.2-codex": { - id: "gpt-5.2-codex", - name: "gpt-5.2-codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14 }, - limit: { context: 400000, output: 128000 }, - }, - "gemini-2.5-flash": { - id: "gemini-2.5-flash", - name: "gemini-2.5-flash", - family: "gemini-flash", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 2.25 }, - limit: { context: 1048576, output: 65535 }, - }, - "gpt-5.1-codex-mini": { - id: "gpt-5.1-codex-mini", - name: "gpt-5.1-codex-mini", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.225, output: 1.8 }, - limit: { context: 400000, output: 128000 }, - }, - "grok-code-fast-1": { - id: "grok-code-fast-1", - name: "grok-code-fast-1", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.18, output: 1.35 }, - limit: { context: 256000, output: 256000 }, - }, - "gpt-5.1": { - id: "gpt-5.1", - name: "gpt-5.1", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02", - last_updated: "2026-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.125, output: 9 }, - limit: { context: 400000, output: 128000 }, - }, - "grok-4-fast-reasoning": { - id: "grok-4-fast-reasoning", - name: "grok-4-fast-reasoning", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.18, output: 0.45 }, - limit: { context: 2000000, output: 2000000 }, - }, - "o3-mini": { - id: "o3-mini", - name: "o3-mini", - family: "o", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4 }, - limit: { context: 131072, output: 131072 }, - }, - "grok-4-0709": { - id: "grok-4-0709", - name: "grok-4-0709", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.7, output: 13.5 }, - limit: { context: 256000, output: 8192 }, - }, - "gpt-5-codex": { - id: "gpt-5-codex", - name: "gpt-5-codex", - family: "gpt-codex", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.125, output: 9 }, - limit: { context: 400000, output: 128000 }, - }, - "claude-opus-4-1-20250805": { - id: "claude-opus-4-1-20250805", - name: "claude-opus-4-1-20250805", - family: "claude-opus", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 13.5, output: 67.5 }, - limit: { context: 200000, output: 32000 }, - }, - "claude-haiku-4-5-20251001": { - id: "claude-haiku-4-5-20251001", - name: "claude-haiku-4-5-20251001", - family: "claude-haiku", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.9, output: 4.5 }, - limit: { context: 20000, output: 64000 }, - }, - "claude-sonnet-4-20250514": { - id: "claude-sonnet-4-20250514", - name: "claude-sonnet-4-20250514", - family: "claude-sonnet", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.7, output: 13.5 }, - limit: { context: 200000, output: 64000 }, - }, - "claude-opus-4-6": { - id: "claude-opus-4-6", - name: "claude-opus-4-6", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02", - last_updated: "2026-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25 }, - limit: { context: 1000000, output: 128000 }, - }, - o3: { - id: "o3", - name: "o3", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 10, output: 40 }, - limit: { context: 131072, output: 131072 }, - }, - "gpt-5-pro": { - id: "gpt-5-pro", - name: "gpt-5-pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 13.5, output: 108 }, - limit: { context: 400000, output: 272000 }, - }, - "gemini-2.5-flash-lite": { - id: "gemini-2.5-flash-lite", - name: "gemini-2.5-flash-lite", - family: "gemini-flash-lite", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.09, output: 0.36 }, - limit: { context: 1048576, output: 65535 }, - }, - "gpt-5-chat-latest": { - id: "gpt-5-chat-latest", - name: "gpt-5-chat-latest", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.125, output: 9 }, - limit: { context: 400000, output: 128000 }, - }, - "claude-opus-4-20250514": { - id: "claude-opus-4-20250514", - name: "claude-opus-4-20250514", - family: "claude-opus", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 13.5, output: 67.5 }, - limit: { context: 200000, output: 32000 }, - }, - "gpt-5.1-codex": { - id: "gpt-5.1-codex", - name: "gpt-5.1-codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.125, output: 9 }, - limit: { context: 400000, output: 128000 }, - }, - "grok-4-fast-non-reasoning": { - id: "grok-4-fast-non-reasoning", - name: "grok-4-fast-non-reasoning", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.18, output: 0.45 }, - limit: { context: 2000000, output: 2000000 }, - }, - "deepseek/deepseek-v3-0324": { - id: "deepseek/deepseek-v3-0324", - name: "DeepSeek V3 0324", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.28, output: 1.14 }, - limit: { context: 163840, output: 163840 }, - }, - "deepseek/deepseek-v3.1": { - id: "deepseek/deepseek-v3.1", - name: "DeepSeek V3.1", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 1 }, - limit: { context: 163840, output: 32768 }, - }, - "deepseek/deepseek-r1-0528": { - id: "deepseek/deepseek-r1-0528", - name: "DeepSeek R1 0528", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.7, output: 2.5 }, - limit: { context: 163840, output: 32768 }, - }, - "zai-org/glm-4.7": { - id: "zai-org/glm-4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2 }, - limit: { context: 204800, output: 131072 }, - }, - "zai-org/glm-4.5": { - id: "zai-org/glm-4.5", - name: "GLM-4.5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2 }, - limit: { context: 131072, output: 98304 }, - }, - "zai-org/glm-4.5v": { - id: "zai-org/glm-4.5v", - name: "GLM 4.5V", - family: "glmv", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 1.8 }, - limit: { context: 65536, output: 16384 }, - }, - "zai-org/glm-4.7-flash": { - id: "zai-org/glm-4.7-flash", - name: "GLM-4.7-Flash", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.4 }, - limit: { context: 200000, output: 128000 }, - }, - "minimaxai/minimax-m1-80k": { - id: "minimaxai/minimax-m1-80k", - name: "MiniMax M1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 2.2 }, - limit: { context: 1000000, output: 40000 }, - }, - "xiaomimimo/mimo-v2-flash": { - id: "xiaomimimo/mimo-v2-flash", - name: "XiaomiMiMo/MiMo-V2-Flash", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 131072 }, - }, - "baidu/ernie-4.5-vl-424b-a47b": { - id: "baidu/ernie-4.5-vl-424b-a47b", - name: "ERNIE 4.5 VL 424B A47B", - family: "ernie", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.42, output: 1.25 }, - limit: { context: 123000, output: 16000 }, - }, - "baidu/ernie-4.5-300b-a47b-paddle": { - id: "baidu/ernie-4.5-300b-a47b-paddle", - name: "ERNIE 4.5 300B A47B", - family: "ernie", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.28, output: 1.1 }, - limit: { context: 123000, output: 12000 }, - }, - "minimax/minimax-m2.1": { - id: "minimax/minimax-m2.1", - name: "Minimax M2.1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 204800, output: 131072 }, - }, - "qwen/qwen3-235b-a22b-instruct-2507": { - id: "qwen/qwen3-235b-a22b-instruct-2507", - name: "Qwen3 235B A22B Instruct 2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.8 }, - limit: { context: 131072, output: 16384 }, - }, - "qwen/qwen3-32b-fp8": { - id: "qwen/qwen3-32b-fp8", - name: "Qwen3 32B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.45 }, - limit: { context: 40960, output: 20000 }, - }, - "qwen/qwen3-235b-a22b-thinking-2507": { - id: "qwen/qwen3-235b-a22b-thinking-2507", - name: "Qwen3 235B A22b Thinking 2507", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 3 }, - limit: { context: 131072, output: 131072 }, - }, - "qwen/qwen3-next-80b-a3b-thinking": { - id: "qwen/qwen3-next-80b-a3b-thinking", - name: "Qwen3 Next 80B A3B Thinking", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 1.5 }, - limit: { context: 65536, output: 65536 }, - }, - "qwen/qwen3-next-80b-a3b-instruct": { - id: "qwen/qwen3-next-80b-a3b-instruct", - name: "Qwen3 Next 80B A3B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 1.5 }, - limit: { context: 65536, output: 65536 }, - }, - "qwen/qwen3-30b-a3b-fp8": { - id: "qwen/qwen3-30b-a3b-fp8", - name: "Qwen3 30B A3B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.09, output: 0.45 }, - limit: { context: 40960, output: 20000 }, - }, - "qwen/qwen3-coder-next": { - id: "qwen/qwen3-coder-next", - name: "qwen/qwen3-coder-next", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02", - last_updated: "2026-02", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 1.5 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen/qwen3-coder-480b-a35b-instruct": { - id: "qwen/qwen3-coder-480b-a35b-instruct", - name: "Qwen3 Coder 480B A35B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.29, output: 1.2 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen/qwen3-235b-a22b-fp8": { - id: "qwen/qwen3-235b-a22b-fp8", - name: "Qwen3 235B A22B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.8 }, - limit: { context: 40960, output: 20000 }, - }, - "moonshotai/kimi-k2.5": { - id: "moonshotai/kimi-k2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3 }, - limit: { context: 262144, output: 262144 }, - }, - "moonshotai/kimi-k2-instruct": { - id: "moonshotai/kimi-k2-instruct", - name: "Kimi K2 Instruct", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.57, output: 2.3 }, - limit: { context: 131072, output: 131072 }, - }, - "moonshotai/kimi-k2-0905": { - id: "moonshotai/kimi-k2-0905", - name: "Kimi K2 0905", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.5 }, - limit: { context: 262144, output: 262144 }, - }, - }, - }, - bailing: { - id: "bailing", - env: ["BAILING_API_TOKEN"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.tbox.cn/api/llm/v1/chat/completions", - name: "Bailing", - doc: "https://alipaytbox.yuque.com/sxs0ba/ling/intro", - models: { - "Ring-1T": { - id: "Ring-1T", - name: "Ring-1T", - family: "ring", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2024-06", - release_date: "2025-10", - last_updated: "2025-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.57, output: 2.29 }, - limit: { context: 128000, output: 32000 }, - }, - "Ling-1T": { - id: "Ling-1T", - name: "Ling-1T", - family: "ling", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-06", - release_date: "2025-10", - last_updated: "2025-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.57, output: 2.29 }, - limit: { context: 128000, output: 32000 }, - }, - }, - }, - iflowcn: { - id: "iflowcn", - env: ["IFLOW_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://apis.iflow.cn/v1", - name: "iFlow", - doc: "https://platform.iflow.cn/en/docs", - models: { - "qwen3-coder-plus": { - id: "qwen3-coder-plus", - name: "Qwen3-Coder-Plus", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-01", - last_updated: "2025-07-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 256000, output: 64000 }, - }, - "qwen3-32b": { - id: "qwen3-32b", - name: "Qwen3-32B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 32000 }, - }, - "deepseek-r1": { - id: "deepseek-r1", - name: "DeepSeek-R1", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 32000 }, - }, - "qwen3-max": { - id: "qwen3-max", - name: "Qwen3-Max", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 256000, output: 32000 }, - }, - "qwen3-235b-a22b-instruct": { - id: "qwen3-235b-a22b-instruct", - name: "Qwen3-235B-A22B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-01", - last_updated: "2025-07-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 256000, output: 64000 }, - }, - "qwen3-235b-a22b-thinking-2507": { - id: "qwen3-235b-a22b-thinking-2507", - name: "Qwen3-235B-A22B-Thinking", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-01", - last_updated: "2025-07-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 256000, output: 64000 }, - }, - "kimi-k2-0905": { - id: "kimi-k2-0905", - name: "Kimi-K2-0905", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 256000, output: 64000 }, - }, - "glm-4.6": { - id: "glm-4.6", - name: "GLM-4.6", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-12-01", - last_updated: "2025-11-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 200000, output: 128000 }, - }, - "qwen3-vl-plus": { - id: "qwen3-vl-plus", - name: "Qwen3-VL-Plus", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 256000, output: 32000 }, - }, - "deepseek-v3.2": { - id: "deepseek-v3.2", - name: "DeepSeek-V3.2-Exp", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 64000 }, - }, - "qwen3-235b": { - id: "qwen3-235b", - name: "Qwen3-235B-A22B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 32000 }, - }, - "kimi-k2": { - id: "kimi-k2", - name: "Kimi-K2", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 64000 }, - }, - "qwen3-max-preview": { - id: "qwen3-max-preview", - name: "Qwen3-Max-Preview", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 256000, output: 32000 }, - }, - "deepseek-v3": { - id: "deepseek-v3", - name: "DeepSeek-V3", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-12-26", - last_updated: "2024-12-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 32000 }, - }, - }, - }, - v0: { - id: "v0", - env: ["V0_API_KEY"], - npm: "@ai-sdk/vercel", - name: "v0", - doc: "https://sdk.vercel.ai/providers/ai-sdk-providers/vercel", - models: { - "v0-1.5-lg": { - id: "v0-1.5-lg", - name: "v0-1.5-lg", - family: "v0", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-06-09", - last_updated: "2025-06-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75 }, - limit: { context: 512000, output: 32000 }, - }, - "v0-1.0-md": { - id: "v0-1.0-md", - name: "v0-1.0-md", - family: "v0", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 128000, output: 32000 }, - }, - "v0-1.5-md": { - id: "v0-1.5-md", - name: "v0-1.5-md", - family: "v0", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-06-09", - last_updated: "2025-06-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 128000, output: 32000 }, - }, - }, - }, - huggingface: { - id: "huggingface", - env: ["HF_TOKEN"], - npm: "@ai-sdk/openai-compatible", - api: "https://router.huggingface.co/v1", - name: "Hugging Face", - doc: "https://huggingface.co/docs/inference-providers", - models: { - "Qwen/Qwen3.5-397B-A17B": { - id: "Qwen/Qwen3.5-397B-A17B", - name: "Qwen3.5-397B-A17B", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02-01", - last_updated: "2026-02-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3.6 }, - limit: { context: 262144, output: 32768 }, - }, - "Qwen/Qwen3-Coder-Next": { - id: "Qwen/Qwen3-Coder-Next", - name: "Qwen3-Coder-Next", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02-03", - last_updated: "2026-02-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 1.5 }, - limit: { context: 262144, output: 65536 }, - }, - "Qwen/Qwen3-Next-80B-A3B-Instruct": { - id: "Qwen/Qwen3-Next-80B-A3B-Instruct", - name: "Qwen3-Next-80B-A3B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-11", - last_updated: "2025-09-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 1 }, - limit: { context: 262144, output: 66536 }, - }, - "Qwen/Qwen3-Embedding-8B": { - id: "Qwen/Qwen3-Embedding-8B", - name: "Qwen 3 Embedding 8B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2024-12", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.01, output: 0 }, - limit: { context: 32000, output: 4096 }, - }, - "Qwen/Qwen3-235B-A22B-Thinking-2507": { - id: "Qwen/Qwen3-235B-A22B-Thinking-2507", - name: "Qwen3-235B-A22B-Thinking-2507", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-25", - last_updated: "2025-07-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 3 }, - limit: { context: 262144, output: 131072 }, - }, - "Qwen/Qwen3-Next-80B-A3B-Thinking": { - id: "Qwen/Qwen3-Next-80B-A3B-Thinking", - name: "Qwen3-Next-80B-A3B-Thinking", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-11", - last_updated: "2025-09-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 2 }, - limit: { context: 262144, output: 131072 }, - }, - "Qwen/Qwen3-Embedding-4B": { - id: "Qwen/Qwen3-Embedding-4B", - name: "Qwen 3 Embedding 4B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2024-12", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.01, output: 0 }, - limit: { context: 32000, output: 2048 }, - }, - "Qwen/Qwen3-Coder-480B-A35B-Instruct": { - id: "Qwen/Qwen3-Coder-480B-A35B-Instruct", - name: "Qwen3-Coder-480B-A35B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2, output: 2 }, - limit: { context: 262144, output: 66536 }, - }, - "zai-org/GLM-4.7-Flash": { - id: "zai-org/GLM-4.7-Flash", - name: "GLM-4.7-Flash", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-08-08", - last_updated: "2025-08-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 200000, output: 128000 }, - }, - "zai-org/GLM-4.7": { - id: "zai-org/GLM-4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2, cache_read: 0.11 }, - limit: { context: 204800, output: 131072 }, - }, - "zai-org/GLM-5.1": { - id: "zai-org/GLM-5.1", - name: "GLM-5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-04-03", - last_updated: "2026-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3.2, cache_read: 0.2 }, - limit: { context: 202752, output: 131072 }, - }, - "zai-org/GLM-5": { - id: "zai-org/GLM-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3.2, cache_read: 0.2 }, - limit: { context: 202752, output: 131072 }, - }, - "XiaomiMiMo/MiMo-V2-Flash": { - id: "XiaomiMiMo/MiMo-V2-Flash", - name: "MiMo-V2-Flash", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-12-16", - last_updated: "2025-12-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 262144, output: 4096 }, - }, - "deepseek-ai/DeepSeek-R1-0528": { - id: "deepseek-ai/DeepSeek-R1-0528", - name: "DeepSeek-R1-0528", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-05", - release_date: "2025-05-28", - last_updated: "2025-05-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 3, output: 5 }, - limit: { context: 163840, output: 163840 }, - }, - "deepseek-ai/DeepSeek-V3.2": { - id: "deepseek-ai/DeepSeek-V3.2", - name: "DeepSeek-V3.2", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.28, output: 0.4 }, - limit: { context: 163840, output: 65536 }, - }, - "moonshotai/Kimi-K2-Thinking": { - id: "moonshotai/Kimi-K2-Thinking", - name: "Kimi-K2-Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-08", - release_date: "2025-11-06", - last_updated: "2025-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.5, cache_read: 0.15 }, - limit: { context: 262144, output: 262144 }, - }, - "moonshotai/Kimi-K2-Instruct": { - id: "moonshotai/Kimi-K2-Instruct", - name: "Kimi-K2-Instruct", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-07-14", - last_updated: "2025-07-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3 }, - limit: { context: 131072, output: 16384 }, - }, - "moonshotai/Kimi-K2-Instruct-0905": { - id: "moonshotai/Kimi-K2-Instruct-0905", - name: "Kimi-K2-Instruct-0905", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-09-04", - last_updated: "2025-09-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3 }, - limit: { context: 262144, output: 16384 }, - }, - "moonshotai/Kimi-K2.5": { - id: "moonshotai/Kimi-K2.5", - name: "Kimi-K2.5", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-01", - release_date: "2026-01-01", - last_updated: "2026-01-01", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3, cache_read: 0.1 }, - limit: { context: 262144, output: 262144 }, - }, - "MiniMaxAI/MiniMax-M2.5": { - id: "MiniMaxAI/MiniMax-M2.5", - name: "MiniMax-M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.03 }, - limit: { context: 204800, output: 131072 }, - }, - "MiniMaxAI/MiniMax-M2.1": { - id: "MiniMaxAI/MiniMax-M2.1", - name: "MiniMax-M2.1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-10", - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 204800, output: 131072 }, - }, - }, - }, - zenmux: { - id: "zenmux", - env: ["ZENMUX_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://zenmux.ai/api/v1", - name: "ZenMux", - doc: "https://docs.zenmux.ai", - models: { - "deepseek/deepseek-chat": { - id: "deepseek/deepseek-chat", - name: "DeepSeek-V3.2 (Non-thinking Mode)", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.28, output: 0.42, cache_read: 0.03 }, - limit: { context: 128000, output: 64000 }, - }, - "deepseek/deepseek-v3.2-exp": { - id: "deepseek/deepseek-v3.2-exp", - name: "DeepSeek-V3.2-Exp", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.22, output: 0.33 }, - limit: { context: 163000, output: 64000 }, - }, - "deepseek/deepseek-v3.2": { - id: "deepseek/deepseek-v3.2", - name: "DeepSeek V3.2", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-12-05", - last_updated: "2025-12-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.28, output: 0.43 }, - limit: { context: 128000, output: 64000 }, - }, - "inclusionai/ring-1t": { - id: "inclusionai/ring-1t", - name: "Ring-1T", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-10-12", - last_updated: "2025-10-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.56, output: 2.24, cache_read: 0.11 }, - limit: { context: 128000, output: 64000 }, - }, - "inclusionai/ling-1t": { - id: "inclusionai/ling-1t", - name: "Ling-1T", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-10-09", - last_updated: "2025-10-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.56, output: 2.24, cache_read: 0.11 }, - limit: { context: 128000, output: 64000 }, - }, - "stepfun/step-3.5-flash-free": { - id: "stepfun/step-3.5-flash-free", - name: "Step 3.5 Flash (Free)", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2026-02-02", - last_updated: "2026-02-02", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 256000, output: 64000 }, - }, - "stepfun/step-3.5-flash": { - id: "stepfun/step-3.5-flash", - name: "Step 3.5 Flash", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2026-02-02", - last_updated: "2026-02-02", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 256000, output: 64000 }, - }, - "stepfun/step-3": { - id: "stepfun/step-3", - name: "Step-3", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-07-31", - last_updated: "2025-07-31", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.21, output: 0.57 }, - limit: { context: 65536, output: 64000 }, - }, - "kuaishou/kat-coder-pro-v2": { - id: "kuaishou/kat-coder-pro-v2", - name: "KAT-Coder-Pro-V2", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2026-03-30", - last_updated: "2026-03-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2, cache_read: 0.06 }, - limit: { context: 256000, output: 80000 }, - }, - "x-ai/grok-4-fast": { - id: "x-ai/grok-4-fast", - name: "Grok 4 Fast", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-09-19", - last_updated: "2025-09-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 64000 }, - }, - "x-ai/grok-code-fast-1": { - id: "x-ai/grok-code-fast-1", - name: "Grok Code Fast 1", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-08-26", - last_updated: "2025-08-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.5, cache_read: 0.02 }, - limit: { context: 256000, output: 64000 }, - }, - "x-ai/grok-4.1-fast-non-reasoning": { - id: "x-ai/grok-4.1-fast-non-reasoning", - name: "Grok 4.1 Fast Non Reasoning", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-11-20", - last_updated: "2025-11-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 64000 }, - }, - "x-ai/grok-4": { - id: "x-ai/grok-4", - name: "Grok 4", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.75 }, - limit: { context: 256000, output: 64000 }, - }, - "x-ai/grok-4.1-fast": { - id: "x-ai/grok-4.1-fast", - name: "Grok 4.1 Fast", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-11-20", - last_updated: "2025-11-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 64000 }, - }, - "x-ai/grok-4.2-fast": { - id: "x-ai/grok-4.2-fast", - name: "Grok 4.2 Fast", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-03-20", - last_updated: "2026-03-20", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 9 }, - limit: { context: 2000000, output: 30000 }, - }, - "x-ai/grok-4.2-fast-non-reasoning": { - id: "x-ai/grok-4.2-fast-non-reasoning", - name: "Grok 4.2 Fast Non Reasoning", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-03-20", - last_updated: "2026-03-20", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 9 }, - limit: { context: 2000000, output: 30000 }, - }, - "openai/gpt-5.3-chat": { - id: "openai/gpt-5.3-chat", - name: "GPT-5.3 Chat", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-03-20", - last_updated: "2026-03-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14 }, - limit: { context: 128000, output: 16380 }, - provider: { npm: "@ai-sdk/openai", api: "https://zenmux.ai/api/v1" }, - }, - "openai/gpt-5.2-pro": { - id: "openai/gpt-5.2-pro", - name: "GPT-5.2-Pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 21, output: 168 }, - limit: { context: 400000, output: 128000 }, - provider: { npm: "@ai-sdk/openai", api: "https://zenmux.ai/api/v1" }, - }, - "openai/gpt-5.3-codex": { - id: "openai/gpt-5.3-codex", - name: "GPT-5.3 Codex", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-03-20", - last_updated: "2026-03-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14 }, - limit: { context: 400000, output: 128000 }, - provider: { npm: "@ai-sdk/openai", api: "https://zenmux.ai/api/v1" }, - }, - "openai/gpt-5.2": { - id: "openai/gpt-5.2", - name: "GPT-5.2", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2025-01-01", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["image", "text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.17 }, - limit: { context: 400000, output: 64000 }, - provider: { npm: "@ai-sdk/openai", api: "https://zenmux.ai/api/v1" }, - }, - "openai/gpt-5.4-mini": { - id: "openai/gpt-5.4-mini", - name: "GPT-5.4 Mini", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-03-20", - last_updated: "2026-03-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.75, output: 4.5 }, - limit: { context: 400000, output: 128000 }, - provider: { npm: "@ai-sdk/openai", api: "https://zenmux.ai/api/v1" }, - }, - "openai/gpt-5.1-chat": { - id: "openai/gpt-5.1-chat", - name: "GPT-5.1 Chat", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["pdf", "image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.12 }, - limit: { context: 128000, output: 64000 }, - provider: { npm: "@ai-sdk/openai", api: "https://zenmux.ai/api/v1" }, - }, - "openai/gpt-5.4-nano": { - id: "openai/gpt-5.4-nano", - name: "GPT-5.4 Nano", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-03-20", - last_updated: "2026-03-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.25 }, - limit: { context: 400000, output: 128000 }, - provider: { npm: "@ai-sdk/openai", api: "https://zenmux.ai/api/v1" }, - }, - "openai/gpt-5.2-codex": { - id: "openai/gpt-5.2-codex", - name: "GPT-5.2-Codex", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2025-01-01", - release_date: "2026-01-15", - last_updated: "2026-01-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.17 }, - limit: { context: 400000, output: 64000 }, - provider: { npm: "@ai-sdk/openai", api: "https://zenmux.ai/api/v1" }, - }, - "openai/gpt-5.1-codex-mini": { - id: "openai/gpt-5.1-codex-mini", - name: "GPT-5.1-Codex-Mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.03 }, - limit: { context: 400000, output: 64000 }, - provider: { npm: "@ai-sdk/openai", api: "https://zenmux.ai/api/v1" }, - }, - "openai/gpt-5.1": { - id: "openai/gpt-5.1", - name: "GPT-5.1", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["image", "text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.12 }, - limit: { context: 400000, output: 64000 }, - provider: { npm: "@ai-sdk/openai", api: "https://zenmux.ai/api/v1" }, - }, - "openai/gpt-5.4-pro": { - id: "openai/gpt-5.4-pro", - name: "GPT-5.4 Pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-03-20", - last_updated: "2026-03-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 45, output: 225 }, - limit: { context: 1050000, output: 128000 }, - provider: { npm: "@ai-sdk/openai", api: "https://zenmux.ai/api/v1" }, - }, - "openai/gpt-5-codex": { - id: "openai/gpt-5-codex", - name: "GPT-5 Codex", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-09-23", - last_updated: "2025-09-23", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.12 }, - limit: { context: 400000, output: 64000 }, - provider: { npm: "@ai-sdk/openai", api: "https://zenmux.ai/api/v1" }, - }, - "openai/gpt-5.4": { - id: "openai/gpt-5.4", - name: "GPT-5.4", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-03-20", - last_updated: "2026-03-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3.75, output: 18.75 }, - limit: { context: 1050000, output: 128000 }, - provider: { npm: "@ai-sdk/openai", api: "https://zenmux.ai/api/v1" }, - }, - "openai/gpt-5": { - id: "openai/gpt-5", - name: "GPT-5", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.12 }, - limit: { context: 400000, output: 64000 }, - provider: { npm: "@ai-sdk/openai", api: "https://zenmux.ai/api/v1" }, - }, - "openai/gpt-5.1-codex": { - id: "openai/gpt-5.1-codex", - name: "GPT-5.1-Codex", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.12 }, - limit: { context: 400000, output: 64000 }, - provider: { npm: "@ai-sdk/openai", api: "https://zenmux.ai/api/v1" }, - }, - "z-ai/glm-4.7-flash-free": { - id: "z-ai/glm-4.7-flash-free", - name: "GLM 4.7 Flash (Free)", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-01-01", - release_date: "2026-01-19", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 200000, output: 64000 }, - }, - "z-ai/glm-5v-turbo": { - id: "z-ai/glm-5v-turbo", - name: "GLM 5V Turbo", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-04-01", - last_updated: "2026-04-01", - modalities: { input: ["text", "image", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.726, output: 3.1946, cache_read: 0.1743 }, - limit: { context: 200000, output: 128000 }, - }, - "z-ai/glm-4.7": { - id: "z-ai/glm-4.7", - name: "GLM 4.7", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.28, output: 1.14, cache_read: 0.06 }, - limit: { context: 200000, output: 64000 }, - }, - "z-ai/glm-5": { - id: "z-ai/glm-5", - name: "GLM 5", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-01-01", - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.58, output: 2.6, cache_read: 0.14 }, - limit: { context: 200000, output: 128000 }, - }, - "z-ai/glm-4.7-flashx": { - id: "z-ai/glm-4.7-flashx", - name: "GLM 4.7 FlashX", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-01-01", - release_date: "2026-01-19", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.07, output: 0.42, cache_read: 0.01 }, - limit: { context: 200000, output: 64000 }, - }, - "z-ai/glm-4.6v-flash-free": { - id: "z-ai/glm-4.6v-flash-free", - name: "GLM 4.6V Flash (Free)", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-12-08", - last_updated: "2025-12-08", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 200000, output: 64000 }, - }, - "z-ai/glm-5.1": { - id: "z-ai/glm-5.1", - name: "GLM-5.1", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-04-03", - last_updated: "2026-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8781, output: 3.5126, cache_read: 0.1903 }, - limit: { context: 200000, output: 131072 }, - }, - "z-ai/glm-4.6v-flash": { - id: "z-ai/glm-4.6v-flash", - name: "GLM 4.6V FlashX", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-12-08", - last_updated: "2025-12-08", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.02, output: 0.21, cache_read: 0.0043 }, - limit: { context: 200000, output: 64000 }, - }, - "z-ai/glm-4.5": { - id: "z-ai/glm-4.5", - name: "GLM 4.5", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-07-25", - last_updated: "2025-07-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.35, output: 1.54, cache_read: 0.07 }, - limit: { context: 128000, output: 64000 }, - }, - "z-ai/glm-4.5-air": { - id: "z-ai/glm-4.5-air", - name: "GLM 4.5 Air", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-07-25", - last_updated: "2025-07-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.11, output: 0.56, cache_read: 0.02 }, - limit: { context: 128000, output: 64000 }, - }, - "z-ai/glm-5-turbo": { - id: "z-ai/glm-5-turbo", - name: "GLM 5 Turbo", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2026-03-20", - last_updated: "2026-03-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.88, output: 3.48 }, - limit: { context: 200000, output: 128000 }, - }, - "z-ai/glm-4.6": { - id: "z-ai/glm-4.6", - name: "GLM 4.6", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.35, output: 1.54, cache_read: 0.07 }, - limit: { context: 200000, output: 64000 }, - }, - "z-ai/glm-4.6v": { - id: "z-ai/glm-4.6v", - name: "GLM 4.6V", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-12-08", - last_updated: "2025-12-08", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.42, cache_read: 0.03 }, - limit: { context: 200000, output: 64000 }, - }, - "volcengine/doubao-seed-2.0-code": { - id: "volcengine/doubao-seed-2.0-code", - name: "Doubao Seed 2.0 Code", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2026-03-20", - last_updated: "2026-03-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.9, output: 4.48 }, - limit: { context: 256000, output: 32000 }, - }, - "volcengine/doubao-seed-code": { - id: "volcengine/doubao-seed-code", - name: "Doubao-Seed-Code", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-11-11", - last_updated: "2025-11-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.17, output: 1.12, cache_read: 0.03 }, - limit: { context: 256000, output: 64000 }, - }, - "volcengine/doubao-seed-2.0-mini": { - id: "volcengine/doubao-seed-2.0-mini", - name: "Doubao-Seed-2.0-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2026-02-14", - release_date: "2026-02-14", - last_updated: "2026-02-14", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.03, output: 0.28, cache_read: 0.01, cache_write: 0.0024 }, - limit: { context: 256000, output: 64000 }, - }, - "volcengine/doubao-seed-2.0-lite": { - id: "volcengine/doubao-seed-2.0-lite", - name: "Doubao-Seed-2.0-lite", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2026-02-14", - release_date: "2026-02-14", - last_updated: "2026-02-14", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.09, output: 0.51, cache_read: 0.02, cache_write: 0.0024 }, - limit: { context: 256000, output: 64000 }, - }, - "volcengine/doubao-seed-1.8": { - id: "volcengine/doubao-seed-1.8", - name: "Doubao-Seed-1.8", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-12-18", - last_updated: "2025-12-18", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.11, output: 0.28, cache_read: 0.02, cache_write: 0.0024 }, - limit: { context: 256000, output: 64000 }, - }, - "volcengine/doubao-seed-2.0-pro": { - id: "volcengine/doubao-seed-2.0-pro", - name: "Doubao-Seed-2.0-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2026-02-14", - release_date: "2026-02-14", - last_updated: "2026-02-14", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.45, output: 2.24, cache_read: 0.09, cache_write: 0.0024 }, - limit: { context: 256000, output: 64000 }, - }, - "baidu/ernie-5.0-thinking-preview": { - id: "baidu/ernie-5.0-thinking-preview", - name: "ERNIE 5.0", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2026-01-22", - last_updated: "2026-01-22", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.84, output: 3.37 }, - limit: { context: 128000, output: 64000 }, - }, - "minimax/minimax-m2.7": { - id: "minimax/minimax-m2.7", - name: "MiniMax M2.7", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2026-03-20", - last_updated: "2026-03-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3055, output: 1.2219 }, - limit: { context: 204800, output: 131070 }, - provider: { npm: "@ai-sdk/anthropic", api: "https://zenmux.ai/api/anthropic/v1" }, - }, - "minimax/minimax-m2.7-highspeed": { - id: "minimax/minimax-m2.7-highspeed", - name: "MiniMax M2.7 highspeed", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2026-03-20", - last_updated: "2026-03-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.611, output: 2.4439 }, - limit: { context: 204800, output: 131070 }, - provider: { npm: "@ai-sdk/anthropic", api: "https://zenmux.ai/api/anthropic/v1" }, - }, - "minimax/minimax-m2": { - id: "minimax/minimax-m2", - name: "MiniMax M2", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-10-27", - last_updated: "2025-10-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2, cache_read: 0.03, cache_write: 0.38 }, - limit: { context: 204000, output: 64000 }, - provider: { npm: "@ai-sdk/anthropic", api: "https://zenmux.ai/api/anthropic/v1" }, - }, - "minimax/minimax-m2.1": { - id: "minimax/minimax-m2.1", - name: "MiniMax M2.1", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2, cache_read: 0.03, cache_write: 0.38 }, - limit: { context: 204000, output: 64000 }, - provider: { npm: "@ai-sdk/anthropic", api: "https://zenmux.ai/api/anthropic/v1" }, - }, - "minimax/minimax-m2.5-lightning": { - id: "minimax/minimax-m2.5-lightning", - name: "MiniMax M2.5 highspeed", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2026-02-13", - last_updated: "2026-02-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 4.8, cache_read: 0.06, cache_write: 0.75 }, - limit: { context: 204800, output: 131072 }, - provider: { npm: "@ai-sdk/anthropic", api: "https://zenmux.ai/api/anthropic/v1" }, - }, - "minimax/minimax-m2.5": { - id: "minimax/minimax-m2.5", - name: "MiniMax M2.5", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2026-02-13", - last_updated: "2026-02-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2, cache_read: 0.03, cache_write: 0.375 }, - limit: { context: 204800, output: 131072 }, - provider: { npm: "@ai-sdk/anthropic", api: "https://zenmux.ai/api/anthropic/v1" }, - }, - "qwen/qwen3-coder-plus": { - id: "qwen/qwen3-coder-plus", - name: "Qwen3-Coder-Plus", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 1000000, output: 64000 }, - }, - "qwen/qwen3.5-flash": { - id: "qwen/qwen3.5-flash", - name: "Qwen3.5 Flash", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2026-03-20", - last_updated: "2026-03-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 1020000, output: 1020000 }, - }, - "qwen/qwen3.6-plus": { - id: "qwen/qwen3.6-plus", - name: "Qwen3.6-Plus", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-30", - last_updated: "2026-03-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { - input: 0.5, - output: 3, - cache_read: 0.05, - cache_write: 0.625, - context_over_200k: { input: 2, output: 6, cache_read: 0.2, cache_write: 2.5 }, - }, - limit: { context: 1000000, output: 64000 }, - }, - "qwen/qwen3-max": { - id: "qwen/qwen3-max", - name: "Qwen3-Max-Thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2026-01-23", - last_updated: "2026-01-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.2, output: 6 }, - limit: { context: 256000, output: 64000 }, - }, - "qwen/qwen3.5-plus": { - id: "qwen/qwen3.5-plus", - name: "Qwen3.5 Plus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2026-03-20", - last_updated: "2026-03-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 4.8 }, - limit: { context: 1000000, output: 64000 }, - }, - "google/gemini-3.1-flash-lite-preview": { - id: "google/gemini-3.1-flash-lite-preview", - name: "Gemini 3.1 Flash Lite Preview", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-03-20", - last_updated: "2025-03-20", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1.5 }, - limit: { context: 1050000, output: 65530 }, - }, - "google/gemini-3.1-pro-preview": { - id: "google/gemini-3.1-pro-preview", - name: "Gemini 3.1 Pro Preview", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2026-02-19", - release_date: "2026-02-19", - last_updated: "2026-02-19", - modalities: { input: ["text", "image", "pdf", "audio", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2, cache_write: 4.5 }, - limit: { context: 1048000, output: 64000 }, - }, - "google/gemini-3-flash-preview": { - id: "google/gemini-3-flash-preview", - name: "Gemini 3 Flash Preview", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text", "image", "pdf", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 3, cache_read: 0.05, cache_write: 1 }, - limit: { context: 1048000, output: 64000 }, - }, - "google/gemini-2.5-pro": { - id: "google/gemini-2.5-pro", - name: "Gemini 2.5 Pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["pdf", "image", "text", "audio", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.31, cache_write: 4.5 }, - limit: { context: 1048000, output: 64000 }, - }, - "google/gemini-2.5-flash": { - id: "google/gemini-2.5-flash", - name: "Gemini 2.5 Flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["pdf", "image", "text", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5, cache_read: 0.07, cache_write: 1 }, - limit: { context: 1048000, output: 64000 }, - }, - "google/gemini-2.5-flash-lite": { - id: "google/gemini-2.5-flash-lite", - name: "Gemini 2.5 Flash Lite", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-07-22", - last_updated: "2025-07-22", - modalities: { input: ["pdf", "image", "text", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.03, cache_write: 1 }, - limit: { context: 1048000, output: 64000 }, - }, - "sapiens-ai/agnes-1.5-lite": { - id: "sapiens-ai/agnes-1.5-lite", - name: "Agnes 1.5 Lite", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2026-03-26", - last_updated: "2026-03-26", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.12, output: 0.6 }, - limit: { context: 256000, output: 256000 }, - }, - "sapiens-ai/agnes-1.5-pro": { - id: "sapiens-ai/agnes-1.5-pro", - name: "Agnes 1.5 Pro", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-21", - last_updated: "2026-03-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.16, output: 0.8 }, - limit: { context: 256000, output: 256000 }, - }, - "moonshotai/kimi-k2.5": { - id: "moonshotai/kimi-k2.5", - name: "Kimi K2.5", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: false, - knowledge: "2025-01-01", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.58, output: 3.02, cache_read: 0.1 }, - limit: { context: 262000, output: 64000 }, - }, - "moonshotai/kimi-k2-thinking-turbo": { - id: "moonshotai/kimi-k2-thinking-turbo", - name: "Kimi K2 Thinking Turbo", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-11-06", - last_updated: "2025-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.15, output: 8, cache_read: 0.15 }, - limit: { context: 262000, output: 64000 }, - }, - "moonshotai/kimi-k2-0905": { - id: "moonshotai/kimi-k2-0905", - name: "Kimi K2 0905", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-09-04", - last_updated: "2025-09-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 2.5, cache_read: 0.15 }, - limit: { context: 262000, output: 64000 }, - }, - "moonshotai/kimi-k2-thinking": { - id: "moonshotai/kimi-k2-thinking", - name: "Kimi K2 Thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-11-06", - last_updated: "2025-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 2.5, cache_read: 0.15 }, - limit: { context: 262000, output: 64000 }, - }, - "anthropic/claude-opus-4.1": { - id: "anthropic/claude-opus-4.1", - name: "Claude Opus 4.1", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["image", "text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 64000 }, - provider: { npm: "@ai-sdk/anthropic", api: "https://zenmux.ai/api/anthropic/v1" }, - }, - "anthropic/claude-3.7-sonnet": { - id: "anthropic/claude-3.7-sonnet", - name: "Claude 3.7 Sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-02-24", - last_updated: "2025-02-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - provider: { npm: "@ai-sdk/anthropic", api: "https://zenmux.ai/api/anthropic/v1" }, - }, - "anthropic/claude-opus-4.6": { - id: "anthropic/claude-opus-4.6", - name: "Claude Opus 4.6", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2026-02-06", - last_updated: "2026-02-06", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 1000000, output: 128000 }, - provider: { npm: "@ai-sdk/anthropic", api: "https://zenmux.ai/api/anthropic/v1" }, - }, - "anthropic/claude-sonnet-4": { - id: "anthropic/claude-sonnet-4", - name: "Claude Sonnet 4", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["image", "text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 1000000, output: 64000 }, - provider: { npm: "@ai-sdk/anthropic", api: "https://zenmux.ai/api/anthropic/v1" }, - }, - "anthropic/claude-sonnet-4.5": { - id: "anthropic/claude-sonnet-4.5", - name: "Claude Sonnet 4.5", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 1000000, output: 64000 }, - provider: { npm: "@ai-sdk/anthropic", api: "https://zenmux.ai/api/anthropic/v1" }, - }, - "anthropic/claude-opus-4.5": { - id: "anthropic/claude-opus-4.5", - name: "Claude Opus 4.5", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-11-24", - last_updated: "2025-11-24", - modalities: { input: ["pdf", "image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 64000 }, - provider: { npm: "@ai-sdk/anthropic", api: "https://zenmux.ai/api/anthropic/v1" }, - }, - "anthropic/claude-opus-4": { - id: "anthropic/claude-opus-4", - name: "Claude Opus 4", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["image", "text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - provider: { npm: "@ai-sdk/anthropic", api: "https://zenmux.ai/api/anthropic/v1" }, - }, - "anthropic/claude-3.5-haiku": { - id: "anthropic/claude-3.5-haiku", - name: "Claude 3.5 Haiku", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2024-11-04", - last_updated: "2024-11-04", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 4, cache_read: 0.08, cache_write: 1 }, - limit: { context: 200000, output: 64000 }, - provider: { npm: "@ai-sdk/anthropic", api: "https://zenmux.ai/api/anthropic/v1" }, - }, - "anthropic/claude-haiku-4.5": { - id: "anthropic/claude-haiku-4.5", - name: "Claude Haiku 4.5", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 200000, output: 64000 }, - provider: { npm: "@ai-sdk/anthropic", api: "https://zenmux.ai/api/anthropic/v1" }, - }, - "anthropic/claude-sonnet-4.6": { - id: "anthropic/claude-sonnet-4.6", - name: "Claude Sonnet 4.6", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2026-02-18", - last_updated: "2026-02-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 1000000, output: 64000 }, - provider: { npm: "@ai-sdk/anthropic", api: "https://zenmux.ai/api/anthropic/v1" }, - }, - "xiaomi/mimo-v2-omni": { - id: "xiaomi/mimo-v2-omni", - name: "MiMo V2 Omni", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2026-03-20", - last_updated: "2026-03-20", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2 }, - limit: { context: 265000, output: 265000 }, - }, - "xiaomi/mimo-v2-pro": { - id: "xiaomi/mimo-v2-pro", - name: "MiMo V2 Pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2026-03-20", - last_updated: "2026-03-20", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 1.5, output: 4.5 }, - limit: { context: 1000000, output: 256000 }, - }, - "xiaomi/mimo-v2-flash": { - id: "xiaomi/mimo-v2-flash", - name: "MiMo-V2-Flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.3, cache_read: 0.01 }, - limit: { context: 262000, output: 64000 }, - }, - }, - }, - upstage: { - id: "upstage", - env: ["UPSTAGE_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.upstage.ai/v1/solar", - name: "Upstage", - doc: "https://developers.upstage.ai/docs/apis/chat", - models: { - "solar-pro2": { - id: "solar-pro2", - name: "solar-pro2", - family: "solar-pro", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03", - release_date: "2025-05-20", - last_updated: "2025-05-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 0.25 }, - limit: { context: 65536, output: 8192 }, - }, - "solar-mini": { - id: "solar-mini", - name: "solar-mini", - family: "solar-mini", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-09", - release_date: "2024-06-12", - last_updated: "2025-04-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.15 }, - limit: { context: 32768, output: 4096 }, - }, - "solar-pro3": { - id: "solar-pro3", - name: "solar-pro3", - family: "solar-pro", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03", - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 0.25 }, - limit: { context: 131072, output: 8192 }, - }, - }, - }, - "novita-ai": { - id: "novita-ai", - env: ["NOVITA_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.novita.ai/openai", - name: "NovitaAI", - doc: "https://novita.ai/docs/guides/introduction", - models: { - "deepseek/deepseek-r1-turbo": { - id: "deepseek/deepseek-r1-turbo", - name: "DeepSeek R1 (Turbo)\t", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-03-05", - last_updated: "2025-03-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.7, output: 2.5 }, - limit: { context: 64000, output: 16000 }, - }, - "deepseek/deepseek-v3-0324": { - id: "deepseek/deepseek-v3-0324", - name: "DeepSeek V3 0324", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-03-25", - last_updated: "2025-03-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 1.12, cache_read: 0.135 }, - limit: { context: 163840, output: 163840 }, - }, - "deepseek/deepseek-ocr-2": { - id: "deepseek/deepseek-ocr-2", - name: "deepseek/deepseek-ocr-2", - attachment: true, - reasoning: false, - tool_call: false, - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.03 }, - limit: { context: 8192, output: 8192 }, - }, - "deepseek/deepseek-ocr": { - id: "deepseek/deepseek-ocr", - name: "DeepSeek-OCR", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-10-24", - last_updated: "2025-10-24", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.03 }, - limit: { context: 8192, output: 8192 }, - }, - "deepseek/deepseek-r1-distill-llama-70b": { - id: "deepseek/deepseek-r1-distill-llama-70b", - name: "DeepSeek R1 Distill LLama 70B", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-01-27", - last_updated: "2025-01-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.8, output: 0.8 }, - limit: { context: 8192, output: 8192 }, - }, - "deepseek/deepseek-prover-v2-671b": { - id: "deepseek/deepseek-prover-v2-671b", - name: "Deepseek Prover V2 671B", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-30", - last_updated: "2025-04-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.7, output: 2.5 }, - limit: { context: 160000, output: 160000 }, - }, - "deepseek/deepseek-r1-0528-qwen3-8b": { - id: "deepseek/deepseek-r1-0528-qwen3-8b", - name: "DeepSeek R1 0528 Qwen3 8B", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-05-29", - last_updated: "2025-05-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.06, output: 0.09 }, - limit: { context: 128000, output: 32000 }, - }, - "deepseek/deepseek-v3.2-exp": { - id: "deepseek/deepseek-v3.2-exp", - name: "Deepseek V3.2 Exp", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 0.41 }, - limit: { context: 163840, output: 65536 }, - }, - "deepseek/deepseek-v3.1": { - id: "deepseek/deepseek-v3.1", - name: "DeepSeek V3.1", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-21", - last_updated: "2025-08-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 1, cache_read: 0.135 }, - limit: { context: 131072, output: 32768 }, - }, - "deepseek/deepseek-v3.2": { - id: "deepseek/deepseek-v3.2", - name: "Deepseek V3.2", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.269, output: 0.4, cache_read: 0.1345 }, - limit: { context: 163840, output: 65536 }, - }, - "deepseek/deepseek-v3-turbo": { - id: "deepseek/deepseek-v3-turbo", - name: "DeepSeek V3 (Turbo)\t", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-03-05", - last_updated: "2025-03-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 1.3 }, - limit: { context: 64000, output: 16000 }, - }, - "deepseek/deepseek-r1-0528": { - id: "deepseek/deepseek-r1-0528", - name: "DeepSeek R1 0528", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-05-28", - last_updated: "2025-05-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.7, output: 2.5, cache_read: 0.35 }, - limit: { context: 163840, output: 32768 }, - }, - "deepseek/deepseek-v3.1-terminus": { - id: "deepseek/deepseek-v3.1-terminus", - name: "Deepseek V3.1 Terminus", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-22", - last_updated: "2025-09-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 1, cache_read: 0.135 }, - limit: { context: 131072, output: 32768 }, - }, - "paddlepaddle/paddleocr-vl": { - id: "paddlepaddle/paddleocr-vl", - name: "PaddleOCR-VL", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-10-22", - last_updated: "2025-10-22", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0.02 }, - limit: { context: 16384, output: 16384 }, - }, - "nousresearch/hermes-2-pro-llama-3-8b": { - id: "nousresearch/hermes-2-pro-llama-3-8b", - name: "Hermes 2 Pro Llama 3 8B", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2024-06-27", - last_updated: "2024-06-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.14, output: 0.14 }, - limit: { context: 8192, output: 8192 }, - }, - "zai-org/glm-4.7": { - id: "zai-org/glm-4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2, cache_read: 0.11 }, - limit: { context: 204800, output: 131072 }, - }, - "zai-org/glm-5": { - id: "zai-org/glm-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3.2, cache_read: 0.2 }, - limit: { context: 202800, output: 131072 }, - }, - "zai-org/glm-5.1": { - id: "zai-org/glm-5.1", - name: "GLM-5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-03-27", - last_updated: "2026-03-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.4, output: 4.4, cache_read: 0.26 }, - limit: { context: 204800, output: 131072 }, - }, - "zai-org/glm-4.5": { - id: "zai-org/glm-4.5", - name: "GLM-4.5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2, cache_read: 0.11 }, - limit: { context: 131072, output: 98304 }, - }, - "zai-org/glm-4.5-air": { - id: "zai-org/glm-4.5-air", - name: "GLM 4.5 Air", - family: "glm-air", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-10-13", - last_updated: "2025-10-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.13, output: 0.85 }, - limit: { context: 131072, output: 98304 }, - }, - "zai-org/glm-4.5v": { - id: "zai-org/glm-4.5v", - name: "GLM 4.5V", - family: "glmv", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-08-11", - last_updated: "2025-08-11", - modalities: { input: ["text", "video", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 1.8, cache_read: 0.11 }, - limit: { context: 65536, output: 16384 }, - }, - "zai-org/glm-4.6": { - id: "zai-org/glm-4.6", - name: "GLM 4.6", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 2.2, cache_read: 0.11 }, - limit: { context: 204800, output: 131072 }, - }, - "zai-org/glm-4.6v": { - id: "zai-org/glm-4.6v", - name: "GLM 4.6V", - family: "glmv", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-08", - last_updated: "2025-12-08", - modalities: { input: ["text", "video", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.9, cache_read: 0.055 }, - limit: { context: 131072, output: 32768 }, - }, - "zai-org/glm-4.7-flash": { - id: "zai-org/glm-4.7-flash", - name: "GLM-4.7-Flash", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-01-19", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.4, cache_read: 0.01 }, - limit: { context: 200000, output: 128000 }, - }, - "zai-org/autoglm-phone-9b-multilingual": { - id: "zai-org/autoglm-phone-9b-multilingual", - name: "AutoGLM-Phone-9B-Multilingual", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-12-10", - last_updated: "2025-12-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.035, output: 0.138 }, - limit: { context: 65536, output: 65536 }, - }, - "mistralai/mistral-nemo": { - id: "mistralai/mistral-nemo", - name: "Mistral Nemo", - family: "mistral-nemo", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2024-07-30", - last_updated: "2024-07-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.04, output: 0.17 }, - limit: { context: 60288, output: 16000 }, - }, - "baichuan/baichuan-m2-32b": { - id: "baichuan/baichuan-m2-32b", - name: "baichuan-m2-32b", - family: "baichuan", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - knowledge: "2024-12", - release_date: "2025-08-13", - last_updated: "2025-08-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.07 }, - limit: { context: 131072, output: 131072 }, - }, - "meta-llama/llama-4-scout-17b-16e-instruct": { - id: "meta-llama/llama-4-scout-17b-16e-instruct", - name: "Llama 4 Scout Instruct", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-06", - last_updated: "2025-04-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.18, output: 0.59 }, - limit: { context: 131072, output: 131072 }, - }, - "meta-llama/llama-3.3-70b-instruct": { - id: "meta-llama/llama-3.3-70b-instruct", - name: "Llama 3.3 70B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-12-07", - last_updated: "2024-12-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.135, output: 0.4 }, - limit: { context: 131072, output: 120000 }, - }, - "meta-llama/llama-3-8b-instruct": { - id: "meta-llama/llama-3-8b-instruct", - name: "Llama 3 8B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-04-25", - last_updated: "2024-04-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.04, output: 0.04 }, - limit: { context: 8192, output: 8192 }, - }, - "meta-llama/llama-3.1-8b-instruct": { - id: "meta-llama/llama-3.1-8b-instruct", - name: "Llama 3.1 8B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-07-24", - last_updated: "2024-07-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0.05 }, - limit: { context: 16384, output: 16384 }, - }, - "meta-llama/llama-3-70b-instruct": { - id: "meta-llama/llama-3-70b-instruct", - name: "Llama3 70B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2024-04-25", - last_updated: "2024-04-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.51, output: 0.74 }, - limit: { context: 8192, output: 8000 }, - }, - "meta-llama/llama-4-maverick-17b-128e-instruct-fp8": { - id: "meta-llama/llama-4-maverick-17b-128e-instruct-fp8", - name: "Llama 4 Maverick Instruct", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-06", - last_updated: "2025-04-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 0.85 }, - limit: { context: 1048576, output: 8192 }, - }, - "gryphe/mythomax-l2-13b": { - id: "gryphe/mythomax-l2-13b", - name: "Mythomax L2 13B", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-04-25", - last_updated: "2024-04-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.09, output: 0.09 }, - limit: { context: 4096, output: 3200 }, - }, - "sao10k/l31-70b-euryale-v2.2": { - id: "sao10k/l31-70b-euryale-v2.2", - name: "L31 70B Euryale V2.2", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-09-19", - last_updated: "2024-09-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.48, output: 1.48 }, - limit: { context: 8192, output: 8192 }, - }, - "sao10k/l3-70b-euryale-v2.1": { - id: "sao10k/l3-70b-euryale-v2.1", - name: "L3 70B Euryale V2.1\t", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-06-18", - last_updated: "2024-06-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.48, output: 1.48 }, - limit: { context: 8192, output: 8192 }, - }, - "sao10k/L3-8B-Stheno-v3.2": { - id: "sao10k/L3-8B-Stheno-v3.2", - name: "L3 8B Stheno V3.2", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-11-29", - last_updated: "2024-11-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.05 }, - limit: { context: 8192, output: 32000 }, - }, - "sao10k/l3-8b-lunaris": { - id: "sao10k/l3-8b-lunaris", - name: "Sao10k L3 8B Lunaris\t", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2024-11-28", - last_updated: "2024-11-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.05 }, - limit: { context: 8192, output: 8192 }, - }, - "microsoft/wizardlm-2-8x22b": { - id: "microsoft/wizardlm-2-8x22b", - name: "Wizardlm 2 8x22B", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-04-24", - last_updated: "2024-04-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.62, output: 0.62 }, - limit: { context: 65535, output: 8000 }, - }, - "openai/gpt-oss-20b": { - id: "openai/gpt-oss-20b", - name: "OpenAI: GPT OSS 20B", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-08-06", - last_updated: "2025-08-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.04, output: 0.15 }, - limit: { context: 131072, output: 32768 }, - }, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "OpenAI GPT OSS 120B", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-06", - last_updated: "2025-08-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.25 }, - limit: { context: 131072, output: 32768 }, - }, - "minimaxai/minimax-m1-80k": { - id: "minimaxai/minimax-m1-80k", - name: "MiniMax M1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 2.2 }, - limit: { context: 1000000, output: 40000 }, - }, - "xiaomimimo/mimo-v2-flash": { - id: "xiaomimimo/mimo-v2-flash", - name: "XiaomiMiMo/MiMo-V2-Flash", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-12-19", - last_updated: "2025-12-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3, cache_read: 0.3 }, - limit: { context: 262144, output: 32000 }, - }, - "baidu/ernie-4.5-vl-28b-a3b-thinking": { - id: "baidu/ernie-4.5-vl-28b-a3b-thinking", - name: "ERNIE-4.5-VL-28B-A3B-Thinking", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-11-26", - last_updated: "2025-11-26", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.39, output: 0.39 }, - limit: { context: 131072, output: 65536 }, - }, - "baidu/ernie-4.5-vl-424b-a47b": { - id: "baidu/ernie-4.5-vl-424b-a47b", - name: "ERNIE 4.5 VL 424B A47B", - attachment: true, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-06-30", - last_updated: "2025-06-30", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.42, output: 1.25 }, - limit: { context: 123000, output: 16000 }, - }, - "baidu/ernie-4.5-21B-a3b": { - id: "baidu/ernie-4.5-21B-a3b", - name: "ERNIE 4.5 21B A3B", - family: "ernie", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-03", - release_date: "2025-06-30", - last_updated: "2025-06-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.28 }, - limit: { context: 120000, output: 8000 }, - }, - "baidu/ernie-4.5-300b-a47b-paddle": { - id: "baidu/ernie-4.5-300b-a47b-paddle", - name: "ERNIE 4.5 300B A47B", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-06-30", - last_updated: "2025-06-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.28, output: 1.1 }, - limit: { context: 123000, output: 12000 }, - }, - "baidu/ernie-4.5-21B-a3b-thinking": { - id: "baidu/ernie-4.5-21B-a3b-thinking", - name: "ERNIE-4.5-21B-A3B-Thinking", - family: "ernie", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2025-03", - release_date: "2025-09-19", - last_updated: "2025-09-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.28 }, - limit: { context: 131072, output: 65536 }, - }, - "baidu/ernie-4.5-vl-28b-a3b": { - id: "baidu/ernie-4.5-vl-28b-a3b", - name: "ERNIE 4.5 VL 28B A3B", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-06-30", - last_updated: "2025-06-30", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 1.4, output: 5.6 }, - limit: { context: 30000, output: 8000 }, - }, - "minimax/minimax-m2.7": { - id: "minimax/minimax-m2.7", - name: "MiniMax M2.7", - family: "minimax-m2.7", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.06 }, - limit: { context: 204800, output: 131072 }, - }, - "minimax/minimax-m2": { - id: "minimax/minimax-m2", - name: "MiniMax-M2", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2025-10-27", - last_updated: "2025-10-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.03 }, - limit: { context: 204800, output: 131072 }, - }, - "minimax/minimax-m2.1": { - id: "minimax/minimax-m2.1", - name: "Minimax M2.1", - family: "minimax", - attachment: false, - reasoning: false, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.03 }, - limit: { context: 204800, output: 131072 }, - }, - "minimax/minimax-m2.5": { - id: "minimax/minimax-m2.5", - name: "MiniMax M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2, cache_read: 0.03 }, - limit: { context: 204800, output: 131100 }, - }, - "minimax/minimax-m2.5-highspeed": { - id: "minimax/minimax-m2.5-highspeed", - name: "MiniMax M2.5 Highspeed", - family: "minimax-m2.5", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 2.4, cache_read: 0.03 }, - limit: { context: 204800, output: 131100 }, - }, - "qwen/qwen2.5-7b-instruct": { - id: "qwen/qwen2.5-7b-instruct", - name: "Qwen2.5 7B Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.07 }, - limit: { context: 32000, output: 32000 }, - }, - "qwen/qwen3.5-122b-a10b": { - id: "qwen/qwen3.5-122b-a10b", - name: "Qwen3.5-122B-A10B", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-26", - last_updated: "2026-02-26", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 3.2 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen/qwen3.5-27b": { - id: "qwen/qwen3.5-27b", - name: "Qwen3.5-27B", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-26", - last_updated: "2026-02-26", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 2.4 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen/qwen3-235b-a22b-instruct-2507": { - id: "qwen/qwen3-235b-a22b-instruct-2507", - name: "Qwen3 235B A22B Instruct 2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-22", - last_updated: "2025-07-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.09, output: 0.58 }, - limit: { context: 131072, output: 16384 }, - }, - "qwen/qwen3-omni-30b-a3b-instruct": { - id: "qwen/qwen3-omni-30b-a3b-instruct", - name: "Qwen3 Omni 30B A3B Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-09-24", - last_updated: "2025-09-24", - modalities: { input: ["text", "video", "audio", "image"], output: ["text", "audio"] }, - open_weights: true, - cost: { input: 0.25, output: 0.97, input_audio: 2.2, output_audio: 1.788 }, - limit: { context: 65536, output: 16384 }, - }, - "qwen/qwen3.5-397b-a17b": { - id: "qwen/qwen3.5-397b-a17b", - name: "Qwen3.5-397B-A17B", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-17", - last_updated: "2026-02-17", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3.6 }, - limit: { context: 262144, output: 64000 }, - }, - "qwen/qwen2.5-vl-72b-instruct": { - id: "qwen/qwen2.5-vl-72b-instruct", - name: "Qwen2.5 VL 72B Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-03-25", - last_updated: "2025-03-25", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.8, output: 0.8 }, - limit: { context: 32768, output: 32768 }, - }, - "qwen/qwen3-vl-235b-a22b-thinking": { - id: "qwen/qwen3-vl-235b-a22b-thinking", - name: "Qwen3 VL 235B A22B Thinking", - attachment: true, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-09-24", - last_updated: "2025-09-24", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.98, output: 3.95 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen/qwen3-vl-30b-a3b-thinking": { - id: "qwen/qwen3-vl-30b-a3b-thinking", - name: "qwen/qwen3-vl-30b-a3b-thinking", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-11", - last_updated: "2025-10-11", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 1 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen/qwen3-omni-30b-a3b-thinking": { - id: "qwen/qwen3-omni-30b-a3b-thinking", - name: "Qwen3 Omni 30B A3B Thinking", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-24", - last_updated: "2025-09-24", - modalities: { input: ["text", "audio", "video", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 0.97, input_audio: 2.2, output_audio: 1.788 }, - limit: { context: 65536, output: 16384 }, - }, - "qwen/qwen3-vl-8b-instruct": { - id: "qwen/qwen3-vl-8b-instruct", - name: "qwen/qwen3-vl-8b-instruct", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-17", - last_updated: "2025-10-17", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.08, output: 0.5 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen/qwen3-max": { - id: "qwen/qwen3-max", - name: "Qwen3 Max", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-24", - last_updated: "2025-09-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.11, output: 8.45 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen/qwen3-32b-fp8": { - id: "qwen/qwen3-32b-fp8", - name: "Qwen3 32B", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-04-29", - last_updated: "2025-04-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.45 }, - limit: { context: 40960, output: 20000 }, - }, - "qwen/qwen3-4b-fp8": { - id: "qwen/qwen3-4b-fp8", - name: "Qwen3 4B", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-04-29", - last_updated: "2025-04-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.03 }, - limit: { context: 128000, output: 20000 }, - }, - "qwen/qwen3-235b-a22b-thinking-2507": { - id: "qwen/qwen3-235b-a22b-thinking-2507", - name: "Qwen3 235B A22b Thinking 2507", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-25", - last_updated: "2025-07-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 3 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen/qwen3-next-80b-a3b-thinking": { - id: "qwen/qwen3-next-80b-a3b-thinking", - name: "Qwen3 Next 80B A3B Thinking", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-10", - last_updated: "2025-09-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 1.5 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen/qwen-mt-plus": { - id: "qwen/qwen-mt-plus", - name: "Qwen MT Plus", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-09-03", - last_updated: "2025-09-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 0.75 }, - limit: { context: 16384, output: 8192 }, - }, - "qwen/qwen3-next-80b-a3b-instruct": { - id: "qwen/qwen3-next-80b-a3b-instruct", - name: "Qwen3 Next 80B A3B Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-10", - last_updated: "2025-09-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 1.5 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen/qwen3-30b-a3b-fp8": { - id: "qwen/qwen3-30b-a3b-fp8", - name: "Qwen3 30B A3B", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-04-29", - last_updated: "2025-04-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.09, output: 0.45 }, - limit: { context: 40960, output: 20000 }, - }, - "qwen/qwen3-coder-next": { - id: "qwen/qwen3-coder-next", - name: "Qwen3 Coder Next", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-03", - last_updated: "2026-02-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 1.5 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen/qwen3-coder-480b-a35b-instruct": { - id: "qwen/qwen3-coder-480b-a35b-instruct", - name: "Qwen3 Coder 480B A35B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.3 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen/qwen3-vl-30b-a3b-instruct": { - id: "qwen/qwen3-vl-30b-a3b-instruct", - name: "qwen/qwen3-vl-30b-a3b-instruct", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-11", - last_updated: "2025-10-11", - modalities: { input: ["text", "video", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.7 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen/qwen3-coder-30b-a3b-instruct": { - id: "qwen/qwen3-coder-30b-a3b-instruct", - name: "Qwen3 Coder 30b A3B Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-09", - last_updated: "2025-10-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.27 }, - limit: { context: 160000, output: 32768 }, - }, - "qwen/qwen3-235b-a22b-fp8": { - id: "qwen/qwen3-235b-a22b-fp8", - name: "Qwen3 235B A22B", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-04-29", - last_updated: "2025-04-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.8 }, - limit: { context: 40960, output: 20000 }, - }, - "qwen/qwen3-8b-fp8": { - id: "qwen/qwen3-8b-fp8", - name: "Qwen3 8B", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-04-29", - last_updated: "2025-04-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.035, output: 0.138 }, - limit: { context: 128000, output: 20000 }, - }, - "qwen/qwen3-vl-235b-a22b-instruct": { - id: "qwen/qwen3-vl-235b-a22b-instruct", - name: "Qwen3 VL 235B A22B Instruct", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-24", - last_updated: "2025-09-24", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.5 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen/qwen-2.5-72b-instruct": { - id: "qwen/qwen-2.5-72b-instruct", - name: "Qwen 2.5 72B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-10-15", - last_updated: "2024-10-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.38, output: 0.4 }, - limit: { context: 32000, output: 8192 }, - }, - "qwen/qwen3.5-35b-a3b": { - id: "qwen/qwen3.5-35b-a3b", - name: "Qwen3.5-35B-A3B", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-26", - last_updated: "2026-02-26", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 2 }, - limit: { context: 262144, output: 65536 }, - }, - "kwaipilot/kat-coder-pro": { - id: "kwaipilot/kat-coder-pro", - name: "Kat Coder Pro", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01-05", - last_updated: "2026-01-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.06 }, - limit: { context: 256000, output: 128000 }, - }, - "google/gemma-4-31b-it": { - id: "google/gemma-4-31b-it", - name: "Gemma 4 31B", - family: "gemma", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-02", - last_updated: "2026-04-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.14, output: 0.4 }, - limit: { context: 262144, output: 131072 }, - }, - "google/gemma-3-27b-it": { - id: "google/gemma-3-27b-it", - name: "Gemma 3 27B", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-03-25", - last_updated: "2025-03-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.119, output: 0.2 }, - limit: { context: 98304, output: 16384 }, - }, - "google/gemma-4-26b-a4b-it": { - id: "google/gemma-4-26b-a4b-it", - name: "Gemma 4 26B A4B", - family: "gemma", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-02", - last_updated: "2026-04-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.13, output: 0.4 }, - limit: { context: 262144, output: 131072 }, - }, - "moonshotai/kimi-k2.5": { - id: "moonshotai/kimi-k2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3, cache_read: 0.1 }, - limit: { context: 262144, output: 262144 }, - }, - "moonshotai/kimi-k2-instruct": { - id: "moonshotai/kimi-k2-instruct", - name: "Kimi K2 Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-11", - last_updated: "2025-07-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.57, output: 2.3 }, - limit: { context: 131072, output: 131072 }, - }, - "moonshotai/kimi-k2-0905": { - id: "moonshotai/kimi-k2-0905", - name: "Kimi K2 0905", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.5 }, - limit: { context: 262144, output: 262144 }, - }, - "moonshotai/kimi-k2-thinking": { - id: "moonshotai/kimi-k2-thinking", - name: "Kimi K2 Thinking", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-11-07", - last_updated: "2025-11-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.5 }, - limit: { context: 262144, output: 262144 }, - }, - }, - }, - "xiaomi-token-plan-cn": { - id: "xiaomi-token-plan-cn", - env: ["XIAOMI_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://token-plan-cn.xiaomimimo.com/v1", - name: "Xiaomi Token Plan (China)", - doc: "https://platform.xiaomimimo.com/#/docs", - models: { - "mimo-v2-omni": { - id: "mimo-v2-omni", - name: "MiMo-V2-Omni", - family: "mimo", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0 }, - limit: { context: 256000, output: 128000 }, - }, - "mimo-v2-tts": { - id: "mimo-v2-tts", - name: "MiMo-V2-TTS", - family: "mimo", - attachment: false, - reasoning: false, - tool_call: false, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["audio"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 8000, output: 16000 }, - }, - "mimo-v2-pro": { - id: "mimo-v2-pro", - name: "MiMo-V2-Pro", - family: "mimo", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0 }, - limit: { context: 1000000, output: 128000 }, - }, - }, - }, - wandb: { - id: "wandb", - env: ["WANDB_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.inference.wandb.ai/v1", - name: "Weights & Biases", - doc: "https://docs.wandb.ai/guides/integrations/inference/", - models: { - "Qwen/Qwen3-30B-A3B-Instruct-2507": { - id: "Qwen/Qwen3-30B-A3B-Instruct-2507", - name: "Qwen3 30B A3B Instruct 2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-29", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 262144, output: 262144 }, - }, - "Qwen/Qwen3-235B-A22B-Instruct-2507": { - id: "Qwen/Qwen3-235B-A22B-Instruct-2507", - name: "Qwen3 235B A22B Instruct 2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04-28", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 262144, output: 262144 }, - }, - "Qwen/Qwen3-235B-A22B-Thinking-2507": { - id: "Qwen/Qwen3-235B-A22B-Thinking-2507", - name: "Qwen3-235B-A22B-Thinking-2507", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-25", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 262144, output: 262144 }, - }, - "Qwen/Qwen3-Coder-480B-A35B-Instruct": { - id: "Qwen/Qwen3-Coder-480B-A35B-Instruct", - name: "Qwen3-Coder-480B-A35B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 1.5 }, - limit: { context: 262144, output: 262144 }, - }, - "zai-org/GLM-5-FP8": { - id: "zai-org/GLM-5-FP8", - name: "GLM 5", - family: "glm", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3.2 }, - limit: { context: 200000, output: 200000 }, - }, - "meta-llama/Llama-4-Scout-17B-16E-Instruct": { - id: "meta-llama/Llama-4-Scout-17B-16E-Instruct", - name: "Llama 4 Scout 17B 16E Instruct", - family: "llama", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-01-31", - last_updated: "2026-03-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.17, output: 0.66 }, - limit: { context: 64000, output: 64000 }, - }, - "meta-llama/Llama-3.3-70B-Instruct": { - id: "meta-llama/Llama-3.3-70B-Instruct", - name: "Llama-3.3-70B-Instruct", - family: "llama", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-12-06", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.71, output: 0.71 }, - limit: { context: 128000, output: 128000 }, - }, - "meta-llama/Llama-3.1-8B-Instruct": { - id: "meta-llama/Llama-3.1-8B-Instruct", - name: "Meta-Llama-3.1-8B-Instruct", - family: "llama", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-07-23", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.22, output: 0.22 }, - limit: { context: 128000, output: 128000 }, - }, - "meta-llama/Llama-3.1-70B-Instruct": { - id: "meta-llama/Llama-3.1-70B-Instruct", - name: "Llama 3.1 70B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-07-23", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.8, output: 0.8 }, - limit: { context: 128000, output: 128000 }, - }, - "OpenPipe/Qwen3-14B-Instruct": { - id: "OpenPipe/Qwen3-14B-Instruct", - name: "OpenPipe Qwen3 14B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-29", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.22 }, - limit: { context: 32768, output: 32768 }, - }, - "microsoft/Phi-4-mini-instruct": { - id: "microsoft/Phi-4-mini-instruct", - name: "Phi-4-mini-instruct", - family: "phi", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-12-11", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.08, output: 0.35 }, - limit: { context: 128000, output: 128000 }, - }, - "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-FP8": { - id: "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-FP8", - name: "NVIDIA Nemotron 3 Super 120B", - family: "nemotron", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-03-11", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.8 }, - limit: { context: 262144, output: 262144 }, - }, - "deepseek-ai/DeepSeek-V3.1": { - id: "deepseek-ai/DeepSeek-V3.1", - name: "DeepSeek V3.1", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-21", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 1.65 }, - limit: { context: 161000, output: 161000 }, - }, - "openai/gpt-oss-20b": { - id: "openai/gpt-oss-20b", - name: "gpt-oss-20b", - family: "gpt-oss", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.2 }, - limit: { context: 131072, output: 131072 }, - }, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "gpt-oss-120b", - family: "gpt-oss", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 131072, output: 131072 }, - }, - "moonshotai/Kimi-K2.5": { - id: "moonshotai/Kimi-K2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-01-27", - last_updated: "2026-03-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 2.85 }, - limit: { context: 262144, output: 262144 }, - }, - "MiniMaxAI/MiniMax-M2.5": { - id: "MiniMaxAI/MiniMax-M2.5", - name: "MiniMax M2.5", - family: "minimax", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 196608, output: 196608 }, - }, - }, - }, - chutes: { - id: "chutes", - env: ["CHUTES_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://llm.chutes.ai/v1", - name: "Chutes", - doc: "https://llm.chutes.ai/v1/models", - models: { - "miromind-ai/MiroThinker-v1.5-235B": { - id: "miromind-ai/MiroThinker-v1.5-235B", - name: "MiroThinker V1.5 235B", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2026-01-10", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.15 }, - limit: { context: 262144, output: 8192 }, - }, - "OpenGVLab/InternVL3-78B-TEE": { - id: "OpenGVLab/InternVL3-78B-TEE", - name: "InternVL3 78B TEE", - family: "opengvlab", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-01-06", - last_updated: "2026-01-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.39 }, - limit: { context: 32768, output: 32768 }, - }, - "NousResearch/DeepHermes-3-Mistral-24B-Preview": { - id: "NousResearch/DeepHermes-3-Mistral-24B-Preview", - name: "DeepHermes 3 Mistral 24B Preview", - family: "nousresearch", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0.1 }, - limit: { context: 32768, output: 32768 }, - }, - "NousResearch/Hermes-4-405B-FP8-TEE": { - id: "NousResearch/Hermes-4-405B-FP8-TEE", - name: "Hermes 4 405B FP8 TEE", - family: "nousresearch", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 131072, output: 65536 }, - }, - "NousResearch/Hermes-4.3-36B": { - id: "NousResearch/Hermes-4.3-36B", - name: "Hermes 4.3 36B", - family: "nousresearch", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.39 }, - limit: { context: 32768, output: 8192 }, - }, - "NousResearch/Hermes-4-14B": { - id: "NousResearch/Hermes-4-14B", - name: "Hermes 4 14B", - family: "nousresearch", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.01, output: 0.05 }, - limit: { context: 40960, output: 40960 }, - }, - "NousResearch/Hermes-4-70B": { - id: "NousResearch/Hermes-4-70B", - name: "Hermes 4 70B", - family: "nousresearch", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.11, output: 0.38 }, - limit: { context: 131072, output: 131072 }, - }, - "Qwen/Qwen3-30B-A3B": { - id: "Qwen/Qwen3-30B-A3B", - name: "Qwen3 30B A3B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.06, output: 0.22 }, - limit: { context: 40960, output: 40960 }, - }, - "Qwen/Qwen3-Coder-Next": { - id: "Qwen/Qwen3-Coder-Next", - name: "Qwen3 Coder Next", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.3 }, - limit: { context: 262144, output: 65536 }, - }, - "Qwen/Qwen2.5-Coder-32B-Instruct": { - id: "Qwen/Qwen2.5-Coder-32B-Instruct", - name: "Qwen2.5 Coder 32B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.11 }, - limit: { context: 32768, output: 32768 }, - }, - "Qwen/Qwen3-235B-A22B": { - id: "Qwen/Qwen3-235B-A22B", - name: "Qwen3 235B A22B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 40960, output: 40960 }, - }, - "Qwen/Qwen2.5-VL-72B-Instruct-TEE": { - id: "Qwen/Qwen2.5-VL-72B-Instruct-TEE", - name: "Qwen2.5 VL 72B Instruct TEE", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 32768, output: 32768 }, - }, - "Qwen/Qwen3Guard-Gen-0.6B": { - id: "Qwen/Qwen3Guard-Gen-0.6B", - name: "Qwen3Guard Gen 0.6B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.01, output: 0.01, cache_read: 0.005 }, - limit: { context: 32768, output: 8192 }, - }, - "Qwen/Qwen3-Next-80B-A3B-Instruct": { - id: "Qwen/Qwen3-Next-80B-A3B-Instruct", - name: "Qwen3 Next 80B A3B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.8 }, - limit: { context: 262144, output: 262144 }, - }, - "Qwen/Qwen3-30B-A3B-Instruct-2507": { - id: "Qwen/Qwen3-30B-A3B-Instruct-2507", - name: "Qwen3 30B A3B Instruct 2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.08, output: 0.33 }, - limit: { context: 262144, output: 262144 }, - }, - "Qwen/Qwen3-32B": { - id: "Qwen/Qwen3-32B", - name: "Qwen3 32B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.08, output: 0.24, cache_read: 0.04 }, - limit: { context: 40960, output: 40960 }, - }, - "Qwen/Qwen3-14B": { - id: "Qwen/Qwen3-14B", - name: "Qwen3 14B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.22 }, - limit: { context: 40960, output: 40960 }, - }, - "Qwen/Qwen3-235B-A22B-Instruct-2507-TEE": { - id: "Qwen/Qwen3-235B-A22B-Instruct-2507-TEE", - name: "Qwen3 235B A22B Instruct 2507 TEE", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.08, output: 0.55, cache_read: 0.04 }, - limit: { context: 262144, output: 65536 }, - }, - "Qwen/Qwen3-235B-A22B-Thinking-2507": { - id: "Qwen/Qwen3-235B-A22B-Thinking-2507", - name: "Qwen3 235B A22B Thinking 2507", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.11, output: 0.6 }, - limit: { context: 262144, output: 262144 }, - }, - "Qwen/Qwen2.5-VL-32B-Instruct": { - id: "Qwen/Qwen2.5-VL-32B-Instruct", - name: "Qwen2.5 VL 32B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.22 }, - limit: { context: 16384, output: 16384 }, - }, - "Qwen/Qwen3.5-397B-A17B-TEE": { - id: "Qwen/Qwen3.5-397B-A17B-TEE", - name: "Qwen3.5 397B A17B TEE", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-02-18", - last_updated: "2026-02-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.39, output: 2.34, cache_read: 0.195 }, - limit: { context: 262144, output: 65536 }, - }, - "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8-TEE": { - id: "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8-TEE", - name: "Qwen3 Coder 480B A35B Instruct FP8 TEE", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.22, output: 0.95, cache_read: 0.11 }, - limit: { context: 262144, output: 262144 }, - }, - "Qwen/Qwen3-VL-235B-A22B-Instruct": { - id: "Qwen/Qwen3-VL-235B-A22B-Instruct", - name: "Qwen3 VL 235B A22B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 262144, output: 262144 }, - }, - "Qwen/Qwen2.5-72B-Instruct": { - id: "Qwen/Qwen2.5-72B-Instruct", - name: "Qwen2.5 72B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.13, output: 0.52 }, - limit: { context: 32768, output: 32768 }, - }, - "zai-org/GLM-5.1-TEE": { - id: "zai-org/GLM-5.1-TEE", - name: "GLM 5.1 TEE", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-04-08", - last_updated: "2026-04-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.95, output: 3.15, cache_read: 0.475 }, - limit: { context: 202752, output: 65535 }, - }, - "zai-org/GLM-4.7-Flash": { - id: "zai-org/GLM-4.7-Flash", - name: "GLM 4.7 Flash", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.06, output: 0.35 }, - limit: { context: 202752, output: 65535 }, - }, - "zai-org/GLM-4.5-TEE": { - id: "zai-org/GLM-4.5-TEE", - name: "GLM 4.5 TEE", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.35, output: 1.55 }, - limit: { context: 131072, output: 65536 }, - }, - "zai-org/GLM-4.6-FP8": { - id: "zai-org/GLM-4.6-FP8", - name: "GLM 4.6 FP8", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 202752, output: 65535 }, - }, - "zai-org/GLM-4.5-Air": { - id: "zai-org/GLM-4.5-Air", - name: "GLM 4.5 Air", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.22 }, - limit: { context: 131072, output: 131072 }, - }, - "zai-org/GLM-4.7-FP8": { - id: "zai-org/GLM-4.7-FP8", - name: "GLM 4.7 FP8", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 202752, output: 65535 }, - }, - "zai-org/GLM-5-TEE": { - id: "zai-org/GLM-5-TEE", - name: "GLM 5 TEE", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-02-14", - last_updated: "2026-02-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.95, output: 3.15, cache_read: 0.475 }, - limit: { context: 202752, output: 65535 }, - }, - "zai-org/GLM-4.5-FP8": { - id: "zai-org/GLM-4.5-FP8", - name: "GLM 4.5 FP8", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 131072, output: 65536 }, - }, - "zai-org/GLM-4.7-TEE": { - id: "zai-org/GLM-4.7-TEE", - name: "GLM 4.7 TEE", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 1.5 }, - limit: { context: 202752, output: 65535 }, - }, - "zai-org/GLM-4.6V": { - id: "zai-org/GLM-4.6V", - name: "GLM 4.6V", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.9, cache_read: 0.15 }, - limit: { context: 131072, output: 65536 }, - }, - "zai-org/GLM-5-Turbo": { - id: "zai-org/GLM-5-Turbo", - name: "GLM 5 Turbo", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-03-11", - last_updated: "2026-03-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.49, output: 1.96, cache_read: 0.245 }, - limit: { context: 202752, output: 65535 }, - }, - "zai-org/GLM-4.6-TEE": { - id: "zai-org/GLM-4.6-TEE", - name: "GLM 4.6 TEE", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 1.7, cache_read: 0.2 }, - limit: { context: 202752, output: 65536 }, - }, - "mistralai/Devstral-2-123B-Instruct-2512-TEE": { - id: "mistralai/Devstral-2-123B-Instruct-2512-TEE", - name: "Devstral 2 123B Instruct 2512 TEE", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01-10", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.22 }, - limit: { context: 262144, output: 65536 }, - }, - "XiaomiMiMo/MiMo-V2-Flash": { - id: "XiaomiMiMo/MiMo-V2-Flash", - name: "MiMo V2 Flash", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.09, output: 0.29 }, - limit: { context: 262144, output: 32000 }, - }, - "chutesai/Mistral-Small-3.2-24B-Instruct-2506": { - id: "chutesai/Mistral-Small-3.2-24B-Instruct-2506", - name: "Mistral Small 3.2 24B Instruct 2506", - family: "chutesai", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.06, output: 0.18 }, - limit: { context: 131072, output: 131072 }, - }, - "chutesai/Mistral-Small-3.1-24B-Instruct-2503": { - id: "chutesai/Mistral-Small-3.1-24B-Instruct-2503", - name: "Mistral Small 3.1 24B Instruct 2503", - family: "chutesai", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.11, cache_read: 0.015 }, - limit: { context: 131072, output: 131072 }, - }, - "nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16": { - id: "nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16", - name: "NVIDIA Nemotron 3 Nano 30B A3B BF16", - family: "nemotron", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.06, output: 0.24 }, - limit: { context: 262144, output: 262144 }, - }, - "deepseek-ai/DeepSeek-R1-TEE": { - id: "deepseek-ai/DeepSeek-R1-TEE", - name: "DeepSeek R1 TEE", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: false, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 163840, output: 163840 }, - }, - "deepseek-ai/DeepSeek-V3": { - id: "deepseek-ai/DeepSeek-V3", - name: "DeepSeek V3", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 163840, output: 163840 }, - }, - "deepseek-ai/DeepSeek-R1-Distill-Llama-70B": { - id: "deepseek-ai/DeepSeek-R1-Distill-Llama-70B", - name: "DeepSeek R1 Distill Llama 70B", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.11 }, - limit: { context: 131072, output: 131072 }, - }, - "deepseek-ai/DeepSeek-V3.1-TEE": { - id: "deepseek-ai/DeepSeek-V3.1-TEE", - name: "DeepSeek V3.1 TEE", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.8 }, - limit: { context: 163840, output: 65536 }, - }, - "deepseek-ai/DeepSeek-V3-0324-TEE": { - id: "deepseek-ai/DeepSeek-V3-0324-TEE", - name: "DeepSeek V3 0324 TEE", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.19, output: 0.87, cache_read: 0.095 }, - limit: { context: 163840, output: 65536 }, - }, - "deepseek-ai/DeepSeek-V3.2-Speciale-TEE": { - id: "deepseek-ai/DeepSeek-V3.2-Speciale-TEE", - name: "DeepSeek V3.2 Speciale TEE", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: false, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 0.41 }, - limit: { context: 163840, output: 65536 }, - }, - "deepseek-ai/DeepSeek-V3.1-Terminus-TEE": { - id: "deepseek-ai/DeepSeek-V3.1-Terminus-TEE", - name: "DeepSeek V3.1 Terminus TEE", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.23, output: 0.9 }, - limit: { context: 163840, output: 65536 }, - }, - "deepseek-ai/DeepSeek-R1-0528-TEE": { - id: "deepseek-ai/DeepSeek-R1-0528-TEE", - name: "DeepSeek R1 0528 TEE", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 1.75 }, - limit: { context: 163840, output: 65536 }, - }, - "deepseek-ai/DeepSeek-V3.2-TEE": { - id: "deepseek-ai/DeepSeek-V3.2-TEE", - name: "DeepSeek V3.2 TEE", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.28, output: 0.42, cache_read: 0.14 }, - limit: { context: 131072, output: 65536 }, - }, - "openai/gpt-oss-20b": { - id: "openai/gpt-oss-20b", - name: "gpt oss 20b", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0.1 }, - limit: { context: 131072, output: 131072 }, - }, - "openai/gpt-oss-120b-TEE": { - id: "openai/gpt-oss-120b-TEE", - name: "gpt oss 120b TEE", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.04, output: 0.18 }, - limit: { context: 131072, output: 65536 }, - }, - "unsloth/gemma-3-12b-it": { - id: "unsloth/gemma-3-12b-it", - name: "gemma 3 12b it", - family: "unsloth", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.1 }, - limit: { context: 131072, output: 131072 }, - }, - "unsloth/Llama-3.2-3B-Instruct": { - id: "unsloth/Llama-3.2-3B-Instruct", - name: "Llama 3.2 3B Instruct", - family: "unsloth", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-02-12", - last_updated: "2025-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.01, output: 0.01, cache_read: 0.005 }, - limit: { context: 16384, output: 16384 }, - }, - "unsloth/gemma-3-4b-it": { - id: "unsloth/gemma-3-4b-it", - name: "gemma 3 4b it", - family: "unsloth", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.01, output: 0.03 }, - limit: { context: 96000, output: 96000 }, - }, - "unsloth/Llama-3.2-1B-Instruct": { - id: "unsloth/Llama-3.2-1B-Instruct", - name: "Llama 3.2 1B Instruct", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.01, output: 0.01, cache_read: 0.005 }, - limit: { context: 32768, output: 8192 }, - }, - "unsloth/Mistral-Nemo-Instruct-2407": { - id: "unsloth/Mistral-Nemo-Instruct-2407", - name: "Mistral Nemo Instruct 2407", - family: "unsloth", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0.04, cache_read: 0.01 }, - limit: { context: 131072, output: 131072 }, - }, - "unsloth/Mistral-Small-24B-Instruct-2501": { - id: "unsloth/Mistral-Small-24B-Instruct-2501", - name: "Mistral Small 24B Instruct 2501", - family: "unsloth", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.11 }, - limit: { context: 32768, output: 32768 }, - }, - "unsloth/gemma-3-27b-it": { - id: "unsloth/gemma-3-27b-it", - name: "gemma 3 27b it", - family: "unsloth", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.04, output: 0.15, cache_read: 0.02 }, - limit: { context: 128000, output: 65536 }, - }, - "moonshotai/Kimi-K2-Thinking-TEE": { - id: "moonshotai/Kimi-K2-Thinking-TEE", - name: "Kimi K2 Thinking TEE", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 1.75 }, - limit: { context: 262144, output: 65535 }, - }, - "moonshotai/Kimi-K2-Instruct-0905": { - id: "moonshotai/Kimi-K2-Instruct-0905", - name: "Kimi K2 Instruct 0905", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.39, output: 1.9, cache_read: 0.195 }, - limit: { context: 262144, output: 262144 }, - }, - "moonshotai/Kimi-K2.5-TEE": { - id: "moonshotai/Kimi-K2.5-TEE", - name: "Kimi K2.5 TEE", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2024-10", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3 }, - limit: { context: 262144, output: 65535 }, - }, - "MiniMaxAI/MiniMax-M2.1-TEE": { - id: "MiniMaxAI/MiniMax-M2.1-TEE", - name: "MiniMax M2.1 TEE", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 1.12 }, - limit: { context: 196608, output: 65536 }, - }, - "MiniMaxAI/MiniMax-M2.5-TEE": { - id: "MiniMaxAI/MiniMax-M2.5-TEE", - name: "MiniMax M2.5 TEE", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-02-15", - last_updated: "2026-02-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.1, cache_read: 0.15 }, - limit: { context: 196608, output: 65536 }, - }, - "rednote-hilab/dots.ocr": { - id: "rednote-hilab/dots.ocr", - name: "dots.ocr", - family: "rednote", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.01, output: 0.01, cache_read: 0.005 }, - limit: { context: 131072, output: 131072 }, - }, - "tngtech/TNG-R1T-Chimera-Turbo": { - id: "tngtech/TNG-R1T-Chimera-Turbo", - name: "TNG R1T Chimera Turbo", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.22, output: 0.6 }, - limit: { context: 163840, output: 65536 }, - }, - "tngtech/DeepSeek-TNG-R1T2-Chimera": { - id: "tngtech/DeepSeek-TNG-R1T2-Chimera", - name: "DeepSeek TNG R1T2 Chimera", - family: "tngtech", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 0.85 }, - limit: { context: 163840, output: 163840 }, - }, - "tngtech/DeepSeek-R1T-Chimera": { - id: "tngtech/DeepSeek-R1T-Chimera", - name: "DeepSeek R1T Chimera", - family: "tngtech", - attachment: false, - reasoning: true, - tool_call: false, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 163840, output: 163840 }, - }, - "tngtech/TNG-R1T-Chimera-TEE": { - id: "tngtech/TNG-R1T-Chimera-TEE", - name: "TNG R1T Chimera TEE", - family: "tngtech", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 0.85 }, - limit: { context: 163840, output: 65536 }, - }, - }, - }, - dinference: { - id: "dinference", - env: ["DINFERENCE_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.dinference.com/v1", - name: "DInference", - doc: "https://dinference.com", - models: { - "glm-4.7": { - id: "glm-4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12", - last_updated: "2025-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.45, output: 1.65 }, - limit: { context: 200000, output: 128000 }, - }, - "glm-5": { - id: "glm-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02", - last_updated: "2026-02", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.75, output: 2.4 }, - limit: { context: 200000, output: 128000 }, - }, - "gpt-oss-120b": { - id: "gpt-oss-120b", - name: "GPT OSS 120B", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-08", - last_updated: "2025-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.0675, output: 0.27 }, - limit: { context: 131072, output: 32768 }, - }, - }, - }, - vivgrid: { - id: "vivgrid", - env: ["VIVGRID_API_KEY"], - npm: "@ai-sdk/openai", - api: "https://api.vivgrid.com/v1", - name: "Vivgrid", - doc: "https://docs.vivgrid.com/models", - models: { - "gpt-5.1-codex-max": { - id: "gpt-5.1-codex-max", - name: "GPT-5.1 Codex Max", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "gemini-3.1-flash-lite-preview": { - id: "gemini-3.1-flash-lite-preview", - name: "Gemini 3.1 Flash Lite Preview", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-03-03", - last_updated: "2026-03-03", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1.5, cache_read: 0.025, cache_write: 1 }, - limit: { context: 1048576, output: 65536 }, - provider: { npm: "@ai-sdk/openai-compatible" }, - }, - "glm-5": { - id: "glm-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3.2, cache_read: 0.2 }, - limit: { context: 202752, output: 131000 }, - provider: { npm: "@ai-sdk/openai-compatible" }, - }, - "gemini-3.1-pro-preview": { - id: "gemini-3.1-pro-preview", - name: "Gemini 3.1 Pro Preview", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-02-19", - last_updated: "2026-02-19", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2, context_over_200k: { input: 4, output: 18, cache_read: 0.4 } }, - limit: { context: 1048576, output: 65536 }, - provider: { npm: "@ai-sdk/openai-compatible" }, - }, - "gpt-5-mini": { - id: "gpt-5-mini", - name: "GPT-5 Mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.03 }, - limit: { context: 272000, output: 128000 }, - provider: { npm: "@ai-sdk/openai-compatible" }, - }, - "gpt-5.3-codex": { - id: "gpt-5.3-codex", - name: "GPT-5.3 Codex", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-02-24", - last_updated: "2026-02-24", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, output: 128000 }, - }, - "gpt-5.4-mini": { - id: "gpt-5.4-mini", - name: "GPT-5.4 Mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.75, output: 4.5, cache_read: 0.075 }, - limit: { context: 400000, input: 272000, output: 128000 }, - provider: { npm: "@ai-sdk/openai-compatible" }, - }, - "gpt-5.4-nano": { - id: "gpt-5.4-nano", - name: "GPT-5.4 Nano", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.25, cache_read: 0.02 }, - limit: { context: 400000, input: 272000, output: 128000 }, - provider: { npm: "@ai-sdk/openai-compatible" }, - }, - "gpt-5.2-codex": { - id: "gpt-5.2-codex", - name: "GPT-5.2 Codex", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-01-14", - last_updated: "2026-01-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, output: 128000 }, - }, - "gpt-5.4": { - id: "gpt-5.4", - name: "GPT-5.4", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 15, cache_read: 0.25 }, - limit: { context: 400000, input: 272000, output: 128000 }, - provider: { npm: "@ai-sdk/openai-compatible" }, - }, - "deepseek-v3.2": { - id: "deepseek-v3.2", - name: "DeepSeek-V3.2", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.28, output: 0.42 }, - limit: { context: 128000, output: 128000 }, - provider: { npm: "@ai-sdk/openai-compatible" }, - }, - "gpt-5.1-codex": { - id: "gpt-5.1-codex", - name: "GPT-5.1 Codex", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - }, - }, - deepinfra: { - id: "deepinfra", - env: ["DEEPINFRA_API_KEY"], - npm: "@ai-sdk/deepinfra", - name: "Deep Infra", - doc: "https://deepinfra.com/models", - models: { - "Qwen/Qwen3-Coder-480B-A35B-Instruct-Turbo": { - id: "Qwen/Qwen3-Coder-480B-A35B-Instruct-Turbo", - name: "Qwen3 Coder 480B A35B Instruct Turbo", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 262144, output: 66536 }, - }, - "Qwen/Qwen3-Coder-480B-A35B-Instruct": { - id: "Qwen/Qwen3-Coder-480B-A35B-Instruct", - name: "Qwen3 Coder 480B A35B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 1.6 }, - limit: { context: 262144, output: 66536 }, - }, - "zai-org/GLM-4.7-Flash": { - id: "zai-org/GLM-4.7-Flash", - name: "GLM-4.7-Flash", - family: "glm-flash", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2026-01-19", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.06, output: 0.4 }, - limit: { context: 202752, output: 16384 }, - }, - "zai-org/GLM-4.5": { - id: "zai-org/GLM-4.5", - name: "GLM-4.5", - family: "glm", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2 }, - limit: { context: 131072, output: 98304 }, - status: "deprecated", - }, - "zai-org/GLM-4.7": { - id: "zai-org/GLM-4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.43, output: 1.75, cache_read: 0.08 }, - limit: { context: 202752, output: 16384 }, - }, - "zai-org/GLM-5.1": { - id: "zai-org/GLM-5.1", - name: "GLM-5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-04-07", - last_updated: "2026-04-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.4, output: 4.4, cache_read: 0.26 }, - limit: { context: 202752, output: 16384 }, - }, - "zai-org/GLM-5": { - id: "zai-org/GLM-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-12", - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.8, output: 2.56, cache_read: 0.16 }, - limit: { context: 202752, output: 16384 }, - }, - "zai-org/GLM-4.6V": { - id: "zai-org/GLM-4.6V", - name: "GLM-4.6V", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.9 }, - limit: { context: 204800, output: 131072 }, - }, - "zai-org/GLM-4.6": { - id: "zai-org/GLM-4.6", - name: "GLM-4.6", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.43, output: 1.74, cache_read: 0.08 }, - limit: { context: 204800, output: 131072 }, - }, - "meta-llama/Llama-4-Scout-17B-16E-Instruct": { - id: "meta-llama/Llama-4-Scout-17B-16E-Instruct", - name: "Llama 4 Scout 17B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.08, output: 0.3 }, - limit: { context: 10000000, output: 16384 }, - }, - "meta-llama/Llama-3.1-8B-Instruct": { - id: "meta-llama/Llama-3.1-8B-Instruct", - name: "Llama 3.1 8B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0.05 }, - limit: { context: 131072, output: 16384 }, - }, - "meta-llama/Llama-3.1-70B-Instruct": { - id: "meta-llama/Llama-3.1-70B-Instruct", - name: "Llama 3.1 70B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 0.4 }, - limit: { context: 131072, output: 16384 }, - }, - "meta-llama/Llama-3.1-8B-Instruct-Turbo": { - id: "meta-llama/Llama-3.1-8B-Instruct-Turbo", - name: "Llama 3.1 8B Turbo", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0.03 }, - limit: { context: 131072, output: 16384 }, - }, - "meta-llama/Llama-3.3-70B-Instruct-Turbo": { - id: "meta-llama/Llama-3.3-70B-Instruct-Turbo", - name: "Llama 3.3 70B Turbo", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.32 }, - limit: { context: 131072, output: 16384 }, - }, - "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8": { - id: "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8", - name: "Llama 4 Maverick 17B FP8", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 1000000, output: 16384 }, - }, - "meta-llama/Llama-3.1-70B-Instruct-Turbo": { - id: "meta-llama/Llama-3.1-70B-Instruct-Turbo", - name: "Llama 3.1 70B Turbo", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 0.4 }, - limit: { context: 131072, output: 16384 }, - }, - "deepseek-ai/DeepSeek-R1-0528": { - id: "deepseek-ai/DeepSeek-R1-0528", - name: "DeepSeek-R1-0528", - attachment: false, - reasoning: true, - tool_call: false, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-07", - release_date: "2025-05-28", - last_updated: "2025-05-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 2.15, cache_read: 0.35 }, - limit: { context: 163840, output: 64000 }, - }, - "deepseek-ai/DeepSeek-V3.2": { - id: "deepseek-ai/DeepSeek-V3.2", - name: "DeepSeek-V3.2", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2025-12-02", - last_updated: "2025-12-02", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.26, output: 0.38, cache_read: 0.13 }, - limit: { context: 163840, output: 64000 }, - }, - "openai/gpt-oss-20b": { - id: "openai/gpt-oss-20b", - name: "GPT OSS 20B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.14 }, - limit: { context: 131072, output: 16384 }, - }, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "GPT OSS 120B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.24 }, - limit: { context: 131072, output: 16384 }, - }, - "moonshotai/Kimi-K2-Thinking": { - id: "moonshotai/Kimi-K2-Thinking", - name: "Kimi K2 Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-10", - release_date: "2025-11-06", - last_updated: "2025-11-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.47, output: 2 }, - limit: { context: 131072, output: 32768 }, - }, - "moonshotai/Kimi-K2-Instruct": { - id: "moonshotai/Kimi-K2-Instruct", - name: "Kimi K2", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-07-11", - last_updated: "2025-07-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 2 }, - limit: { context: 131072, output: 32768 }, - }, - "moonshotai/Kimi-K2-Instruct-0905": { - id: "moonshotai/Kimi-K2-Instruct-0905", - name: "Kimi K2 0905", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 2, cache_read: 0.15 }, - limit: { context: 262144, output: 262144 }, - }, - "moonshotai/Kimi-K2.5": { - id: "moonshotai/Kimi-K2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 2.8 }, - limit: { context: 262144, output: 32768 }, - }, - "MiniMaxAI/MiniMax-M2": { - id: "MiniMaxAI/MiniMax-M2", - name: "MiniMax M2", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-10", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.254, output: 1.02 }, - limit: { context: 262144, output: 32768 }, - }, - "MiniMaxAI/MiniMax-M2.5": { - id: "MiniMaxAI/MiniMax-M2.5", - name: "MiniMax M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-06", - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 0.95, cache_read: 0.03, cache_write: 0.375 }, - limit: { context: 204800, output: 131072 }, - }, - "MiniMaxAI/MiniMax-M2.1": { - id: "MiniMaxAI/MiniMax-M2.1", - name: "MiniMax M2.1", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-06", - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.28, output: 1.2 }, - limit: { context: 196608, output: 196608 }, - }, - "anthropic/claude-3-7-sonnet-latest": { - id: "anthropic/claude-3-7-sonnet-latest", - name: "Claude Sonnet 3.7 (Latest)", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10-31", - release_date: "2025-03-13", - last_updated: "2025-03-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3.3, output: 16.5, cache_read: 0.33 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-4-opus": { - id: "anthropic/claude-4-opus", - name: "Claude Opus 4", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-06-12", - last_updated: "2025-06-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 16.5, output: 82.5 }, - limit: { context: 200000, output: 32000 }, - }, - }, - }, - "qiniu-ai": { - id: "qiniu-ai", - env: ["QINIU_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.qnaigc.com/v1", - name: "Qiniu", - doc: "https://developer.qiniu.com/aitokenapi", - models: { - "qwen3-235b-a22b": { - id: "qwen3-235b-a22b", - name: "Qwen 3 235B A22B", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 32000 }, - }, - "doubao-seed-1.6-flash": { - id: "doubao-seed-1.6-flash", - name: "Doubao-Seed 1.6 Flash", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-15", - last_updated: "2025-08-15", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 256000, output: 32000 }, - }, - "qwen3-235b-a22b-instruct-2507": { - id: "qwen3-235b-a22b-instruct-2507", - name: "Qwen3 235b A22B Instruct 2507", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-12", - last_updated: "2025-08-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 262144, output: 64000 }, - }, - "doubao-seed-2.0-code": { - id: "doubao-seed-2.0-code", - name: "Doubao Seed 2.0 Code", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2026-02-14", - last_updated: "2026-02-14", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 256000, output: 128000 }, - }, - "deepseek-v3-0324": { - id: "deepseek-v3-0324", - name: "DeepSeek-V3-0324", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 16000 }, - }, - "doubao-1.5-thinking-pro": { - id: "doubao-1.5-thinking-pro", - name: "Doubao 1.5 Thinking Pro", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 16000 }, - }, - "claude-3.7-sonnet": { - id: "claude-3.7-sonnet", - name: "Claude 3.7 Sonnet", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - limit: { context: 200000, output: 128000 }, - }, - "qwen3.5-397b-a17b": { - id: "qwen3.5-397b-a17b", - name: "Qwen3.5 397B A17B", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2026-02-22", - last_updated: "2026-02-22", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - limit: { context: 256000, output: 64000 }, - }, - "qwen-vl-max-2025-01-25": { - id: "qwen-vl-max-2025-01-25", - name: "Qwen VL-MAX-2025-01-25", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 4096 }, - }, - "qwen3-32b": { - id: "qwen3-32b", - name: "Qwen3 32B", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 40000, output: 4096 }, - }, - "doubao-1.5-pro-32k": { - id: "doubao-1.5-pro-32k", - name: "Doubao 1.5 Pro 32k", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 12000 }, - }, - "qwen2.5-vl-72b-instruct": { - id: "qwen2.5-vl-72b-instruct", - name: "Qwen 2.5 VL 72B Instruct", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 8192 }, - }, - "gemini-2.0-flash": { - id: "gemini-2.0-flash", - name: "Gemini 2.0 Flash", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 1048576, output: 8192 }, - }, - "qwen3-vl-30b-a3b-thinking": { - id: "qwen3-vl-30b-a3b-thinking", - name: "Qwen3-Vl 30b A3b Thinking", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2026-02-09", - last_updated: "2026-02-09", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 32000 }, - }, - "gemini-3.0-pro-image-preview": { - id: "gemini-3.0-pro-image-preview", - name: "Gemini 3.0 Pro Image Preview", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-11-20", - last_updated: "2025-11-20", - modalities: { input: ["text", "image"], output: ["text", "image"] }, - open_weights: false, - limit: { context: 32768, output: 8192 }, - }, - "gemini-2.5-pro": { - id: "gemini-2.5-pro", - name: "Gemini 2.5 Pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: false, - limit: { context: 1048576, output: 65536 }, - }, - "claude-4.5-opus": { - id: "claude-4.5-opus", - name: "Claude 4.5 Opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-11-25", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - limit: { context: 200000, output: 200000 }, - }, - "deepseek-r1": { - id: "deepseek-r1", - name: "DeepSeek-R1", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 32000 }, - }, - "claude-4.0-opus": { - id: "claude-4.0-opus", - name: "Claude 4.0 Opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - limit: { context: 200000, output: 32000 }, - }, - "claude-4.5-haiku": { - id: "claude-4.5-haiku", - name: "Claude 4.5 Haiku", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-10-16", - last_updated: "2025-10-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - limit: { context: 200000, output: 64000 }, - }, - "qwen3-max": { - id: "qwen3-max", - name: "Qwen3 Max", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-09-24", - last_updated: "2025-09-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 262144, output: 65536 }, - }, - "gemini-3.0-flash-preview": { - id: "gemini-3.0-flash-preview", - name: "Gemini 3.0 Flash Preview", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-12-18", - last_updated: "2025-12-18", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - limit: { context: 1000000, output: 64000 }, - }, - "gemini-2.5-flash-image": { - id: "gemini-2.5-flash-image", - name: "Gemini 2.5 Flash Image", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-10-22", - last_updated: "2025-10-22", - modalities: { input: ["text", "image"], output: ["image"] }, - open_weights: false, - limit: { context: 32768, output: 8192 }, - }, - "glm-4.5": { - id: "glm-4.5", - name: "GLM 4.5", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 131072, output: 98304 }, - }, - "claude-3.5-sonnet": { - id: "claude-3.5-sonnet", - name: "Claude 3.5 Sonnet", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-09-09", - last_updated: "2025-09-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - limit: { context: 200000, output: 8200 }, - }, - "claude-4.0-sonnet": { - id: "claude-4.0-sonnet", - name: "Claude 4.0 Sonnet", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - limit: { context: 200000, output: 64000 }, - }, - "qwen3-30b-a3b-instruct-2507": { - id: "qwen3-30b-a3b-instruct-2507", - name: "Qwen3 30b A3b Instruct 2507", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2026-02-04", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 32000 }, - }, - "doubao-seed-1.6-thinking": { - id: "doubao-seed-1.6-thinking", - name: "Doubao-Seed 1.6 Thinking", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-15", - last_updated: "2025-08-15", - modalities: { input: ["image", "text", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 256000, output: 32000 }, - }, - "gemini-2.5-flash": { - id: "gemini-2.5-flash", - name: "Gemini 2.5 Flash", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 1048576, output: 64000 }, - }, - "qwen3-235b-a22b-thinking-2507": { - id: "qwen3-235b-a22b-thinking-2507", - name: "Qwen3 235B A22B Thinking 2507", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-12", - last_updated: "2025-08-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 262144, output: 4096 }, - }, - "qwen3-next-80b-a3b-thinking": { - id: "qwen3-next-80b-a3b-thinking", - name: "Qwen3 Next 80B A3B Thinking", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-09-12", - last_updated: "2025-09-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 131072, output: 32768 }, - }, - "qwen3-30b-a3b-thinking-2507": { - id: "qwen3-30b-a3b-thinking-2507", - name: "Qwen3 30b A3b Thinking 2507", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2026-02-04", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 126000, output: 32000 }, - }, - "glm-4.5-air": { - id: "glm-4.5-air", - name: "GLM 4.5 Air", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 131000, output: 4096 }, - }, - "deepseek-v3.1": { - id: "deepseek-v3.1", - name: "DeepSeek-V3.1", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-19", - last_updated: "2025-08-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 32000 }, - }, - "qwen3-30b-a3b": { - id: "qwen3-30b-a3b", - name: "Qwen3 30B A3B", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 40000, output: 4096 }, - }, - "claude-4.1-opus": { - id: "claude-4.1-opus", - name: "Claude 4.1 Opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-06", - last_updated: "2025-08-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - limit: { context: 200000, output: 32000 }, - }, - "doubao-seed-2.0-mini": { - id: "doubao-seed-2.0-mini", - name: "Doubao Seed 2.0 Mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2026-02-14", - last_updated: "2026-02-14", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 256000, output: 32000 }, - }, - "qwen3-next-80b-a3b-instruct": { - id: "qwen3-next-80b-a3b-instruct", - name: "Qwen3 Next 80B A3B Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-09-12", - last_updated: "2025-09-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 131072, output: 32768 }, - }, - "doubao-seed-1.6": { - id: "doubao-seed-1.6", - name: "Doubao-Seed 1.6", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-15", - last_updated: "2025-08-15", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 256000, output: 32000 }, - }, - "qwen2.5-vl-7b-instruct": { - id: "qwen2.5-vl-7b-instruct", - name: "Qwen 2.5 VL 7B Instruct", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 8192 }, - }, - "kling-v2-6": { - id: "kling-v2-6", - name: "Kling-V2 6", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2026-01-13", - last_updated: "2026-01-13", - modalities: { input: ["text", "image", "video"], output: ["video"] }, - open_weights: false, - limit: { context: 99999999, output: 99999999 }, - }, - "MiniMax-M1": { - id: "MiniMax-M1", - name: "MiniMax M1", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 1000000, output: 80000 }, - }, - "gemini-3.0-pro-preview": { - id: "gemini-3.0-pro-preview", - name: "Gemini 3.0 Pro Preview", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-11-19", - last_updated: "2025-11-19", - modalities: { input: ["text", "image", "video", "pdf", "audio"], output: ["text"] }, - open_weights: false, - limit: { context: 1000000, output: 64000 }, - }, - "doubao-seed-2.0-lite": { - id: "doubao-seed-2.0-lite", - name: "Doubao Seed 2.0 Lite", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2026-02-14", - last_updated: "2026-02-14", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 256000, output: 32000 }, - }, - "qwen3-coder-480b-a35b-instruct": { - id: "qwen3-coder-480b-a35b-instruct", - name: "Qwen3 Coder 480B A35B Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-14", - last_updated: "2025-08-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 262000, output: 4096 }, - }, - "claude-3.5-haiku": { - id: "claude-3.5-haiku", - name: "Claude 3.5 Haiku", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-26", - last_updated: "2025-08-26", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - limit: { context: 200000, output: 8192 }, - }, - "gpt-oss-20b": { - id: "gpt-oss-20b", - name: "gpt-oss-20b", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-06", - last_updated: "2025-08-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 4096 }, - }, - "qwen-turbo": { - id: "qwen-turbo", - name: "Qwen-Turbo", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 1000000, output: 4096 }, - }, - "kimi-k2": { - id: "kimi-k2", - name: "Kimi K2", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 128000 }, - }, - "gemini-2.5-flash-lite": { - id: "gemini-2.5-flash-lite", - name: "Gemini 2.5 Flash Lite", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 1048576, output: 64000 }, - }, - "mimo-v2-flash": { - id: "mimo-v2-flash", - name: "Mimo-V2-Flash", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 256000, output: 256000 }, - }, - "qwen3-max-preview": { - id: "qwen3-max-preview", - name: "Qwen3 Max Preview", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-09-06", - last_updated: "2025-09-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 256000, output: 64000 }, - }, - "gpt-oss-120b": { - id: "gpt-oss-120b", - name: "gpt-oss-120b", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-06", - last_updated: "2025-08-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 4096 }, - }, - "doubao-1.5-vision-pro": { - id: "doubao-1.5-vision-pro", - name: "Doubao 1.5 Vision Pro", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 16000 }, - }, - "claude-4.5-sonnet": { - id: "claude-4.5-sonnet", - name: "Claude 4.5 Sonnet", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - limit: { context: 200000, output: 64000 }, - }, - "deepseek-v3": { - id: "deepseek-v3", - name: "DeepSeek-V3", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-08-13", - last_updated: "2025-08-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 16000 }, - }, - "deepseek-r1-0528": { - id: "deepseek-r1-0528", - name: "DeepSeek-R1-0528", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 32000 }, - }, - "gemini-2.0-flash-lite": { - id: "gemini-2.0-flash-lite", - name: "Gemini 2.0 Flash Lite", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 1048576, output: 8192 }, - }, - "qwen-max-2025-01-25": { - id: "qwen-max-2025-01-25", - name: "Qwen2.5-Max-2025-01-25", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 4096 }, - }, - "doubao-seed-2.0-pro": { - id: "doubao-seed-2.0-pro", - name: "Doubao Seed 2.0 Pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2026-02-14", - last_updated: "2026-02-14", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 256000, output: 128000 }, - }, - "deepseek/deepseek-v3.2-exp-thinking": { - id: "deepseek/deepseek-v3.2-exp-thinking", - name: "DeepSeek/DeepSeek-V3.2-Exp-Thinking", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 32000 }, - }, - "deepseek/deepseek-v3.1-terminus-thinking": { - id: "deepseek/deepseek-v3.1-terminus-thinking", - name: "DeepSeek/DeepSeek-V3.1-Terminus-Thinking", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-09-22", - last_updated: "2025-09-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 32000 }, - }, - "deepseek/deepseek-v3.2-exp": { - id: "deepseek/deepseek-v3.2-exp", - name: "DeepSeek/DeepSeek-V3.2-Exp", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 32000 }, - }, - "deepseek/deepseek-v3.2-251201": { - id: "deepseek/deepseek-v3.2-251201", - name: "Deepseek/DeepSeek-V3.2", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 32000 }, - }, - "deepseek/deepseek-math-v2": { - id: "deepseek/deepseek-math-v2", - name: "Deepseek/Deepseek-Math-V2", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-12-04", - last_updated: "2025-12-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 160000, output: 160000 }, - }, - "deepseek/deepseek-v3.1-terminus": { - id: "deepseek/deepseek-v3.1-terminus", - name: "DeepSeek/DeepSeek-V3.1-Terminus", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-09-22", - last_updated: "2025-09-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 32000 }, - }, - "stepfun-ai/gelab-zero-4b-preview": { - id: "stepfun-ai/gelab-zero-4b-preview", - name: "Stepfun-Ai/Gelab Zero 4b Preview", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - limit: { context: 8192, output: 4096 }, - }, - "stepfun/step-3.5-flash": { - id: "stepfun/step-3.5-flash", - name: "Stepfun/Step-3.5 Flash", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2026-02-02", - last_updated: "2026-02-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - limit: { context: 64000, output: 4096 }, - }, - "x-ai/grok-4-fast": { - id: "x-ai/grok-4-fast", - name: "x-AI/Grok-4-Fast", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-09-20", - last_updated: "2025-09-20", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 2000000, output: 2000000 }, - }, - "x-ai/grok-code-fast-1": { - id: "x-ai/grok-code-fast-1", - name: "x-AI/Grok-Code-Fast 1", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-09-02", - last_updated: "2025-09-02", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 256000, output: 10000 }, - }, - "x-ai/grok-4-fast-reasoning": { - id: "x-ai/grok-4-fast-reasoning", - name: "X-Ai/Grok-4-Fast-Reasoning", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-12-18", - last_updated: "2025-12-18", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 2000000, output: 2000000 }, - }, - "x-ai/grok-4.1-fast-non-reasoning": { - id: "x-ai/grok-4.1-fast-non-reasoning", - name: "X-Ai/Grok 4.1 Fast Non Reasoning", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-12-19", - last_updated: "2025-12-19", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 2000000, output: 2000000 }, - }, - "x-ai/grok-4.1-fast": { - id: "x-ai/grok-4.1-fast", - name: "x-AI/Grok-4.1-Fast", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-11-20", - last_updated: "2025-11-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 2000000, output: 2000000 }, - }, - "x-ai/grok-4-fast-non-reasoning": { - id: "x-ai/grok-4-fast-non-reasoning", - name: "X-Ai/Grok-4-Fast-Non-Reasoning", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-12-18", - last_updated: "2025-12-18", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 2000000, output: 2000000 }, - }, - "x-ai/grok-4.1-fast-reasoning": { - id: "x-ai/grok-4.1-fast-reasoning", - name: "X-Ai/Grok 4.1 Fast Reasoning", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-12-19", - last_updated: "2025-12-19", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 20000000, output: 2000000 }, - }, - "openai/gpt-5.2": { - id: "openai/gpt-5.2", - name: "OpenAI/GPT-5.2", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5": { - id: "openai/gpt-5", - name: "OpenAI/GPT-5", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-09-19", - last_updated: "2025-09-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 400000, output: 128000 }, - }, - "z-ai/glm-4.7": { - id: "z-ai/glm-4.7", - name: "Z-Ai/GLM 4.7", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 200000, output: 200000 }, - }, - "z-ai/glm-5": { - id: "z-ai/glm-5", - name: "Z-Ai/GLM 5", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 200000, output: 128000 }, - }, - "z-ai/autoglm-phone-9b": { - id: "z-ai/autoglm-phone-9b", - name: "Z-Ai/Autoglm Phone 9b", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - limit: { context: 12800, output: 4096 }, - }, - "z-ai/glm-4.6": { - id: "z-ai/glm-4.6", - name: "Z-AI/GLM 4.6", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-10-11", - last_updated: "2025-10-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 200000, output: 200000 }, - }, - "minimax/minimax-m2": { - id: "minimax/minimax-m2", - name: "Minimax/Minimax-M2", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-10-28", - last_updated: "2025-10-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 200000, output: 128000 }, - }, - "minimax/minimax-m2.1": { - id: "minimax/minimax-m2.1", - name: "Minimax/Minimax-M2.1", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 204800, output: 128000 }, - }, - "minimax/minimax-m2.5": { - id: "minimax/minimax-m2.5", - name: "Minimax/Minimax-M2.5", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 204800, output: 128000 }, - }, - "minimax/minimax-m2.5-highspeed": { - id: "minimax/minimax-m2.5-highspeed", - name: "Minimax/Minimax-M2.5 Highspeed", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2026-02-14", - last_updated: "2026-02-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 204800, output: 128000 }, - }, - "moonshotai/kimi-k2.5": { - id: "moonshotai/kimi-k2.5", - name: "Moonshotai/Kimi-K2.5", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2026-01-28", - last_updated: "2026-01-28", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 256000, output: 256000 }, - }, - "moonshotai/kimi-k2-0905": { - id: "moonshotai/kimi-k2-0905", - name: "Kimi K2 0905", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-09-08", - last_updated: "2025-09-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 256000, output: 100000 }, - }, - "moonshotai/kimi-k2-thinking": { - id: "moonshotai/kimi-k2-thinking", - name: "Kimi K2 Thinking", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-11-07", - last_updated: "2025-11-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 256000, output: 100000 }, - }, - "xiaomi/mimo-v2-flash": { - id: "xiaomi/mimo-v2-flash", - name: "Xiaomi/Mimo-V2-Flash", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-12-26", - last_updated: "2025-12-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 256000, output: 256000 }, - }, - "meituan/longcat-flash-chat": { - id: "meituan/longcat-flash-chat", - name: "Meituan/Longcat-Flash-Chat", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-11-05", - last_updated: "2025-11-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 131072, output: 131072 }, - }, - "meituan/longcat-flash-lite": { - id: "meituan/longcat-flash-lite", - name: "Meituan/Longcat-Flash-Lite", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2026-02-06", - last_updated: "2026-02-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 256000, output: 320000 }, - }, - }, - }, - kilo: { - id: "kilo", - env: ["KILO_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.kilo.ai/api/gateway", - name: "Kilo Gateway", - doc: "https://kilo.ai", - models: { - "giga-potato": { - id: "giga-potato", - name: "Giga Potato (free)", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-08-27", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 256000, output: 32000 }, - }, - "corethink:free": { - id: "corethink:free", - name: "CoreThink (free)", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-08-27", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 78000, output: 8192 }, - }, - "giga-potato-thinking": { - id: "giga-potato-thinking", - name: "Giga Potato Thinking (free)", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-27", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 256000, output: 32000 }, - }, - "morph-warp-grep-v2": { - id: "morph-warp-grep-v2", - name: "Morph: WarpGrep V2", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-08-27", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 256000, output: 32000 }, - }, - "ai21/jamba-large-1.7": { - id: "ai21/jamba-large-1.7", - name: "AI21: Jamba Large 1.7", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-08-09", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8 }, - limit: { context: 256000, output: 4096 }, - }, - "alibaba/tongyi-deepresearch-30b-a3b": { - id: "alibaba/tongyi-deepresearch-30b-a3b", - name: "Tongyi DeepResearch 30B A3B", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-09-18", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.09, output: 0.45 }, - limit: { context: 131072, output: 131072 }, - }, - "inflection/inflection-3-pi": { - id: "inflection/inflection-3-pi", - name: "Inflection: Inflection 3 Pi", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-10-11", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10 }, - limit: { context: 8000, output: 1024 }, - }, - "inflection/inflection-3-productivity": { - id: "inflection/inflection-3-productivity", - name: "Inflection: Inflection 3 Productivity", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-10-11", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10 }, - limit: { context: 8000, output: 1024 }, - }, - "liquid/lfm2-8b-a1b": { - id: "liquid/lfm2-8b-a1b", - name: "LiquidAI: LFM2-8B-A1B", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-10-20", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.01, output: 0.02 }, - limit: { context: 32768, output: 32768 }, - }, - "liquid/lfm-2.2-6b": { - id: "liquid/lfm-2.2-6b", - name: "LiquidAI: LFM2-2.6B", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-10-20", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.01, output: 0.02 }, - limit: { context: 32768, output: 32768 }, - }, - "liquid/lfm-2-24b-a2b": { - id: "liquid/lfm-2-24b-a2b", - name: "LiquidAI: LFM2-24B-A2B", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2026-02-26", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.12 }, - limit: { context: 32768, output: 32768 }, - }, - "writer/palmyra-x5": { - id: "writer/palmyra-x5", - name: "Writer: Palmyra X5", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-28", - last_updated: "2025-04-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 6 }, - limit: { context: 1040000, output: 8192 }, - }, - "ibm-granite/granite-4.0-h-micro": { - id: "ibm-granite/granite-4.0-h-micro", - name: "IBM: Granite 4.0 Micro", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-10-20", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.017, output: 0.11 }, - limit: { context: 131000, output: 32768 }, - }, - "essentialai/rnj-1-instruct": { - id: "essentialai/rnj-1-instruct", - name: "EssentialAI: Rnj 1 Instruct", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-12-05", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.15 }, - limit: { context: 32768, output: 6554 }, - }, - "perplexity/sonar-pro": { - id: "perplexity/sonar-pro", - name: "Perplexity: Sonar Pro", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-01-01", - last_updated: "2025-09-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 200000, output: 8000 }, - }, - "perplexity/sonar-deep-research": { - id: "perplexity/sonar-deep-research", - name: "Perplexity: Sonar Deep Research", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-01-27", - last_updated: "2025-01-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8 }, - limit: { context: 128000, output: 25600 }, - }, - "perplexity/sonar": { - id: "perplexity/sonar", - name: "Perplexity: Sonar", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-01-01", - last_updated: "2025-09-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 1 }, - limit: { context: 127072, output: 25415 }, - }, - "perplexity/sonar-pro-search": { - id: "perplexity/sonar-pro-search", - name: "Perplexity: Sonar Pro Search", - attachment: true, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-10-31", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 200000, output: 8000 }, - }, - "perplexity/sonar-reasoning-pro": { - id: "perplexity/sonar-reasoning-pro", - name: "Perplexity: Sonar Reasoning Pro", - attachment: true, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2024-01-01", - last_updated: "2025-09-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8 }, - limit: { context: 128000, output: 25600 }, - }, - "deepseek/deepseek-chat-v3.1": { - id: "deepseek/deepseek-chat-v3.1", - name: "DeepSeek: DeepSeek V3.1", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-21", - last_updated: "2025-08-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.75 }, - limit: { context: 32768, output: 7168 }, - }, - "deepseek/deepseek-chat": { - id: "deepseek/deepseek-chat", - name: "DeepSeek: DeepSeek V3", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-12-01", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.32, output: 0.89, cache_read: 0.15 }, - limit: { context: 163840, output: 163840 }, - }, - "deepseek/deepseek-r1-distill-llama-70b": { - id: "deepseek/deepseek-r1-distill-llama-70b", - name: "DeepSeek: R1 Distill Llama 70B", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-01-23", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.7, output: 0.8, cache_read: 0.015 }, - limit: { context: 131072, output: 16384 }, - }, - "deepseek/deepseek-r1": { - id: "deepseek/deepseek-r1", - name: "DeepSeek: R1", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.7, output: 2.5 }, - limit: { context: 64000, output: 16000 }, - }, - "deepseek/deepseek-v3.2-speciale": { - id: "deepseek/deepseek-v3.2-speciale", - name: "DeepSeek: DeepSeek V3.2 Speciale", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-12-01", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 1.2, cache_read: 0.135 }, - limit: { context: 163840, output: 163840 }, - }, - "deepseek/deepseek-r1-distill-qwen-32b": { - id: "deepseek/deepseek-r1-distill-qwen-32b", - name: "DeepSeek: R1 Distill Qwen 32B", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-01-01", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.29, output: 0.29 }, - limit: { context: 32768, output: 32768 }, - }, - "deepseek/deepseek-v3.2-exp": { - id: "deepseek/deepseek-v3.2-exp", - name: "DeepSeek: DeepSeek V3.2 Exp", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-01-01", - last_updated: "2025-09-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 0.41 }, - limit: { context: 163840, output: 65536 }, - }, - "deepseek/deepseek-v3.2": { - id: "deepseek/deepseek-v3.2", - name: "DeepSeek: DeepSeek V3.2", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-12-01", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.26, output: 0.38, cache_read: 0.125 }, - limit: { context: 163840, output: 65536 }, - }, - "deepseek/deepseek-chat-v3-0324": { - id: "deepseek/deepseek-chat-v3-0324", - name: "DeepSeek: DeepSeek V3 0324", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-03-24", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.77, cache_read: 0.095 }, - limit: { context: 163840, output: 65536 }, - }, - "deepseek/deepseek-r1-0528": { - id: "deepseek/deepseek-r1-0528", - name: "DeepSeek: R1 0528", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-05-28", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.45, output: 2.15, cache_read: 0.2 }, - limit: { context: 163840, output: 65536 }, - }, - "deepseek/deepseek-v3.1-terminus": { - id: "deepseek/deepseek-v3.1-terminus", - name: "DeepSeek: DeepSeek V3.1 Terminus", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-09-22", - last_updated: "2025-09-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.21, output: 0.79, cache_read: 0.13 }, - limit: { context: 163840, output: 32768 }, - }, - "openrouter/hunter-alpha": { - id: "openrouter/hunter-alpha", - name: "Hunter Alpha", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-12", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 1048576, output: 32000 }, - }, - "openrouter/auto": { - id: "openrouter/auto", - name: "Auto Router", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-15", - last_updated: "2026-03-15", - modalities: { input: ["audio", "image", "pdf", "text", "video"], output: ["image", "text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 2000000, output: 32768 }, - }, - "openrouter/bodybuilder": { - id: "openrouter/bodybuilder", - name: "Body Builder (beta)", - attachment: false, - reasoning: false, - tool_call: false, - release_date: "2026-03-15", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 32768 }, - status: "beta", - }, - "openrouter/healer-alpha": { - id: "openrouter/healer-alpha", - name: "Healer Alpha", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-12", - last_updated: "2026-03-15", - modalities: { input: ["audio", "image", "text", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 32000 }, - }, - "openrouter/free": { - id: "openrouter/free", - name: "Free Models Router", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-01", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 200000, output: 32768 }, - }, - "arcee-ai/trinity-mini": { - id: "arcee-ai/trinity-mini", - name: "Arcee AI: Trinity Mini", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-12", - last_updated: "2026-01-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.045, output: 0.15 }, - limit: { context: 131072, output: 131072 }, - }, - "arcee-ai/virtuoso-large": { - id: "arcee-ai/virtuoso-large", - name: "Arcee AI: Virtuoso Large", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-05-06", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.75, output: 1.2 }, - limit: { context: 131072, output: 64000 }, - }, - "arcee-ai/spotlight": { - id: "arcee-ai/spotlight", - name: "Arcee AI: Spotlight", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-05-06", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.18, output: 0.18 }, - limit: { context: 131072, output: 65537 }, - }, - "arcee-ai/maestro-reasoning": { - id: "arcee-ai/maestro-reasoning", - name: "Arcee AI: Maestro Reasoning", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-05-06", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.9, output: 3.3 }, - limit: { context: 131072, output: 32000 }, - }, - "arcee-ai/trinity-large-preview:free": { - id: "arcee-ai/trinity-large-preview:free", - name: "Arcee AI: Trinity Large Preview (free)", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2026-01-28", - last_updated: "2026-01-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 131000, output: 26200 }, - }, - "arcee-ai/coder-large": { - id: "arcee-ai/coder-large", - name: "Arcee AI: Coder Large", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-05-06", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 0.8 }, - limit: { context: 32768, output: 32768 }, - }, - "deepcogito/cogito-v2.1-671b": { - id: "deepcogito/cogito-v2.1-671b", - name: "Deep Cogito: Cogito v2.1 671B", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-11-14", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.25, output: 1.25 }, - limit: { context: 128000, output: 32768 }, - }, - "upstage/solar-pro-3": { - id: "upstage/solar-pro-3", - name: "Upstage: Solar Pro 3", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-01-27", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 128000, output: 32768 }, - }, - "nex-agi/deepseek-v3.1-nex-n1": { - id: "nex-agi/deepseek-v3.1-nex-n1", - name: "Nex AGI: DeepSeek V3.1 Nex N1", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-01-01", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 1 }, - limit: { context: 131072, output: 163840 }, - }, - "bytedance-seed/seed-1.6": { - id: "bytedance-seed/seed-1.6", - name: "ByteDance Seed: Seed 1.6", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-09", - last_updated: "2025-09", - modalities: { input: ["image", "text", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2 }, - limit: { context: 262144, output: 32768 }, - }, - "bytedance-seed/seed-2.0-lite": { - id: "bytedance-seed/seed-2.0-lite", - name: "ByteDance Seed: Seed-2.0-Lite", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-10", - last_updated: "2026-03-15", - modalities: { input: ["image", "text", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 2 }, - limit: { context: 262144, output: 131072 }, - }, - "bytedance-seed/seed-1.6-flash": { - id: "bytedance-seed/seed-1.6-flash", - name: "ByteDance Seed: Seed 1.6 Flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-12-23", - last_updated: "2026-03-15", - modalities: { input: ["image", "text", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.075, output: 0.3 }, - limit: { context: 262144, output: 32768 }, - }, - "bytedance-seed/seed-2.0-mini": { - id: "bytedance-seed/seed-2.0-mini", - name: "ByteDance Seed: Seed-2.0-Mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-27", - last_updated: "2026-03-15", - modalities: { input: ["image", "text", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 262144, output: 131072 }, - }, - "mancer/weaver": { - id: "mancer/weaver", - name: "Mancer: Weaver (alpha)", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2023-08-02", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.75, output: 1 }, - limit: { context: 8000, output: 2000 }, - }, - "anthracite-org/magnum-v4-72b": { - id: "anthracite-org/magnum-v4-72b", - name: "Magnum v4 72B", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-10-22", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 3, output: 5 }, - limit: { context: 16384, output: 2048 }, - }, - "kilo-auto/balanced": { - id: "kilo-auto/balanced", - name: "Kilo Auto Balanced", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-15", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 3 }, - limit: { context: 204800, output: 131072 }, - }, - "kilo-auto/frontier": { - id: "kilo-auto/frontier", - name: "Kilo Auto Frontier", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-15", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25 }, - limit: { context: 1000000, output: 128000 }, - }, - "kilo-auto/small": { - id: "kilo-auto/small", - name: "Kilo Auto Small", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-15", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.4 }, - limit: { context: 400000, output: 128000 }, - }, - "kilo-auto/free": { - id: "kilo-auto/free", - name: "Kilo Auto Free", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-15", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "undi95/remm-slerp-l2-13b": { - id: "undi95/remm-slerp-l2-13b", - name: "ReMM SLERP 13B", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2023-07-22", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.45, output: 0.65 }, - limit: { context: 6144, output: 4096 }, - }, - "allenai/olmo-2-0325-32b-instruct": { - id: "allenai/olmo-2-0325-32b-instruct", - name: "AllenAI: Olmo 2 32B Instruct", - attachment: false, - reasoning: false, - tool_call: false, - release_date: "2025-03-15", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.2 }, - limit: { context: 128000, output: 32768 }, - }, - "allenai/molmo-2-8b": { - id: "allenai/molmo-2-8b", - name: "AllenAI: Molmo2 8B", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2026-01-09", - last_updated: "2026-01-31", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 36864, output: 36864 }, - }, - "allenai/olmo-3-7b-think": { - id: "allenai/olmo-3-7b-think", - name: "AllenAI: Olmo 3 7B Think", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-11-22", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.12, output: 0.2 }, - limit: { context: 65536, output: 65536 }, - }, - "allenai/olmo-3.1-32b-instruct": { - id: "allenai/olmo-3.1-32b-instruct", - name: "AllenAI: Olmo 3.1 32B Instruct", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2026-01-07", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.6 }, - limit: { context: 65536, output: 32768 }, - }, - "allenai/olmo-3.1-32b-think": { - id: "allenai/olmo-3.1-32b-think", - name: "AllenAI: Olmo 3.1 32B Think", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-12-17", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.5 }, - limit: { context: 65536, output: 65536 }, - }, - "allenai/olmo-3-7b-instruct": { - id: "allenai/olmo-3-7b-instruct", - name: "AllenAI: Olmo 3 7B Instruct", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-11-22", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.2 }, - limit: { context: 65536, output: 65536 }, - }, - "allenai/olmo-3-32b-think": { - id: "allenai/olmo-3-32b-think", - name: "AllenAI: Olmo 3 32B Think", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-11-22", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.5 }, - limit: { context: 65536, output: 65536 }, - }, - "nousresearch/hermes-2-pro-llama-3-8b": { - id: "nousresearch/hermes-2-pro-llama-3-8b", - name: "NousResearch: Hermes 2 Pro - Llama-3 8B", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-05-27", - last_updated: "2024-06-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.14, output: 0.14 }, - limit: { context: 8192, output: 8192 }, - }, - "nousresearch/hermes-4-405b": { - id: "nousresearch/hermes-4-405b", - name: "Nous: Hermes 4 405B", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-08-25", - last_updated: "2025-08-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3 }, - limit: { context: 131072, output: 26215 }, - }, - "nousresearch/hermes-3-llama-3.1-70b": { - id: "nousresearch/hermes-3-llama-3.1-70b", - name: "Nous: Hermes 3 70B Instruct", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-08-18", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.3 }, - limit: { context: 131072, output: 32768 }, - }, - "nousresearch/hermes-4-70b": { - id: "nousresearch/hermes-4-70b", - name: "Nous: Hermes 4 70B", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-08-25", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.13, output: 0.4, cache_read: 0.055 }, - limit: { context: 131072, output: 131072 }, - }, - "nousresearch/hermes-3-llama-3.1-405b": { - id: "nousresearch/hermes-3-llama-3.1-405b", - name: "Nous: Hermes 3 405B Instruct", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-08-16", - last_updated: "2024-08-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 1 }, - limit: { context: 131072, output: 16384 }, - }, - "kilo/auto": { - id: "kilo/auto", - name: "Kilo: Auto", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2024-06-01", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25 }, - limit: { context: 1000000, output: 128000 }, - }, - "kilo/auto-small": { - id: "kilo/auto-small", - name: "Deprecated Kilo Auto Small", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-15", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.4 }, - limit: { context: 400000, output: 128000 }, - }, - "kilo/auto-free": { - id: "kilo/auto-free", - name: "Deprecated Kilo Auto Free", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-15", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "morph/morph-v3-fast": { - id: "morph/morph-v3-fast", - name: "Morph: Morph V3 Fast", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-08-15", - last_updated: "2024-08-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 1.2 }, - limit: { context: 81920, output: 38000 }, - }, - "morph/morph-v3-large": { - id: "morph/morph-v3-large", - name: "Morph: Morph V3 Large", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-08-15", - last_updated: "2024-08-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.9, output: 1.9 }, - limit: { context: 262144, output: 131072 }, - }, - "eleutherai/llemma_7b": { - id: "eleutherai/llemma_7b", - name: "EleutherAI: Llemma 7b", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-14", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.8, output: 1.2 }, - limit: { context: 4096, output: 4096 }, - }, - "stepfun/step-3.5-flash:free": { - id: "stepfun/step-3.5-flash:free", - name: "StepFun: Step 3.5 Flash (free)", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-01-29", - last_updated: "2026-01-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 256000, output: 256000 }, - }, - "stepfun/step-3.5-flash": { - id: "stepfun/step-3.5-flash", - name: "StepFun: Step 3.5 Flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-01-29", - last_updated: "2026-01-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3, cache_read: 0.02 }, - limit: { context: 256000, output: 256000 }, - }, - "alpindale/goliath-120b": { - id: "alpindale/goliath-120b", - name: "Goliath 120B", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2023-11-10", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 3.75, output: 7.5 }, - limit: { context: 6144, output: 1024 }, - }, - "mistralai/mistral-nemo": { - id: "mistralai/mistral-nemo", - name: "Mistral: Mistral Nemo", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-07-01", - last_updated: "2024-07-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0.04 }, - limit: { context: 131072, output: 16384 }, - }, - "mistralai/mistral-saba": { - id: "mistralai/mistral-saba", - name: "Mistral: Saba", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-02-17", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.6 }, - limit: { context: 32768, output: 32768 }, - }, - "mistralai/mistral-large-2512": { - id: "mistralai/mistral-large-2512", - name: "Mistral: Mistral Large 3 2512", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-11-01", - last_updated: "2025-12-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 1.5 }, - limit: { context: 262144, output: 52429 }, - }, - "mistralai/devstral-medium": { - id: "mistralai/devstral-medium", - name: "Mistral: Devstral Medium", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-07-10", - last_updated: "2025-07-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 2 }, - limit: { context: 131072, output: 26215 }, - }, - "mistralai/mistral-small-3.1-24b-instruct": { - id: "mistralai/mistral-small-3.1-24b-instruct", - name: "Mistral: Mistral Small 3.1 24B", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-03-17", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.35, output: 0.56, cache_read: 0.015 }, - limit: { context: 128000, output: 131072 }, - }, - "mistralai/pixtral-large-2411": { - id: "mistralai/pixtral-large-2411", - name: "Mistral: Pixtral Large 2411", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-11-19", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: true, - cost: { input: 2, output: 6 }, - limit: { context: 131072, output: 32768 }, - }, - "mistralai/devstral-2512": { - id: "mistralai/devstral-2512", - name: "Mistral: Devstral 2 2512", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-09-12", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 2, cache_read: 0.025 }, - limit: { context: 262144, output: 65536 }, - }, - "mistralai/codestral-2508": { - id: "mistralai/codestral-2508", - name: "Mistral: Codestral 2508", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-08-01", - last_updated: "2025-08-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.9 }, - limit: { context: 256000, output: 51200 }, - }, - "mistralai/mistral-small-24b-instruct-2501": { - id: "mistralai/mistral-small-24b-instruct-2501", - name: "Mistral: Mistral Small 3", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.08 }, - limit: { context: 32768, output: 16384 }, - }, - "mistralai/mistral-large-2411": { - id: "mistralai/mistral-large-2411", - name: "Mistral Large 2411", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-07-24", - last_updated: "2024-11-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2, output: 6 }, - limit: { context: 131072, output: 26215 }, - }, - "mistralai/mixtral-8x22b-instruct": { - id: "mistralai/mixtral-8x22b-instruct", - name: "Mistral: Mixtral 8x22B Instruct", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-04-17", - last_updated: "2024-04-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2, output: 6 }, - limit: { context: 65536, output: 13108 }, - }, - "mistralai/mistral-large-2407": { - id: "mistralai/mistral-large-2407", - name: "Mistral Large 2407", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-11-19", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2, output: 6 }, - limit: { context: 131072, output: 32768 }, - }, - "mistralai/ministral-8b-2512": { - id: "mistralai/ministral-8b-2512", - name: "Mistral: Ministral 3 8B 2512", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-12-02", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.15 }, - limit: { context: 262144, output: 32768 }, - }, - "mistralai/mistral-medium-3.1": { - id: "mistralai/mistral-medium-3.1", - name: "Mistral: Mistral Medium 3.1", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-08-12", - last_updated: "2025-08-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2 }, - limit: { context: 131072, output: 26215 }, - }, - "mistralai/ministral-3b-2512": { - id: "mistralai/ministral-3b-2512", - name: "Mistral: Ministral 3 3B 2512", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-12-02", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 131072, output: 32768 }, - }, - "mistralai/voxtral-small-24b-2507": { - id: "mistralai/voxtral-small-24b-2507", - name: "Mistral: Voxtral Small 24B 2507", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-07-01", - last_updated: "2025-07-01", - modalities: { input: ["text", "audio"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 32000, output: 6400 }, - }, - "mistralai/mixtral-8x7b-instruct": { - id: "mistralai/mixtral-8x7b-instruct", - name: "Mistral: Mixtral 8x7B Instruct", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2023-12-10", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.54, output: 0.54 }, - limit: { context: 32768, output: 16384 }, - }, - "mistralai/mistral-medium-3": { - id: "mistralai/mistral-medium-3", - name: "Mistral: Mistral Medium 3", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-05-07", - last_updated: "2025-05-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2 }, - limit: { context: 131072, output: 26215 }, - }, - "mistralai/mistral-small-3.2-24b-instruct": { - id: "mistralai/mistral-small-3.2-24b-instruct", - name: "Mistral: Mistral Small 3.2 24B", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-06-20", - last_updated: "2025-06-20", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.06, output: 0.18, cache_read: 0.03 }, - limit: { context: 131072, output: 131072 }, - }, - "mistralai/mistral-small-creative": { - id: "mistralai/mistral-small-creative", - name: "Mistral: Mistral Small Creative", - attachment: false, - reasoning: false, - tool_call: true, - release_date: "2025-12-17", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 32768, output: 32768 }, - }, - "mistralai/devstral-small": { - id: "mistralai/devstral-small", - name: "Mistral: Devstral Small 1.1", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-05-07", - last_updated: "2025-07-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 131072, output: 26215 }, - }, - "mistralai/mistral-large": { - id: "mistralai/mistral-large", - name: "Mistral Large", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-07-24", - last_updated: "2025-12-02", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2, output: 6 }, - limit: { context: 128000, output: 25600 }, - }, - "mistralai/mistral-7b-instruct-v0.1": { - id: "mistralai/mistral-7b-instruct-v0.1", - name: "Mistral: Mistral 7B Instruct v0.1", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.11, output: 0.19 }, - limit: { context: 2824, output: 565 }, - }, - "mistralai/ministral-14b-2512": { - id: "mistralai/ministral-14b-2512", - name: "Mistral: Ministral 3 14B 2512", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-12-16", - last_updated: "2025-12-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 262144, output: 52429 }, - }, - "meta-llama/llama-3.3-70b-instruct": { - id: "meta-llama/llama-3.3-70b-instruct", - name: "Meta: Llama 3.3 70B Instruct", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-08-01", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.32 }, - limit: { context: 131072, output: 16384 }, - }, - "meta-llama/llama-4-scout": { - id: "meta-llama/llama-4-scout", - name: "Meta: Llama 4 Scout", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.08, output: 0.3 }, - limit: { context: 327680, output: 16384 }, - }, - "meta-llama/llama-guard-3-8b": { - id: "meta-llama/llama-guard-3-8b", - name: "Llama Guard 3 8B", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-04-18", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0.06 }, - limit: { context: 131072, output: 26215 }, - }, - "meta-llama/llama-4-maverick": { - id: "meta-llama/llama-4-maverick", - name: "Meta: Llama 4 Maverick", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-04-05", - last_updated: "2025-12-24", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 1048576, output: 16384 }, - }, - "meta-llama/llama-3.2-11b-vision-instruct": { - id: "meta-llama/llama-3.2-11b-vision-instruct", - name: "Meta: Llama 3.2 11B Vision Instruct", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-09-25", - last_updated: "2024-09-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.049, output: 0.049 }, - limit: { context: 131072, output: 16384 }, - }, - "meta-llama/llama-guard-4-12b": { - id: "meta-llama/llama-guard-4-12b", - name: "Meta: Llama Guard 4 12B", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.18, output: 0.18 }, - limit: { context: 163840, output: 32768 }, - }, - "meta-llama/llama-3.1-70b-instruct": { - id: "meta-llama/llama-3.1-70b-instruct", - name: "Meta: Llama 3.1 70B Instruct", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-07-16", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 0.4 }, - limit: { context: 131072, output: 26215 }, - }, - "meta-llama/llama-3.1-405b": { - id: "meta-llama/llama-3.1-405b", - name: "Meta: Llama 3.1 405B (base)", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-08-02", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 4, output: 4 }, - limit: { context: 32768, output: 32768 }, - }, - "meta-llama/llama-3.2-1b-instruct": { - id: "meta-llama/llama-3.2-1b-instruct", - name: "Meta: Llama 3.2 1B Instruct", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-09-18", - last_updated: "2026-01-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.027, output: 0.2 }, - limit: { context: 60000, output: 12000 }, - }, - "meta-llama/llama-3.2-3b-instruct": { - id: "meta-llama/llama-3.2-3b-instruct", - name: "Meta: Llama 3.2 3B Instruct", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-09-18", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.051, output: 0.34 }, - limit: { context: 80000, output: 16384 }, - }, - "meta-llama/llama-3-8b-instruct": { - id: "meta-llama/llama-3-8b-instruct", - name: "Meta: Llama 3 8B Instruct", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-04-25", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.04 }, - limit: { context: 8192, output: 16384 }, - }, - "meta-llama/llama-3.1-8b-instruct": { - id: "meta-llama/llama-3.1-8b-instruct", - name: "Meta: Llama 3.1 8B Instruct", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-07-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0.05 }, - limit: { context: 16384, output: 16384 }, - }, - "meta-llama/llama-3-70b-instruct": { - id: "meta-llama/llama-3-70b-instruct", - name: "Meta: Llama 3 70B Instruct", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.51, output: 0.74 }, - limit: { context: 8192, output: 8000 }, - }, - "meta-llama/llama-3.1-405b-instruct": { - id: "meta-llama/llama-3.1-405b-instruct", - name: "Meta: Llama 3.1 405B Instruct", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-07-16", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 4, output: 4 }, - limit: { context: 131000, output: 26200 }, - }, - "x-ai/grok-code-fast-1:optimized:free": { - id: "x-ai/grok-code-fast-1:optimized:free", - name: "xAI: Grok Code Fast 1 Optimized (experimental, free)", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-27", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 256000, output: 10000 }, - }, - "x-ai/grok-4.20-multi-agent-beta": { - id: "x-ai/grok-4.20-multi-agent-beta", - name: "xAI: Grok 4.20 Multi-Agent Beta", - attachment: true, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2026-03-12", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6 }, - limit: { context: 2000000, output: 32768 }, - }, - "x-ai/grok-4-fast": { - id: "x-ai/grok-4-fast", - name: "xAI: Grok 4 Fast", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-19", - last_updated: "2025-08-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - "x-ai/grok-code-fast-1": { - id: "x-ai/grok-code-fast-1", - name: "xAI: Grok Code Fast 1", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-26", - last_updated: "2025-08-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.5, cache_read: 0.02 }, - limit: { context: 256000, output: 10000 }, - }, - "x-ai/grok-3-beta": { - id: "x-ai/grok-3-beta", - name: "xAI: Grok 3 Beta", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.75 }, - limit: { context: 131072, output: 26215 }, - }, - "x-ai/grok-4": { - id: "x-ai/grok-4", - name: "xAI: Grok 4", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.75 }, - limit: { context: 256000, output: 51200 }, - }, - "x-ai/grok-3-mini": { - id: "x-ai/grok-3-mini", - name: "xAI: Grok 3 Mini", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.5, cache_read: 0.075 }, - limit: { context: 131072, output: 26215 }, - }, - "x-ai/grok-4.1-fast": { - id: "x-ai/grok-4.1-fast", - name: "xAI: Grok 4.1 Fast", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-11-19", - last_updated: "2025-11-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - "x-ai/grok-4.20-beta": { - id: "x-ai/grok-4.20-beta", - name: "xAI: Grok 4.20 Beta", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-12", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6 }, - limit: { context: 2000000, output: 32768 }, - }, - "x-ai/grok-3-mini-beta": { - id: "x-ai/grok-3-mini-beta", - name: "xAI: Grok 3 Mini Beta", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.5, cache_read: 0.075 }, - limit: { context: 131072, output: 26215 }, - }, - "x-ai/grok-3": { - id: "x-ai/grok-3", - name: "xAI: Grok 3", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.75 }, - limit: { context: 131072, output: 26215 }, - }, - "tencent/hunyuan-a13b-instruct": { - id: "tencent/hunyuan-a13b-instruct", - name: "Tencent: Hunyuan A13B Instruct", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-06-30", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.57 }, - limit: { context: 131072, output: 131072 }, - }, - "gryphe/mythomax-l2-13b": { - id: "gryphe/mythomax-l2-13b", - name: "MythoMax 13B", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-04-25", - last_updated: "2024-04-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.06, output: 0.06 }, - limit: { context: 4096, output: 4096 }, - }, - "sao10k/l3-euryale-70b": { - id: "sao10k/l3-euryale-70b", - name: "Sao10k: Llama 3 Euryale 70B v2.1", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-06-18", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.48, output: 1.48 }, - limit: { context: 8192, output: 8192 }, - }, - "sao10k/l3-lunaris-8b": { - id: "sao10k/l3-lunaris-8b", - name: "Sao10K: Llama 3 8B Lunaris", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-08-13", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.04, output: 0.05 }, - limit: { context: 8192, output: 8192 }, - }, - "sao10k/l3.3-euryale-70b": { - id: "sao10k/l3.3-euryale-70b", - name: "Sao10K: Llama 3.3 Euryale 70B", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-12-18", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.65, output: 0.75 }, - limit: { context: 131072, output: 16384 }, - }, - "sao10k/l3.1-70b-hanami-x1": { - id: "sao10k/l3.1-70b-hanami-x1", - name: "Sao10K: Llama 3.1 70B Hanami x1", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-01-08", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 3, output: 3 }, - limit: { context: 16000, output: 16000 }, - }, - "sao10k/l3.1-euryale-70b": { - id: "sao10k/l3.1-euryale-70b", - name: "Sao10K: Llama 3.1 Euryale 70B v2.2", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-08-28", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.85, output: 0.85 }, - limit: { context: 131072, output: 16384 }, - }, - "microsoft/wizardlm-2-8x22b": { - id: "microsoft/wizardlm-2-8x22b", - name: "WizardLM-2 8x22B", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-04-24", - last_updated: "2024-04-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.62, output: 0.62 }, - limit: { context: 65535, output: 8000 }, - }, - "microsoft/phi-4": { - id: "microsoft/phi-4", - name: "Microsoft: Phi 4", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.06, output: 0.14 }, - limit: { context: 16384, output: 16384 }, - }, - "cohere/command-r7b-12-2024": { - id: "cohere/command-r7b-12-2024", - name: "Cohere: Command R7B (12-2024)", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-02-27", - last_updated: "2024-02-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.0375, output: 0.15 }, - limit: { context: 128000, output: 4000 }, - }, - "cohere/command-a": { - id: "cohere/command-a", - name: "Cohere: Command A", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-03-13", - last_updated: "2025-03-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2.5, output: 10 }, - limit: { context: 256000, output: 8192 }, - }, - "cohere/command-r-plus-08-2024": { - id: "cohere/command-r-plus-08-2024", - name: "Cohere: Command R+ (08-2024)", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-08-30", - last_updated: "2024-08-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2.5, output: 10 }, - limit: { context: 128000, output: 4000 }, - }, - "cohere/command-r-08-2024": { - id: "cohere/command-r-08-2024", - name: "Cohere: Command R (08-2024)", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-08-30", - last_updated: "2024-08-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 128000, output: 4000 }, - }, - "prime-intellect/intellect-3": { - id: "prime-intellect/intellect-3", - name: "Prime Intellect: INTELLECT-3", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-11-26", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 1.1 }, - limit: { context: 131072, output: 131072 }, - }, - "nvidia/llama-3.3-nemotron-super-49b-v1.5": { - id: "nvidia/llama-3.3-nemotron-super-49b-v1.5", - name: "NVIDIA: Llama 3.3 Nemotron Super 49B V1.5", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-03-16", - last_updated: "2025-03-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 131072, output: 26215 }, - }, - "nvidia/nemotron-3-nano-30b-a3b": { - id: "nvidia/nemotron-3-nano-30b-a3b", - name: "NVIDIA: Nemotron 3 Nano 30B A3B", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2024-12", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.2 }, - limit: { context: 262144, output: 52429 }, - }, - "nvidia/nemotron-nano-12b-v2-vl": { - id: "nvidia/nemotron-nano-12b-v2-vl", - name: "NVIDIA: Nemotron Nano 12B 2 VL", - attachment: true, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-10-28", - last_updated: "2026-01-31", - modalities: { input: ["image", "text", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.6 }, - limit: { context: 131072, output: 26215 }, - }, - "nvidia/nemotron-3-super-120b-a12b:free": { - id: "nvidia/nemotron-3-super-120b-a12b:free", - name: "NVIDIA: Nemotron 3 Super (free)", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-12", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 262144 }, - }, - "nvidia/nemotron-nano-9b-v2": { - id: "nvidia/nemotron-nano-9b-v2", - name: "NVIDIA: Nemotron Nano 9B V2", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-18", - last_updated: "2025-08-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.04, output: 0.16 }, - limit: { context: 131072, output: 26215 }, - }, - "nvidia/llama-3.1-nemotron-70b-instruct": { - id: "nvidia/llama-3.1-nemotron-70b-instruct", - name: "NVIDIA: Llama 3.1 Nemotron 70B Instruct", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-10-12", - last_updated: "2024-10-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.2, output: 1.2 }, - limit: { context: 131072, output: 16384 }, - }, - "inception/mercury-2": { - id: "inception/mercury-2", - name: "Inception: Mercury 2", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-24", - last_updated: "2026-02-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 0.75, cache_read: 0.025 }, - limit: { context: 128000, output: 50000 }, - }, - "inception/mercury": { - id: "inception/mercury", - name: "Inception: Mercury", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-06-26", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 0.75 }, - limit: { context: 128000, output: 32000 }, - }, - "inception/mercury-coder": { - id: "inception/mercury-coder", - name: "Inception: Mercury Coder", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-02-26", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 0.75 }, - limit: { context: 128000, output: 32000 }, - }, - "openai/gpt-5.1-codex-max": { - id: "openai/gpt-5.1-codex-max", - name: "OpenAI: GPT-5.1-Codex-Max", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.2-chat": { - id: "openai/gpt-5.2-chat", - name: "OpenAI: GPT-5.2 Chat", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-12-11", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-4o-mini-search-preview": { - id: "openai/gpt-4o-mini-search-preview", - name: "OpenAI: GPT-4o-mini Search Preview", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-01", - last_updated: "2025-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-5-chat": { - id: "openai/gpt-5-chat", - name: "OpenAI: GPT-5 Chat", - attachment: true, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-08-07", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-4o-2024-05-13": { - id: "openai/gpt-4o-2024-05-13", - name: "OpenAI: GPT-4o (2024-05-13)", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-05-13", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 15 }, - limit: { context: 128000, output: 4096 }, - }, - "openai/gpt-5.3-chat": { - id: "openai/gpt-5.3-chat", - name: "OpenAI: GPT-5.3 Chat", - attachment: true, - reasoning: false, - tool_call: true, - release_date: "2026-03-04", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-5.2-pro": { - id: "openai/gpt-5.2-pro", - name: "OpenAI: GPT-5.2 Pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-12-11", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 21, output: 168 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-4-1106-preview": { - id: "openai/gpt-4-1106-preview", - name: "OpenAI: GPT-4 Turbo (older v1106)", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2023-11-06", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 10, output: 30 }, - limit: { context: 128000, output: 4096 }, - }, - "openai/gpt-4o-audio-preview": { - id: "openai/gpt-4o-audio-preview", - name: "OpenAI: GPT-4o Audio", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-08-15", - last_updated: "2026-03-15", - modalities: { input: ["audio", "text"], output: ["audio", "text"] }, - open_weights: false, - cost: { input: 2.5, output: 10 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-5-mini": { - id: "openai/gpt-5-mini", - name: "OpenAI: GPT-5 Mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-08-07", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.025 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5-nano": { - id: "openai/gpt-5-nano", - name: "OpenAI: GPT-5 Nano", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-08-07", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.4, cache_read: 0.005 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.3-codex": { - id: "openai/gpt-5.3-codex", - name: "OpenAI: GPT-5.3-Codex", - attachment: true, - reasoning: true, - tool_call: true, - release_date: "2026-02-25", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-3.5-turbo-16k": { - id: "openai/gpt-3.5-turbo-16k", - name: "OpenAI: GPT-3.5 Turbo 16k", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2023-08-28", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 4 }, - limit: { context: 16385, output: 4096 }, - }, - "openai/gpt-4-turbo": { - id: "openai/gpt-4-turbo", - name: "OpenAI: GPT-4 Turbo", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2023-09-13", - last_updated: "2024-04-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 10, output: 30 }, - limit: { context: 128000, output: 4096 }, - }, - "openai/gpt-5.2": { - id: "openai/gpt-5.2", - name: "OpenAI: GPT-5.2", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-12-11", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/o3-pro": { - id: "openai/o3-pro", - name: "OpenAI: o3 Pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-04-16", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 20, output: 80 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/o3-mini-high": { - id: "openai/o3-mini-high", - name: "OpenAI: o3 Mini High", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-01-31", - last_updated: "2026-03-15", - modalities: { input: ["pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.55 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-4o-mini": { - id: "openai/gpt-4o-mini", - name: "OpenAI: GPT-4o-mini", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-07-18", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6, cache_read: 0.075 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/o4-mini-deep-research": { - id: "openai/o4-mini-deep-research", - name: "OpenAI: o4 Mini Deep Research", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2024-06-26", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-5.1-chat": { - id: "openai/gpt-5.1-chat", - name: "OpenAI: GPT-5.1 Chat", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-11-13", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/o4-mini": { - id: "openai/o4-mini", - name: "OpenAI: o4 Mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-04-16", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.275 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-5.2-codex": { - id: "openai/gpt-5.2-codex", - name: "OpenAI: GPT-5.2-Codex", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2026-01-14", - last_updated: "2026-01-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-4o-mini-2024-07-18": { - id: "openai/gpt-4o-mini-2024-07-18", - name: "OpenAI: GPT-4o-mini (2024-07-18)", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-07-18", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-5.1-codex-mini": { - id: "openai/gpt-5.1-codex-mini", - name: "OpenAI: GPT-5.1-Codex-Mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.025 }, - limit: { context: 400000, output: 100000 }, - }, - "openai/gpt-4o-2024-08-06": { - id: "openai/gpt-4o-2024-08-06", - name: "OpenAI: GPT-4o (2024-08-06)", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-08-06", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10, cache_read: 1.25 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-5-image": { - id: "openai/gpt-5-image", - name: "OpenAI: GPT-5 Image", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-10-14", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["image", "text"] }, - open_weights: false, - cost: { input: 10, output: 10 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.1": { - id: "openai/gpt-5.1", - name: "OpenAI: GPT-5.1", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-11-13", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/o1": { - id: "openai/o1", - name: "OpenAI: o1", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-12-05", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 60, cache_read: 7.5 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-5.4-pro": { - id: "openai/gpt-5.4-pro", - name: "OpenAI: GPT-5.4 Pro", - attachment: true, - reasoning: true, - tool_call: true, - release_date: "2026-03-06", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 30, output: 180 }, - limit: { context: 1050000, output: 128000 }, - }, - "openai/gpt-3.5-turbo": { - id: "openai/gpt-3.5-turbo", - name: "OpenAI: GPT-3.5 Turbo", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2023-03-01", - last_updated: "2023-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 1.5 }, - limit: { context: 16385, output: 4096 }, - }, - "openai/o3-deep-research": { - id: "openai/o3-deep-research", - name: "OpenAI: o3 Deep Research", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2024-06-26", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 10, output: 40, cache_read: 2.5 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/o3-mini": { - id: "openai/o3-mini", - name: "OpenAI: o3 Mini", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-12-20", - last_updated: "2026-03-15", - modalities: { input: ["pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.55 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-4-turbo-preview": { - id: "openai/gpt-4-turbo-preview", - name: "OpenAI: GPT-4 Turbo Preview", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-01-25", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 10, output: 30 }, - limit: { context: 128000, output: 4096 }, - }, - "openai/o1-pro": { - id: "openai/o1-pro", - name: "OpenAI: o1-pro", - attachment: true, - reasoning: true, - tool_call: false, - temperature: false, - release_date: "2025-03-19", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 150, output: 600 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-4": { - id: "openai/gpt-4", - name: "OpenAI: GPT-4", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2023-03-14", - last_updated: "2024-04-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 30, output: 60 }, - limit: { context: 8191, output: 4096 }, - }, - "openai/gpt-4-0314": { - id: "openai/gpt-4-0314", - name: "OpenAI: GPT-4 (older v0314)", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2023-05-28", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 30, output: 60 }, - limit: { context: 8191, output: 4096 }, - }, - "openai/gpt-5-codex": { - id: "openai/gpt-5-codex", - name: "OpenAI: GPT-5 Codex", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-09-15", - last_updated: "2025-09-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.4": { - id: "openai/gpt-5.4", - name: "OpenAI: GPT-5.4", - attachment: true, - reasoning: true, - tool_call: true, - release_date: "2026-03-06", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 15 }, - limit: { context: 1050000, output: 128000 }, - }, - "openai/gpt-audio": { - id: "openai/gpt-audio", - name: "OpenAI: GPT Audio", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2026-01-20", - last_updated: "2026-03-15", - modalities: { input: ["audio", "text"], output: ["audio", "text"] }, - open_weights: false, - cost: { input: 2.5, output: 10 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-4o:extended": { - id: "openai/gpt-4o:extended", - name: "OpenAI: GPT-4o (extended)", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-05-13", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 6, output: 18 }, - limit: { context: 128000, output: 64000 }, - }, - "openai/gpt-4o-search-preview": { - id: "openai/gpt-4o-search-preview", - name: "OpenAI: GPT-4o Search Preview", - attachment: false, - reasoning: false, - tool_call: false, - release_date: "2025-03-13", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-4.1-nano": { - id: "openai/gpt-4.1-nano", - name: "OpenAI: GPT-4.1 Nano", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-04-14", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.025 }, - limit: { context: 1047576, output: 32768 }, - }, - "openai/o4-mini-high": { - id: "openai/o4-mini-high", - name: "OpenAI: o4 Mini High", - attachment: true, - reasoning: true, - tool_call: true, - release_date: "2025-04-17", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/o3": { - id: "openai/o3", - name: "OpenAI: o3", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-04-16", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-oss-20b": { - id: "openai/gpt-oss-20b", - name: "OpenAI: gpt-oss-20b", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.14 }, - limit: { context: 131072, output: 26215 }, - }, - "openai/gpt-5-pro": { - id: "openai/gpt-5-pro", - name: "OpenAI: GPT-5 Pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-10-06", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 120 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-audio-mini": { - id: "openai/gpt-audio-mini", - name: "OpenAI: GPT Audio Mini", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2026-01-20", - last_updated: "2026-03-15", - modalities: { input: ["audio", "text"], output: ["audio", "text"] }, - open_weights: false, - cost: { input: 0.6, output: 2.4 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-4o": { - id: "openai/gpt-4o", - name: "OpenAI: GPT-4o", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-05-13", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10, cache_read: 1.25 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-3.5-turbo-0613": { - id: "openai/gpt-3.5-turbo-0613", - name: "OpenAI: GPT-3.5 Turbo (older v0613)", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2023-06-13", - last_updated: "2023-06-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 2 }, - limit: { context: 4095, output: 4096 }, - }, - "openai/gpt-5-image-mini": { - id: "openai/gpt-5-image-mini", - name: "OpenAI: GPT-5 Image Mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-10-16", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["image", "text"] }, - open_weights: false, - cost: { input: 2.5, output: 2 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5": { - id: "openai/gpt-5", - name: "OpenAI: GPT-5", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-08-07", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-oss-safeguard-20b": { - id: "openai/gpt-oss-safeguard-20b", - name: "OpenAI: gpt-oss-safeguard-20b", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-10-29", - last_updated: "2025-10-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.075, output: 0.3, cache_read: 0.037 }, - limit: { context: 131072, output: 65536 }, - }, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "OpenAI: gpt-oss-120b", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.039, output: 0.19 }, - limit: { context: 131072, output: 26215 }, - }, - "openai/gpt-3.5-turbo-instruct": { - id: "openai/gpt-3.5-turbo-instruct", - name: "OpenAI: GPT-3.5 Turbo Instruct", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2023-03-01", - last_updated: "2023-09-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.5, output: 2 }, - limit: { context: 4095, output: 4096 }, - }, - "openai/gpt-4.1": { - id: "openai/gpt-4.1", - name: "OpenAI: GPT-4.1", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-04-14", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 1047576, output: 32768 }, - }, - "openai/gpt-4.1-mini": { - id: "openai/gpt-4.1-mini", - name: "OpenAI: GPT-4.1 Mini", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-04-14", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.6, cache_read: 0.1 }, - limit: { context: 1047576, output: 32768 }, - }, - "openai/gpt-5.1-codex": { - id: "openai/gpt-5.1-codex", - name: "OpenAI: GPT-5.1-Codex", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-4o-2024-11-20": { - id: "openai/gpt-4o-2024-11-20", - name: "OpenAI: GPT-4o (2024-11-20)", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-11-20", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10, cache_read: 1.25 }, - limit: { context: 128000, output: 16384 }, - }, - "amazon/nova-lite-v1": { - id: "amazon/nova-lite-v1", - name: "Amazon: Nova Lite 1.0", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-12-06", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.06, output: 0.24 }, - limit: { context: 300000, output: 5120 }, - }, - "amazon/nova-pro-v1": { - id: "amazon/nova-pro-v1", - name: "Amazon: Nova Pro 1.0", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-12-03", - last_updated: "2024-12-03", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 3.2 }, - limit: { context: 300000, output: 5120 }, - }, - "amazon/nova-premier-v1": { - id: "amazon/nova-premier-v1", - name: "Amazon: Nova Premier 1.0", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-11-01", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 12.5 }, - limit: { context: 1000000, output: 32000 }, - }, - "amazon/nova-2-lite-v1": { - id: "amazon/nova-2-lite-v1", - name: "Amazon: Nova 2 Lite", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2024-12-01", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5 }, - limit: { context: 1000000, output: 65535 }, - }, - "amazon/nova-micro-v1": { - id: "amazon/nova-micro-v1", - name: "Amazon: Nova Micro 1.0", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-12-06", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.035, output: 0.14 }, - limit: { context: 128000, output: 5120 }, - }, - "z-ai/glm-4.7": { - id: "z-ai/glm-4.7", - name: "Z.ai: GLM 4.7", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-12-22", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.38, output: 1.98, cache_read: 0.2 }, - limit: { context: 202752, output: 65535 }, - }, - "z-ai/glm-5": { - id: "z-ai/glm-5", - name: "Z.ai: GLM 5", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.72, output: 2.3 }, - limit: { context: 202752, output: 131072 }, - }, - "z-ai/glm-4-32b": { - id: "z-ai/glm-4-32b", - name: "Z.ai: GLM 4 32B ", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-07-25", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 128000, output: 32768 }, - }, - "z-ai/glm-5.1": { - id: "z-ai/glm-5.1", - name: "Z.ai: GLM 5.1", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-27", - last_updated: "2026-03-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.26, output: 3.96 }, - limit: { context: 202752, output: 131072 }, - }, - "z-ai/glm-4.5": { - id: "z-ai/glm-4.5", - name: "Z.ai: GLM 4.5", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-07-28", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2, cache_read: 0.175 }, - limit: { context: 131072, output: 98304 }, - }, - "z-ai/glm-4.5-air": { - id: "z-ai/glm-4.5-air", - name: "Z.ai: GLM 4.5 Air", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.13, output: 0.85, cache_read: 0.025 }, - limit: { context: 131072, output: 98304 }, - }, - "z-ai/glm-4.5v": { - id: "z-ai/glm-4.5v", - name: "Z.ai: GLM 4.5V", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-11", - last_updated: "2025-08-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 1.8, cache_read: 0.11 }, - limit: { context: 65536, output: 16384 }, - }, - "z-ai/glm-4.6": { - id: "z-ai/glm-4.6", - name: "Z.ai: GLM 4.6", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-09-30", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.39, output: 1.9, cache_read: 0.175 }, - limit: { context: 204800, output: 204800 }, - }, - "z-ai/glm-4.6v": { - id: "z-ai/glm-4.6v", - name: "Z.ai: GLM 4.6V", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-09-30", - last_updated: "2026-01-10", - modalities: { input: ["image", "text", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.9 }, - limit: { context: 131072, output: 131072 }, - }, - "z-ai/glm-4.7-flash": { - id: "z-ai/glm-4.7-flash", - name: "Z.ai: GLM 4.7 Flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-01-19", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.06, output: 0.4, cache_read: 0.01 }, - limit: { context: 202752, output: 40551 }, - }, - "baidu/ernie-4.5-vl-424b-a47b": { - id: "baidu/ernie-4.5-vl-424b-a47b", - name: "Baidu: ERNIE 4.5 VL 424B A47B ", - attachment: true, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-06-30", - last_updated: "2026-01", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.42, output: 1.25 }, - limit: { context: 123000, output: 16000 }, - }, - "baidu/ernie-4.5-vl-28b-a3b": { - id: "baidu/ernie-4.5-vl-28b-a3b", - name: "Baidu: ERNIE 4.5 VL 28B A3B", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-06-30", - last_updated: "2025-06-30", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.14, output: 0.56 }, - limit: { context: 30000, output: 8000 }, - }, - "baidu/ernie-4.5-21b-a3b": { - id: "baidu/ernie-4.5-21b-a3b", - name: "Baidu: ERNIE 4.5 21B A3B", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-06-30", - last_updated: "2025-06-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.28 }, - limit: { context: 120000, output: 8000 }, - }, - "baidu/ernie-4.5-300b-a47b": { - id: "baidu/ernie-4.5-300b-a47b", - name: "Baidu: ERNIE 4.5 300B A47B ", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-06-30", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.28, output: 1.1 }, - limit: { context: 123000, output: 12000 }, - }, - "baidu/ernie-4.5-21b-a3b-thinking": { - id: "baidu/ernie-4.5-21b-a3b-thinking", - name: "Baidu: ERNIE 4.5 21B A3B Thinking", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-09-19", - last_updated: "2025-09-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.28 }, - limit: { context: 131072, output: 65536 }, - }, - "relace/relace-apply-3": { - id: "relace/relace-apply-3", - name: "Relace: Relace Apply 3", - attachment: false, - reasoning: false, - tool_call: false, - release_date: "2025-09-26", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.85, output: 1.25 }, - limit: { context: 256000, output: 128000 }, - }, - "relace/relace-search": { - id: "relace/relace-search", - name: "Relace: Relace Search", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-12-09", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 3 }, - limit: { context: 256000, output: 128000 }, - }, - "minimax/minimax-m2.7": { - id: "minimax/minimax-m2.7", - name: "MiniMax: MiniMax M2.7", - family: "minimax-m2.7", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.06 }, - limit: { context: 204800, output: 131072 }, - }, - "minimax/minimax-m2": { - id: "minimax/minimax-m2", - name: "MiniMax: MiniMax M2", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-10-23", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.255, output: 1, cache_read: 0.03 }, - limit: { context: 196608, output: 196608 }, - }, - "minimax/minimax-01": { - id: "minimax/minimax-01", - name: "MiniMax: MiniMax-01", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-01-15", - last_updated: "2025-01-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 1.1 }, - limit: { context: 1000192, output: 1000192 }, - }, - "minimax/minimax-m2.1": { - id: "minimax/minimax-m2.1", - name: "MiniMax: MiniMax M2.1", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 0.95, cache_read: 0.03 }, - limit: { context: 196608, output: 39322 }, - }, - "minimax/minimax-m1": { - id: "minimax/minimax-m1", - name: "MiniMax: MiniMax M1", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 2.2 }, - limit: { context: 1000000, output: 40000 }, - }, - "minimax/minimax-m2-her": { - id: "minimax/minimax-m2-her", - name: "MiniMax: MiniMax M2-her", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2026-01-23", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 65536, output: 2048 }, - }, - "minimax/minimax-m2.5": { - id: "minimax/minimax-m2.5", - name: "MiniMax: MiniMax M2.5", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 1.2, cache_read: 0.029 }, - limit: { context: 196608, output: 196608 }, - }, - "qwen/qwen3-235b-a22b": { - id: "qwen/qwen3-235b-a22b", - name: "Qwen: Qwen3 235B A22B", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2024-12-01", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.455, output: 1.82, cache_read: 0.15 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen/qwen3.5-122b-a10b": { - id: "qwen/qwen3.5-122b-a10b", - name: "Qwen: Qwen3.5-122B-A10B", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-26", - last_updated: "2026-03-15", - modalities: { input: ["image", "text", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.26, output: 2.08 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen/qwen2.5-coder-7b-instruct": { - id: "qwen/qwen2.5-coder-7b-instruct", - name: "Qwen: Qwen2.5 Coder 7B Instruct", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-09-17", - last_updated: "2024-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.09 }, - limit: { context: 32768, output: 6554 }, - }, - "qwen/qwen3-coder-plus": { - id: "qwen/qwen3-coder-plus", - name: "Qwen: Qwen3 Coder Plus", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-07-01", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.65, output: 3.25, cache_read: 0.2 }, - limit: { context: 1000000, output: 65536 }, - }, - "qwen/qwen3.5-27b": { - id: "qwen/qwen3.5-27b", - name: "Qwen: Qwen3.5-27B", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-26", - last_updated: "2026-03-15", - modalities: { input: ["image", "text", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.195, output: 1.56 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen/qwen3-235b-a22b-2507": { - id: "qwen/qwen3-235b-a22b-2507", - name: "Qwen: Qwen3 235B A22B Instruct 2507", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-04", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.071, output: 0.1 }, - limit: { context: 262144, output: 52429 }, - }, - "qwen/qwen3-8b": { - id: "qwen/qwen3-8b", - name: "Qwen: Qwen3 8B", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-04", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.4, cache_read: 0.05 }, - limit: { context: 40960, output: 8192 }, - }, - "qwen/qwen3.5-397b-a17b": { - id: "qwen/qwen3.5-397b-a17b", - name: "Qwen: Qwen3.5 397B A17B", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-15", - last_updated: "2026-03-15", - modalities: { input: ["image", "text", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.39, output: 2.34 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen/qwen-vl-plus": { - id: "qwen/qwen-vl-plus", - name: "Qwen: Qwen VL Plus", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-01-25", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1365, output: 0.4095, cache_read: 0.042 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen/qwen3-32b": { - id: "qwen/qwen3-32b", - name: "Qwen: Qwen3 32B", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2024-12-01", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.08, output: 0.24, cache_read: 0.04 }, - limit: { context: 40960, output: 40960 }, - }, - "qwen/qwen2.5-vl-72b-instruct": { - id: "qwen/qwen2.5-vl-72b-instruct", - name: "Qwen: Qwen2.5 VL 72B Instruct", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-02-01", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.8, output: 0.8, cache_read: 0.075 }, - limit: { context: 32768, output: 32768 }, - }, - "qwen/qwen-max": { - id: "qwen/qwen-max", - name: "Qwen: Qwen-Max ", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-04-03", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.04, output: 4.16, cache_read: 0.32 }, - limit: { context: 32768, output: 8192 }, - }, - "qwen/qwen-plus": { - id: "qwen/qwen-plus", - name: "Qwen: Qwen-Plus", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-01-25", - last_updated: "2025-09-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.2, cache_read: 0.08 }, - limit: { context: 1000000, output: 32768 }, - }, - "qwen/qwen3-vl-235b-a22b-thinking": { - id: "qwen/qwen3-vl-235b-a22b-thinking", - name: "Qwen: Qwen3 VL 235B A22B Thinking", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-09-24", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.26, output: 2.6 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen/qwen3-vl-30b-a3b-thinking": { - id: "qwen/qwen3-vl-30b-a3b-thinking", - name: "Qwen: Qwen3 VL 30B A3B Thinking", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-10-11", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.13, output: 1.56 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen/qwen2.5-vl-32b-instruct": { - id: "qwen/qwen2.5-vl-32b-instruct", - name: "Qwen: Qwen2.5 VL 32B Instruct", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-03-24", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.6, cache_read: 0.025 }, - limit: { context: 128000, output: 16384 }, - }, - "qwen/qwen3-vl-8b-instruct": { - id: "qwen/qwen3-vl-8b-instruct", - name: "Qwen: Qwen3 VL 8B Instruct", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-10-15", - last_updated: "2025-11-25", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.08, output: 0.5 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen/qwen3.5-flash-02-23": { - id: "qwen/qwen3.5-flash-02-23", - name: "Qwen: Qwen3.5-Flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-26", - last_updated: "2026-03-15", - modalities: { input: ["image", "text", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 1000000, output: 65536 }, - }, - "qwen/qwen3-max": { - id: "qwen/qwen3-max", - name: "Qwen: Qwen3 Max", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-09-05", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.2, output: 6, cache_read: 0.24 }, - limit: { context: 262144, output: 32768 }, - }, - "qwen/qwen-plus-2025-07-28": { - id: "qwen/qwen-plus-2025-07-28", - name: "Qwen: Qwen Plus 0728", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-09-09", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.26, output: 0.78 }, - limit: { context: 1000000, output: 32768 }, - }, - "qwen/qwen3-30b-a3b-instruct-2507": { - id: "qwen/qwen3-30b-a3b-instruct-2507", - name: "Qwen: Qwen3 30B A3B Instruct 2507", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-07-29", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.09, output: 0.3, cache_read: 0.04 }, - limit: { context: 262144, output: 262144 }, - }, - "qwen/qwen3-vl-32b-instruct": { - id: "qwen/qwen3-vl-32b-instruct", - name: "Qwen: Qwen3 VL 32B Instruct", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-10-21", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.104, output: 0.416 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen/qwen3-235b-a22b-thinking-2507": { - id: "qwen/qwen3-235b-a22b-thinking-2507", - name: "Qwen: Qwen3 235B A22B Thinking 2507", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-07-25", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.11, output: 0.6 }, - limit: { context: 262144, output: 262144 }, - }, - "qwen/qwen3-next-80b-a3b-thinking": { - id: "qwen/qwen3-next-80b-a3b-thinking", - name: "Qwen: Qwen3 Next 80B A3B Thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-09-11", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.0975, output: 0.78 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen/qwen3-30b-a3b-thinking-2507": { - id: "qwen/qwen3-30b-a3b-thinking-2507", - name: "Qwen: Qwen3 30B A3B Thinking 2507", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-07-29", - last_updated: "2025-07-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.051, output: 0.34 }, - limit: { context: 32768, output: 6554 }, - }, - "qwen/qwen-2.5-7b-instruct": { - id: "qwen/qwen-2.5-7b-instruct", - name: "Qwen: Qwen2.5 7B Instruct", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-09", - last_updated: "2025-04-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.04, output: 0.1 }, - limit: { context: 32768, output: 6554 }, - }, - "qwen/qwen-vl-max": { - id: "qwen/qwen-vl-max", - name: "Qwen: Qwen VL Max", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-04-08", - last_updated: "2025-08-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 3.2 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen/qwen3-coder-flash": { - id: "qwen/qwen3-coder-flash", - name: "Qwen: Qwen3 Coder Flash", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-07-23", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.195, output: 0.975, cache_read: 0.06 }, - limit: { context: 1000000, output: 65536 }, - }, - "qwen/qwen3-30b-a3b": { - id: "qwen/qwen3-30b-a3b", - name: "Qwen: Qwen3 30B A3B", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-04", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.08, output: 0.28, cache_read: 0.03 }, - limit: { context: 40960, output: 40960 }, - }, - "qwen/qwq-32b": { - id: "qwen/qwq-32b", - name: "Qwen: QwQ 32B", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2024-11-28", - last_updated: "2025-04-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.4 }, - limit: { context: 32768, output: 32768 }, - }, - "qwen/qwen3-next-80b-a3b-instruct": { - id: "qwen/qwen3-next-80b-a3b-instruct", - name: "Qwen: Qwen3 Next 80B A3B Instruct", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-09-11", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.09, output: 1.1 }, - limit: { context: 131072, output: 52429 }, - }, - "qwen/qwen3-coder-next": { - id: "qwen/qwen3-coder-next", - name: "Qwen: Qwen3 Coder Next", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2026-02-02", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.12, output: 0.75, cache_read: 0.035 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen/qwen-2.5-coder-32b-instruct": { - id: "qwen/qwen-2.5-coder-32b-instruct", - name: "Qwen2.5 Coder 32B Instruct", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-11-11", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.2, cache_read: 0.015 }, - limit: { context: 32768, output: 8192 }, - }, - "qwen/qwen3-vl-30b-a3b-instruct": { - id: "qwen/qwen3-vl-30b-a3b-instruct", - name: "Qwen: Qwen3 VL 30B A3B Instruct", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-10-05", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.13, output: 0.52 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen/qwen3-coder-30b-a3b-instruct": { - id: "qwen/qwen3-coder-30b-a3b-instruct", - name: "Qwen: Qwen3 Coder 30B A3B Instruct", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-07-31", - last_updated: "2025-07-31", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.27 }, - limit: { context: 160000, output: 32768 }, - }, - "qwen/qwen3-max-thinking": { - id: "qwen/qwen3-max-thinking", - name: "Qwen: Qwen3 Max Thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-01-23", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.78, output: 3.9 }, - limit: { context: 262144, output: 32768 }, - }, - "qwen/qwen-turbo": { - id: "qwen/qwen-turbo", - name: "Qwen: Qwen-Turbo", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-11-01", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.0325, output: 0.13, cache_read: 0.01 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen/qwen3-vl-235b-a22b-instruct": { - id: "qwen/qwen3-vl-235b-a22b-instruct", - name: "Qwen: Qwen3 VL 235B A22B Instruct", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-09-23", - last_updated: "2026-01-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.88, cache_read: 0.11 }, - limit: { context: 262144, output: 52429 }, - }, - "qwen/qwen3-coder": { - id: "qwen/qwen3-coder", - name: "Qwen: Qwen3 Coder 480B A35B", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.22, output: 1, cache_read: 0.022 }, - limit: { context: 262144, output: 52429 }, - }, - "qwen/qwen-2.5-vl-7b-instruct": { - id: "qwen/qwen-2.5-vl-7b-instruct", - name: "Qwen: Qwen2.5-VL 7B Instruct", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-08-28", - last_updated: "2024-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 32768, output: 6554 }, - }, - "qwen/qwen3.5-9b": { - id: "qwen/qwen3.5-9b", - name: "Qwen: Qwen3.5-9B", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-10", - last_updated: "2026-03-15", - modalities: { input: ["image", "text", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.15 }, - limit: { context: 256000, output: 32768 }, - }, - "qwen/qwen3-vl-8b-thinking": { - id: "qwen/qwen3-vl-8b-thinking", - name: "Qwen: Qwen3 VL 8B Thinking", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-10-15", - last_updated: "2025-11-25", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.117, output: 1.365 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen/qwen-plus-2025-07-28:thinking": { - id: "qwen/qwen-plus-2025-07-28:thinking", - name: "Qwen: Qwen Plus 0728 (thinking)", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-09-09", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.26, output: 0.78 }, - limit: { context: 1000000, output: 32768 }, - }, - "qwen/qwen-2.5-72b-instruct": { - id: "qwen/qwen-2.5-72b-instruct", - name: "Qwen2.5 72B Instruct", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-09", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.12, output: 0.39 }, - limit: { context: 32768, output: 16384 }, - }, - "qwen/qwen3-14b": { - id: "qwen/qwen3-14b", - name: "Qwen: Qwen3 14B", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-04", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.06, output: 0.24, cache_read: 0.025 }, - limit: { context: 40960, output: 40960 }, - }, - "qwen/qwen3.5-35b-a3b": { - id: "qwen/qwen3.5-35b-a3b", - name: "Qwen: Qwen3.5-35B-A3B", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-26", - last_updated: "2026-03-15", - modalities: { input: ["image", "text", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1625, output: 1.3 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen/qwen3.5-plus-02-15": { - id: "qwen/qwen3.5-plus-02-15", - name: "Qwen: Qwen3.5 Plus 2026-02-15", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-15", - last_updated: "2026-03-15", - modalities: { input: ["image", "text", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.26, output: 1.56 }, - limit: { context: 1000000, output: 65536 }, - }, - "alfredpros/codellama-7b-instruct-solidity": { - id: "alfredpros/codellama-7b-instruct-solidity", - name: "AlfredPros: CodeLLaMa 7B Instruct Solidity", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-14", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.8, output: 1.2 }, - limit: { context: 4096, output: 4096 }, - }, - "kwaipilot/kat-coder-pro": { - id: "kwaipilot/kat-coder-pro", - name: "Kwaipilot: KAT-Coder-Pro V1", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-09-30", - last_updated: "2025-10-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.207, output: 0.828, cache_read: 0.0414 }, - limit: { context: 256000, output: 128000 }, - }, - "google/gemini-2.5-pro-preview-05-06": { - id: "google/gemini-2.5-pro-preview-05-06", - name: "Google: Gemini 2.5 Pro Preview 05-06", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-05-06", - last_updated: "2026-03-15", - modalities: { input: ["audio", "image", "pdf", "text", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, reasoning: 10, cache_read: 0.125, cache_write: 0.375 }, - limit: { context: 1048576, output: 65535 }, - }, - "google/gemini-3.1-pro-preview-customtools": { - id: "google/gemini-3.1-pro-preview-customtools", - name: "Google: Gemini 3.1 Pro Preview Custom Tools", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-26", - last_updated: "2026-03-15", - modalities: { input: ["audio", "image", "pdf", "text", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, reasoning: 12 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-2.5-flash-lite-preview-09-2025": { - id: "google/gemini-2.5-flash-lite-preview-09-2025", - name: "Google: Gemini 2.5 Flash Lite Preview 09-2025", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-09-25", - last_updated: "2026-03-15", - modalities: { input: ["audio", "image", "pdf", "text", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, reasoning: 0.4, cache_read: 0.01, cache_write: 0.083333 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-2.0-flash-001": { - id: "google/gemini-2.0-flash-001", - name: "Google: Gemini 2.0 Flash", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-12-11", - last_updated: "2026-03-15", - modalities: { input: ["audio", "image", "pdf", "text", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.025, cache_write: 0.083333 }, - limit: { context: 1048576, output: 8192 }, - }, - "google/gemma-3n-e4b-it": { - id: "google/gemma-3n-e4b-it", - name: "Google: Gemma 3n 4B", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-05-20", - last_updated: "2025-05-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0.04 }, - limit: { context: 32768, output: 6554 }, - }, - "google/gemini-3.1-flash-lite-preview": { - id: "google/gemini-3.1-flash-lite-preview", - name: "Google: Gemini 3.1 Flash Lite Preview", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-03", - last_updated: "2026-03-15", - modalities: { input: ["audio", "image", "pdf", "text", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1.5, reasoning: 1.5 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-3.1-pro-preview": { - id: "google/gemini-3.1-pro-preview", - name: "Google: Gemini 3.1 Pro Preview", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-19", - last_updated: "2026-03-15", - modalities: { input: ["audio", "image", "pdf", "text", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, reasoning: 12 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-3-flash-preview": { - id: "google/gemini-3-flash-preview", - name: "Google: Gemini 3 Flash Preview", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-12-17", - last_updated: "2026-03-15", - modalities: { input: ["audio", "image", "pdf", "text", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 3, reasoning: 3, cache_read: 0.05, cache_write: 0.083333 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-3-pro-preview": { - id: "google/gemini-3-pro-preview", - name: "Google: Gemini 3 Pro Preview", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-11-18", - last_updated: "2026-03-15", - modalities: { input: ["audio", "image", "pdf", "text", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, reasoning: 12, cache_read: 0.2, cache_write: 0.375 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-2.5-pro": { - id: "google/gemini-2.5-pro", - name: "Google: Gemini 2.5 Pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-03-20", - last_updated: "2026-03-15", - modalities: { input: ["audio", "image", "pdf", "text", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, reasoning: 10, cache_read: 0.125, cache_write: 0.375 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemma-2-9b-it": { - id: "google/gemma-2-9b-it", - name: "Google: Gemma 2 9B", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-06-28", - last_updated: "2024-06-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.09 }, - limit: { context: 8192, output: 1639 }, - }, - "google/gemini-3-pro-image-preview": { - id: "google/gemini-3-pro-image-preview", - name: "Google: Nano Banana Pro (Gemini 3 Pro Image Preview)", - attachment: true, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-11-20", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["image", "text"] }, - open_weights: false, - cost: { input: 2, output: 12, reasoning: 12 }, - limit: { context: 65536, output: 32768 }, - }, - "google/gemini-2.5-flash-image": { - id: "google/gemini-2.5-flash-image", - name: "Google: Nano Banana (Gemini 2.5 Flash Image)", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-10-08", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["image", "text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5 }, - limit: { context: 32768, output: 32768 }, - }, - "google/gemma-3-12b-it": { - id: "google/gemma-3-12b-it", - name: "Google: Gemma 3 12B", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-03-13", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.04, output: 0.13, cache_read: 0.015 }, - limit: { context: 131072, output: 131072 }, - }, - "google/gemini-2.5-flash": { - id: "google/gemini-2.5-flash", - name: "Google: Gemini 2.5 Flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-07-17", - last_updated: "2026-03-15", - modalities: { input: ["audio", "image", "pdf", "text", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5, reasoning: 2.5, cache_read: 0.03, cache_write: 0.083333 }, - limit: { context: 1048576, output: 65535 }, - }, - "google/gemini-3.1-flash-image-preview": { - id: "google/gemini-3.1-flash-image-preview", - name: "Google: Nano Banana 2 (Gemini 3.1 Flash Image Preview)", - attachment: true, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2026-02-26", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["image", "text"] }, - open_weights: false, - cost: { input: 0.5, output: 3 }, - limit: { context: 65536, output: 65536 }, - }, - "google/gemma-3-4b-it": { - id: "google/gemma-3-4b-it", - name: "Google: Gemma 3 4B", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-03-13", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.04, output: 0.08 }, - limit: { context: 131072, output: 19200 }, - }, - "google/gemini-2.5-pro-preview": { - id: "google/gemini-2.5-pro-preview", - name: "Google: Gemini 2.5 Pro Preview 06-05", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-06-05", - last_updated: "2026-03-15", - modalities: { input: ["audio", "image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, reasoning: 10, cache_read: 0.125, cache_write: 0.375 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemma-2-27b-it": { - id: "google/gemma-2-27b-it", - name: "Google: Gemma 2 27B", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-06-24", - last_updated: "2024-06-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.65, output: 0.65 }, - limit: { context: 8192, output: 2048 }, - }, - "google/gemma-3-27b-it": { - id: "google/gemma-3-27b-it", - name: "Google: Gemma 3 27B", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-03-12", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.11, cache_read: 0.02 }, - limit: { context: 128000, output: 65536 }, - }, - "google/gemini-2.5-flash-lite": { - id: "google/gemini-2.5-flash-lite", - name: "Google: Gemini 2.5 Flash Lite", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-06-17", - last_updated: "2026-03-15", - modalities: { input: ["audio", "image", "pdf", "text", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, reasoning: 0.4, cache_read: 0.01, cache_write: 0.083333 }, - limit: { context: 1048576, output: 65535 }, - }, - "google/gemini-2.0-flash-lite-001": { - id: "google/gemini-2.0-flash-lite-001", - name: "Google: Gemini 2.0 Flash Lite", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-12-11", - last_updated: "2026-03-15", - modalities: { input: ["audio", "image", "pdf", "text", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.075, output: 0.3 }, - limit: { context: 1048576, output: 8192 }, - }, - "moonshotai/kimi-k2.5": { - id: "moonshotai/kimi-k2.5", - name: "MoonshotAI: Kimi K2.5", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-01-27", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.45, output: 2.2 }, - limit: { context: 262144, output: 65535 }, - }, - "moonshotai/kimi-k2-0905": { - id: "moonshotai/kimi-k2-0905", - name: "MoonshotAI: Kimi K2 0905", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 2, cache_read: 0.15 }, - limit: { context: 131072, output: 26215 }, - }, - "moonshotai/kimi-k2": { - id: "moonshotai/kimi-k2", - name: "MoonshotAI: Kimi K2 0711", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-07-11", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 2.2 }, - limit: { context: 131000, output: 26215 }, - }, - "moonshotai/kimi-k2-thinking": { - id: "moonshotai/kimi-k2-thinking", - name: "MoonshotAI: Kimi K2 Thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-11-06", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.47, output: 2, cache_read: 0.2 }, - limit: { context: 131072, output: 65535 }, - }, - "aion-labs/aion-1.0": { - id: "aion-labs/aion-1.0", - name: "AionLabs: Aion-1.0", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-02-05", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 4, output: 8 }, - limit: { context: 131072, output: 32768 }, - }, - "aion-labs/aion-rp-llama-3.1-8b": { - id: "aion-labs/aion-rp-llama-3.1-8b", - name: "AionLabs: Aion-RP 1.0 (8B)", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-02-05", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 1.6 }, - limit: { context: 32768, output: 32768 }, - }, - "aion-labs/aion-2.0": { - id: "aion-labs/aion-2.0", - name: "AionLabs: Aion-2.0", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2026-02-24", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 1.6 }, - limit: { context: 131072, output: 32768 }, - }, - "aion-labs/aion-1.0-mini": { - id: "aion-labs/aion-1.0-mini", - name: "AionLabs: Aion-1.0-Mini", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-02-05", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.7, output: 1.4 }, - limit: { context: 131072, output: 32768 }, - }, - "thedrummer/unslopnemo-12b": { - id: "thedrummer/unslopnemo-12b", - name: "TheDrummer: UnslopNemo 12B", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-11-09", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 0.4 }, - limit: { context: 32768, output: 32768 }, - }, - "thedrummer/cydonia-24b-v4.1": { - id: "thedrummer/cydonia-24b-v4.1", - name: "TheDrummer: Cydonia 24B V4.1", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-09-27", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.5 }, - limit: { context: 131072, output: 131072 }, - }, - "thedrummer/skyfall-36b-v2": { - id: "thedrummer/skyfall-36b-v2", - name: "TheDrummer: Skyfall 36B V2", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-03-11", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 0.8 }, - limit: { context: 32768, output: 32768 }, - }, - "thedrummer/rocinante-12b": { - id: "thedrummer/rocinante-12b", - name: "TheDrummer: Rocinante 12B", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-09-30", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.17, output: 0.43 }, - limit: { context: 32768, output: 32768 }, - }, - "anthropic/claude-opus-4.1": { - id: "anthropic/claude-opus-4.1", - name: "Anthropic: Claude Opus 4.1", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "anthropic/claude-3.7-sonnet:thinking": { - id: "anthropic/claude-3.7-sonnet:thinking", - name: "Anthropic: Claude 3.7 Sonnet (thinking)", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-02-19", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-3.7-sonnet": { - id: "anthropic/claude-3.7-sonnet", - name: "Anthropic: Claude 3.7 Sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-02-19", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-opus-4.6": { - id: "anthropic/claude-opus-4.6", - name: "Anthropic: Claude Opus 4.6", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 1000000, output: 128000 }, - }, - "anthropic/claude-3.5-sonnet": { - id: "anthropic/claude-3.5-sonnet", - name: "Anthropic: Claude 3.5 Sonnet", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-10-22", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 6, output: 30 }, - limit: { context: 200000, output: 8192 }, - }, - "anthropic/claude-sonnet-4": { - id: "anthropic/claude-sonnet-4", - name: "Anthropic: Claude Sonnet 4", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-05-22", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-sonnet-4.5": { - id: "anthropic/claude-sonnet-4.5", - name: "Anthropic: Claude Sonnet 4.5", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-09-29", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 1000000, output: 64000 }, - }, - "anthropic/claude-opus-4.5": { - id: "anthropic/claude-opus-4.5", - name: "Anthropic: Claude Opus 4.5", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-11-24", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-3-haiku": { - id: "anthropic/claude-3-haiku", - name: "Anthropic: Claude 3 Haiku", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-03-07", - last_updated: "2024-03-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1.25, cache_read: 0.03, cache_write: 0.3 }, - limit: { context: 200000, output: 4096 }, - }, - "anthropic/claude-opus-4": { - id: "anthropic/claude-opus-4", - name: "Anthropic: Claude Opus 4", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-05-22", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "anthropic/claude-3.5-haiku": { - id: "anthropic/claude-3.5-haiku", - name: "Anthropic: Claude 3.5 Haiku", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 4, cache_read: 0.08, cache_write: 1 }, - limit: { context: 200000, output: 8192 }, - }, - "anthropic/claude-haiku-4.5": { - id: "anthropic/claude-haiku-4.5", - name: "Anthropic: Claude Haiku 4.5", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-sonnet-4.6": { - id: "anthropic/claude-sonnet-4.6", - name: "Anthropic: Claude Sonnet 4.6", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-17", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 1000000, output: 128000 }, - }, - "switchpoint/router": { - id: "switchpoint/router", - name: "Switchpoint Router", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-07-12", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.85, output: 3.4 }, - limit: { context: 131072, output: 32768 }, - }, - "xiaomi/mimo-v2-flash": { - id: "xiaomi/mimo-v2-flash", - name: "Xiaomi: MiMo-V2-Flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-12-14", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.09, output: 0.29, cache_read: 0.045 }, - limit: { context: 262144, output: 65536 }, - }, - "bytedance/ui-tars-1.5-7b": { - id: "bytedance/ui-tars-1.5-7b", - name: "ByteDance: UI-TARS 7B ", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-07-23", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.2 }, - limit: { context: 128000, output: 2048 }, - }, - "tngtech/deepseek-r1t2-chimera": { - id: "tngtech/deepseek-r1t2-chimera", - name: "TNG: DeepSeek R1T2 Chimera", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-07-08", - last_updated: "2025-07-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 0.85, cache_read: 0.125 }, - limit: { context: 163840, output: 163840 }, - }, - "meituan/longcat-flash-chat": { - id: "meituan/longcat-flash-chat", - name: "Meituan: LongCat Flash Chat", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-08-30", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.8, cache_read: 0.2 }, - limit: { context: 131072, output: 131072 }, - }, - }, - }, - "sap-ai-core": { - id: "sap-ai-core", - env: ["AICORE_SERVICE_KEY"], - npm: "@jerome-benoit/sap-ai-provider-v2", - name: "SAP AI Core", - doc: "https://help.sap.com/docs/sap-ai-core", - models: { - "anthropic--claude-4.6-opus": { - id: "anthropic--claude-4.6-opus", - name: "anthropic--claude-4.6-opus", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-02-05", - last_updated: "2026-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 1000000, output: 128000 }, - }, - "anthropic--claude-3-haiku": { - id: "anthropic--claude-3-haiku", - name: "anthropic--claude-3-haiku", - family: "claude-haiku", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-08-31", - release_date: "2024-03-13", - last_updated: "2024-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1.25, cache_read: 0.03, cache_write: 0.3 }, - limit: { context: 200000, output: 4096 }, - }, - "anthropic--claude-3-opus": { - id: "anthropic--claude-3-opus", - name: "anthropic--claude-3-opus", - family: "claude-opus", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-08-31", - release_date: "2024-02-29", - last_updated: "2024-02-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 4096 }, - }, - "gpt-5-mini": { - id: "gpt-5-mini", - name: "gpt-5-mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.025 }, - limit: { context: 400000, output: 128000 }, - }, - "gpt-5-nano": { - id: "gpt-5-nano", - name: "gpt-5-nano", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.4, cache_read: 0.005 }, - limit: { context: 400000, output: 128000 }, - }, - "gemini-2.5-pro": { - id: "gemini-2.5-pro", - name: "gemini-2.5-pro", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-03-25", - last_updated: "2025-06-05", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 1048576, output: 65536 }, - }, - "anthropic--claude-3.7-sonnet": { - id: "anthropic--claude-3.7-sonnet", - name: "anthropic--claude-3.7-sonnet", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10-31", - release_date: "2025-02-24", - last_updated: "2025-02-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "sonar-pro": { - id: "sonar-pro", - name: "sonar-pro", - family: "sonar-pro", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-09-01", - release_date: "2024-01-01", - last_updated: "2025-09-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 200000, output: 8192 }, - }, - "anthropic--claude-4.5-sonnet": { - id: "anthropic--claude-4.5-sonnet", - name: "anthropic--claude-4.5-sonnet", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic--claude-4.6-sonnet": { - id: "anthropic--claude-4.6-sonnet", - name: "anthropic--claude-4.6-sonnet", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08", - release_date: "2026-02-17", - last_updated: "2026-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 1000000, output: 64000 }, - }, - "sonar-deep-research": { - id: "sonar-deep-research", - name: "sonar-deep-research", - family: "sonar-deep-research", - attachment: false, - reasoning: true, - tool_call: false, - temperature: false, - knowledge: "2025-01", - release_date: "2025-02-01", - last_updated: "2025-09-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, reasoning: 3 }, - limit: { context: 128000, output: 32768 }, - }, - "gemini-2.5-flash": { - id: "gemini-2.5-flash", - name: "gemini-2.5-flash", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-03-25", - last_updated: "2025-06-05", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5, cache_read: 0.03, input_audio: 1 }, - limit: { context: 1048576, output: 65536 }, - }, - "anthropic--claude-4.5-opus": { - id: "anthropic--claude-4.5-opus", - name: "anthropic--claude-4.5-opus", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-05", - release_date: "2025-11-24", - last_updated: "2025-11-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 64000 }, - }, - sonar: { - id: "sonar", - name: "sonar", - family: "sonar", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-09-01", - release_date: "2024-01-01", - last_updated: "2025-09-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 1 }, - limit: { context: 128000, output: 4096 }, - }, - "anthropic--claude-4-opus": { - id: "anthropic--claude-4-opus", - name: "anthropic--claude-4-opus", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "anthropic--claude-3-sonnet": { - id: "anthropic--claude-3-sonnet", - name: "anthropic--claude-3-sonnet", - family: "claude-sonnet", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-08-31", - release_date: "2024-03-04", - last_updated: "2024-03-04", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 4096 }, - }, - "anthropic--claude-4-sonnet": { - id: "anthropic--claude-4-sonnet", - name: "anthropic--claude-4-sonnet", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "gemini-2.5-flash-lite": { - id: "gemini-2.5-flash-lite", - name: "gemini-2.5-flash-lite", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.025 }, - limit: { context: 1048576, output: 65536 }, - }, - "anthropic--claude-4.5-haiku": { - id: "anthropic--claude-4.5-haiku", - name: "anthropic--claude-4.5-haiku", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 200000, output: 64000 }, - }, - "gpt-5": { - id: "gpt-5", - name: "gpt-5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "gpt-4.1": { - id: "gpt-4.1", - name: "gpt-4.1", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 1047576, output: 32768 }, - }, - "gpt-4.1-mini": { - id: "gpt-4.1-mini", - name: "gpt-4.1-mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.6, cache_read: 0.1 }, - limit: { context: 1047576, output: 32768 }, - }, - "anthropic--claude-3.5-sonnet": { - id: "anthropic--claude-3.5-sonnet", - name: "anthropic--claude-3.5-sonnet", - family: "claude-sonnet", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04-30", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 8192 }, - }, - }, - }, - morph: { - id: "morph", - env: ["MORPH_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.morphllm.com/v1", - name: "Morph", - doc: "https://docs.morphllm.com/api-reference/introduction", - models: { - auto: { - id: "auto", - name: "Auto", - family: "auto", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-06-01", - last_updated: "2024-06-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.85, output: 1.55 }, - limit: { context: 32000, output: 32000 }, - }, - "morph-v3-fast": { - id: "morph-v3-fast", - name: "Morph v3 Fast", - family: "morph", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-08-15", - last_updated: "2024-08-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 1.2 }, - limit: { context: 16000, output: 16000 }, - }, - "morph-v3-large": { - id: "morph-v3-large", - name: "Morph v3 Large", - family: "morph", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-08-15", - last_updated: "2024-08-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.9, output: 1.9 }, - limit: { context: 32000, output: 32000 }, - }, - }, - }, - "cloudflare-ai-gateway": { - id: "cloudflare-ai-gateway", - env: ["CLOUDFLARE_API_TOKEN", "CLOUDFLARE_ACCOUNT_ID", "CLOUDFLARE_GATEWAY_ID"], - npm: "ai-gateway-provider", - name: "Cloudflare AI Gateway", - doc: "https://developers.cloudflare.com/ai-gateway/", - models: { - "workers-ai/@cf/myshell-ai/melotts": { - id: "workers-ai/@cf/myshell-ai/melotts", - name: "MyShell MeloTTS", - family: "melotts", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-11-14", - last_updated: "2025-11-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/ibm-granite/granite-4.0-h-micro": { - id: "workers-ai/@cf/ibm-granite/granite-4.0-h-micro", - name: "IBM Granite 4.0 H Micro", - family: "granite", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.017, output: 0.11 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/huggingface/distilbert-sst-2-int8": { - id: "workers-ai/@cf/huggingface/distilbert-sst-2-int8", - name: "DistilBERT SST-2 INT8", - family: "distilbert", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.026, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/zai-org/glm-4.7-flash": { - id: "workers-ai/@cf/zai-org/glm-4.7-flash", - name: "GLM-4.7-Flash", - family: "glm-flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-01-19", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.06, output: 0.4 }, - limit: { context: 131072, output: 131072 }, - }, - "workers-ai/@cf/pipecat-ai/smart-turn-v2": { - id: "workers-ai/@cf/pipecat-ai/smart-turn-v2", - name: "Pipecat Smart Turn v2", - family: "smart-turn", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-11-14", - last_updated: "2025-11-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/mistralai/mistral-small-3.1-24b-instruct": { - id: "workers-ai/@cf/mistralai/mistral-small-3.1-24b-instruct", - name: "Mistral Small 3.1 24B Instruct", - family: "mistral-small", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-11", - last_updated: "2025-04-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.35, output: 0.56 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/facebook/bart-large-cnn": { - id: "workers-ai/@cf/facebook/bart-large-cnn", - name: "BART Large CNN", - family: "bart", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-09", - last_updated: "2025-04-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/aisingapore/gemma-sea-lion-v4-27b-it": { - id: "workers-ai/@cf/aisingapore/gemma-sea-lion-v4-27b-it", - name: "Gemma SEA-LION v4 27B IT", - family: "gemma", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.35, output: 0.56 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/nvidia/nemotron-3-120b-a12b": { - id: "workers-ai/@cf/nvidia/nemotron-3-120b-a12b", - name: "Nemotron 3 Super 120B", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-03-11", - last_updated: "2026-03-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 1.5 }, - limit: { context: 256000, output: 256000 }, - }, - "workers-ai/@cf/deepseek-ai/deepseek-r1-distill-qwen-32b": { - id: "workers-ai/@cf/deepseek-ai/deepseek-r1-distill-qwen-32b", - name: "DeepSeek R1 Distill Qwen 32B", - family: "deepseek-thinking", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 4.88 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/openai/gpt-oss-20b": { - id: "workers-ai/@cf/openai/gpt-oss-20b", - name: "GPT OSS 20B", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.3 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/openai/gpt-oss-120b": { - id: "workers-ai/@cf/openai/gpt-oss-120b", - name: "GPT OSS 120B", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.35, output: 0.75 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/mistral/mistral-7b-instruct-v0.1": { - id: "workers-ai/@cf/mistral/mistral-7b-instruct-v0.1", - name: "Mistral 7B Instruct v0.1", - family: "mistral", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.11, output: 0.19 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/meta/llama-4-scout-17b-16e-instruct": { - id: "workers-ai/@cf/meta/llama-4-scout-17b-16e-instruct", - name: "Llama 4 Scout 17B 16E Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 0.85 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/meta/llama-3-8b-instruct-awq": { - id: "workers-ai/@cf/meta/llama-3-8b-instruct-awq", - name: "Llama 3 8B Instruct AWQ", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.12, output: 0.27 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/meta/llama-guard-3-8b": { - id: "workers-ai/@cf/meta/llama-guard-3-8b", - name: "Llama Guard 3 8B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.48, output: 0.03 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/meta/m2m100-1.2b": { - id: "workers-ai/@cf/meta/m2m100-1.2b", - name: "M2M100 1.2B", - family: "m2m", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.34, output: 0.34 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/meta/llama-2-7b-chat-fp16": { - id: "workers-ai/@cf/meta/llama-2-7b-chat-fp16", - name: "Llama 2 7B Chat FP16", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.56, output: 6.67 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/meta/llama-3.2-11b-vision-instruct": { - id: "workers-ai/@cf/meta/llama-3.2-11b-vision-instruct", - name: "Llama 3.2 11B Vision Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.049, output: 0.68 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/meta/llama-3.3-70b-instruct-fp8-fast": { - id: "workers-ai/@cf/meta/llama-3.3-70b-instruct-fp8-fast", - name: "Llama 3.3 70B Instruct FP8 Fast", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.29, output: 2.25 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/meta/llama-3.2-1b-instruct": { - id: "workers-ai/@cf/meta/llama-3.2-1b-instruct", - name: "Llama 3.2 1B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.027, output: 0.2 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/meta/llama-3.1-8b-instruct-fp8": { - id: "workers-ai/@cf/meta/llama-3.1-8b-instruct-fp8", - name: "Llama 3.1 8B Instruct FP8", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.29 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/meta/llama-3.2-3b-instruct": { - id: "workers-ai/@cf/meta/llama-3.2-3b-instruct", - name: "Llama 3.2 3B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.051, output: 0.34 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/meta/llama-3.1-8b-instruct-awq": { - id: "workers-ai/@cf/meta/llama-3.1-8b-instruct-awq", - name: "Llama 3.1 8B Instruct AWQ", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.12, output: 0.27 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/meta/llama-3-8b-instruct": { - id: "workers-ai/@cf/meta/llama-3-8b-instruct", - name: "Llama 3 8B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.28, output: 0.83 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/meta/llama-3.1-8b-instruct": { - id: "workers-ai/@cf/meta/llama-3.1-8b-instruct", - name: "Llama 3.1 8B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.28, output: 0.8299999999999998 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/qwen/qwen2.5-coder-32b-instruct": { - id: "workers-ai/@cf/qwen/qwen2.5-coder-32b-instruct", - name: "Qwen 2.5 Coder 32B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-11", - last_updated: "2025-04-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.66, output: 1 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/qwen/qwen3-embedding-0.6b": { - id: "workers-ai/@cf/qwen/qwen3-embedding-0.6b", - name: "Qwen3 Embedding 0.6B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-11-14", - last_updated: "2025-11-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.012, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/qwen/qwq-32b": { - id: "workers-ai/@cf/qwen/qwq-32b", - name: "QwQ 32B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-11", - last_updated: "2025-04-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.66, output: 1 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/qwen/qwen3-30b-a3b-fp8": { - id: "workers-ai/@cf/qwen/qwen3-30b-a3b-fp8", - name: "Qwen3 30B A3B FP8", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-11-14", - last_updated: "2025-11-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.051, output: 0.34 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/google/gemma-3-12b-it": { - id: "workers-ai/@cf/google/gemma-3-12b-it", - name: "Gemma 3 12B IT", - family: "gemma", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-11", - last_updated: "2025-04-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.35, output: 0.56 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/moonshotai/kimi-k2.5": { - id: "workers-ai/@cf/moonshotai/kimi-k2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3, cache_read: 0.1 }, - limit: { context: 256000, output: 256000 }, - }, - "workers-ai/@cf/ai4bharat/indictrans2-en-indic-1B": { - id: "workers-ai/@cf/ai4bharat/indictrans2-en-indic-1B", - name: "IndicTrans2 EN-Indic 1B", - family: "indictrans", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.34, output: 0.34 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/pfnet/plamo-embedding-1b": { - id: "workers-ai/@cf/pfnet/plamo-embedding-1b", - name: "PLaMo Embedding 1B", - family: "plamo", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.019, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/baai/bge-small-en-v1.5": { - id: "workers-ai/@cf/baai/bge-small-en-v1.5", - name: "BGE Small EN v1.5", - family: "bge", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.02, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/baai/bge-large-en-v1.5": { - id: "workers-ai/@cf/baai/bge-large-en-v1.5", - name: "BGE Large EN v1.5", - family: "bge", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/baai/bge-reranker-base": { - id: "workers-ai/@cf/baai/bge-reranker-base", - name: "BGE Reranker Base", - family: "bge", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-09", - last_updated: "2025-04-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.0031, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/baai/bge-base-en-v1.5": { - id: "workers-ai/@cf/baai/bge-base-en-v1.5", - name: "BGE Base EN v1.5", - family: "bge", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.067, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/baai/bge-m3": { - id: "workers-ai/@cf/baai/bge-m3", - name: "BGE M3", - family: "bge", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.012, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/deepgram/aura-2-en": { - id: "workers-ai/@cf/deepgram/aura-2-en", - name: "Deepgram Aura 2 (EN)", - family: "aura", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-11-14", - last_updated: "2025-11-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/deepgram/aura-2-es": { - id: "workers-ai/@cf/deepgram/aura-2-es", - name: "Deepgram Aura 2 (ES)", - family: "aura", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-11-14", - last_updated: "2025-11-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/deepgram/nova-3": { - id: "workers-ai/@cf/deepgram/nova-3", - name: "Deepgram Nova 3", - family: "nova", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-11-14", - last_updated: "2025-11-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-5.3-codex": { - id: "openai/gpt-5.3-codex", - name: "GPT-5.3 Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, input: 272000, output: 128000 }, - provider: { npm: "ai-gateway-provider" }, - }, - "openai/gpt-4-turbo": { - id: "openai/gpt-4-turbo", - name: "GPT-4 Turbo", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - knowledge: "2023-12", - release_date: "2023-11-06", - last_updated: "2024-04-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 10, output: 30 }, - limit: { context: 128000, output: 4096 }, - }, - "openai/gpt-5.2": { - id: "openai/gpt-5.2", - name: "GPT-5.2", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/o3-pro": { - id: "openai/o3-pro", - name: "o3-pro", - family: "o-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05", - release_date: "2025-06-10", - last_updated: "2025-06-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 20, output: 80 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-4o-mini": { - id: "openai/gpt-4o-mini", - name: "GPT-4o mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-09", - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6, cache_read: 0.08 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/o4-mini": { - id: "openai/o4-mini", - name: "o4-mini", - family: "o-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05", - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.28 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-5.2-codex": { - id: "openai/gpt-5.2-codex", - name: "GPT-5.2 Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, input: 272000, output: 128000 }, - provider: { npm: "ai-gateway-provider" }, - }, - "openai/gpt-5.1": { - id: "openai/gpt-5.1", - name: "GPT-5.1", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.13 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/o1": { - id: "openai/o1", - name: "o1", - family: "o", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2023-09", - release_date: "2024-12-05", - last_updated: "2024-12-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 60, cache_read: 7.5 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-3.5-turbo": { - id: "openai/gpt-3.5-turbo", - name: "GPT-3.5-turbo", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - knowledge: "2021-09-01", - release_date: "2023-03-01", - last_updated: "2023-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 1.5, cache_read: 1.25 }, - limit: { context: 16385, output: 4096 }, - }, - "openai/o3-mini": { - id: "openai/o3-mini", - name: "o3-mini", - family: "o-mini", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05", - release_date: "2024-12-20", - last_updated: "2025-01-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.55 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-4": { - id: "openai/gpt-4", - name: "GPT-4", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - knowledge: "2023-11", - release_date: "2023-11-06", - last_updated: "2024-04-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 30, output: 60 }, - limit: { context: 8192, output: 8192 }, - }, - "openai/gpt-5.4": { - id: "openai/gpt-5.4", - name: "GPT-5.4", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 15, cache_read: 0.25 }, - limit: { context: 1050000, input: 922000, output: 128000 }, - provider: { npm: "ai-gateway-provider" }, - }, - "openai/o3": { - id: "openai/o3", - name: "o3", - family: "o", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05", - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-4o": { - id: "openai/gpt-4o", - name: "GPT-4o", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-09", - release_date: "2024-05-13", - last_updated: "2024-08-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10, cache_read: 1.25 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-5.1-codex": { - id: "openai/gpt-5.1-codex", - name: "GPT-5.1 Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "anthropic/claude-haiku-4-5": { - id: "anthropic/claude-haiku-4-5", - name: "Claude Haiku 4.5 (latest)", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-sonnet-4-6": { - id: "anthropic/claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2026-02-17", - last_updated: "2026-02-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 3, - output: 15, - cache_read: 0.3, - cache_write: 3.75, - context_over_200k: { input: 6, output: 22.5, cache_read: 0.6, cache_write: 7.5 }, - }, - limit: { context: 1000000, output: 64000 }, - provider: { npm: "ai-gateway-provider" }, - }, - "anthropic/claude-opus-4-1": { - id: "anthropic/claude-opus-4-1", - name: "Claude Opus 4.1 (latest)", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "anthropic/claude-3-5-haiku": { - id: "anthropic/claude-3-5-haiku", - name: "Claude Haiku 3.5 (latest)", - family: "claude-haiku", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07-31", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 4, cache_read: 0.08, cache_write: 1 }, - limit: { context: 200000, output: 8192 }, - }, - "anthropic/claude-3.5-sonnet": { - id: "anthropic/claude-3.5-sonnet", - name: "Claude Sonnet 3.5 v2", - family: "claude-sonnet", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04-30", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 8192 }, - }, - "anthropic/claude-sonnet-4": { - id: "anthropic/claude-sonnet-4", - name: "Claude Sonnet 4 (latest)", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-opus-4-5": { - id: "anthropic/claude-opus-4-5", - name: "Claude Opus 4.5 (latest)", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-24", - last_updated: "2025-11-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-3-haiku": { - id: "anthropic/claude-3-haiku", - name: "Claude Haiku 3", - family: "claude-haiku", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-08-31", - release_date: "2024-03-13", - last_updated: "2024-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1.25, cache_read: 0.03, cache_write: 0.3 }, - limit: { context: 200000, output: 4096 }, - }, - "anthropic/claude-opus-4": { - id: "anthropic/claude-opus-4", - name: "Claude Opus 4 (latest)", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "anthropic/claude-opus-4-6": { - id: "anthropic/claude-opus-4-6", - name: "Claude Opus 4.6 (latest)", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 5, - output: 25, - cache_read: 0.5, - cache_write: 6.25, - context_over_200k: { input: 10, output: 37.5, cache_read: 1, cache_write: 12.5 }, - }, - limit: { context: 1000000, output: 128000 }, - }, - "anthropic/claude-3.5-haiku": { - id: "anthropic/claude-3.5-haiku", - name: "Claude Haiku 3.5 (latest)", - family: "claude-haiku", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07-31", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 4, cache_read: 0.08, cache_write: 1 }, - limit: { context: 200000, output: 8192 }, - }, - "anthropic/claude-sonnet-4-5": { - id: "anthropic/claude-sonnet-4-5", - name: "Claude Sonnet 4.5 (latest)", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-3-sonnet": { - id: "anthropic/claude-3-sonnet", - name: "Claude Sonnet 3", - family: "claude-sonnet", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-08-31", - release_date: "2024-03-04", - last_updated: "2024-03-04", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 0.3 }, - limit: { context: 200000, output: 4096 }, - }, - "anthropic/claude-3-opus": { - id: "anthropic/claude-3-opus", - name: "Claude Opus 3", - family: "claude-opus", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-08-31", - release_date: "2024-02-29", - last_updated: "2024-02-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 4096 }, - }, - }, - }, - "github-copilot": { - id: "github-copilot", - env: ["GITHUB_TOKEN"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.githubcopilot.com", - name: "GitHub Copilot", - doc: "https://docs.github.com/en/copilot", - models: { - "gpt-5.1-codex-max": { - id: "gpt-5.1-codex-max", - name: "GPT-5.1-Codex-max", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-12-04", - last_updated: "2025-12-04", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 400000, input: 128000, output: 128000 }, - }, - "claude-opus-4.6": { - id: "claude-opus-4.6", - name: "Claude Opus 4.6", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 144000, input: 128000, output: 64000 }, - }, - "gemini-3.1-pro-preview": { - id: "gemini-3.1-pro-preview", - name: "Gemini 3.1 Pro Preview", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-02-19", - last_updated: "2026-02-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, input: 128000, output: 64000 }, - }, - "gemini-3-flash-preview": { - id: "gemini-3-flash-preview", - name: "Gemini 3 Flash", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, input: 128000, output: 64000 }, - }, - "gpt-5-mini": { - id: "gpt-5-mini", - name: "GPT-5-mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-06", - release_date: "2025-08-13", - last_updated: "2025-08-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 264000, input: 128000, output: 64000 }, - }, - "gemini-3-pro-preview": { - id: "gemini-3-pro-preview", - name: "Gemini 3 Pro Preview", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-11-18", - last_updated: "2025-11-18", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, input: 128000, output: 64000 }, - }, - "gpt-5.3-codex": { - id: "gpt-5.3-codex", - name: "GPT-5.3-Codex", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-02-24", - last_updated: "2026-02-24", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gemini-2.5-pro": { - id: "gemini-2.5-pro", - name: "Gemini 2.5 Pro", - family: "gemini-pro", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-03-20", - last_updated: "2025-06-05", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, input: 128000, output: 64000 }, - }, - "gpt-5.2": { - id: "gpt-5.2", - name: "GPT-5.2", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 264000, input: 128000, output: 64000 }, - }, - "gpt-5.4-mini": { - id: "gpt-5.4-mini", - name: "GPT-5.4 Mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gpt-5.2-codex": { - id: "gpt-5.2-codex", - name: "GPT-5.2-Codex", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gpt-5.1-codex-mini": { - id: "gpt-5.1-codex-mini", - name: "GPT-5.1-Codex-mini", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 400000, input: 128000, output: 128000 }, - }, - "claude-sonnet-4": { - id: "claude-sonnet-4", - name: "Claude Sonnet 4", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 216000, input: 128000, output: 16000 }, - }, - "grok-code-fast-1": { - id: "grok-code-fast-1", - name: "Grok Code Fast 1", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08", - release_date: "2025-08-27", - last_updated: "2025-08-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, input: 128000, output: 64000 }, - }, - "gpt-5.1": { - id: "gpt-5.1", - name: "GPT-5.1", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 264000, input: 128000, output: 64000 }, - }, - "claude-sonnet-4.5": { - id: "claude-sonnet-4.5", - name: "Claude Sonnet 4.5", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 144000, input: 128000, output: 32000 }, - }, - "claude-opus-41": { - id: "claude-opus-41", - name: "Claude Opus 4.1", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 80000, output: 16000 }, - }, - "claude-opus-4.5": { - id: "claude-opus-4.5", - name: "Claude Opus 4.5", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-24", - last_updated: "2025-08-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 160000, input: 128000, output: 32000 }, - }, - "gpt-5.4": { - id: "gpt-5.4", - name: "GPT-5.4", - family: "gpt", - attachment: false, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gpt-4o": { - id: "gpt-4o", - name: "GPT-4o", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-09", - release_date: "2024-05-13", - last_updated: "2024-05-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, input: 64000, output: 4096 }, - }, - "gpt-5": { - id: "gpt-5", - name: "GPT-5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 128000 }, - }, - "claude-haiku-4.5": { - id: "claude-haiku-4.5", - name: "Claude Haiku 4.5", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 144000, input: 128000, output: 32000 }, - }, - "gpt-4.1": { - id: "gpt-4.1", - name: "GPT-4.1", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, input: 64000, output: 16384 }, - }, - "claude-sonnet-4.6": { - id: "claude-sonnet-4.6", - name: "Claude Sonnet 4.6", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-17", - last_updated: "2026-02-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 200000, input: 128000, output: 32000 }, - }, - "gpt-5.1-codex": { - id: "gpt-5.1-codex", - name: "GPT-5.1-Codex", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 400000, input: 128000, output: 128000 }, - }, - }, - }, - mixlayer: { - id: "mixlayer", - env: ["MIXLAYER_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://models.mixlayer.ai/v1", - name: "Mixlayer", - doc: "https://docs.mixlayer.com", - models: { - "qwen/qwen3.5-122b-a10b": { - id: "qwen/qwen3.5-122b-a10b", - name: "Qwen3.5 122B A10B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 3.2 }, - limit: { context: 262144, output: 262144 }, - }, - "qwen/qwen3.5-27b": { - id: "qwen/qwen3.5-27b", - name: "Qwen3.5 27B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 2.4 }, - limit: { context: 262144, output: 262144 }, - }, - "qwen/qwen3.5-397b-a17b": { - id: "qwen/qwen3.5-397b-a17b", - name: "Qwen3.5 397B A17B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3.6 }, - limit: { context: 262144, output: 262144 }, - }, - "qwen/qwen3.5-9b": { - id: "qwen/qwen3.5-9b", - name: "Qwen3.5 9B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 262144, output: 262144 }, - }, - "qwen/qwen3.5-35b-a3b": { - id: "qwen/qwen3.5-35b-a3b", - name: "Qwen3.5 35B A3B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 1.3 }, - limit: { context: 262144, output: 262144 }, - }, - }, - }, - "xiaomi-token-plan-sgp": { - id: "xiaomi-token-plan-sgp", - env: ["XIAOMI_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://token-plan-sgp.xiaomimimo.com/v1", - name: "Xiaomi Token Plan (Singapore)", - doc: "https://platform.xiaomimimo.com/#/docs", - models: { - "mimo-v2-pro": { - id: "mimo-v2-pro", - name: "MiMo-V2-Pro", - family: "mimo", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0 }, - limit: { context: 1000000, output: 128000 }, - }, - "mimo-v2-tts": { - id: "mimo-v2-tts", - name: "MiMo-V2-TTS", - family: "mimo", - attachment: false, - reasoning: false, - tool_call: false, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["audio"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 8000, output: 16000 }, - }, - "mimo-v2-omni": { - id: "mimo-v2-omni", - name: "MiMo-V2-Omni", - family: "mimo", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0 }, - limit: { context: 256000, output: 128000 }, - }, - }, - }, - zai: { - id: "zai", - env: ["ZHIPU_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.z.ai/api/paas/v4", - name: "Z.AI", - doc: "https://docs.z.ai/guides/overview/pricing", - models: { - "glm-5v-turbo": { - id: "glm-5v-turbo", - name: "glm-5v-turbo", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-04-01", - last_updated: "2026-04-01", - modalities: { input: ["text", "image", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.2, output: 4, cache_read: 0.24, cache_write: 0 }, - limit: { context: 200000, output: 131072 }, - }, - "glm-4.7": { - id: "glm-4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2, cache_read: 0.11, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "glm-5": { - id: "glm-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3.2, cache_read: 0.2, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "glm-4.7-flashx": { - id: "glm-4.7-flashx", - name: "GLM-4.7-FlashX", - family: "glm-flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-01-19", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.4, cache_read: 0.01, cache_write: 0 }, - limit: { context: 200000, output: 131072 }, - }, - "glm-5.1": { - id: "glm-5.1", - name: "GLM-5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-03-27", - last_updated: "2026-03-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.4, output: 4.4, cache_read: 0.26, cache_write: 0 }, - limit: { context: 200000, output: 131072 }, - }, - "glm-4.5": { - id: "glm-4.5", - name: "GLM-4.5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2, cache_read: 0.11, cache_write: 0 }, - limit: { context: 131072, output: 98304 }, - }, - "glm-4.5-air": { - id: "glm-4.5-air", - name: "GLM-4.5-Air", - family: "glm-air", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 1.1, cache_read: 0.03, cache_write: 0 }, - limit: { context: 131072, output: 98304 }, - }, - "glm-5-turbo": { - id: "glm-5-turbo", - name: "GLM-5-Turbo", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-03-16", - last_updated: "2026-03-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.2, output: 4, cache_read: 0.24, cache_write: 0 }, - limit: { context: 200000, output: 131072 }, - }, - "glm-4.5v": { - id: "glm-4.5v", - name: "GLM-4.5V", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-08-11", - last_updated: "2025-08-11", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 1.8 }, - limit: { context: 64000, output: 16384 }, - }, - "glm-4.6": { - id: "glm-4.6", - name: "GLM-4.6", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2, cache_read: 0.11, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "glm-4.6v": { - id: "glm-4.6v", - name: "GLM-4.6V", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-08", - last_updated: "2025-12-08", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.9 }, - limit: { context: 128000, output: 32768 }, - }, - "glm-4.5-flash": { - id: "glm-4.5-flash", - name: "GLM-4.5-Flash", - family: "glm-flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 131072, output: 98304 }, - }, - "glm-4.7-flash": { - id: "glm-4.7-flash", - name: "GLM-4.7-Flash", - family: "glm-flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-01-19", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 200000, output: 131072 }, - }, - }, - }, - opencode: { - id: "opencode", - env: ["OPENCODE_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://opencode.ai/zen/v1", - name: "OpenCode Zen", - doc: "https://opencode.ai/docs/zen", - models: { - "gpt-5.1-codex-max": { - id: "gpt-5.1-codex-max", - name: "GPT-5.1 Codex Max", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, input: 272000, output: 128000 }, - provider: { npm: "@ai-sdk/openai" }, - }, - "claude-haiku-4-5": { - id: "claude-haiku-4-5", - name: "Claude Haiku 4.5", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 200000, output: 64000 }, - provider: { npm: "@ai-sdk/anthropic" }, - }, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-10", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3, cache_read: 0.08 }, - limit: { context: 262144, output: 65536 }, - }, - "glm-4.7": { - id: "glm-4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2, cache_read: 0.1 }, - limit: { context: 204800, output: 131072 }, - status: "deprecated", - }, - "glm-5": { - id: "glm-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3.2, cache_read: 0.2 }, - limit: { context: 204800, output: 131072 }, - }, - "glm-4.7-free": { - id: "glm-4.7-free", - name: "GLM-4.7 Free", - family: "glm-free", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0 }, - limit: { context: 204800, output: 131072 }, - status: "deprecated", - }, - "gemini-3.1-pro": { - id: "gemini-3.1-pro", - name: "Gemini 3.1 Pro Preview", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-02-19", - last_updated: "2026-02-19", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2, context_over_200k: { input: 4, output: 18, cache_read: 0.4 } }, - limit: { context: 1048576, output: 65536 }, - provider: { npm: "@ai-sdk/google" }, - }, - "claude-sonnet-4-6": { - id: "claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2026-02-17", - last_updated: "2026-02-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 1000000, output: 64000 }, - provider: { npm: "@ai-sdk/anthropic" }, - }, - "kimi-k2.5-free": { - id: "kimi-k2.5-free", - name: "Kimi K2.5 Free", - family: "kimi-free", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-10", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0 }, - limit: { context: 262144, output: 262144 }, - status: "deprecated", - }, - "gpt-5-nano": { - id: "gpt-5-nano", - name: "GPT-5 Nano", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0 }, - limit: { context: 400000, input: 272000, output: 128000 }, - provider: { npm: "@ai-sdk/openai" }, - }, - "gpt-5.3-codex": { - id: "gpt-5.3-codex", - name: "GPT-5.3 Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-02-24", - last_updated: "2026-02-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, input: 272000, output: 128000 }, - provider: { npm: "@ai-sdk/openai" }, - }, - "minimax-m2.5-free": { - id: "minimax-m2.5-free", - name: "MiniMax M2.5 Free", - family: "minimax-free", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0 }, - limit: { context: 204800, output: 131072 }, - provider: { npm: "@ai-sdk/anthropic" }, - }, - "gpt-5.2": { - id: "gpt-5.2", - name: "GPT-5.2", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, input: 272000, output: 128000 }, - provider: { npm: "@ai-sdk/openai" }, - }, - "big-pickle": { - id: "big-pickle", - name: "Big Pickle", - family: "big-pickle", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-10-17", - last_updated: "2025-10-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 200000, output: 128000 }, - provider: { npm: "@ai-sdk/anthropic" }, - }, - "claude-opus-4-1": { - id: "claude-opus-4-1", - name: "Claude Opus 4.1", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - provider: { npm: "@ai-sdk/anthropic" }, - }, - "gpt-5.4-mini": { - id: "gpt-5.4-mini", - name: "GPT-5.4 Mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.75, output: 4.5, cache_read: 0.075 }, - limit: { context: 400000, input: 272000, output: 128000 }, - provider: { npm: "@ai-sdk/openai" }, - }, - "claude-3-5-haiku": { - id: "claude-3-5-haiku", - name: "Claude Haiku 3.5", - family: "claude-haiku", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07-31", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 4, cache_read: 0.08, cache_write: 1 }, - limit: { context: 200000, output: 8192 }, - provider: { npm: "@ai-sdk/anthropic" }, - }, - "minimax-m2.1": { - id: "minimax-m2.1", - name: "MiniMax M2.1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-01", - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.1 }, - limit: { context: 204800, output: 131072 }, - status: "deprecated", - }, - "glm-5.1": { - id: "glm-5.1", - name: "GLM-5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2026-04-07", - last_updated: "2026-04-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.4, output: 4.4, cache_read: 0.26 }, - limit: { context: 204800, output: 131072 }, - }, - "gpt-5.4-nano": { - id: "gpt-5.4-nano", - name: "GPT-5.4 Nano", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.25, cache_read: 0.02 }, - limit: { context: 400000, input: 272000, output: 128000 }, - provider: { npm: "@ai-sdk/openai" }, - }, - "gpt-5.2-codex": { - id: "gpt-5.2-codex", - name: "GPT-5.2 Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-01-14", - last_updated: "2026-01-14", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, input: 272000, output: 128000 }, - provider: { npm: "@ai-sdk/openai" }, - }, - "gpt-5.1-codex-mini": { - id: "gpt-5.1-codex-mini", - name: "GPT-5.1 Codex Mini", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.025 }, - limit: { context: 400000, input: 272000, output: 128000 }, - provider: { npm: "@ai-sdk/openai" }, - }, - "claude-sonnet-4": { - id: "claude-sonnet-4", - name: "Claude Sonnet 4", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 3, - output: 15, - cache_read: 0.3, - cache_write: 3.75, - context_over_200k: { input: 6, output: 22.5, cache_read: 0.6, cache_write: 7.5 }, - }, - limit: { context: 1000000, output: 64000 }, - provider: { npm: "@ai-sdk/anthropic" }, - }, - "gemini-3-flash": { - id: "gemini-3-flash", - name: "Gemini 3 Flash", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 3, cache_read: 0.05 }, - limit: { context: 1048576, output: 65536 }, - provider: { npm: "@ai-sdk/google" }, - }, - "trinity-large-preview-free": { - id: "trinity-large-preview-free", - name: "Trinity Large Preview", - family: "trinity", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2026-01-28", - last_updated: "2026-01-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 131072 }, - status: "deprecated", - }, - "gpt-5.1": { - id: "gpt-5.1", - name: "GPT-5.1", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.07, output: 8.5, cache_read: 0.107 }, - limit: { context: 400000, input: 272000, output: 128000 }, - provider: { npm: "@ai-sdk/openai" }, - }, - "gpt-5.4-pro": { - id: "gpt-5.4-pro", - name: "GPT-5.4 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 30, output: 180, cache_read: 30 }, - limit: { context: 1050000, input: 922000, output: 128000 }, - provider: { npm: "@ai-sdk/openai" }, - }, - "glm-5-free": { - id: "glm-5-free", - name: "GLM-5 Free", - family: "glm-free", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0 }, - limit: { context: 204800, output: 131072 }, - status: "deprecated", - }, - "claude-opus-4-5": { - id: "claude-opus-4-5", - name: "Claude Opus 4.5", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-24", - last_updated: "2025-11-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 64000 }, - provider: { npm: "@ai-sdk/anthropic" }, - }, - "minimax-m2.1-free": { - id: "minimax-m2.1-free", - name: "MiniMax M2.1 Free", - family: "minimax-free", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0 }, - limit: { context: 204800, output: 131072 }, - status: "deprecated", - provider: { npm: "@ai-sdk/anthropic" }, - }, - "qwen3.6-plus-free": { - id: "qwen3.6-plus-free", - name: "Qwen3.6 Plus Free", - family: "qwen-free", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-03-30", - last_updated: "2026-03-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0 }, - limit: { context: 1048576, output: 64000 }, - status: "deprecated", - }, - "glm-4.6": { - id: "glm-4.6", - name: "GLM-4.6", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2, cache_read: 0.1 }, - limit: { context: 204800, output: 131072 }, - status: "deprecated", - }, - "gemini-3-pro": { - id: "gemini-3-pro", - name: "Gemini 3 Pro", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-11-18", - last_updated: "2025-11-18", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2, context_over_200k: { input: 4, output: 18, cache_read: 0.4 } }, - limit: { context: 1048576, output: 65536 }, - status: "deprecated", - provider: { npm: "@ai-sdk/google" }, - }, - "gpt-5-codex": { - id: "gpt-5-codex", - name: "GPT-5 Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-09-15", - last_updated: "2025-09-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.07, output: 8.5, cache_read: 0.107 }, - limit: { context: 400000, input: 272000, output: 128000 }, - provider: { npm: "@ai-sdk/openai" }, - }, - "gpt-5.4": { - id: "gpt-5.4", - name: "GPT-5.4", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 15, cache_read: 0.25 }, - limit: { context: 1050000, input: 922000, output: 128000 }, - provider: { npm: "@ai-sdk/openai" }, - }, - "grok-code": { - id: "grok-code", - name: "Grok Code Fast 1", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-20", - last_updated: "2025-08-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 256000, output: 256000 }, - status: "deprecated", - }, - "mimo-v2-flash-free": { - id: "mimo-v2-flash-free", - name: "MiMo V2 Flash Free", - family: "mimo-flash-free", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2025-12-16", - last_updated: "2025-12-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0 }, - limit: { context: 262144, output: 65536 }, - status: "deprecated", - }, - "gpt-5.3-codex-spark": { - id: "gpt-5.3-codex-spark", - name: "GPT-5.3 Codex Spark", - family: "gpt-codex-spark", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 128000, input: 128000, output: 128000 }, - provider: { npm: "@ai-sdk/openai" }, - }, - "claude-opus-4-6": { - id: "claude-opus-4-6", - name: "Claude Opus 4.6", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 1000000, output: 128000 }, - provider: { npm: "@ai-sdk/anthropic" }, - }, - "minimax-m2.5": { - id: "minimax-m2.5", - name: "MiniMax M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-01", - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.06 }, - limit: { context: 204800, output: 131072 }, - }, - "kimi-k2": { - id: "kimi-k2", - name: "Kimi K2", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 2.5, cache_read: 0.4 }, - limit: { context: 262144, output: 262144 }, - status: "deprecated", - }, - "claude-sonnet-4-5": { - id: "claude-sonnet-4-5", - name: "Claude Sonnet 4.5", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 3, - output: 15, - cache_read: 0.3, - cache_write: 3.75, - context_over_200k: { input: 6, output: 22.5, cache_read: 0.6, cache_write: 7.5 }, - }, - limit: { context: 1000000, output: 64000 }, - provider: { npm: "@ai-sdk/anthropic" }, - }, - "qwen3-coder": { - id: "qwen3-coder", - name: "Qwen3 Coder", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.45, output: 1.8 }, - limit: { context: 262144, output: 65536 }, - status: "deprecated", - }, - "gpt-5": { - id: "gpt-5", - name: "GPT-5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.07, output: 8.5, cache_read: 0.107 }, - limit: { context: 400000, input: 272000, output: 128000 }, - provider: { npm: "@ai-sdk/openai" }, - }, - "mimo-v2-pro-free": { - id: "mimo-v2-pro-free", - name: "MiMo V2 Pro Free", - family: "mimo-pro-free", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0 }, - limit: { context: 1048576, output: 64000 }, - status: "deprecated", - }, - "nemotron-3-super-free": { - id: "nemotron-3-super-free", - name: "Nemotron 3 Super Free", - family: "nemotron-free", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2026-02", - release_date: "2026-03-11", - last_updated: "2026-03-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0 }, - limit: { context: 204800, output: 128000 }, - }, - "kimi-k2-thinking": { - id: "kimi-k2-thinking", - name: "Kimi K2 Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-10", - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 2.5, cache_read: 0.4 }, - limit: { context: 262144, output: 262144 }, - status: "deprecated", - }, - "gpt-5.1-codex": { - id: "gpt-5.1-codex", - name: "GPT-5.1 Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.07, output: 8.5, cache_read: 0.107 }, - limit: { context: 400000, input: 272000, output: 128000 }, - provider: { npm: "@ai-sdk/openai" }, - }, - "mimo-v2-omni-free": { - id: "mimo-v2-omni-free", - name: "MiMo V2 Omni Free", - family: "mimo-omni-free", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text", "image", "audio", "pdf"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0 }, - limit: { context: 262144, output: 64000 }, - status: "deprecated", - }, - }, - }, - stepfun: { - id: "stepfun", - env: ["STEPFUN_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.stepfun.com/v1", - name: "StepFun", - doc: "https://platform.stepfun.com/docs/zh/overview/concept", - models: { - "step-3.5-flash-2603": { - id: "step-3.5-flash-2603", - name: "Step 3.5 Flash 2603", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-04-02", - last_updated: "2026-04-02", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3, cache_read: 0.02 }, - limit: { context: 256000, input: 256000, output: 256000 }, - }, - "step-1-32k": { - id: "step-1-32k", - name: "Step 1 (32K)", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-06", - release_date: "2025-01-01", - last_updated: "2026-02-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.05, output: 9.59, cache_read: 0.41 }, - limit: { context: 32768, input: 32768, output: 32768 }, - }, - "step-3.5-flash": { - id: "step-3.5-flash", - name: "Step 3.5 Flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-01-29", - last_updated: "2026-02-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.096, output: 0.288, cache_read: 0.019 }, - limit: { context: 256000, input: 256000, output: 256000 }, - }, - "step-2-16k": { - id: "step-2-16k", - name: "Step 2 (16K)", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-06", - release_date: "2025-01-01", - last_updated: "2026-02-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 5.21, output: 16.44, cache_read: 1.04 }, - limit: { context: 16384, input: 16384, output: 8192 }, - }, - }, - }, - nebius: { - id: "nebius", - env: ["NEBIUS_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.tokenfactory.nebius.com/v1", - name: "Nebius Token Factory", - doc: "https://docs.tokenfactory.nebius.com/", - models: { - "NousResearch/Hermes-4-70B": { - id: "NousResearch/Hermes-4-70B", - name: "Hermes-4-70B", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-11", - release_date: "2026-01-30", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.13, output: 0.4, reasoning: 0.4, cache_read: 0.013, cache_write: 0.16 }, - limit: { context: 128000, input: 120000, output: 8192 }, - }, - "NousResearch/Hermes-4-405B": { - id: "NousResearch/Hermes-4-405B", - name: "Hermes-4-405B", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-11", - release_date: "2026-01-30", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3, reasoning: 3, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 128000, input: 120000, output: 8192 }, - }, - "black-forest-labs/flux-schnell": { - id: "black-forest-labs/flux-schnell", - name: "FLUX.1-schnell", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: false, - knowledge: "2024-07", - release_date: "2024-08-01", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["image"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 77, input: 77, output: 0 }, - }, - "black-forest-labs/flux-dev": { - id: "black-forest-labs/flux-dev", - name: "FLUX.1-dev", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: false, - knowledge: "2024-07", - release_date: "2024-08-01", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["image"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 77, input: 77, output: 0 }, - }, - "Qwen/Qwen2.5-VL-72B-Instruct": { - id: "Qwen/Qwen2.5-VL-72B-Instruct", - name: "Qwen2.5-VL-72B-Instruct", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-01-20", - last_updated: "2026-02-04", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 0.75, cache_read: 0.025, cache_write: 0.31 }, - limit: { context: 128000, input: 120000, output: 8192 }, - }, - "Qwen/Qwen3-30B-A3B-Thinking-2507": { - id: "Qwen/Qwen3-30B-A3B-Thinking-2507", - name: "Qwen3-30B-A3B-Thinking-2507", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-12", - release_date: "2026-01-28", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3, reasoning: 0.3, cache_read: 0.01, cache_write: 0.125 }, - limit: { context: 128000, input: 120000, output: 16384 }, - }, - "Qwen/Qwen3-32B-fast": { - id: "Qwen/Qwen3-32B-fast", - name: "Qwen3-32B (Fast)", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-12", - release_date: "2026-01-28", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.6, cache_read: 0.02, cache_write: 0.25 }, - limit: { context: 128000, input: 120000, output: 8192 }, - }, - "Qwen/Qwen3-Embedding-8B": { - id: "Qwen/Qwen3-Embedding-8B", - name: "Qwen3-Embedding-8B", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: false, - knowledge: "2025-10", - release_date: "2026-01-10", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.01, output: 0 }, - limit: { context: 32768, input: 32768, output: 0 }, - }, - "Qwen/Qwen3-30B-A3B-Instruct-2507": { - id: "Qwen/Qwen3-30B-A3B-Instruct-2507", - name: "Qwen3-30B-A3B-Instruct-2507", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-12", - release_date: "2026-01-28", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3, cache_read: 0.01, cache_write: 0.125 }, - limit: { context: 128000, input: 120000, output: 8192 }, - }, - "Qwen/Qwen3-235B-A22B-Instruct-2507": { - id: "Qwen/Qwen3-235B-A22B-Instruct-2507", - name: "Qwen3 235B A22B Instruct 2507", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-07-25", - last_updated: "2025-10-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.6 }, - limit: { context: 262144, output: 8192 }, - }, - "Qwen/Qwen3-32B": { - id: "Qwen/Qwen3-32B", - name: "Qwen3-32B", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-12", - release_date: "2026-01-28", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3, cache_read: 0.01, cache_write: 0.125 }, - limit: { context: 128000, input: 120000, output: 8192 }, - }, - "Qwen/Qwen3-Coder-30B-A3B-Instruct": { - id: "Qwen/Qwen3-Coder-30B-A3B-Instruct", - name: "Qwen3-Coder-30B-A3B-Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-12", - release_date: "2026-01-28", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3, cache_read: 0.01, cache_write: 0.125 }, - limit: { context: 128000, input: 120000, output: 8192 }, - }, - "Qwen/Qwen3-235B-A22B-Thinking-2507": { - id: "Qwen/Qwen3-235B-A22B-Thinking-2507", - name: "Qwen3 235B A22B Thinking 2507", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-07-25", - last_updated: "2025-10-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.8 }, - limit: { context: 262144, output: 8192 }, - }, - "Qwen/Qwen3-Next-80B-A3B-Thinking": { - id: "Qwen/Qwen3-Next-80B-A3B-Thinking", - name: "Qwen3-Next-80B-A3B-Thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-12", - release_date: "2026-01-28", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 1.2, reasoning: 1.2, cache_read: 0.015, cache_write: 0.18 }, - limit: { context: 128000, input: 120000, output: 16384 }, - }, - "Qwen/Qwen3-Coder-480B-A35B-Instruct": { - id: "Qwen/Qwen3-Coder-480B-A35B-Instruct", - name: "Qwen3 Coder 480B A35B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2025-10-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.8 }, - limit: { context: 262144, output: 66536 }, - }, - "Qwen/Qwen2.5-Coder-7B-fast": { - id: "Qwen/Qwen2.5-Coder-7B-fast", - name: "Qwen2.5-Coder-7B (Fast)", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-09", - release_date: "2024-09-19", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.09, cache_read: 0.003, cache_write: 0.03 }, - limit: { context: 128000, input: 120000, output: 8192 }, - }, - "PrimeIntellect/INTELLECT-3": { - id: "PrimeIntellect/INTELLECT-3", - name: "INTELLECT-3", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-10", - release_date: "2026-01-25", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 1.1, cache_read: 0.02, cache_write: 0.25 }, - limit: { context: 128000, input: 120000, output: 8192 }, - }, - "zai-org/GLM-4.5": { - id: "zai-org/GLM-4.5", - name: "GLM-4.5", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-11-15", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 2.2, cache_read: 0.06, cache_write: 0.75 }, - limit: { context: 128000, input: 124000, output: 4096 }, - }, - "zai-org/GLM-4.5-Air": { - id: "zai-org/GLM-4.5-Air", - name: "GLM-4.5-Air", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-11-15", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.2, cache_read: 0.02, cache_write: 0.25 }, - limit: { context: 128000, input: 124000, output: 4096 }, - }, - "zai-org/GLM-4.7-FP8": { - id: "zai-org/GLM-4.7-FP8", - name: "GLM-4.7 (FP8)", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-12", - release_date: "2026-01-15", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2, cache_read: 0.04, cache_write: 0.5 }, - limit: { context: 128000, input: 124000, output: 4096 }, - }, - "zai-org/GLM-5": { - id: "zai-org/GLM-5", - name: "GLM-5", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2026-01", - release_date: "2026-03-01", - last_updated: "2026-03-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 3.2, cache_read: 0.1, cache_write: 1 }, - limit: { context: 200000, input: 200000, output: 16384 }, - }, - "BAAI/bge-en-icl": { - id: "BAAI/bge-en-icl", - name: "BGE-ICL", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: false, - knowledge: "2024-06", - release_date: "2024-07-30", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.01, output: 0 }, - limit: { context: 32768, input: 32768, output: 0 }, - }, - "BAAI/bge-multilingual-gemma2": { - id: "BAAI/bge-multilingual-gemma2", - name: "bge-multilingual-gemma2", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: false, - knowledge: "2024-06", - release_date: "2024-07-30", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.01, output: 0 }, - limit: { context: 8192, input: 8192, output: 0 }, - }, - "meta-llama/Llama-3.3-70B-Instruct": { - id: "meta-llama/Llama-3.3-70B-Instruct", - name: "Llama-3.3-70B-Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-08", - release_date: "2025-12-05", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.13, output: 0.4, cache_read: 0.013, cache_write: 0.16 }, - limit: { context: 128000, input: 120000, output: 8192 }, - }, - "meta-llama/Meta-Llama-3.1-8B-Instruct-fast": { - id: "meta-llama/Meta-Llama-3.1-8B-Instruct-fast", - name: "Meta-Llama-3.1-8B-Instruct (Fast)", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-12", - release_date: "2024-07-23", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.09, cache_read: 0.003, cache_write: 0.03 }, - limit: { context: 128000, input: 120000, output: 4096 }, - }, - "meta-llama/Llama-3.3-70B-Instruct-fast": { - id: "meta-llama/Llama-3.3-70B-Instruct-fast", - name: "Llama-3.3-70B-Instruct (Fast)", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-08", - release_date: "2025-12-05", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 0.75, cache_read: 0.025, cache_write: 0.31 }, - limit: { context: 128000, input: 120000, output: 8192 }, - }, - "meta-llama/Llama-Guard-3-8B": { - id: "meta-llama/Llama-Guard-3-8B", - name: "Llama-Guard-3-8B", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: false, - knowledge: "2024-04", - release_date: "2024-04-18", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0.06, cache_read: 0.002, cache_write: 0.025 }, - limit: { context: 8192, input: 8000, output: 1024 }, - }, - "meta-llama/Meta-Llama-3.1-8B-Instruct": { - id: "meta-llama/Meta-Llama-3.1-8B-Instruct", - name: "Meta-Llama-3.1-8B-Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-12", - release_date: "2024-07-23", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0.06, cache_read: 0.002, cache_write: 0.025 }, - limit: { context: 128000, input: 120000, output: 4096 }, - }, - "nvidia/Nemotron-Nano-V2-12b": { - id: "nvidia/Nemotron-Nano-V2-12b", - name: "Nemotron-Nano-V2-12b", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-03-15", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.2, cache_read: 0.007, cache_write: 0.08 }, - limit: { context: 32000, input: 30000, output: 4096 }, - }, - "nvidia/nemotron-3-super-120b-a12b": { - id: "nvidia/nemotron-3-super-120b-a12b", - name: "Nemotron-3-Super-120B-A12B", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2026-02", - release_date: "2026-03-11", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.9 }, - limit: { context: 256000, input: 256000, output: 32768 }, - }, - "nvidia/Llama-3_1-Nemotron-Ultra-253B-v1": { - id: "nvidia/Llama-3_1-Nemotron-Ultra-253B-v1", - name: "Llama-3.1-Nemotron-Ultra-253B-v1", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-01-15", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 1.8, cache_read: 0.06, cache_write: 0.75 }, - limit: { context: 128000, input: 120000, output: 4096 }, - }, - "nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B": { - id: "nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B", - name: "Nemotron-3-Nano-30B-A3B", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2025-08-10", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.06, output: 0.24, cache_read: 0.006, cache_write: 0.075 }, - limit: { context: 32000, input: 30000, output: 4096 }, - }, - "deepseek-ai/DeepSeek-V3-0324": { - id: "deepseek-ai/DeepSeek-V3-0324", - name: "DeepSeek-V3-0324", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-03-24", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 1.5, cache_read: 0.05, cache_write: 0.1875 }, - limit: { context: 128000, input: 120000, output: 8192 }, - }, - "deepseek-ai/DeepSeek-V3-0324-fast": { - id: "deepseek-ai/DeepSeek-V3-0324-fast", - name: "DeepSeek-V3-0324 (Fast)", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-03-24", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.75, output: 2.25, cache_read: 0.075, cache_write: 0.28125 }, - limit: { context: 128000, input: 120000, output: 8192 }, - }, - "deepseek-ai/DeepSeek-R1-0528-fast": { - id: "deepseek-ai/DeepSeek-R1-0528-fast", - name: "DeepSeek R1 0528 Fast", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-01-01", - last_updated: "2025-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2, output: 6 }, - limit: { context: 131072, output: 8192 }, - }, - "deepseek-ai/DeepSeek-R1-0528": { - id: "deepseek-ai/DeepSeek-R1-0528", - name: "DeepSeek-R1-0528", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-11", - release_date: "2026-01-15", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.8, output: 2.4, reasoning: 2.4, cache_read: 0.08, cache_write: 1 }, - limit: { context: 128000, input: 120000, output: 32768 }, - }, - "deepseek-ai/DeepSeek-V3.2": { - id: "deepseek-ai/DeepSeek-V3.2", - name: "DeepSeek-V3.2", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-11", - release_date: "2026-01-20", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.45, reasoning: 0.45, cache_read: 0.03, cache_write: 0.375 }, - limit: { context: 163000, input: 160000, output: 16384 }, - }, - "openai/gpt-oss-20b": { - id: "openai/gpt-oss-20b", - name: "gpt-oss-20b", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-09", - release_date: "2026-01-10", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.2, cache_read: 0.005, cache_write: 0.06 }, - limit: { context: 128000, input: 124000, output: 4096 }, - }, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "gpt-oss-120b", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-09", - release_date: "2026-01-10", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6, reasoning: 0.6, cache_read: 0.015, cache_write: 0.18 }, - limit: { context: 128000, input: 124000, output: 8192 }, - }, - "google/gemma-2-2b-it": { - id: "google/gemma-2-2b-it", - name: "Gemma-2-2b-it", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - knowledge: "2024-06", - release_date: "2024-07-31", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0.06, cache_read: 0.002, cache_write: 0.025 }, - limit: { context: 8192, input: 8000, output: 4096 }, - }, - "google/gemma-2-9b-it-fast": { - id: "google/gemma-2-9b-it-fast", - name: "Gemma-2-9b-it (Fast)", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - knowledge: "2024-06", - release_date: "2024-06-27", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.09, cache_read: 0.003, cache_write: 0.0375 }, - limit: { context: 8192, input: 8000, output: 4096 }, - }, - "google/gemma-3-27b-it-fast": { - id: "google/gemma-3-27b-it-fast", - name: "Gemma-3-27b-it (Fast)", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-10", - release_date: "2026-01-20", - last_updated: "2026-02-04", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.6, cache_read: 0.02, cache_write: 0.25 }, - limit: { context: 110000, input: 100000, output: 8192 }, - }, - "google/gemma-3-27b-it": { - id: "google/gemma-3-27b-it", - name: "Gemma-3-27b-it", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-10", - release_date: "2026-01-20", - last_updated: "2026-02-04", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3, cache_read: 0.01, cache_write: 0.125 }, - limit: { context: 110000, input: 100000, output: 8192 }, - }, - "moonshotai/Kimi-K2-Thinking": { - id: "moonshotai/Kimi-K2-Thinking", - name: "Kimi-K2-Thinking", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-10", - release_date: "2026-01-05", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.5, reasoning: 2.5, cache_read: 0.06, cache_write: 0.75 }, - limit: { context: 128000, input: 120000, output: 16384 }, - }, - "moonshotai/Kimi-K2-Instruct": { - id: "moonshotai/Kimi-K2-Instruct", - name: "Kimi-K2-Instruct", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-10", - release_date: "2026-01-05", - last_updated: "2026-02-04", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 2.4, cache_read: 0.05, cache_write: 0.625 }, - limit: { context: 200000, input: 190000, output: 8192 }, - }, - "moonshotai/Kimi-K2.5-fast": { - id: "moonshotai/Kimi-K2.5-fast", - name: "Kimi-K2.5-fast", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-12-15", - last_updated: "2026-02-04", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 2.5, cache_read: 0.05, cache_write: 0.625 }, - limit: { context: 256000, input: 256000, output: 8192 }, - }, - "moonshotai/Kimi-K2.5": { - id: "moonshotai/Kimi-K2.5", - name: "Kimi-K2.5", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-12-15", - last_updated: "2026-02-04", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 2.5, reasoning: 2.5, cache_read: 0.05, cache_write: 0.625 }, - limit: { context: 256000, input: 256000, output: 8192 }, - }, - "MiniMaxAI/MiniMax-M2.1": { - id: "MiniMaxAI/MiniMax-M2.1", - name: "MiniMax-M2.1", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-10", - release_date: "2026-02-01", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, reasoning: 1.2, cache_read: 0.03, cache_write: 0.375 }, - limit: { context: 128000, input: 120000, output: 8192 }, - }, - "intfloat/e5-mistral-7b-instruct": { - id: "intfloat/e5-mistral-7b-instruct", - name: "e5-mistral-7b-instruct", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: false, - knowledge: "2023-12", - release_date: "2024-01-01", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.01, output: 0 }, - limit: { context: 32768, input: 32768, output: 0 }, - }, - }, - }, - poe: { - id: "poe", - env: ["POE_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.poe.com/v1", - name: "Poe", - doc: "https://creator.poe.com/docs/external-applications/openai-compatible-api", - models: { - "topazlabs-co/topazlabs": { - id: "topazlabs-co/topazlabs", - name: "TopazLabs", - family: "topazlabs", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-12-03", - last_updated: "2024-12-03", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 204, output: 0 }, - }, - "novita/kimi-k2.5": { - id: "novita/kimi-k2.5", - name: "kimi-k2.5", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 256000, output: 262144 }, - }, - "novita/glm-4.7": { - id: "novita/glm-4.7", - name: "glm-4.7", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 205000, output: 131072 }, - }, - "novita/glm-5": { - id: "novita/glm-5", - name: "glm-5", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 205000, output: 131072 }, - }, - "novita/minimax-m2.1": { - id: "novita/minimax-m2.1", - name: "minimax-m2.1", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-12-26", - last_updated: "2025-12-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 205000, output: 131072 }, - }, - "novita/glm-4.6": { - id: "novita/glm-4.6", - name: "GLM-4.6", - family: "glm", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 0, output: 0 }, - }, - "novita/glm-4.6v": { - id: "novita/glm-4.6v", - name: "glm-4.6v", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-12-09", - last_updated: "2025-12-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - limit: { context: 131000, output: 32768 }, - }, - "novita/deepseek-v3.2": { - id: "novita/deepseek-v3.2", - name: "DeepSeek-V3.2", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 0.4, cache_read: 0.13 }, - limit: { context: 128000, output: 0 }, - }, - "novita/glm-4.7-flash": { - id: "novita/glm-4.7-flash", - name: "glm-4.7-flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2026-01-19", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 200000, output: 65500 }, - }, - "novita/glm-4.7-n": { - id: "novita/glm-4.7-n", - name: "glm-4.7-n", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 205000, output: 131072 }, - }, - "novita/kimi-k2-thinking": { - id: "novita/kimi-k2-thinking", - name: "kimi-k2-thinking", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-11-07", - last_updated: "2025-11-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 256000, output: 0 }, - }, - "fireworks-ai/kimi-k2.5-fw": { - id: "fireworks-ai/kimi-k2.5-fw", - name: "Kimi-K2.5-FW", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 262144, input: 245760, output: 16384 }, - }, - "elevenlabs/elevenlabs-v2.5-turbo": { - id: "elevenlabs/elevenlabs-v2.5-turbo", - name: "ElevenLabs-v2.5-Turbo", - family: "elevenlabs", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-10-28", - last_updated: "2024-10-28", - modalities: { input: ["text"], output: ["audio"] }, - open_weights: false, - limit: { context: 128000, output: 0 }, - }, - "elevenlabs/elevenlabs-v3": { - id: "elevenlabs/elevenlabs-v3", - name: "ElevenLabs-v3", - family: "elevenlabs", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-06-05", - last_updated: "2025-06-05", - modalities: { input: ["text"], output: ["audio"] }, - open_weights: false, - limit: { context: 128000, output: 0 }, - }, - "elevenlabs/elevenlabs-music": { - id: "elevenlabs/elevenlabs-music", - name: "ElevenLabs-Music", - family: "elevenlabs", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-08-29", - last_updated: "2025-08-29", - modalities: { input: ["text"], output: ["audio"] }, - open_weights: false, - limit: { context: 2000, output: 0 }, - }, - "cerebras/gpt-oss-120b-cs": { - id: "cerebras/gpt-oss-120b-cs", - name: "gpt-oss-120b-cs", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-08-06", - last_updated: "2025-08-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 0, output: 0 }, - }, - "cerebras/llama-3.1-8b-cs": { - id: "cerebras/llama-3.1-8b-cs", - name: "llama-3.1-8b-cs", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-05-13", - last_updated: "2025-05-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 0, output: 0 }, - }, - "cerebras/qwen3-32b-cs": { - id: "cerebras/qwen3-32b-cs", - name: "qwen3-32b-cs", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-05-15", - last_updated: "2025-05-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 0, output: 0 }, - }, - "cerebras/qwen3-235b-2507-cs": { - id: "cerebras/qwen3-235b-2507-cs", - name: "qwen3-235b-2507-cs", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-08-06", - last_updated: "2025-08-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 0, output: 0 }, - }, - "cerebras/llama-3.3-70b-cs": { - id: "cerebras/llama-3.3-70b-cs", - name: "llama-3.3-70b-cs", - attachment: true, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-05-13", - last_updated: "2025-05-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 0, output: 0 }, - }, - "stabilityai/stablediffusionxl": { - id: "stabilityai/stablediffusionxl", - name: "StableDiffusionXL", - family: "stable-diffusion", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2023-07-09", - last_updated: "2023-07-09", - modalities: { input: ["text", "image"], output: ["image"] }, - open_weights: false, - limit: { context: 200, output: 0 }, - }, - "xai/grok-code-fast-1": { - id: "xai/grok-code-fast-1", - name: "Grok Code Fast 1", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-08-22", - last_updated: "2025-08-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.5, cache_read: 0.02 }, - limit: { context: 256000, output: 128000 }, - }, - "xai/grok-4-fast-reasoning": { - id: "xai/grok-4-fast-reasoning", - name: "Grok-4-Fast-Reasoning", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-09-16", - last_updated: "2025-09-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 128000 }, - }, - "xai/grok-4.1-fast-non-reasoning": { - id: "xai/grok-4.1-fast-non-reasoning", - name: "Grok-4.1-Fast-Non-Reasoning", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-11-19", - last_updated: "2025-11-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - limit: { context: 2000000, output: 30000 }, - }, - "xai/grok-4": { - id: "xai/grok-4", - name: "Grok-4", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-07-10", - last_updated: "2025-07-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.75 }, - limit: { context: 256000, output: 128000 }, - }, - "xai/grok-3-mini": { - id: "xai/grok-3-mini", - name: "Grok 3 Mini", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-04-11", - last_updated: "2025-04-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.5, cache_read: 0.075 }, - limit: { context: 131072, output: 8192 }, - }, - "xai/grok-4.20-multi-agent": { - id: "xai/grok-4.20-multi-agent", - name: "Grok-4.20-Multi-Agent", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2026-03-13", - last_updated: "2026-03-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6, cache_read: 0.2 }, - limit: { context: 128000, output: 0 }, - }, - "xai/grok-3": { - id: "xai/grok-3", - name: "Grok 3", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-04-11", - last_updated: "2025-04-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.75 }, - limit: { context: 131072, output: 8192 }, - }, - "xai/grok-4-fast-non-reasoning": { - id: "xai/grok-4-fast-non-reasoning", - name: "Grok-4-Fast-Non-Reasoning", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-09-16", - last_updated: "2025-09-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 128000 }, - }, - "xai/grok-4.1-fast-reasoning": { - id: "xai/grok-4.1-fast-reasoning", - name: "Grok-4.1-Fast-Reasoning", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-11-19", - last_updated: "2025-11-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - limit: { context: 2000000, output: 30000 }, - }, - "runwayml/runway": { - id: "runwayml/runway", - name: "Runway", - family: "runway", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-10-11", - last_updated: "2024-10-11", - modalities: { input: ["text", "image"], output: ["video"] }, - open_weights: false, - limit: { context: 256, output: 0 }, - }, - "runwayml/runway-gen-4-turbo": { - id: "runwayml/runway-gen-4-turbo", - name: "Runway-Gen-4-Turbo", - family: "runway", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-05-09", - last_updated: "2025-05-09", - modalities: { input: ["text", "image"], output: ["video"] }, - open_weights: false, - limit: { context: 256, output: 0 }, - }, - "openai/gpt-5.1-codex-max": { - id: "openai/gpt-5.1-codex-max", - name: "GPT 5.1 Codex Max", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-12-08", - last_updated: "2025-12-08", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 9, cache_read: 0.11 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/sora-2-pro": { - id: "openai/sora-2-pro", - name: "Sora-2-Pro", - family: "sora", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-10-06", - last_updated: "2025-10-06", - modalities: { input: ["text", "image"], output: ["video"] }, - open_weights: false, - limit: { context: 0, output: 0 }, - }, - "openai/chatgpt-4o-latest": { - id: "openai/chatgpt-4o-latest", - name: "ChatGPT-4o-Latest", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-08-14", - last_updated: "2024-08-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 4.5, output: 14 }, - limit: { context: 128000, output: 8192 }, - }, - "openai/gpt-5-chat": { - id: "openai/gpt-5-chat", - name: "GPT-5-Chat", - family: "gpt-codex", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 9, cache_read: 0.11 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-5.2-pro": { - id: "openai/gpt-5.2-pro", - name: "GPT-5.2-Pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 19, output: 150 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-4o-aug": { - id: "openai/gpt-4o-aug", - name: "GPT-4o-Aug", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-11-21", - last_updated: "2024-11-21", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.2, output: 9, cache_read: 1.1 }, - limit: { context: 128000, output: 8192 }, - }, - "openai/gpt-4-classic-0314": { - id: "openai/gpt-4-classic-0314", - name: "GPT-4-Classic-0314", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-08-26", - last_updated: "2024-08-26", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 27, output: 54 }, - limit: { context: 8192, output: 4096 }, - }, - "openai/gpt-5-mini": { - id: "openai/gpt-5-mini", - name: "GPT-5-mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-06-25", - last_updated: "2025-06-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.22, output: 1.8, cache_read: 0.022 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5-nano": { - id: "openai/gpt-5-nano", - name: "GPT-5-nano", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.045, output: 0.36, cache_read: 0.0045 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.3-codex": { - id: "openai/gpt-5.3-codex", - name: "GPT-5.3-Codex", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2026-02-10", - last_updated: "2026-02-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.6, output: 13, cache_read: 0.16 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-4-turbo": { - id: "openai/gpt-4-turbo", - name: "GPT-4-Turbo", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2023-09-13", - last_updated: "2023-09-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 9, output: 27 }, - limit: { context: 128000, output: 4096 }, - }, - "openai/gpt-5.2": { - id: "openai/gpt-5.2", - name: "GPT-5.2", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-12-08", - last_updated: "2025-12-08", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.6, output: 13, cache_read: 0.16 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/o3-pro": { - id: "openai/o3-pro", - name: "o3-pro", - family: "o-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-06-10", - last_updated: "2025-06-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 18, output: 72 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/o3-mini-high": { - id: "openai/o3-mini-high", - name: "o3-mini-high", - family: "o-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-01-31", - last_updated: "2025-01-31", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.99, output: 4 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-4o-mini": { - id: "openai/gpt-4o-mini", - name: "GPT-4o-mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.54, cache_read: 0.068 }, - limit: { context: 124096, output: 4096 }, - }, - "openai/o4-mini-deep-research": { - id: "openai/o4-mini-deep-research", - name: "o4-mini-deep-research", - family: "o-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-06-27", - last_updated: "2025-06-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.8, output: 7.2, cache_read: 0.45 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-5.4-mini": { - id: "openai/gpt-5.4-mini", - name: "GPT-5.4-Mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2026-03-12", - last_updated: "2026-03-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.68, output: 4, cache_read: 0.068 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "openai/dall-e-3": { - id: "openai/dall-e-3", - name: "DALL-E-3", - family: "dall-e", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2023-11-06", - last_updated: "2023-11-06", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 800, output: 0 }, - }, - "openai/o4-mini": { - id: "openai/o4-mini", - name: "o4-mini", - family: "o-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.99, output: 4, cache_read: 0.25 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-5.4-nano": { - id: "openai/gpt-5.4-nano", - name: "GPT-5.4-Nano", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2026-03-11", - last_updated: "2026-03-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.18, output: 1.1, cache_read: 0.018 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "openai/gpt-image-1": { - id: "openai/gpt-image-1", - name: "GPT-Image-1", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-03-31", - last_updated: "2025-03-31", - modalities: { input: ["text", "image"], output: ["image"] }, - open_weights: false, - limit: { context: 128000, output: 0 }, - }, - "openai/gpt-5.2-codex": { - id: "openai/gpt-5.2-codex", - name: "GPT-5.2-Codex", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2026-01-14", - last_updated: "2026-01-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.6, output: 13, cache_read: 0.16 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.1-codex-mini": { - id: "openai/gpt-5.1-codex-mini", - name: "GPT-5.1-Codex-Mini", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-11-12", - last_updated: "2025-11-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.22, output: 1.8, cache_read: 0.022 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.1": { - id: "openai/gpt-5.1", - name: "GPT-5.1", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-11-12", - last_updated: "2025-11-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 9, cache_read: 0.11 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-image-1-mini": { - id: "openai/gpt-image-1-mini", - name: "GPT-Image-1-Mini", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-08-26", - last_updated: "2025-08-26", - modalities: { input: ["text", "image"], output: ["image"] }, - open_weights: false, - limit: { context: 0, output: 0 }, - }, - "openai/o1": { - id: "openai/o1", - name: "o1", - family: "o", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2024-12-18", - last_updated: "2024-12-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 14, output: 54 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-5.4-pro": { - id: "openai/gpt-5.4-pro", - name: "GPT-5.4-Pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image"], output: ["image"] }, - open_weights: false, - cost: { input: 27, output: 160 }, - limit: { context: 1050000, input: 922000, output: 128000 }, - }, - "openai/gpt-3.5-turbo": { - id: "openai/gpt-3.5-turbo", - name: "GPT-3.5-Turbo", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2023-09-13", - last_updated: "2023-09-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.45, output: 1.4 }, - limit: { context: 16384, output: 2048 }, - }, - "openai/o3-deep-research": { - id: "openai/o3-deep-research", - name: "o3-deep-research", - family: "o", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-06-27", - last_updated: "2025-06-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 9, output: 36, cache_read: 2.2 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/o3-mini": { - id: "openai/o3-mini", - name: "o3-mini", - family: "o-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-01-31", - last_updated: "2025-01-31", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.99, output: 4 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/o1-pro": { - id: "openai/o1-pro", - name: "o1-pro", - family: "o-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-03-19", - last_updated: "2025-03-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 140, output: 540 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-4o-search": { - id: "openai/gpt-4o-search", - name: "GPT-4o-Search", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-03-11", - last_updated: "2025-03-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.2, output: 9 }, - limit: { context: 128000, output: 8192 }, - }, - "openai/gpt-5-codex": { - id: "openai/gpt-5-codex", - name: "GPT-5-Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-09-23", - last_updated: "2025-09-23", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 9 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.4": { - id: "openai/gpt-5.4", - name: "GPT-5.4", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2026-02-26", - last_updated: "2026-02-26", - modalities: { input: ["text", "image", "pdf"], output: ["image"] }, - open_weights: false, - cost: { input: 2.2, output: 14, cache_read: 0.22 }, - limit: { context: 1050000, input: 922000, output: 128000 }, - }, - "openai/gpt-5.3-codex-spark": { - id: "openai/gpt-5.3-codex-spark", - name: "GPT-5.3-Codex-Spark", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2026-03-04", - last_updated: "2026-03-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-3.5-turbo-raw": { - id: "openai/gpt-3.5-turbo-raw", - name: "GPT-3.5-Turbo-Raw", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2023-09-27", - last_updated: "2023-09-27", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.45, output: 1.4 }, - limit: { context: 4524, output: 2048 }, - }, - "openai/gpt-4.1-nano": { - id: "openai/gpt-4.1-nano", - name: "GPT-4.1-nano", - family: "gpt-nano", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-04-15", - last_updated: "2025-04-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.09, output: 0.36, cache_read: 0.022 }, - limit: { context: 1047576, output: 32768 }, - }, - "openai/o3": { - id: "openai/o3", - name: "o3", - family: "o", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.8, output: 7.2, cache_read: 0.45 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-5-pro": { - id: "openai/gpt-5-pro", - name: "GPT-5-Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-10-06", - last_updated: "2025-10-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 14, output: 110 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/sora-2": { - id: "openai/sora-2", - name: "Sora-2", - family: "sora", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-10-06", - last_updated: "2025-10-06", - modalities: { input: ["text", "image"], output: ["video"] }, - open_weights: false, - limit: { context: 0, output: 0 }, - }, - "openai/gpt-4o": { - id: "openai/gpt-4o", - name: "GPT-4o", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-05-13", - last_updated: "2024-05-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 8192 }, - }, - "openai/gpt-5": { - id: "openai/gpt-5", - name: "GPT-5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 9, cache_read: 0.11 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.2-instant": { - id: "openai/gpt-5.2-instant", - name: "GPT-5.2-Instant", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.6, output: 13, cache_read: 0.16 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-4o-mini-search": { - id: "openai/gpt-4o-mini-search", - name: "GPT-4o-mini-Search", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-03-11", - last_updated: "2025-03-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.54 }, - limit: { context: 128000, output: 8192 }, - }, - "openai/gpt-image-1.5": { - id: "openai/gpt-image-1.5", - name: "gpt-image-1.5", - attachment: true, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-12-16", - last_updated: "2025-12-16", - modalities: { input: ["text", "image"], output: ["image"] }, - open_weights: false, - limit: { context: 128000, output: 0 }, - }, - "openai/gpt-3.5-turbo-instruct": { - id: "openai/gpt-3.5-turbo-instruct", - name: "GPT-3.5-Turbo-Instruct", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2023-09-20", - last_updated: "2023-09-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.4, output: 1.8 }, - limit: { context: 3500, output: 1024 }, - }, - "openai/gpt-4.1": { - id: "openai/gpt-4.1", - name: "GPT-4.1", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.8, output: 7.2, cache_read: 0.45 }, - limit: { context: 1047576, output: 32768 }, - }, - "openai/gpt-5.1-instant": { - id: "openai/gpt-5.1-instant", - name: "GPT-5.1-Instant", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-11-12", - last_updated: "2025-11-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 9, cache_read: 0.11 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-4.1-mini": { - id: "openai/gpt-4.1-mini", - name: "GPT-4.1-mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-04-15", - last_updated: "2025-04-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.36, output: 1.4, cache_read: 0.09 }, - limit: { context: 1047576, output: 32768 }, - }, - "openai/gpt-4-classic": { - id: "openai/gpt-4-classic", - name: "GPT-4-Classic", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-03-25", - last_updated: "2024-03-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 27, output: 54 }, - limit: { context: 8192, output: 4096 }, - }, - "openai/gpt-5.1-codex": { - id: "openai/gpt-5.1-codex", - name: "GPT-5.1-Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-11-12", - last_updated: "2025-11-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 9, cache_read: 0.11 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.3-instant": { - id: "openai/gpt-5.3-instant", - name: "GPT-5.3-Instant", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2026-03-03", - last_updated: "2026-03-03", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.6, output: 13, cache_read: 0.16 }, - limit: { context: 128000, input: 111616, output: 16384 }, - }, - "google/veo-3-fast": { - id: "google/veo-3-fast", - name: "Veo-3-Fast", - family: "veo", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-10-13", - last_updated: "2025-10-13", - modalities: { input: ["text"], output: ["video"] }, - open_weights: false, - limit: { context: 480, output: 0 }, - }, - "google/veo-3.1-fast": { - id: "google/veo-3.1-fast", - name: "Veo-3.1-Fast", - family: "veo", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image"], output: ["video"] }, - open_weights: false, - limit: { context: 480, output: 0 }, - }, - "google/gemini-3.1-pro": { - id: "google/gemini-3.1-pro", - name: "Gemini-3.1-Pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2026-02-19", - last_updated: "2026-02-19", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/imagen-3-fast": { - id: "google/imagen-3-fast", - name: "Imagen-3-Fast", - family: "imagen", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-10-17", - last_updated: "2024-10-17", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 480, output: 0 }, - }, - "google/gemini-2.0-flash": { - id: "google/gemini-2.0-flash", - name: "Gemini-2.0-Flash", - family: "gemini-flash", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.42 }, - limit: { context: 990000, output: 8192 }, - }, - "google/gemini-deep-research": { - id: "google/gemini-deep-research", - name: "gemini-deep-research", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 1.6, output: 9.6 }, - limit: { context: 1048576, output: 0 }, - }, - "google/gemini-2.5-pro": { - id: "google/gemini-2.5-pro", - name: "Gemini-2.5-Pro", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-02-05", - last_updated: "2025-02-05", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.87, output: 7, cache_read: 0.087 }, - limit: { context: 1065535, output: 65535 }, - }, - "google/imagen-3": { - id: "google/imagen-3", - name: "Imagen-3", - family: "imagen", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-10-15", - last_updated: "2024-10-15", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 480, output: 0 }, - }, - "google/nano-banana": { - id: "google/nano-banana", - name: "Nano-Banana", - family: "nano-banana", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-08-21", - last_updated: "2025-08-21", - modalities: { input: ["text", "image"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 0.21, output: 1.8, cache_read: 0.021 }, - limit: { context: 65536, output: 0 }, - }, - "google/gemini-2.5-flash": { - id: "google/gemini-2.5-flash", - name: "Gemini-2.5-Flash", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-04-26", - last_updated: "2025-04-26", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.21, output: 1.8, cache_read: 0.021 }, - limit: { context: 1065535, output: 65535 }, - }, - "google/gemini-3.1-flash-lite": { - id: "google/gemini-3.1-flash-lite", - name: "Gemini-3.1-Flash-Lite", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2026-02-18", - last_updated: "2026-02-18", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1.5 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-3-flash": { - id: "google/gemini-3-flash", - name: "Gemini-3-Flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-10-07", - last_updated: "2025-10-07", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2.4, cache_read: 0.04 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/veo-3.1": { - id: "google/veo-3.1", - name: "Veo-3.1", - family: "veo", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text"], output: ["video"] }, - open_weights: false, - limit: { context: 480, output: 0 }, - }, - "google/lyria": { - id: "google/lyria", - name: "Lyria", - family: "lyria", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-06-04", - last_updated: "2025-06-04", - modalities: { input: ["text"], output: ["audio"] }, - open_weights: false, - limit: { context: 0, output: 0 }, - }, - "google/imagen-4-ultra": { - id: "google/imagen-4-ultra", - name: "Imagen-4-Ultra", - family: "imagen", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-05-24", - last_updated: "2025-05-24", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 480, output: 0 }, - }, - "google/nano-banana-pro": { - id: "google/nano-banana-pro", - name: "Nano-Banana-Pro", - family: "nano-banana", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-11-19", - last_updated: "2025-11-19", - modalities: { input: ["text", "image"], output: ["image"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2 }, - limit: { context: 65536, output: 0 }, - }, - "google/gemini-3-pro": { - id: "google/gemini-3-pro", - name: "Gemini-3-Pro", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-10-22", - last_updated: "2025-10-22", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 1.6, output: 9.6, cache_read: 0.16 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/imagen-4-fast": { - id: "google/imagen-4-fast", - name: "Imagen-4-Fast", - family: "imagen", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-06-25", - last_updated: "2025-06-25", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 480, output: 0 }, - }, - "google/veo-3": { - id: "google/veo-3", - name: "Veo-3", - family: "veo", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-05-21", - last_updated: "2025-05-21", - modalities: { input: ["text"], output: ["video"] }, - open_weights: false, - limit: { context: 480, output: 0 }, - }, - "google/gemini-2.5-flash-lite": { - id: "google/gemini-2.5-flash-lite", - name: "Gemini-2.5-Flash-Lite", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-06-19", - last_updated: "2025-06-19", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.07, output: 0.28 }, - limit: { context: 1024000, output: 64000 }, - }, - "google/imagen-4": { - id: "google/imagen-4", - name: "Imagen-4", - family: "imagen", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 480, output: 0 }, - }, - "google/gemma-4-31b": { - id: "google/gemma-4-31b", - name: "Gemma-4-31B", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2026-04-02", - last_updated: "2026-04-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 8192 }, - }, - "google/gemini-2.0-flash-lite": { - id: "google/gemini-2.0-flash-lite", - name: "Gemini-2.0-Flash-Lite", - family: "gemini-flash-lite", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-02-05", - last_updated: "2025-02-05", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.052, output: 0.21 }, - limit: { context: 990000, output: 8192 }, - }, - "google/veo-2": { - id: "google/veo-2", - name: "Veo-2", - family: "veo", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-12-02", - last_updated: "2024-12-02", - modalities: { input: ["text"], output: ["video"] }, - open_weights: false, - limit: { context: 480, output: 0 }, - }, - "lumalabs/ray2": { - id: "lumalabs/ray2", - name: "Ray2", - family: "ray", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-02-20", - last_updated: "2025-02-20", - modalities: { input: ["text", "image"], output: ["video"] }, - open_weights: false, - limit: { context: 5000, output: 0 }, - }, - "anthropic/claude-opus-4.1": { - id: "anthropic/claude-opus-4.1", - name: "Claude-Opus-4.1", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 13, output: 64, cache_read: 1.3, cache_write: 16 }, - limit: { context: 196608, output: 32000 }, - }, - "anthropic/claude-sonnet-3.5": { - id: "anthropic/claude-sonnet-3.5", - name: "Claude-Sonnet-3.5", - family: "claude-sonnet", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-06-05", - last_updated: "2024-06-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.6, output: 13, cache_read: 0.26, cache_write: 3.2 }, - limit: { context: 189096, output: 8192 }, - }, - "anthropic/claude-haiku-3": { - id: "anthropic/claude-haiku-3", - name: "Claude-Haiku-3", - family: "claude-haiku", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-03-09", - last_updated: "2024-03-09", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.21, output: 1.1, cache_read: 0.021, cache_write: 0.26 }, - limit: { context: 189096, output: 8192 }, - }, - "anthropic/claude-opus-4.6": { - id: "anthropic/claude-opus-4.6", - name: "Claude-Opus-4.6", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2026-02-04", - last_updated: "2026-02-04", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 4.3, output: 21, cache_read: 0.43, cache_write: 5.3 }, - limit: { context: 983040, output: 128000 }, - }, - "anthropic/claude-sonnet-4": { - id: "anthropic/claude-sonnet-4", - name: "Claude-Sonnet-4", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-05-21", - last_updated: "2025-05-21", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.6, output: 13, cache_read: 0.26, cache_write: 3.2 }, - limit: { context: 983040, output: 64000 }, - }, - "anthropic/claude-sonnet-4.5": { - id: "anthropic/claude-sonnet-4.5", - name: "Claude-Sonnet-4.5", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-09-26", - last_updated: "2025-09-26", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.6, output: 13, cache_read: 0.26, cache_write: 3.2 }, - limit: { context: 983040, output: 32768 }, - }, - "anthropic/claude-opus-4.5": { - id: "anthropic/claude-opus-4.5", - name: "Claude-Opus-4.5", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-11-21", - last_updated: "2025-11-21", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 4.3, output: 21, cache_read: 0.43, cache_write: 5.3 }, - limit: { context: 196608, output: 64000 }, - }, - "anthropic/claude-sonnet-3.7": { - id: "anthropic/claude-sonnet-3.7", - name: "Claude-Sonnet-3.7", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-02-19", - last_updated: "2025-02-19", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.6, output: 13, cache_read: 0.26, cache_write: 3.2 }, - limit: { context: 196608, output: 128000 }, - }, - "anthropic/claude-opus-4": { - id: "anthropic/claude-opus-4", - name: "Claude-Opus-4", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-05-21", - last_updated: "2025-05-21", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 13, output: 64, cache_read: 1.3, cache_write: 16 }, - limit: { context: 192512, output: 28672 }, - }, - "anthropic/claude-haiku-3.5": { - id: "anthropic/claude-haiku-3.5", - name: "Claude-Haiku-3.5", - family: "claude-haiku", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-10-01", - last_updated: "2024-10-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.68, output: 3.4, cache_read: 0.068, cache_write: 0.85 }, - limit: { context: 189096, output: 8192 }, - }, - "anthropic/claude-haiku-4.5": { - id: "anthropic/claude-haiku-4.5", - name: "Claude-Haiku-4.5", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.85, output: 4.3, cache_read: 0.085, cache_write: 1.1 }, - limit: { context: 192000, output: 64000 }, - }, - "anthropic/claude-sonnet-3.5-june": { - id: "anthropic/claude-sonnet-3.5-june", - name: "Claude-Sonnet-3.5-June", - family: "claude-sonnet", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-11-18", - last_updated: "2024-11-18", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.6, output: 13, cache_read: 0.26, cache_write: 3.2 }, - limit: { context: 189096, output: 8192 }, - }, - "anthropic/claude-sonnet-4.6": { - id: "anthropic/claude-sonnet-4.6", - name: "Claude-Sonnet-4.6", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.6, output: 13, cache_read: 0.26, cache_write: 3.2 }, - limit: { context: 983040, output: 128000 }, - }, - "ideogramai/ideogram": { - id: "ideogramai/ideogram", - name: "Ideogram", - family: "ideogram", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-04-03", - last_updated: "2024-04-03", - modalities: { input: ["text", "image"], output: ["image"] }, - open_weights: false, - limit: { context: 150, output: 0 }, - }, - "ideogramai/ideogram-v2": { - id: "ideogramai/ideogram-v2", - name: "Ideogram-v2", - family: "ideogram", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-08-21", - last_updated: "2024-08-21", - modalities: { input: ["text", "image"], output: ["image"] }, - open_weights: false, - limit: { context: 150, output: 0 }, - }, - "ideogramai/ideogram-v2a-turbo": { - id: "ideogramai/ideogram-v2a-turbo", - name: "Ideogram-v2a-Turbo", - family: "ideogram", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-02-27", - last_updated: "2025-02-27", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 150, output: 0 }, - }, - "ideogramai/ideogram-v2a": { - id: "ideogramai/ideogram-v2a", - name: "Ideogram-v2a", - family: "ideogram", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-02-27", - last_updated: "2025-02-27", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 150, output: 0 }, - }, - "trytako/tako": { - id: "trytako/tako", - name: "Tako", - family: "tako", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-08-15", - last_updated: "2024-08-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 2048, output: 0 }, - }, - "poetools/claude-code": { - id: "poetools/claude-code", - name: "claude-code", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-11-27", - last_updated: "2025-11-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 0, output: 0 }, - }, - }, - }, - helicone: { - id: "helicone", - env: ["HELICONE_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://ai-gateway.helicone.ai/v1", - name: "Helicone", - doc: "https://helicone.ai/models", - models: { - "mistral-nemo": { - id: "mistral-nemo", - name: "Mistral Nemo", - family: "mistral-nemo", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-07", - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 20, output: 40 }, - limit: { context: 128000, output: 16400 }, - }, - "grok-4-1-fast-reasoning": { - id: "grok-4-1-fast-reasoning", - name: "xAI Grok 4.1 Fast Reasoning", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-11", - release_date: "2025-11-17", - last_updated: "2025-11-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.19999999999999998, output: 0.5, cache_read: 0.049999999999999996 }, - limit: { context: 2000000, output: 2000000 }, - }, - "gemma2-9b-it": { - id: "gemma2-9b-it", - name: "Google Gemma 2", - family: "gemma", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-06", - release_date: "2024-06-25", - last_updated: "2024-06-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.01, output: 0.03 }, - limit: { context: 8192, output: 8192 }, - }, - "llama-3.3-70b-instruct": { - id: "llama-3.3-70b-instruct", - name: "Meta Llama 3.3 70B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.13, output: 0.39 }, - limit: { context: 128000, output: 16400 }, - }, - "llama-4-scout": { - id: "llama-4-scout", - name: "Meta Llama 4 Scout 17B 16E", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.08, output: 0.3 }, - limit: { context: 131072, output: 8192 }, - }, - "chatgpt-4o-latest": { - id: "chatgpt-4o-latest", - name: "OpenAI ChatGPT-4o", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2024-08-14", - last_updated: "2024-08-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 20, cache_read: 2.5 }, - limit: { context: 128000, output: 16384 }, - }, - "claude-3.5-sonnet-v2": { - id: "claude-3.5-sonnet-v2", - name: "Anthropic: Claude 3.5 Sonnet v2", - family: "claude-sonnet", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.30000000000000004, cache_write: 3.75 }, - limit: { context: 200000, output: 8192 }, - }, - "hermes-2-pro-llama-3-8b": { - id: "hermes-2-pro-llama-3-8b", - name: "Hermes 2 Pro Llama 3 8B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-05", - release_date: "2024-05-27", - last_updated: "2024-05-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.14 }, - limit: { context: 131072, output: 131072 }, - }, - "claude-3.7-sonnet": { - id: "claude-3.7-sonnet", - name: "Anthropic: Claude 3.7 Sonnet", - family: "claude-sonnet", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-02", - release_date: "2025-02-19", - last_updated: "2025-02-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.30000000000000004, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "llama-prompt-guard-2-22m": { - id: "llama-prompt-guard-2-22m", - name: "Meta Llama Prompt Guard 2 22M", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2024-10-01", - last_updated: "2024-10-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.01, output: 0.01 }, - limit: { context: 512, output: 2 }, - }, - "o1-mini": { - id: "o1-mini", - name: "OpenAI: o1-mini", - family: "o-mini", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2025-01", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.55 }, - limit: { context: 128000, output: 65536 }, - }, - "gpt-4.1-mini-2025-04-14": { - id: "gpt-4.1-mini-2025-04-14", - name: "OpenAI GPT-4.1 Mini", - family: "gpt-mini", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.39999999999999997, output: 1.5999999999999999, cache_read: 0.09999999999999999 }, - limit: { context: 1047576, output: 32768 }, - }, - "deepseek-r1-distill-llama-70b": { - id: "deepseek-r1-distill-llama-70b", - name: "DeepSeek R1 Distill Llama 70B", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.03, output: 0.13 }, - limit: { context: 128000, output: 4096 }, - }, - "qwen3-32b": { - id: "qwen3-32b", - name: "Qwen3 32B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04-28", - last_updated: "2025-04-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.29, output: 0.59 }, - limit: { context: 131072, output: 40960 }, - }, - "llama-3.3-70b-versatile": { - id: "llama-3.3-70b-versatile", - name: "Meta Llama 3.3 70B Versatile", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.59, output: 0.7899999999999999 }, - limit: { context: 131072, output: 32678 }, - }, - "gpt-5-mini": { - id: "gpt-5-mini", - name: "OpenAI GPT-5 Mini", - family: "gpt-mini", - attachment: false, - reasoning: false, - tool_call: true, - temperature: false, - knowledge: "2025-01", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.024999999999999998 }, - limit: { context: 400000, output: 128000 }, - }, - "gpt-5-nano": { - id: "gpt-5-nano", - name: "OpenAI GPT-5 Nano", - family: "gpt-nano", - attachment: false, - reasoning: false, - tool_call: true, - temperature: false, - knowledge: "2025-01", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.049999999999999996, output: 0.39999999999999997, cache_read: 0.005 }, - limit: { context: 400000, output: 128000 }, - }, - "gemini-3-pro-preview": { - id: "gemini-3-pro-preview", - name: "Google Gemini 3 Pro Preview", - family: "gemini-pro", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-11", - release_date: "2025-11-18", - last_updated: "2025-11-18", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.19999999999999998 }, - limit: { context: 1048576, output: 65536 }, - }, - "claude-3-haiku-20240307": { - id: "claude-3-haiku-20240307", - name: "Anthropic: Claude 3 Haiku", - family: "claude-haiku", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-03", - release_date: "2024-03-07", - last_updated: "2024-03-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1.25, cache_read: 0.03, cache_write: 0.3 }, - limit: { context: 200000, output: 4096 }, - }, - "llama-4-maverick": { - id: "llama-4-maverick", - name: "Meta Llama 4 Maverick 17B 128E", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 131072, output: 8192 }, - }, - "claude-sonnet-4-5-20250929": { - id: "claude-sonnet-4-5-20250929", - name: "Anthropic: Claude Sonnet 4.5 (20250929)", - family: "claude-sonnet", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-09", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.30000000000000004, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "gemini-2.5-pro": { - id: "gemini-2.5-pro", - name: "Google Gemini 2.5 Pro", - family: "gemini-pro", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.3125, cache_write: 1.25 }, - limit: { context: 1048576, output: 65536 }, - }, - "claude-4.5-opus": { - id: "claude-4.5-opus", - name: "Anthropic: Claude Opus 4.5", - family: "claude-opus", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-11", - release_date: "2025-11-24", - last_updated: "2025-11-24", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 64000 }, - }, - "grok-4-1-fast-non-reasoning": { - id: "grok-4-1-fast-non-reasoning", - name: "xAI Grok 4.1 Fast Non-Reasoning", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-11", - release_date: "2025-11-17", - last_updated: "2025-11-17", - modalities: { input: ["text", "image"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 0.19999999999999998, output: 0.5, cache_read: 0.049999999999999996 }, - limit: { context: 2000000, output: 30000 }, - }, - "sonar-pro": { - id: "sonar-pro", - name: "Perplexity Sonar Pro", - family: "sonar-pro", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-01", - release_date: "2025-01-27", - last_updated: "2025-01-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 200000, output: 4096 }, - }, - "mistral-large-2411": { - id: "mistral-large-2411", - name: "Mistral-Large", - family: "mistral-large", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2024-07-24", - last_updated: "2024-07-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6 }, - limit: { context: 128000, output: 32768 }, - }, - "o3-pro": { - id: "o3-pro", - name: "OpenAI o3 Pro", - family: "o-pro", - attachment: false, - reasoning: false, - tool_call: true, - temperature: false, - knowledge: "2024-06", - release_date: "2024-06-01", - last_updated: "2024-06-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 20, output: 80 }, - limit: { context: 200000, output: 100000 }, - }, - "claude-opus-4-1": { - id: "claude-opus-4-1", - name: "Anthropic: Claude Opus 4.1", - family: "claude-opus", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "gpt-4o-mini": { - id: "gpt-4o-mini", - name: "OpenAI GPT-4o-mini", - family: "gpt-mini", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6, cache_read: 0.075 }, - limit: { context: 128000, output: 16384 }, - }, - "claude-4.5-haiku": { - id: "claude-4.5-haiku", - name: "Anthropic: Claude 4.5 Haiku", - family: "claude-haiku", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-10", - release_date: "2025-10-01", - last_updated: "2025-10-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.09999999999999999, cache_write: 1.25 }, - limit: { context: 200000, output: 8192 }, - }, - "kimi-k2-0711": { - id: "kimi-k2-0711", - name: "Kimi K2 (07/11)", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5700000000000001, output: 2.3 }, - limit: { context: 131072, output: 16384 }, - }, - "o4-mini": { - id: "o4-mini", - name: "OpenAI o4 Mini", - family: "o-mini", - attachment: false, - reasoning: false, - tool_call: true, - temperature: false, - knowledge: "2024-06", - release_date: "2024-06-01", - last_updated: "2024-06-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.275 }, - limit: { context: 200000, output: 100000 }, - }, - "sonar-deep-research": { - id: "sonar-deep-research", - name: "Perplexity Sonar Deep Research", - family: "sonar-deep-research", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2025-01", - release_date: "2025-01-27", - last_updated: "2025-01-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8 }, - limit: { context: 127000, output: 4096 }, - }, - "gemma-3-12b-it": { - id: "gemma-3-12b-it", - name: "Google Gemma 3 12B", - family: "gemma", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-12", - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.049999999999999996, output: 0.09999999999999999 }, - limit: { context: 131072, output: 8192 }, - }, - "gemini-2.5-flash": { - id: "gemini-2.5-flash", - name: "Google Gemini 2.5 Flash", - family: "gemini-flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5, cache_read: 0.075, cache_write: 0.3 }, - limit: { context: 1048576, output: 65535 }, - }, - "deepseek-tng-r1t2-chimera": { - id: "deepseek-tng-r1t2-chimera", - name: "DeepSeek TNG R1T2 Chimera", - family: "deepseek-thinking", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-07-02", - last_updated: "2025-07-02", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 130000, output: 163840 }, - }, - "gpt-5.1-codex-mini": { - id: "gpt-5.1-codex-mini", - name: "OpenAI: GPT-5.1 Codex Mini", - family: "gpt-codex", - attachment: false, - reasoning: false, - tool_call: true, - temperature: false, - knowledge: "2025-01", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text", "image"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.024999999999999998 }, - limit: { context: 400000, output: 128000 }, - }, - "claude-sonnet-4": { - id: "claude-sonnet-4", - name: "Anthropic: Claude Sonnet 4", - family: "claude-sonnet", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-05", - release_date: "2025-05-14", - last_updated: "2025-05-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.30000000000000004, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "grok-code-fast-1": { - id: "grok-code-fast-1", - name: "xAI Grok Code Fast 1", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2024-08-25", - last_updated: "2024-08-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.19999999999999998, output: 1.5, cache_read: 0.02 }, - limit: { context: 256000, output: 10000 }, - }, - "gpt-5.1": { - id: "gpt-5.1", - name: "OpenAI GPT-5.1", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: true, - temperature: false, - knowledge: "2025-01", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text", "image"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.12500000000000003 }, - limit: { context: 400000, output: 128000 }, - }, - "deepseek-reasoner": { - id: "deepseek-reasoner", - name: "DeepSeek Reasoner", - family: "deepseek-thinking", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-01", - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.56, output: 1.68, cache_read: 0.07 }, - limit: { context: 128000, output: 64000 }, - }, - "grok-4-fast-reasoning": { - id: "grok-4-fast-reasoning", - name: "xAI: Grok 4 Fast Reasoning", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-09", - release_date: "2025-09-01", - last_updated: "2025-09-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.19999999999999998, output: 0.5, cache_read: 0.049999999999999996 }, - limit: { context: 2000000, output: 2000000 }, - }, - o1: { - id: "o1", - name: "OpenAI: o1", - family: "o", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2025-01", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 60, cache_read: 7.5 }, - limit: { context: 200000, output: 100000 }, - }, - "llama-3.1-8b-instant": { - id: "llama-3.1-8b-instant", - name: "Meta Llama 3.1 8B Instant", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2024-07-01", - last_updated: "2024-07-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.049999999999999996, output: 0.08 }, - limit: { context: 131072, output: 32678 }, - }, - "o3-mini": { - id: "o3-mini", - name: "OpenAI o3 Mini", - family: "o-mini", - attachment: false, - reasoning: false, - tool_call: true, - temperature: false, - knowledge: "2023-10", - release_date: "2023-10-01", - last_updated: "2023-10-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.55 }, - limit: { context: 200000, output: 100000 }, - }, - sonar: { - id: "sonar", - name: "Perplexity Sonar", - family: "sonar", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-01", - release_date: "2025-01-27", - last_updated: "2025-01-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 1 }, - limit: { context: 127000, output: 4096 }, - }, - "kimi-k2-0905": { - id: "kimi-k2-0905", - name: "Kimi K2 (09/05)", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-09", - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 2, cache_read: 0.39999999999999997 }, - limit: { context: 262144, output: 16384 }, - }, - "mistral-small": { - id: "mistral-small", - name: "Mistral Small", - family: "mistral-small", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-02", - release_date: "2024-02-26", - last_updated: "2024-02-26", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 75, output: 200 }, - limit: { context: 128000, output: 128000 }, - }, - "qwen3-30b-a3b": { - id: "qwen3-30b-a3b", - name: "Qwen3 30B A3B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-06-01", - last_updated: "2025-06-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.08, output: 0.29 }, - limit: { context: 41000, output: 41000 }, - }, - "codex-mini-latest": { - id: "codex-mini-latest", - name: "OpenAI Codex Mini Latest", - family: "gpt-codex-mini", - attachment: false, - reasoning: false, - tool_call: true, - temperature: false, - knowledge: "2025-01", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.5, output: 6, cache_read: 0.375 }, - limit: { context: 200000, output: 100000 }, - }, - "grok-4": { - id: "grok-4", - name: "xAI Grok 4", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2024-07-09", - last_updated: "2024-07-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.75 }, - limit: { context: 256000, output: 256000 }, - }, - "qwen3-235b-a22b-thinking": { - id: "qwen3-235b-a22b-thinking", - name: "Qwen3 235B A22B Thinking", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2025-07", - release_date: "2025-07-25", - last_updated: "2025-07-25", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.9000000000000004 }, - limit: { context: 262144, output: 81920 }, - }, - "qwen2.5-coder-7b-fast": { - id: "qwen2.5-coder-7b-fast", - name: "Qwen2.5 Coder 7B fast", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-09", - release_date: "2024-09-15", - last_updated: "2024-09-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.03, output: 0.09 }, - limit: { context: 32000, output: 8192 }, - }, - "llama-3.1-8b-instruct-turbo": { - id: "llama-3.1-8b-instruct-turbo", - name: "Meta Llama 3.1 8B Instruct Turbo", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.02, output: 0.03 }, - limit: { context: 128000, output: 128000 }, - }, - "qwen3-next-80b-a3b-instruct": { - id: "qwen3-next-80b-a3b-instruct", - name: "Qwen3 Next 80B A3B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 1.4 }, - limit: { context: 262000, output: 16384 }, - }, - "glm-4.6": { - id: "glm-4.6", - name: "Zai GLM-4.6", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.44999999999999996, output: 1.5 }, - limit: { context: 204800, output: 131072 }, - }, - "gpt-5-codex": { - id: "gpt-5-codex", - name: "OpenAI: GPT-5 Codex", - family: "gpt-codex", - attachment: false, - reasoning: false, - tool_call: true, - temperature: false, - knowledge: "2025-01", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.12500000000000003 }, - limit: { context: 400000, output: 128000 }, - }, - "claude-opus-4-1-20250805": { - id: "claude-opus-4-1-20250805", - name: "Anthropic: Claude Opus 4.1 (20250805)", - family: "claude-opus", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "gpt-5.1-chat-latest": { - id: "gpt-5.1-chat-latest", - name: "OpenAI GPT-5.1 Chat", - family: "gpt-codex", - attachment: false, - reasoning: false, - tool_call: true, - temperature: false, - knowledge: "2025-01", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text", "image"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.12500000000000003 }, - limit: { context: 128000, output: 16384 }, - }, - "claude-haiku-4-5-20251001": { - id: "claude-haiku-4-5-20251001", - name: "Anthropic: Claude 4.5 Haiku (20251001)", - family: "claude-haiku", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-10", - release_date: "2025-10-01", - last_updated: "2025-10-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.09999999999999999, cache_write: 1.25 }, - limit: { context: 200000, output: 8192 }, - }, - "sonar-reasoning": { - id: "sonar-reasoning", - name: "Perplexity Sonar Reasoning", - family: "sonar-reasoning", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2025-01", - release_date: "2025-01-27", - last_updated: "2025-01-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5 }, - limit: { context: 127000, output: 4096 }, - }, - "claude-opus-4": { - id: "claude-opus-4", - name: "Anthropic: Claude Opus 4", - family: "claude-opus", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-05", - release_date: "2025-05-14", - last_updated: "2025-05-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "llama-prompt-guard-2-86m": { - id: "llama-prompt-guard-2-86m", - name: "Meta Llama Prompt Guard 2 86M", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2024-10-01", - last_updated: "2024-10-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.01, output: 0.01 }, - limit: { context: 512, output: 2 }, - }, - "gpt-4.1-nano": { - id: "gpt-4.1-nano", - name: "OpenAI GPT-4.1 Nano", - family: "gpt-nano", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.09999999999999999, output: 0.39999999999999997, cache_read: 0.024999999999999998 }, - limit: { context: 1047576, output: 32768 }, - }, - "qwen3-coder-30b-a3b-instruct": { - id: "qwen3-coder-30b-a3b-instruct", - name: "Qwen3 Coder 30B A3B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-07-31", - last_updated: "2025-07-31", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.09999999999999999, output: 0.3 }, - limit: { context: 262144, output: 262144 }, - }, - "claude-3.5-haiku": { - id: "claude-3.5-haiku", - name: "Anthropic: Claude 3.5 Haiku", - family: "claude-haiku", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.7999999999999999, output: 4, cache_read: 0.08, cache_write: 1 }, - limit: { context: 200000, output: 8192 }, - }, - "grok-3-mini": { - id: "grok-3-mini", - name: "xAI Grok 3 Mini", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-06", - release_date: "2024-06-01", - last_updated: "2024-06-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.5, cache_read: 0.075 }, - limit: { context: 131072, output: 131072 }, - }, - o3: { - id: "o3", - name: "OpenAI o3", - family: "o", - attachment: false, - reasoning: false, - tool_call: true, - temperature: false, - knowledge: "2024-06", - release_date: "2024-06-01", - last_updated: "2024-06-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 200000, output: 100000 }, - }, - "deepseek-v3.2": { - id: "deepseek-v3.2", - name: "DeepSeek V3.2", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-09", - release_date: "2025-09-22", - last_updated: "2025-09-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 0.41 }, - limit: { context: 163840, output: 65536 }, - }, - "gpt-oss-20b": { - id: "gpt-oss-20b", - name: "OpenAI GPT-OSS 20b", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-06", - release_date: "2024-06-01", - last_updated: "2024-06-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.049999999999999996, output: 0.19999999999999998 }, - limit: { context: 131072, output: 131072 }, - }, - "gpt-5-pro": { - id: "gpt-5-pro", - name: "OpenAI: GPT-5 Pro", - family: "gpt-pro", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2025-01", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 120 }, - limit: { context: 128000, output: 32768 }, - }, - "llama-guard-4": { - id: "llama-guard-4", - name: "Meta Llama Guard 4 12B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-01", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.21, output: 0.21 }, - limit: { context: 131072, output: 1024 }, - }, - "gpt-4o": { - id: "gpt-4o", - name: "OpenAI GPT-4o", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-05", - release_date: "2024-05-13", - last_updated: "2024-05-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10, cache_read: 1.25 }, - limit: { context: 128000, output: 16384 }, - }, - "qwen3-vl-235b-a22b-instruct": { - id: "qwen3-vl-235b-a22b-instruct", - name: "Qwen3 VL 235B A22B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-09", - release_date: "2025-09-23", - last_updated: "2025-09-23", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.5 }, - limit: { context: 256000, output: 16384 }, - }, - "gemini-2.5-flash-lite": { - id: "gemini-2.5-flash-lite", - name: "Google Gemini 2.5 Flash Lite", - family: "gemini-flash-lite", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-07-22", - last_updated: "2025-07-22", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { - input: 0.09999999999999999, - output: 0.39999999999999997, - cache_read: 0.024999999999999998, - cache_write: 0.09999999999999999, - }, - limit: { context: 1048576, output: 65535 }, - }, - "qwen3-coder": { - id: "qwen3-coder", - name: "Qwen3 Coder 480B A35B Instruct Turbo", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.22, output: 0.95 }, - limit: { context: 262144, output: 16384 }, - }, - "gpt-5": { - id: "gpt-5", - name: "OpenAI GPT-5", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: true, - temperature: false, - knowledge: "2025-01", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.12500000000000003 }, - limit: { context: 400000, output: 128000 }, - }, - "ernie-4.5-21b-a3b-thinking": { - id: "ernie-4.5-21b-a3b-thinking", - name: "Baidu Ernie 4.5 21B A3B Thinking", - family: "ernie", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2025-03", - release_date: "2025-03-16", - last_updated: "2025-03-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.07, output: 0.28 }, - limit: { context: 128000, output: 8000 }, - }, - "gpt-oss-120b": { - id: "gpt-oss-120b", - name: "OpenAI GPT-OSS 120b", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-06", - release_date: "2024-06-01", - last_updated: "2024-06-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.04, output: 0.16 }, - limit: { context: 131072, output: 131072 }, - }, - "gpt-5-chat-latest": { - id: "gpt-5-chat-latest", - name: "OpenAI GPT-5 Chat Latest", - family: "gpt-codex", - attachment: false, - reasoning: false, - tool_call: true, - temperature: false, - knowledge: "2024-09", - release_date: "2024-09-30", - last_updated: "2024-09-30", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.12500000000000003 }, - limit: { context: 128000, output: 16384 }, - }, - "claude-4.5-sonnet": { - id: "claude-4.5-sonnet", - name: "Anthropic: Claude Sonnet 4.5", - family: "claude-sonnet", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-09", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.30000000000000004, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "deepseek-v3": { - id: "deepseek-v3", - name: "DeepSeek V3", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2024-12-26", - last_updated: "2024-12-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.56, output: 1.68, cache_read: 0.07 }, - limit: { context: 128000, output: 8192 }, - }, - "llama-3.1-8b-instruct": { - id: "llama-3.1-8b-instruct", - name: "Meta Llama 3.1 8B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.02, output: 0.049999999999999996 }, - limit: { context: 16384, output: 16384 }, - }, - "gpt-4.1": { - id: "gpt-4.1", - name: "OpenAI GPT-4.1", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 1047576, output: 32768 }, - }, - "kimi-k2-thinking": { - id: "kimi-k2-thinking", - name: "Kimi K2 Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-11", - release_date: "2025-11-06", - last_updated: "2025-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.48, output: 2 }, - limit: { context: 256000, output: 262144 }, - }, - "gpt-4.1-mini": { - id: "gpt-4.1-mini", - name: "OpenAI GPT-4.1 Mini", - family: "gpt-mini", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.39999999999999997, output: 1.5999999999999999, cache_read: 0.09999999999999999 }, - limit: { context: 1047576, output: 32768 }, - }, - "deepseek-v3.1-terminus": { - id: "deepseek-v3.1-terminus", - name: "DeepSeek V3.1 Terminus", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-09", - release_date: "2025-09-22", - last_updated: "2025-09-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 1, cache_read: 0.21600000000000003 }, - limit: { context: 128000, output: 16384 }, - }, - "gpt-5.1-codex": { - id: "gpt-5.1-codex", - name: "OpenAI: GPT-5.1 Codex", - family: "gpt-codex", - attachment: false, - reasoning: false, - tool_call: true, - temperature: false, - knowledge: "2025-01", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text", "image"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.12500000000000003 }, - limit: { context: 400000, output: 128000 }, - }, - "grok-3": { - id: "grok-3", - name: "xAI Grok 3", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-06", - release_date: "2024-06-01", - last_updated: "2024-06-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.75 }, - limit: { context: 131072, output: 131072 }, - }, - "grok-4-fast-non-reasoning": { - id: "grok-4-fast-non-reasoning", - name: "xAI Grok 4 Fast Non-Reasoning", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-09", - release_date: "2025-09-19", - last_updated: "2025-09-19", - modalities: { input: ["text", "image", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.19999999999999998, output: 0.5, cache_read: 0.049999999999999996 }, - limit: { context: 2000000, output: 2000000 }, - }, - "sonar-reasoning-pro": { - id: "sonar-reasoning-pro", - name: "Perplexity Sonar Reasoning Pro", - family: "sonar-reasoning", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2025-01", - release_date: "2025-01-27", - last_updated: "2025-01-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8 }, - limit: { context: 127000, output: 4096 }, - }, - }, - }, - "ollama-cloud": { - id: "ollama-cloud", - env: ["OLLAMA_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://ollama.com/v1", - name: "Ollama Cloud", - doc: "https://docs.ollama.com/cloud", - models: { - "minimax-m2.7": { - id: "minimax-m2.7", - name: "minimax-m2.7", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 204800, output: 131072 }, - }, - "gpt-oss:20b": { - id: "gpt-oss:20b", - name: "gpt-oss:20b", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - release_date: "2025-08-05", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 131072, output: 32768 }, - }, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "kimi-k2.5", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - limit: { context: 262144, output: 262144 }, - }, - "glm-4.7": { - id: "glm-4.7", - name: "glm-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - release_date: "2025-12-22", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 202752, output: 131072 }, - }, - "gemma4:31b": { - id: "gemma4:31b", - name: "gemma4:31b", - family: "gemma", - attachment: true, - reasoning: true, - tool_call: true, - knowledge: "2025-01", - release_date: "2026-04-02", - last_updated: "2026-04-08", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - limit: { context: 262144, output: 8192 }, - }, - "gpt-oss:120b": { - id: "gpt-oss:120b", - name: "gpt-oss:120b", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - release_date: "2025-08-05", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 131072, output: 32768 }, - }, - "qwen3.5:397b": { - id: "qwen3.5:397b", - name: "qwen3.5:397b", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_details" }, - release_date: "2026-02-15", - last_updated: "2026-02-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - limit: { context: 262144, output: 81920 }, - }, - "deepseek-v3.1:671b": { - id: "deepseek-v3.1:671b", - name: "deepseek-v3.1:671b", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - release_date: "2025-08-21", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 163840, output: 163840 }, - }, - "glm-5": { - id: "glm-5", - name: "glm-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 202752, output: 131072 }, - }, - "qwen3-vl:235b-instruct": { - id: "qwen3-vl:235b-instruct", - name: "qwen3-vl:235b-instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - release_date: "2025-09-22", - last_updated: "2026-01-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - limit: { context: 262144, output: 131072 }, - }, - "gemma3:4b": { - id: "gemma3:4b", - name: "gemma3:4b", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: false, - release_date: "2024-12-01", - last_updated: "2026-01-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - limit: { context: 131072, output: 131072 }, - }, - "gemini-3-flash-preview": { - id: "gemini-3-flash-preview", - name: "gemini-3-flash-preview", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - knowledge: "2025-01", - release_date: "2025-12-17", - last_updated: "2026-04-08", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - limit: { context: 1048576, output: 65536 }, - }, - "ministral-3:14b": { - id: "ministral-3:14b", - name: "ministral-3:14b", - family: "ministral", - attachment: true, - reasoning: false, - tool_call: true, - release_date: "2024-12-01", - last_updated: "2026-01-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - limit: { context: 262144, output: 128000 }, - }, - "minimax-m2": { - id: "minimax-m2", - name: "minimax-m2", - family: "minimax", - attachment: false, - reasoning: false, - tool_call: true, - release_date: "2025-10-23", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 204800, output: 128000 }, - }, - "qwen3-next:80b": { - id: "qwen3-next:80b", - name: "qwen3-next:80b", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - release_date: "2025-09-15", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 262144, output: 32768 }, - }, - "qwen3-vl:235b": { - id: "qwen3-vl:235b", - name: "qwen3-vl:235b", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - release_date: "2025-09-22", - last_updated: "2026-01-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - limit: { context: 262144, output: 32768 }, - }, - "rnj-1:8b": { - id: "rnj-1:8b", - name: "rnj-1:8b", - family: "rnj", - attachment: false, - reasoning: false, - tool_call: true, - release_date: "2025-12-06", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 32768, output: 4096 }, - }, - "minimax-m2.1": { - id: "minimax-m2.1", - name: "minimax-m2.1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - release_date: "2025-12-23", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 204800, output: 131072 }, - }, - "glm-5.1": { - id: "glm-5.1", - name: "glm-5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - release_date: "2026-03-27", - last_updated: "2026-04-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 202752, output: 131072 }, - }, - "mistral-large-3:675b": { - id: "mistral-large-3:675b", - name: "mistral-large-3:675b", - family: "mistral-large", - attachment: true, - reasoning: false, - tool_call: true, - release_date: "2025-12-02", - last_updated: "2026-01-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - limit: { context: 262144, output: 262144 }, - }, - "ministral-3:8b": { - id: "ministral-3:8b", - name: "ministral-3:8b", - family: "ministral", - attachment: true, - reasoning: false, - tool_call: true, - release_date: "2024-12-01", - last_updated: "2026-01-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - limit: { context: 262144, output: 128000 }, - }, - "gemma3:12b": { - id: "gemma3:12b", - name: "gemma3:12b", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: false, - release_date: "2024-12-01", - last_updated: "2026-01-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - limit: { context: 131072, output: 131072 }, - }, - "qwen3-coder:480b": { - id: "qwen3-coder:480b", - name: "qwen3-coder:480b", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - release_date: "2025-07-22", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 262144, output: 65536 }, - }, - "nemotron-3-nano:30b": { - id: "nemotron-3-nano:30b", - name: "nemotron-3-nano:30b", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: true, - release_date: "2025-12-15", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 1048576, output: 131072 }, - }, - "glm-4.6": { - id: "glm-4.6", - name: "glm-4.6", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - release_date: "2025-09-29", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 202752, output: 131072 }, - }, - "ministral-3:3b": { - id: "ministral-3:3b", - name: "ministral-3:3b", - family: "ministral", - attachment: true, - reasoning: false, - tool_call: true, - release_date: "2024-10-22", - last_updated: "2026-01-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - limit: { context: 262144, output: 128000 }, - }, - "gemma3:27b": { - id: "gemma3:27b", - name: "gemma3:27b", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: false, - release_date: "2025-07-27", - last_updated: "2026-01-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - limit: { context: 131072, output: 131072 }, - }, - "devstral-2:123b": { - id: "devstral-2:123b", - name: "devstral-2:123b", - family: "devstral", - attachment: false, - reasoning: false, - tool_call: true, - release_date: "2025-12-09", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 262144, output: 262144 }, - }, - "cogito-2.1:671b": { - id: "cogito-2.1:671b", - name: "cogito-2.1:671b", - family: "cogito", - attachment: false, - reasoning: true, - tool_call: true, - release_date: "2025-11-19", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 163840, output: 32000 }, - }, - "qwen3-coder-next": { - id: "qwen3-coder-next", - name: "qwen3-coder-next", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - release_date: "2026-02-02", - last_updated: "2026-02-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 262144, output: 65536 }, - }, - "nemotron-3-super": { - id: "nemotron-3-super", - name: "nemotron-3-super", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: true, - release_date: "2026-03-11", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 262144, output: 65536 }, - }, - "minimax-m2.5": { - id: "minimax-m2.5", - name: "minimax-m2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - knowledge: "2025-01", - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 204800, output: 131072 }, - }, - "deepseek-v3.2": { - id: "deepseek-v3.2", - name: "deepseek-v3.2", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - release_date: "2025-06-15", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 163840, output: 65536 }, - }, - "kimi-k2-thinking": { - id: "kimi-k2-thinking", - name: "kimi-k2-thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - knowledge: "2024-08", - release_date: "2025-11-06", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 262144, output: 262144 }, - }, - "devstral-small-2:24b": { - id: "devstral-small-2:24b", - name: "devstral-small-2:24b", - family: "devstral", - attachment: true, - reasoning: false, - tool_call: true, - release_date: "2025-12-09", - last_updated: "2026-01-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - limit: { context: 262144, output: 262144 }, - }, - "kimi-k2:1t": { - id: "kimi-k2:1t", - name: "kimi-k2:1t", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - knowledge: "2024-10", - release_date: "2025-07-11", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 262144, output: 262144 }, - }, - }, - }, - "zai-coding-plan": { - id: "zai-coding-plan", - env: ["ZHIPU_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.z.ai/api/coding/paas/v4", - name: "Z.AI Coding Plan", - doc: "https://docs.z.ai/devpack/overview", - models: { - "glm-5v-turbo": { - id: "glm-5v-turbo", - name: "glm-5v-turbo", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-04-01", - last_updated: "2026-04-01", - modalities: { input: ["text", "image", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 200000, output: 131072 }, - }, - "glm-4.7": { - id: "glm-4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "glm-5": { - id: "glm-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "glm-4.7-flashx": { - id: "glm-4.7-flashx", - name: "GLM-4.7-FlashX", - family: "glm-flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-01-19", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.4, cache_read: 0.01, cache_write: 0 }, - limit: { context: 200000, output: 131072 }, - }, - "glm-5.1": { - id: "glm-5.1", - name: "GLM-5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-03-27", - last_updated: "2026-03-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 200000, output: 131072 }, - }, - "glm-4.5": { - id: "glm-4.5", - name: "GLM-4.5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 131072, output: 98304 }, - }, - "glm-4.5-air": { - id: "glm-4.5-air", - name: "GLM-4.5-Air", - family: "glm-air", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 131072, output: 98304 }, - }, - "glm-5-turbo": { - id: "glm-5-turbo", - name: "GLM-5-Turbo", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-03-16", - last_updated: "2026-03-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 200000, output: 131072 }, - }, - "glm-4.5v": { - id: "glm-4.5v", - name: "GLM-4.5V", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-08-11", - last_updated: "2025-08-11", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 64000, output: 16384 }, - }, - "glm-4.6": { - id: "glm-4.6", - name: "GLM-4.6", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "glm-4.6v": { - id: "glm-4.6v", - name: "GLM-4.6V", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-08", - last_updated: "2025-12-08", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 32768 }, - }, - "glm-4.5-flash": { - id: "glm-4.5-flash", - name: "GLM-4.5-Flash", - family: "glm-flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 131072, output: 98304 }, - }, - "glm-4.7-flash": { - id: "glm-4.7-flash", - name: "GLM-4.7-Flash", - family: "glm-flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-01-19", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 200000, output: 131072 }, - }, - }, - }, - "amazon-bedrock": { - id: "amazon-bedrock", - env: ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_REGION", "AWS_BEARER_TOKEN_BEDROCK"], - npm: "@ai-sdk/amazon-bedrock", - name: "Amazon Bedrock", - doc: "https://docs.aws.amazon.com/bedrock/latest/userguide/models-supported.html", - models: { - "anthropic.claude-opus-4-1-20250805-v1:0": { - id: "anthropic.claude-opus-4-1-20250805-v1:0", - name: "Claude Opus 4.1", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "anthropic.claude-3-5-sonnet-20241022-v2:0": { - id: "anthropic.claude-3-5-sonnet-20241022-v2:0", - name: "Claude Sonnet 3.5 v2", - family: "claude-sonnet", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 8192 }, - }, - "openai.gpt-oss-safeguard-120b": { - id: "openai.gpt-oss-safeguard-120b", - name: "GPT OSS Safeguard 120B", - family: "gpt-oss", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 128000, output: 4096 }, - }, - "nvidia.nemotron-nano-3-30b": { - id: "nvidia.nemotron-nano-3-30b", - name: "NVIDIA Nemotron Nano 3 30B", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.06, output: 0.24 }, - limit: { context: 128000, output: 4096 }, - }, - "meta.llama3-2-90b-instruct-v1:0": { - id: "meta.llama3-2-90b-instruct-v1:0", - name: "Llama 3.2 90B Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-09-25", - last_updated: "2024-09-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.72, output: 0.72 }, - limit: { context: 128000, output: 4096 }, - }, - "nvidia.nemotron-super-3-120b": { - id: "nvidia.nemotron-super-3-120b", - name: "NVIDIA Nemotron 3 Super 120B A12B", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-11", - last_updated: "2026-03-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.65 }, - limit: { context: 262144, output: 131072 }, - }, - "writer.palmyra-x5-v1:0": { - id: "writer.palmyra-x5-v1:0", - name: "Palmyra X5", - family: "palmyra", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-04-28", - last_updated: "2025-04-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 6 }, - limit: { context: 1040000, output: 8192 }, - }, - "mistral.ministral-3-8b-instruct": { - id: "mistral.ministral-3-8b-instruct", - name: "Ministral 3 8B", - family: "ministral", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.15 }, - limit: { context: 128000, output: 4096 }, - }, - "anthropic.claude-3-5-sonnet-20240620-v1:0": { - id: "anthropic.claude-3-5-sonnet-20240620-v1:0", - name: "Claude Sonnet 3.5", - family: "claude-sonnet", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-06-20", - last_updated: "2024-06-20", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 8192 }, - }, - "mistral.ministral-3-3b-instruct": { - id: "mistral.ministral-3-3b-instruct", - name: "Ministral 3 3B", - family: "ministral", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-02", - last_updated: "2025-12-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 256000, output: 8192 }, - }, - "eu.anthropic.claude-opus-4-6-v1": { - id: "eu.anthropic.claude-opus-4-6-v1", - name: "Claude Opus 4.6 (EU)", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-02-05", - last_updated: "2026-03-18", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 1000000, output: 128000 }, - }, - "amazon.nova-premier-v1:0": { - id: "amazon.nova-premier-v1:0", - name: "Nova Premier", - family: "nova", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-12-03", - last_updated: "2024-12-03", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 12.5 }, - limit: { context: 1000000, output: 16384 }, - }, - "eu.anthropic.claude-sonnet-4-20250514-v1:0": { - id: "eu.anthropic.claude-sonnet-4-20250514-v1:0", - name: "Claude Sonnet 4 (EU)", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic.claude-sonnet-4-5-20250929-v1:0": { - id: "anthropic.claude-sonnet-4-5-20250929-v1:0", - name: "Claude Sonnet 4.5", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "mistral.devstral-2-123b": { - id: "mistral.devstral-2-123b", - name: "Devstral 2 123B", - family: "devstral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2026-02-17", - last_updated: "2026-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 2 }, - limit: { context: 256000, output: 8192 }, - }, - "us.anthropic.claude-opus-4-20250514-v1:0": { - id: "us.anthropic.claude-opus-4-20250514-v1:0", - name: "Claude Opus 4 (US)", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "global.anthropic.claude-opus-4-5-20251101-v1:0": { - id: "global.anthropic.claude-opus-4-5-20251101-v1:0", - name: "Claude Opus 4.5 (Global)", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-24", - last_updated: "2025-08-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 64000 }, - }, - "mistral.voxtral-small-24b-2507": { - id: "mistral.voxtral-small-24b-2507", - name: "Voxtral Small 24B 2507", - family: "mistral", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-01", - last_updated: "2025-07-01", - modalities: { input: ["text", "audio"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.35 }, - limit: { context: 32000, output: 8192 }, - }, - "google.gemma-3-12b-it": { - id: "google.gemma-3-12b-it", - name: "Google Gemma 3 12B", - family: "gemma", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - knowledge: "2024-12", - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.049999999999999996, output: 0.09999999999999999 }, - limit: { context: 131072, output: 8192 }, - }, - "amazon.nova-pro-v1:0": { - id: "amazon.nova-pro-v1:0", - name: "Nova Pro", - family: "nova-pro", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-12-03", - last_updated: "2024-12-03", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 3.2, cache_read: 0.2 }, - limit: { context: 300000, output: 8192 }, - }, - "anthropic.claude-haiku-4-5-20251001-v1:0": { - id: "anthropic.claude-haiku-4-5-20251001-v1:0", - name: "Claude Haiku 4.5", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 200000, output: 64000 }, - }, - "minimax.minimax-m2": { - id: "minimax.minimax-m2", - name: "MiniMax M2", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-10-27", - last_updated: "2025-10-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 204608, output: 128000 }, - }, - "mistral.pixtral-large-2502-v1:0": { - id: "mistral.pixtral-large-2502-v1:0", - name: "Pixtral Large (25.02)", - family: "mistral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-04-08", - last_updated: "2025-04-08", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6 }, - limit: { context: 128000, output: 8192 }, - }, - "meta.llama4-maverick-17b-instruct-v1:0": { - id: "meta.llama4-maverick-17b-instruct-v1:0", - name: "Llama 4 Maverick 17B Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.24, output: 0.97 }, - limit: { context: 1000000, output: 16384 }, - }, - "us.anthropic.claude-sonnet-4-5-20250929-v1:0": { - id: "us.anthropic.claude-sonnet-4-5-20250929-v1:0", - name: "Claude Sonnet 4.5 (US)", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "us.anthropic.claude-haiku-4-5-20251001-v1:0": { - id: "us.anthropic.claude-haiku-4-5-20251001-v1:0", - name: "Claude Haiku 4.5 (US)", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 200000, output: 64000 }, - }, - "amazon.nova-micro-v1:0": { - id: "amazon.nova-micro-v1:0", - name: "Nova Micro", - family: "nova-micro", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-12-03", - last_updated: "2024-12-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.035, output: 0.14, cache_read: 0.00875 }, - limit: { context: 128000, output: 8192 }, - }, - "global.anthropic.claude-sonnet-4-5-20250929-v1:0": { - id: "global.anthropic.claude-sonnet-4-5-20250929-v1:0", - name: "Claude Sonnet 4.5 (Global)", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic.claude-sonnet-4-6": { - id: "anthropic.claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08", - release_date: "2026-02-17", - last_updated: "2026-03-18", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 1000000, output: 64000 }, - }, - "openai.gpt-oss-20b-1:0": { - id: "openai.gpt-oss-20b-1:0", - name: "gpt-oss-20b", - family: "gpt-oss", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.07, output: 0.3 }, - limit: { context: 128000, output: 4096 }, - }, - "us.anthropic.claude-sonnet-4-20250514-v1:0": { - id: "us.anthropic.claude-sonnet-4-20250514-v1:0", - name: "Claude Sonnet 4 (US)", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "zai.glm-5": { - id: "zai.glm-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3.2 }, - limit: { context: 202752, output: 101376 }, - }, - "qwen.qwen3-32b-v1:0": { - id: "qwen.qwen3-32b-v1:0", - name: "Qwen3 32B (dense)", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-09-18", - last_updated: "2025-09-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 16384, output: 16384 }, - }, - "deepseek.v3.2": { - id: "deepseek.v3.2", - name: "DeepSeek-V3.2", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2026-02-06", - last_updated: "2026-02-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.62, output: 1.85 }, - limit: { context: 163840, output: 81920 }, - }, - "eu.anthropic.claude-haiku-4-5-20251001-v1:0": { - id: "eu.anthropic.claude-haiku-4-5-20251001-v1:0", - name: "Claude Haiku 4.5 (EU)", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 200000, output: 64000 }, - }, - "zai.glm-4.7-flash": { - id: "zai.glm-4.7-flash", - name: "GLM-4.7-Flash", - family: "glm-flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-01-19", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.4 }, - limit: { context: 200000, output: 131072 }, - }, - "amazon.nova-2-lite-v1:0": { - id: "amazon.nova-2-lite-v1:0", - name: "Nova 2 Lite", - family: "nova", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.33, output: 2.75 }, - limit: { context: 128000, output: 4096 }, - }, - "anthropic.claude-opus-4-5-20251101-v1:0": { - id: "anthropic.claude-opus-4-5-20251101-v1:0", - name: "Claude Opus 4.5", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-24", - last_updated: "2025-08-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 64000 }, - }, - "qwen.qwen3-coder-480b-a35b-v1:0": { - id: "qwen.qwen3-coder-480b-a35b-v1:0", - name: "Qwen3 Coder 480B A35B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-09-18", - last_updated: "2025-09-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.22, output: 1.8 }, - limit: { context: 131072, output: 65536 }, - }, - "meta.llama3-2-1b-instruct-v1:0": { - id: "meta.llama3-2-1b-instruct-v1:0", - name: "Llama 3.2 1B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-09-25", - last_updated: "2024-09-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 131000, output: 4096 }, - }, - "amazon.nova-lite-v1:0": { - id: "amazon.nova-lite-v1:0", - name: "Nova Lite", - family: "nova-lite", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-12-03", - last_updated: "2024-12-03", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.06, output: 0.24, cache_read: 0.015 }, - limit: { context: 300000, output: 8192 }, - }, - "meta.llama3-1-8b-instruct-v1:0": { - id: "meta.llama3-1-8b-instruct-v1:0", - name: "Llama 3.1 8B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.22, output: 0.22 }, - limit: { context: 128000, output: 4096 }, - }, - "global.anthropic.claude-sonnet-4-6": { - id: "global.anthropic.claude-sonnet-4-6", - name: "Claude Sonnet 4.6 (Global)", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08", - release_date: "2026-02-17", - last_updated: "2026-03-18", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 1000000, output: 64000 }, - }, - "us.anthropic.claude-sonnet-4-6": { - id: "us.anthropic.claude-sonnet-4-6", - name: "Claude Sonnet 4.6 (US)", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08", - release_date: "2026-02-17", - last_updated: "2026-03-18", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 1000000, output: 64000 }, - }, - "global.anthropic.claude-opus-4-6-v1": { - id: "global.anthropic.claude-opus-4-6-v1", - name: "Claude Opus 4.6 (Global)", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-02-05", - last_updated: "2026-03-18", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 1000000, output: 128000 }, - }, - "google.gemma-3-27b-it": { - id: "google.gemma-3-27b-it", - name: "Google Gemma 3 27B Instruct", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-07-27", - last_updated: "2025-07-27", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.12, output: 0.2 }, - limit: { context: 202752, output: 8192 }, - }, - "global.anthropic.claude-haiku-4-5-20251001-v1:0": { - id: "global.anthropic.claude-haiku-4-5-20251001-v1:0", - name: "Claude Haiku 4.5 (Global)", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 200000, output: 64000 }, - }, - "google.gemma-3-4b-it": { - id: "google.gemma-3-4b-it", - name: "Gemma 3 4B IT", - family: "gemma", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.04, output: 0.08 }, - limit: { context: 128000, output: 4096 }, - }, - "us.anthropic.claude-opus-4-6-v1": { - id: "us.anthropic.claude-opus-4-6-v1", - name: "Claude Opus 4.6 (US)", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-02-05", - last_updated: "2026-03-18", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 1000000, output: 128000 }, - }, - "meta.llama4-scout-17b-instruct-v1:0": { - id: "meta.llama4-scout-17b-instruct-v1:0", - name: "Llama 4 Scout 17B Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.17, output: 0.66 }, - limit: { context: 3500000, output: 16384 }, - }, - "us.anthropic.claude-opus-4-1-20250805-v1:0": { - id: "us.anthropic.claude-opus-4-1-20250805-v1:0", - name: "Claude Opus 4.1 (US)", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "deepseek.v3-v1:0": { - id: "deepseek.v3-v1:0", - name: "DeepSeek-V3.1", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-09-18", - last_updated: "2025-09-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.58, output: 1.68 }, - limit: { context: 163840, output: 81920 }, - }, - "mistral.magistral-small-2509": { - id: "mistral.magistral-small-2509", - name: "Magistral Small 1.2", - family: "magistral", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-02", - last_updated: "2025-12-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 1.5 }, - limit: { context: 128000, output: 40000 }, - }, - "qwen.qwen3-next-80b-a3b": { - id: "qwen.qwen3-next-80b-a3b", - name: "Qwen/Qwen3-Next-80B-A3B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 1.4 }, - limit: { context: 262000, output: 262000 }, - }, - "zai.glm-4.7": { - id: "zai.glm-4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2 }, - limit: { context: 204800, output: 131072 }, - }, - "moonshot.kimi-k2-thinking": { - id: "moonshot.kimi-k2-thinking", - name: "Kimi K2 Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: true, - structured_output: true, - temperature: true, - release_date: "2025-12-02", - last_updated: "2025-12-02", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.5 }, - limit: { context: 256000, output: 256000 }, - }, - "us.anthropic.claude-opus-4-5-20251101-v1:0": { - id: "us.anthropic.claude-opus-4-5-20251101-v1:0", - name: "Claude Opus 4.5 (US)", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-24", - last_updated: "2025-08-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 64000 }, - }, - "mistral.ministral-3-14b-instruct": { - id: "mistral.ministral-3-14b-instruct", - name: "Ministral 14B 3.0", - family: "ministral", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 128000, output: 4096 }, - }, - "anthropic.claude-3-haiku-20240307-v1:0": { - id: "anthropic.claude-3-haiku-20240307-v1:0", - name: "Claude Haiku 3", - family: "claude-haiku", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-02", - release_date: "2024-03-13", - last_updated: "2024-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1.25 }, - limit: { context: 200000, output: 4096 }, - }, - "global.anthropic.claude-sonnet-4-20250514-v1:0": { - id: "global.anthropic.claude-sonnet-4-20250514-v1:0", - name: "Claude Sonnet 4 (Global)", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "deepseek.r1-v1:0": { - id: "deepseek.r1-v1:0", - name: "DeepSeek-R1", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-01-20", - last_updated: "2025-05-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.35, output: 5.4 }, - limit: { context: 128000, output: 32768 }, - }, - "meta.llama3-1-405b-instruct-v1:0": { - id: "meta.llama3-1-405b-instruct-v1:0", - name: "Llama 3.1 405B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2.4, output: 2.4 }, - limit: { context: 128000, output: 4096 }, - }, - "mistral.voxtral-mini-3b-2507": { - id: "mistral.voxtral-mini-3b-2507", - name: "Voxtral Mini 3B 2507", - family: "mistral", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["audio", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.04, output: 0.04 }, - limit: { context: 128000, output: 4096 }, - }, - "eu.anthropic.claude-sonnet-4-6": { - id: "eu.anthropic.claude-sonnet-4-6", - name: "Claude Sonnet 4.6 (EU)", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08", - release_date: "2026-02-17", - last_updated: "2026-03-18", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 1000000, output: 64000 }, - }, - "openai.gpt-oss-120b-1:0": { - id: "openai.gpt-oss-120b-1:0", - name: "gpt-oss-120b", - family: "gpt-oss", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 128000, output: 4096 }, - }, - "nvidia.nemotron-nano-12b-v2": { - id: "nvidia.nemotron-nano-12b-v2", - name: "NVIDIA Nemotron Nano 12B v2 VL BF16", - family: "nemotron", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.6 }, - limit: { context: 128000, output: 4096 }, - }, - "minimax.minimax-m2.5": { - id: "minimax.minimax-m2.5", - name: "MiniMax M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 196608, output: 98304 }, - }, - "meta.llama3-3-70b-instruct-v1:0": { - id: "meta.llama3-3-70b-instruct-v1:0", - name: "Llama 3.3 70B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.72, output: 0.72 }, - limit: { context: 128000, output: 4096 }, - }, - "meta.llama3-1-70b-instruct-v1:0": { - id: "meta.llama3-1-70b-instruct-v1:0", - name: "Llama 3.1 70B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.72, output: 0.72 }, - limit: { context: 128000, output: 4096 }, - }, - "meta.llama3-2-3b-instruct-v1:0": { - id: "meta.llama3-2-3b-instruct-v1:0", - name: "Llama 3.2 3B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-09-25", - last_updated: "2024-09-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.15 }, - limit: { context: 131000, output: 4096 }, - }, - "eu.anthropic.claude-sonnet-4-5-20250929-v1:0": { - id: "eu.anthropic.claude-sonnet-4-5-20250929-v1:0", - name: "Claude Sonnet 4.5 (EU)", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic.claude-opus-4-20250514-v1:0": { - id: "anthropic.claude-opus-4-20250514-v1:0", - name: "Claude Opus 4", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "eu.anthropic.claude-opus-4-5-20251101-v1:0": { - id: "eu.anthropic.claude-opus-4-5-20251101-v1:0", - name: "Claude Opus 4.5 (EU)", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-24", - last_updated: "2025-08-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic.claude-sonnet-4-20250514-v1:0": { - id: "anthropic.claude-sonnet-4-20250514-v1:0", - name: "Claude Sonnet 4", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "meta.llama3-2-11b-instruct-v1:0": { - id: "meta.llama3-2-11b-instruct-v1:0", - name: "Llama 3.2 11B Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-09-25", - last_updated: "2024-09-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.16, output: 0.16 }, - limit: { context: 128000, output: 4096 }, - }, - "moonshotai.kimi-k2.5": { - id: "moonshotai.kimi-k2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: true, - temperature: true, - release_date: "2026-02-06", - last_updated: "2026-02-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3 }, - limit: { context: 256000, output: 256000 }, - }, - "openai.gpt-oss-safeguard-20b": { - id: "openai.gpt-oss-safeguard-20b", - name: "GPT OSS Safeguard 20B", - family: "gpt-oss", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.07, output: 0.2 }, - limit: { context: 128000, output: 4096 }, - }, - "anthropic.claude-opus-4-6-v1": { - id: "anthropic.claude-opus-4-6-v1", - name: "Claude Opus 4.6", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-02-05", - last_updated: "2026-03-18", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 1000000, output: 128000 }, - }, - "qwen.qwen3-coder-30b-a3b-v1:0": { - id: "qwen.qwen3-coder-30b-a3b-v1:0", - name: "Qwen3 Coder 30B A3B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-09-18", - last_updated: "2025-09-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 262144, output: 131072 }, - }, - "minimax.minimax-m2.1": { - id: "minimax.minimax-m2.1", - name: "MiniMax M2.1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 204800, output: 131072 }, - }, - "qwen.qwen3-vl-235b-a22b": { - id: "qwen.qwen3-vl-235b-a22b", - name: "Qwen/Qwen3-VL-235B-A22B-Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-04", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.5 }, - limit: { context: 262000, output: 262000 }, - }, - "qwen.qwen3-coder-next": { - id: "qwen.qwen3-coder-next", - name: "Qwen3 Coder Next", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-06", - last_updated: "2026-02-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.22, output: 1.8 }, - limit: { context: 131072, output: 65536 }, - }, - "anthropic.claude-3-5-haiku-20241022-v1:0": { - id: "anthropic.claude-3-5-haiku-20241022-v1:0", - name: "Claude Haiku 3.5", - family: "claude-haiku", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 4, cache_read: 0.08, cache_write: 1 }, - limit: { context: 200000, output: 8192 }, - }, - "nvidia.nemotron-nano-9b-v2": { - id: "nvidia.nemotron-nano-9b-v2", - name: "NVIDIA Nemotron Nano 9B v2", - family: "nemotron", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.06, output: 0.23 }, - limit: { context: 128000, output: 4096 }, - }, - "mistral.mistral-large-3-675b-instruct": { - id: "mistral.mistral-large-3-675b-instruct", - name: "Mistral Large 3", - family: "mistral", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-02", - last_updated: "2025-12-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 1.5 }, - limit: { context: 256000, output: 8192 }, - }, - "qwen.qwen3-235b-a22b-2507-v1:0": { - id: "qwen.qwen3-235b-a22b-2507-v1:0", - name: "Qwen3 235B A22B 2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-09-18", - last_updated: "2025-09-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.22, output: 0.88 }, - limit: { context: 262144, output: 131072 }, - }, - "anthropic.claude-3-7-sonnet-20250219-v1:0": { - id: "anthropic.claude-3-7-sonnet-20250219-v1:0", - name: "Claude Sonnet 3.7", - family: "claude-sonnet", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-02-19", - last_updated: "2025-02-19", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 8192 }, - }, - "writer.palmyra-x4-v1:0": { - id: "writer.palmyra-x4-v1:0", - name: "Palmyra X4", - family: "palmyra", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-04-28", - last_updated: "2025-04-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10 }, - limit: { context: 122880, output: 8192 }, - }, - }, - }, - "the-grid-ai": { - id: "the-grid-ai", - env: ["THEGRIDAI_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.thegrid.ai/v1", - name: "The Grid AI", - doc: "https://thegrid.ai/docs", - models: { - "text-prime": { - id: "text-prime", - name: "Text Prime", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-26", - last_updated: "2026-02-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 30000 }, - status: "beta", - }, - "text-standard": { - id: "text-standard", - name: "Text Standard", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-26", - last_updated: "2026-02-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 16000 }, - status: "beta", - }, - "text-max": { - id: "text-max", - name: "Text Max", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-03-24", - last_updated: "2026-03-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 1000000, output: 128000 }, - status: "beta", - }, - }, - }, - baseten: { - id: "baseten", - env: ["BASETEN_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://inference.baseten.co/v1", - name: "Baseten", - doc: "https://docs.baseten.co/development/model-apis/overview", - models: { - "zai-org/GLM-4.7": { - id: "zai-org/GLM-4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2 }, - limit: { context: 204800, output: 131072 }, - }, - "zai-org/GLM-5": { - id: "zai-org/GLM-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2026-01", - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.95, output: 3.15 }, - limit: { context: 202752, output: 131072 }, - }, - "zai-org/GLM-4.6": { - id: "zai-org/GLM-4.6", - name: "GLM 4.6", - family: "glm", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2025-09-16", - last_updated: "2025-09-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2 }, - limit: { context: 200000, output: 200000 }, - }, - "nvidia/Nemotron-120B-A12B": { - id: "nvidia/Nemotron-120B-A12B", - name: "Nemotron 3 Super", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2026-02", - release_date: "2026-03-11", - last_updated: "2026-03-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.75 }, - limit: { context: 262144, output: 32678 }, - }, - "deepseek-ai/DeepSeek-V3.1": { - id: "deepseek-ai/DeepSeek-V3.1", - name: "DeepSeek V3.1", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-25", - last_updated: "2025-08-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 1.5 }, - limit: { context: 164000, output: 131000 }, - }, - "deepseek-ai/DeepSeek-V3-0324": { - id: "deepseek-ai/DeepSeek-V3-0324", - name: "DeepSeek V3 0324", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-03-24", - last_updated: "2025-03-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.77, output: 0.77 }, - limit: { context: 164000, output: 131000 }, - }, - "deepseek-ai/DeepSeek-V3.2": { - id: "deepseek-ai/DeepSeek-V3.2", - name: "DeepSeek V3.2", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-10", - release_date: "2025-12-01", - last_updated: "2026-03-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.45 }, - limit: { context: 163800, output: 131100 }, - status: "deprecated", - }, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "GPT OSS 120B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.5 }, - limit: { context: 128000, output: 128000 }, - }, - "moonshotai/Kimi-K2-Thinking": { - id: "moonshotai/Kimi-K2-Thinking", - name: "Kimi K2 Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-08", - release_date: "2025-11-06", - last_updated: "2026-03-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.5 }, - limit: { context: 262144, output: 262144 }, - status: "deprecated", - }, - "moonshotai/Kimi-K2-Instruct-0905": { - id: "moonshotai/Kimi-K2-Instruct-0905", - name: "Kimi K2 Instruct 0905", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-08", - release_date: "2025-09-05", - last_updated: "2026-03-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.5 }, - limit: { context: 262144, output: 262144 }, - status: "deprecated", - }, - "moonshotai/Kimi-K2.5": { - id: "moonshotai/Kimi-K2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-12", - release_date: "2026-01-30", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3 }, - limit: { context: 262144, output: 8192 }, - }, - "MiniMaxAI/MiniMax-M2.5": { - id: "MiniMaxAI/MiniMax-M2.5", - name: "MiniMax-M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2026-01", - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 204000, output: 204000 }, - }, - }, - }, - "zhipuai-coding-plan": { - id: "zhipuai-coding-plan", - env: ["ZHIPU_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://open.bigmodel.cn/api/coding/paas/v4", - name: "Zhipu AI Coding Plan", - doc: "https://docs.bigmodel.cn/cn/coding-plan/overview", - models: { - "glm-5v-turbo": { - id: "glm-5v-turbo", - name: "glm-5v-turbo", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-04-01", - last_updated: "2026-04-01", - modalities: { input: ["text", "image", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 200000, output: 131072 }, - }, - "glm-5.1": { - id: "glm-5.1", - name: "GLM-5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-03-27", - last_updated: "2026-03-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 200000, output: 131072 }, - }, - "glm-4.6v-flash": { - id: "glm-4.6v-flash", - name: "GLM-4.6V-Flash", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-08", - last_updated: "2025-12-08", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 32768 }, - }, - "glm-4.7-flash": { - id: "glm-4.7-flash", - name: "GLM-4.7-Flash", - family: "glm-flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-01-19", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 200000, output: 131072 }, - }, - "glm-4.5-flash": { - id: "glm-4.5-flash", - name: "GLM-4.5-Flash", - family: "glm-flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 131072, output: 98304 }, - }, - "glm-4.6v": { - id: "glm-4.6v", - name: "GLM-4.6V", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-08", - last_updated: "2025-12-08", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 32768 }, - }, - "glm-4.6": { - id: "glm-4.6", - name: "GLM-4.6", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "glm-4.5v": { - id: "glm-4.5v", - name: "GLM-4.5V", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-08-11", - last_updated: "2025-08-11", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 64000, output: 16384 }, - }, - "glm-5-turbo": { - id: "glm-5-turbo", - name: "GLM-5-Turbo", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-03-16", - last_updated: "2026-03-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 200000, output: 131072 }, - }, - "glm-4.5-air": { - id: "glm-4.5-air", - name: "GLM-4.5-Air", - family: "glm-air", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 131072, output: 98304 }, - }, - "glm-4.5": { - id: "glm-4.5", - name: "GLM-4.5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 131072, output: 98304 }, - }, - "glm-4.7-flashx": { - id: "glm-4.7-flashx", - name: "GLM-4.7-FlashX", - family: "glm-flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-01-19", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.4, cache_read: 0.01, cache_write: 0 }, - limit: { context: 200000, output: 131072 }, - }, - "glm-5": { - id: "glm-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "glm-4.7": { - id: "glm-4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - }, - }, - "alibaba-coding-plan": { - id: "alibaba-coding-plan", - env: ["ALIBABA_CODING_PLAN_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://coding-intl.dashscope.aliyuncs.com/v1", - name: "Alibaba Coding Plan", - doc: "https://www.alibabacloud.com/help/en/model-studio/coding-plan", - models: { - "qwen3-coder-plus": { - id: "qwen3-coder-plus", - name: "Qwen3 Coder Plus", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 1000000, output: 65536 }, - }, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-01", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 262144, output: 32768 }, - }, - "glm-4.7": { - id: "glm-4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 202752, output: 16384 }, - }, - "glm-5": { - id: "glm-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 202752, output: 16384 }, - }, - "MiniMax-M2.5": { - id: "MiniMax-M2.5", - name: "MiniMax-M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 196608, input: 196601, output: 24576 }, - }, - "qwen3.6-plus": { - id: "qwen3.6-plus", - name: "Qwen3.6 Plus", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-04-02", - last_updated: "2026-04-02", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 1000000, output: 65536 }, - }, - "qwen3-max-2026-01-23": { - id: "qwen3-max-2026-01-23", - name: "Qwen3 Max", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-01-23", - last_updated: "2026-01-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 262144, output: 32768 }, - }, - "qwen3-coder-next": { - id: "qwen3-coder-next", - name: "Qwen3 Coder Next", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-03", - last_updated: "2026-02-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen3.5-plus": { - id: "qwen3.5-plus", - name: "Qwen3.5 Plus", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02-16", - last_updated: "2026-02-16", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 1000000, output: 65536 }, - }, - }, - }, - venice: { - id: "venice", - env: ["VENICE_API_KEY"], - npm: "venice-ai-sdk-provider", - name: "Venice AI", - doc: "https://docs.venice.ai", - models: { - "openai-gpt-4o-mini-2024-07-18": { - id: "openai-gpt-4o-mini-2024-07-18", - name: "GPT-4o Mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-28", - last_updated: "2026-03-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1875, output: 0.75, cache_read: 0.09375 }, - limit: { context: 128000, output: 16384 }, - }, - "qwen3-next-80b": { - id: "qwen3-next-80b", - name: "Qwen 3 Next 80b", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-04-29", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.35, output: 1.9 }, - limit: { context: 256000, output: 16384 }, - }, - "qwen3-235b-a22b-instruct-2507": { - id: "qwen3-235b-a22b-instruct-2507", - name: "Qwen 3 235B A22B Instruct 2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-04-29", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.75 }, - limit: { context: 128000, output: 16384 }, - }, - "grok-41-fast": { - id: "grok-41-fast", - name: "Grok 4.1 Fast", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-12-01", - last_updated: "2026-04-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.23, output: 0.57, cache_read: 0.06 }, - limit: { context: 1000000, output: 30000 }, - }, - "claude-sonnet-4-6": { - id: "claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-17", - last_updated: "2026-03-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3.6, output: 18, cache_read: 0.36, cache_write: 4.5 }, - limit: { context: 1000000, output: 64000 }, - }, - "nvidia-nemotron-cascade-2-30b-a3b": { - id: "nvidia-nemotron-cascade-2-30b-a3b", - name: "Nemotron Cascade 2 30B A3B", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-03-24", - last_updated: "2026-04-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.14, output: 0.8 }, - limit: { context: 256000, output: 32768 }, - }, - "gemini-3-flash-preview": { - id: "gemini-3-flash-preview", - name: "Gemini 3 Flash Preview", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-12-19", - last_updated: "2026-03-12", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.7, output: 3.75, cache_read: 0.07 }, - limit: { context: 256000, output: 65536 }, - }, - "qwen3-coder-480b-a35b-instruct-turbo": { - id: "qwen3-coder-480b-a35b-instruct-turbo", - name: "Qwen 3 Coder 480B Turbo", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01-27", - last_updated: "2026-02-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.35, output: 1.5, cache_read: 0.04 }, - limit: { context: 256000, output: 65536 }, - }, - "qwen3-5-397b-a17b": { - id: "qwen3-5-397b-a17b", - name: "Qwen 3.5 397B", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-16", - last_updated: "2026-04-09", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.75, output: 4.5 }, - limit: { context: 128000, output: 32768 }, - }, - "zai-org-glm-4.7": { - id: "zai-org-glm-4.7", - name: "GLM 4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-24", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 2.65, cache_read: 0.11 }, - limit: { context: 198000, output: 16384 }, - }, - "openai-gpt-54": { - id: "openai-gpt-54", - name: "GPT-5.4", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-03-05", - last_updated: "2026-03-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3.13, output: 18.8, cache_read: 0.313 }, - limit: { context: 1000000, output: 131072 }, - }, - "zai-org-glm-4.7-flash": { - id: "zai-org-glm-4.7-flash", - name: "GLM 4.7 Flash", - family: "glm-flash", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01-29", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.125, output: 0.5 }, - limit: { context: 128000, output: 16384 }, - }, - "nvidia-nemotron-3-nano-30b-a3b": { - id: "nvidia-nemotron-3-nano-30b-a3b", - name: "NVIDIA Nemotron 3 Nano 30B", - family: "nemotron", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01-27", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.075, output: 0.3 }, - limit: { context: 128000, output: 16384 }, - }, - "qwen3-vl-235b-a22b": { - id: "qwen3-vl-235b-a22b", - name: "Qwen3 VL 235B", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01-16", - last_updated: "2026-03-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 1.5 }, - limit: { context: 256000, output: 16384 }, - }, - "openai-gpt-53-codex": { - id: "openai-gpt-53-codex", - name: "GPT-5.3 Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-24", - last_updated: "2026-03-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.19, output: 17.5, cache_read: 0.219 }, - limit: { context: 400000, output: 128000 }, - }, - "claude-sonnet-45": { - id: "claude-sonnet-45", - name: "Claude Sonnet 4.5", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-09", - release_date: "2025-01-15", - last_updated: "2026-01-28", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3.75, output: 18.75, cache_read: 0.375, cache_write: 4.69 }, - limit: { context: 198000, output: 49500 }, - }, - "openai-gpt-52": { - id: "openai-gpt-52", - name: "GPT-5.2", - family: "gpt", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2025-12-13", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.19, output: 17.5, cache_read: 0.219 }, - limit: { context: 256000, output: 65536 }, - }, - "mistral-small-3-2-24b-instruct": { - id: "mistral-small-3-2-24b-instruct", - name: "Mistral Small 3.2 24B Instruct", - family: "mistral-small", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01-15", - last_updated: "2026-03-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.09375, output: 0.25 }, - limit: { context: 256000, output: 16384 }, - }, - "claude-opus-45": { - id: "claude-opus-45", - name: "Claude Opus 4.5", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-03", - release_date: "2025-12-06", - last_updated: "2026-01-28", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 6, output: 30, cache_read: 0.6, cache_write: 7.5 }, - limit: { context: 198000, output: 49500 }, - }, - "grok-4-20-multi-agent-beta": { - id: "grok-4-20-multi-agent-beta", - name: "Grok 4.20 Multi-Agent Beta", - family: "grok-beta", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2026-03-12", - last_updated: "2026-04-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { - input: 2.27, - output: 6.8, - cache_read: 0.23, - context_over_200k: { input: 4.53, output: 13.6, cache_read: 0.23 }, - }, - limit: { context: 2000000, output: 128000 }, - }, - "minimax-m27": { - id: "minimax-m27", - name: "MiniMax M2.7", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.375, output: 1.5, cache_read: 0.075 }, - limit: { context: 198000, output: 32768 }, - }, - "qwen3-235b-a22b-thinking-2507": { - id: "qwen3-235b-a22b-thinking-2507", - name: "Qwen 3 235B A22B Thinking 2507", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-04-29", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.45, output: 3.5 }, - limit: { context: 128000, output: 16384 }, - }, - "grok-code-fast-1": { - id: "grok-code-fast-1", - name: "Grok Code Fast 1", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-01", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1.87, cache_read: 0.03 }, - limit: { context: 256000, output: 10000 }, - }, - "qwen3-5-35b-a3b": { - id: "qwen3-5-35b-a3b", - name: "Qwen 3.5 35B A3B", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-25", - last_updated: "2026-03-09", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3125, output: 1.25, cache_read: 0.15625 }, - limit: { context: 256000, output: 65536 }, - }, - "mercury-2": { - id: "mercury-2", - name: "Mercury 2", - family: "mercury", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-20", - last_updated: "2026-04-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3125, output: 0.9375, cache_read: 0.03125 }, - limit: { context: 128000, output: 50000 }, - }, - "google-gemma-3-27b-it": { - id: "google-gemma-3-27b-it", - name: "Google Gemma 3 27B Instruct", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-11-04", - last_updated: "2026-03-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.12, output: 0.2 }, - limit: { context: 198000, output: 16384 }, - }, - "olafangensan-glm-4.7-flash-heretic": { - id: "olafangensan-glm-4.7-flash-heretic", - name: "GLM 4.7 Flash Heretic", - family: "glm-flash", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-04", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.14, output: 0.8 }, - limit: { context: 200000, output: 24000 }, - }, - "openai-gpt-52-codex": { - id: "openai-gpt-52-codex", - name: "GPT-5.2 Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-08", - release_date: "2025-01-15", - last_updated: "2026-03-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.19, output: 17.5, cache_read: 0.219 }, - limit: { context: 256000, output: 65536 }, - }, - "venice-uncensored-role-play": { - id: "venice-uncensored-role-play", - name: "Venice Role Play Uncensored", - family: "venice", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-20", - last_updated: "2026-03-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 2 }, - limit: { context: 128000, output: 4096 }, - }, - "zai-org-glm-5": { - id: "zai-org-glm-5", - name: "GLM 5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3.2, cache_read: 0.2 }, - limit: { context: 198000, output: 32000 }, - }, - "zai-org-glm-4.6": { - id: "zai-org-glm-4.6", - name: "GLM 4.6", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2024-04-01", - last_updated: "2026-04-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.85, output: 2.75, cache_read: 0.3 }, - limit: { context: 198000, output: 16384 }, - }, - "mistral-small-2603": { - id: "mistral-small-2603", - name: "Mistral Small 4", - family: "mistral-small", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-03-16", - last_updated: "2026-04-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1875, output: 0.75 }, - limit: { context: 256000, output: 65536 }, - }, - "openai-gpt-oss-120b": { - id: "openai-gpt-oss-120b", - name: "OpenAI GPT OSS 120B", - family: "gpt-oss", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-11-06", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.3 }, - limit: { context: 128000, output: 16384 }, - }, - "qwen3-5-9b": { - id: "qwen3-5-9b", - name: "Qwen 3.5 9B", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-03-05", - last_updated: "2026-04-04", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.15 }, - limit: { context: 256000, output: 32768 }, - }, - "openai-gpt-54-pro": { - id: "openai-gpt-54-pro", - name: "GPT-5.4 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-03-05", - last_updated: "2026-03-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 37.5, output: 225, context_over_200k: { input: 75, output: 337.5 } }, - limit: { context: 1000000, output: 128000 }, - }, - "aion-labs.aion-2-0": { - id: "aion-labs.aion-2-0", - name: "Aion 2.0", - family: "o", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2026-03-24", - last_updated: "2026-03-31", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 2, cache_read: 0.25 }, - limit: { context: 128000, output: 32768 }, - }, - "openai-gpt-54-mini": { - id: "openai-gpt-54-mini", - name: "GPT-5.4 Mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-03-27", - last_updated: "2026-03-31", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.9375, output: 5.625, cache_read: 0.09375 }, - limit: { context: 400000, output: 128000 }, - }, - "google.gemma-4-31b-it": { - id: "google.gemma-4-31b-it", - name: "Google Gemma 4 31B Instruct", - family: "gemma", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-04-03", - last_updated: "2026-04-09", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.175, output: 0.5 }, - limit: { context: 256000, output: 8192 }, - }, - "minimax-m25": { - id: "minimax-m25", - name: "MiniMax M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-03-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.34, output: 1.19, cache_read: 0.04 }, - limit: { context: 198000, output: 32768 }, - }, - "qwen3-coder-480b-a35b-instruct": { - id: "qwen3-coder-480b-a35b-instruct", - name: "Qwen 3 Coder 480b", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-04-29", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.75, output: 3 }, - limit: { context: 256000, output: 65536 }, - }, - "zai-org-glm-5-1": { - id: "zai-org-glm-5-1", - name: "GLM 5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-04-07", - last_updated: "2026-04-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.75, output: 5.5, cache_read: 0.325 }, - limit: { context: 200000, output: 24000 }, - }, - "claude-opus-4-6": { - id: "claude-opus-4-6", - name: "Claude Opus 4.6", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-05", - last_updated: "2026-03-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 6, output: 30, cache_read: 0.6, cache_write: 7.5 }, - limit: { context: 1000000, output: 128000 }, - }, - "deepseek-v3.2": { - id: "deepseek-v3.2", - name: "DeepSeek V3.2", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-10", - release_date: "2025-12-04", - last_updated: "2026-03-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.33, output: 0.48, cache_read: 0.16 }, - limit: { context: 160000, output: 32768 }, - }, - "venice-uncensored": { - id: "venice-uncensored", - name: "Venice Uncensored 1.1", - family: "venice", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - knowledge: "2023-10", - release_date: "2025-03-18", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.9 }, - limit: { context: 32000, output: 8192 }, - }, - "qwen-3-6-plus": { - id: "qwen-3-6-plus", - name: "Qwen 3.6 Plus Uncensored", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-04-06", - last_updated: "2026-04-09", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { - input: 0.625, - output: 3.75, - cache_read: 0.0625, - cache_write: 0.78, - context_over_200k: { input: 2.5, output: 7.5 }, - }, - limit: { context: 1000000, output: 65536 }, - }, - "google.gemma-4-26b-a4b-it": { - id: "google.gemma-4-26b-a4b-it", - name: "Google Gemma 4 26B A4B Instruct", - family: "gemma", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-04-02", - last_updated: "2026-04-09", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1625, output: 0.5 }, - limit: { context: 256000, output: 8192 }, - }, - "openai-gpt-4o-2024-11-20": { - id: "openai-gpt-4o-2024-11-20", - name: "GPT-4o", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-28", - last_updated: "2026-03-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3.125, output: 12.5 }, - limit: { context: 128000, output: 16384 }, - }, - "llama-3.3-70b": { - id: "llama-3.3-70b", - name: "Llama 3.3 70B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2025-04-06", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.7, output: 2.8 }, - limit: { context: 128000, output: 4096 }, - }, - "kimi-k2-5": { - id: "kimi-k2-5", - name: "Kimi K2.5", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2026-01-27", - last_updated: "2026-03-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.56, output: 3.5, cache_read: 0.11 }, - limit: { context: 256000, output: 65536 }, - }, - "grok-4-20-beta": { - id: "grok-4-20-beta", - name: "Grok 4.20 Beta", - family: "grok-beta", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-03-12", - last_updated: "2026-04-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { - input: 2.27, - output: 6.8, - cache_read: 0.23, - context_over_200k: { input: 4.53, output: 13.6, cache_read: 0.23 }, - }, - limit: { context: 2000000, output: 128000 }, - }, - "minimax-m21": { - id: "minimax-m21", - name: "MiniMax M2.1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-01", - last_updated: "2026-03-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.35, output: 1.5, cache_read: 0.04 }, - limit: { context: 198000, output: 32768 }, - }, - "llama-3.2-3b": { - id: "llama-3.2-3b", - name: "Llama 3.2 3B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-10-03", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 128000, output: 4096 }, - }, - "arcee-trinity-large-thinking": { - id: "arcee-trinity-large-thinking", - name: "Trinity Large Thinking", - family: "trinity", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-04-02", - last_updated: "2026-04-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3125, output: 1.125, cache_read: 0.075 }, - limit: { context: 256000, output: 65536 }, - }, - "hermes-3-llama-3.1-405b": { - id: "hermes-3-llama-3.1-405b", - name: "Hermes 3 Llama 3.1 405b", - family: "hermes", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-04", - release_date: "2025-09-25", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.1, output: 3 }, - limit: { context: 128000, output: 16384 }, - }, - "gemini-3-1-pro-preview": { - id: "gemini-3-1-pro-preview", - name: "Gemini 3.1 Pro Preview", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-19", - last_updated: "2026-03-12", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - cost: { - input: 2.5, - output: 15, - cache_read: 0.5, - cache_write: 0.5, - context_over_200k: { input: 5, output: 22.5, cache_read: 0.5 }, - }, - limit: { context: 1000000, output: 32768 }, - }, - "kimi-k2-thinking": { - id: "kimi-k2-thinking", - name: "Kimi K2 Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-12-10", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.75, output: 3.2, cache_read: 0.375 }, - limit: { context: 256000, output: 65536 }, - }, - "claude-opus-4-6-fast": { - id: "claude-opus-4-6-fast", - name: "Claude Opus 4.6 Fast", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-04-08", - last_updated: "2026-04-08", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 36, output: 180, cache_read: 3.6, cache_write: 45 }, - limit: { context: 1000000, output: 128000 }, - }, - }, - }, - aihubmix: { - id: "aihubmix", - env: ["AIHUBMIX_API_KEY"], - npm: "@aihubmix/ai-sdk-provider", - name: "AIHubMix", - doc: "https://docs.aihubmix.com", - models: { - "gpt-5.1-codex-max": { - id: "gpt-5.1-codex-max", - name: "GPT-5.1-Codex-Max", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "coding-glm-5-free": { - id: "coding-glm-5-free", - name: "Coding-GLM-5-Free", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "claude-haiku-4-5": { - id: "claude-haiku-4-5", - name: "Claude Haiku 4.5", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 5.5, cache_read: 0.11, cache_write: 1.25 }, - limit: { context: 200000, output: 64000 }, - }, - "coding-glm-4.7": { - id: "coding-glm-4.7", - name: "Coding-GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_details" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 1.1, cache_read: 0.548 }, - limit: { context: 204800, output: 131072 }, - }, - "qwen3-235b-a22b-instruct-2507": { - id: "qwen3-235b-a22b-instruct-2507", - name: "Qwen3 235B A22B Instruct 2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-30", - last_updated: "2025-07-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.28, output: 1.12 }, - limit: { context: 262144, output: 262144 }, - }, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-07", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3, cache_read: 0.1 }, - limit: { context: 262144, output: 262144 }, - }, - "glm-4.7": { - id: "glm-4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_details" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 1.1, cache_read: 0.548 }, - limit: { context: 204800, output: 131072 }, - }, - "gemini-3-pro-preview-search": { - id: "gemini-3-pro-preview-search", - name: "Gemini 3 Pro Preview Search", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-11", - release_date: "2025-11-19", - last_updated: "2025-11-19", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.5 }, - limit: { context: 1000000, output: 65000 }, - }, - "glm-5": { - id: "glm-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.88, output: 2.82 }, - limit: { context: 204800, output: 131072 }, - }, - "claude-sonnet-4-6": { - id: "claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08", - release_date: "2026-02-17", - last_updated: "2026-02-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 3, - output: 15, - cache_read: 0.3, - cache_write: 3.75, - context_over_200k: { input: 6, output: 22.5, cache_read: 0.6, cache_write: 7.5 }, - }, - limit: { context: 200000, output: 64000 }, - }, - "Kimi-K2-0905": { - id: "Kimi-K2-0905", - name: "Kimi K2 0905", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 2.19 }, - limit: { context: 262144, output: 262144 }, - }, - "gpt-5-mini": { - id: "gpt-5-mini", - name: "GPT-5-Mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-09-30", - release_date: "2025-09-15", - last_updated: "2025-09-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.5, output: 6, cache_read: 0.75 }, - limit: { context: 200000, output: 64000 }, - }, - "gpt-5-nano": { - id: "gpt-5-nano", - name: "GPT-5-Nano", - family: "gpt-nano", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-09-30", - release_date: "2025-09-15", - last_updated: "2025-09-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 2, cache_read: 0.25 }, - limit: { context: 128000, output: 16384 }, - }, - "gemini-3-pro-preview": { - id: "gemini-3-pro-preview", - name: "Gemini 3 Pro Preview", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-11", - release_date: "2025-11-19", - last_updated: "2025-11-19", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.5 }, - limit: { context: 1000000, output: 65000 }, - }, - "gemini-2.5-pro": { - id: "gemini-2.5-pro", - name: "Gemini 2.5 Pro", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-15", - last_updated: "2025-09-15", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 5, cache_read: 0.31 }, - limit: { context: 2000000, output: 65000 }, - }, - "coding-minimax-m2.1-free": { - id: "coding-minimax-m2.1-free", - name: "Coding MiniMax M2.1 Free", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_details" }, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "gpt-5.2": { - id: "gpt-5.2", - name: "GPT-5.2", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, output: 128000 }, - }, - "claude-opus-4-1": { - id: "claude-opus-4-1", - name: "Claude Opus 4.1", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 16.5, output: 82.5, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "deepseek-v3.2-fast": { - id: "deepseek-v3.2-fast", - name: "DeepSeek-V3.2-Fast", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: false, - knowledge: "2024-07", - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.1, output: 3.29 }, - limit: { context: 128000, output: 128000 }, - }, - "minimax-m2.1": { - id: "minimax-m2.1", - name: "MiniMax M2.1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_details" }, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.29, output: 1.15 }, - limit: { context: 204800, output: 131072 }, - }, - "o4-mini": { - id: "o4-mini", - name: "o4-mini", - family: "o-mini", - attachment: false, - reasoning: true, - tool_call: false, - temperature: false, - knowledge: "2024-09", - release_date: "2025-09-15", - last_updated: "2025-09-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.5, output: 6, cache_read: 0.75 }, - limit: { context: 200000, output: 65536 }, - }, - "deepseek-v3.2-think": { - id: "deepseek-v3.2-think", - name: "DeepSeek-V3.2-Think", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.45 }, - limit: { context: 131000, output: 64000 }, - }, - "gpt-5.2-codex": { - id: "gpt-5.2-codex", - name: "GPT-5.2-Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-01-14", - last_updated: "2026-01-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, output: 128000 }, - }, - "gemini-2.5-flash": { - id: "gemini-2.5-flash", - name: "Gemini 2.5 Flash", - family: "gemini-flash", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-15", - last_updated: "2025-09-15", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.075, output: 0.3, cache_read: 0.02 }, - limit: { context: 1000000, output: 65000 }, - }, - "gpt-5.1-codex-mini": { - id: "gpt-5.1-codex-mini", - name: "GPT-5.1 Codex Mini", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-11", - release_date: "2025-11-15", - last_updated: "2025-11-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.03 }, - limit: { context: 400000, output: 128000 }, - }, - "qwen3-235b-a22b-thinking-2507": { - id: "qwen3-235b-a22b-thinking-2507", - name: "Qwen3 235B A22B Thinking 2507", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-30", - last_updated: "2025-07-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.28, output: 2.8 }, - limit: { context: 262144, output: 262144 }, - }, - "gpt-5.1": { - id: "gpt-5.1", - name: "GPT-5.1", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-11", - release_date: "2025-11-15", - last_updated: "2025-11-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "claude-opus-4-6-think": { - id: "claude-opus-4-6-think", - name: "Claude Opus 4.6 Think", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 5, - output: 25, - cache_read: 0.3, - cache_write: 3.75, - context_over_200k: { input: 6, output: 22, cache_read: 0.6, cache_write: 7.5 }, - }, - limit: { context: 200000, output: 128000 }, - }, - "claude-opus-4-5": { - id: "claude-opus-4-5", - name: "Claude Opus 4.5", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03", - release_date: "2025-11-25", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 32000 }, - }, - "glm-4.6v": { - id: "glm-4.6v", - name: "GLM-4.6V", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-08", - last_updated: "2025-12-08", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.14, output: 0.41 }, - limit: { context: 128000, output: 32768 }, - }, - "gpt-5-codex": { - id: "gpt-5-codex", - name: "GPT-5-Codex", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-09-15", - last_updated: "2025-09-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.13 }, - limit: { context: 400000, output: 128000 }, - }, - "qwen3-max-2026-01-23": { - id: "qwen3-max-2026-01-23", - name: "Qwen3 Max", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-23", - last_updated: "2025-09-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.34, output: 1.37 }, - limit: { context: 262144, output: 65536 }, - }, - "coding-glm-4.7-free": { - id: "coding-glm-4.7-free", - name: "Coding GLM 4.7 Free", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_details" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "qwen3-coder-next": { - id: "qwen3-coder-next", - name: "Qwen3 Coder Next", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2026-02-04", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.14, output: 0.55 }, - limit: { context: 262144, input: 262144, output: 65536 }, - }, - "qwen3-coder-480b-a35b-instruct": { - id: "qwen3-coder-480b-a35b-instruct", - name: "Qwen3 Coder 480B A35B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-08-01", - last_updated: "2025-08-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.82, output: 3.29 }, - limit: { context: 262144, output: 131000 }, - }, - "claude-opus-4-6": { - id: "claude-opus-4-6", - name: "Claude Opus 4.6", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 5, - output: 25, - cache_read: 0.3, - cache_write: 3.75, - context_over_200k: { input: 6, output: 22, cache_read: 0.6, cache_write: 7.5 }, - }, - limit: { context: 200000, output: 128000 }, - }, - "gpt-4.1-nano": { - id: "gpt-4.1-nano", - name: "GPT-4.1 nano", - family: "gpt-nano", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.03 }, - limit: { context: 1047576, output: 32768 }, - }, - "minimax-m2.5": { - id: "minimax-m2.5", - name: "MiniMax-M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.29, output: 1.15 }, - limit: { context: 204800, output: 131072 }, - }, - "deepseek-v3.2": { - id: "deepseek-v3.2", - name: "DeepSeek-V3.2", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.45 }, - limit: { context: 131000, output: 64000 }, - }, - "gpt-5-pro": { - id: "gpt-5-pro", - name: "GPT-5-Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-09-30", - release_date: "2025-09-15", - last_updated: "2025-09-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 7, output: 28, cache_read: 3.5 }, - limit: { context: 400000, output: 128000 }, - }, - "gpt-4o": { - id: "gpt-4o", - name: "GPT-4o", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-09", - release_date: "2024-05-13", - last_updated: "2024-08-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10, cache_read: 1.25 }, - limit: { context: 128000, output: 16384 }, - }, - "claude-sonnet-4-5": { - id: "claude-sonnet-4-5", - name: "Claude Sonnet 4.5", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3.3, output: 16.5, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "gpt-5": { - id: "gpt-5", - name: "GPT-5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-09-30", - release_date: "2025-09-15", - last_updated: "2025-09-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 20, cache_read: 2.5 }, - limit: { context: 400000, output: 128000 }, - }, - "qwen3.5-plus": { - id: "qwen3.5-plus", - name: "Qwen 3.5 Plus", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02-16", - last_updated: "2026-02-16", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.11, output: 0.66 }, - limit: { context: 1000000, output: 65536 }, - }, - "gpt-4.1": { - id: "gpt-4.1", - name: "GPT-4.1", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 1047576, output: 32768 }, - }, - "gpt-4.1-mini": { - id: "gpt-4.1-mini", - name: "GPT-4.1 mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.6, cache_read: 0.1 }, - limit: { context: 1047576, output: 32768 }, - }, - "gpt-5.1-codex": { - id: "gpt-5.1-codex", - name: "GPT-5.1 Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-11", - release_date: "2025-11-15", - last_updated: "2025-11-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.13 }, - limit: { context: 400000, output: 128000 }, - }, - "claude-sonnet-4-6-think": { - id: "claude-sonnet-4-6-think", - name: "Claude Sonnet 4.6 Think", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08", - release_date: "2026-02-17", - last_updated: "2026-02-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 3, - output: 15, - cache_read: 0.3, - cache_write: 3.75, - context_over_200k: { input: 6, output: 22.5, cache_read: 0.6, cache_write: 7.5 }, - }, - limit: { context: 200000, output: 64000 }, - }, - }, - }, - cerebras: { - id: "cerebras", - env: ["CEREBRAS_API_KEY"], - npm: "@ai-sdk/cerebras", - name: "Cerebras", - doc: "https://inference-docs.cerebras.ai/models/overview", - models: { - "llama3.1-8b": { - id: "llama3.1-8b", - name: "Llama 3.1 8B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 32000, output: 8000 }, - }, - "qwen-3-235b-a22b-instruct-2507": { - id: "qwen-3-235b-a22b-instruct-2507", - name: "Qwen 3 235B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-22", - last_updated: "2025-07-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 1.2 }, - limit: { context: 131000, output: 32000 }, - }, - "zai-glm-4.7": { - id: "zai-glm-4.7", - name: "Z.AI GLM-4.7", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2026-01-10", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2.25, output: 2.75, cache_read: 0, cache_write: 0 }, - limit: { context: 131072, output: 40000 }, - }, - "gpt-oss-120b": { - id: "gpt-oss-120b", - name: "GPT OSS 120B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 0.69 }, - limit: { context: 131072, output: 32768 }, - }, - }, - }, - firmware: { - id: "firmware", - env: ["FIRMWARE_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://app.frogbot.ai/api/v1", - name: "Firmware", - doc: "https://docs.frogbot.ai", - models: { - "grok-4-1-fast-reasoning": { - id: "grok-4-1-fast-reasoning", - name: "Grok 4.1 Fast (Reasoning)", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-11", - release_date: "2025-11-25", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 128000 }, - }, - "claude-haiku-4-5": { - id: "claude-haiku-4-5", - name: "Claude Haiku 4.5", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 200000, output: 64000 }, - }, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Kimi-K2.5", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "1970-01-01", - last_updated: "1970-01-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 3, cache_read: 0.1 }, - limit: { context: 256000, output: 128000 }, - }, - "gpt-5-4": { - id: "gpt-5-4", - name: "GPT-5.4", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 15, cache_read: 0.25 }, - limit: { context: 272000, output: 128000 }, - }, - "deepseek-v3-2": { - id: "deepseek-v3-2", - name: "DeepSeek v3.2", - family: "deepseek", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2024-12-26", - last_updated: "2025-09-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.58, output: 1.68, cache_read: 0.28 }, - limit: { context: 128000, output: 8192 }, - }, - "claude-sonnet-4-6": { - id: "claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2026-02-17", - release_date: "2026-02-17", - last_updated: "2026-02-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "gemini-3-flash-preview": { - id: "gemini-3-flash-preview", - name: "Gemini 3 Flash Preview", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 3, cache_read: 0.05 }, - limit: { context: 1048576, output: 65536 }, - }, - "gpt-5-mini": { - id: "gpt-5-mini", - name: "GPT-5 Mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.03 }, - limit: { context: 400000, output: 128000 }, - }, - "gpt-5-nano": { - id: "gpt-5-nano", - name: "GPT-5 Nano", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.4, cache_read: 0.01 }, - limit: { context: 400000, output: 128000 }, - }, - "gemini-3-pro-preview": { - id: "gemini-3-pro-preview", - name: "Gemini 3 Pro Preview", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-11-18", - last_updated: "2025-11-18", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2 }, - limit: { context: 1000000, output: 64000 }, - }, - "zai-glm-5-1": { - id: "zai-glm-5-1", - name: "GLM-5", - family: "glm", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-01-20", - last_updated: "2025-02-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.4, output: 4.4, cache_read: 0.26 }, - limit: { context: 198000, output: 8192 }, - }, - "gemini-2.5-pro": { - id: "gemini-2.5-pro", - name: "Gemini 2.5 Pro", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-03-20", - last_updated: "2025-06-05", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.31 }, - limit: { context: 1048576, output: 65536 }, - }, - "grok-4-1-fast-non-reasoning": { - id: "grok-4-1-fast-non-reasoning", - name: "Grok 4.1 Fast (Non-Reasoning)", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-11", - release_date: "2025-11-25", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 128000 }, - }, - "gemini-2.5-flash": { - id: "gemini-2.5-flash", - name: "Gemini 2.5 Flash", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-07-17", - last_updated: "2025-07-17", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5, cache_read: 0.075 }, - limit: { context: 1048576, output: 65536 }, - }, - "grok-code-fast-1": { - id: "grok-code-fast-1", - name: "Grok 4.1 Fast (Reasoning)", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2025-08-28", - last_updated: "2025-08-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.5, cache_read: 0.02 }, - limit: { context: 256000, output: 128000 }, - }, - "claude-opus-4-5": { - id: "claude-opus-4-5", - name: "Claude Opus 4.5", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-24", - last_updated: "2025-11-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 64000 }, - }, - "claude-opus-4-6": { - id: "claude-opus-4-6", - name: "Claude Opus 4.6", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-05-31", - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 128000 }, - }, - "gpt-oss-20b": { - id: "gpt-oss-20b", - name: "GPT OSS 20B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "1970-01-01", - last_updated: "1970-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.2 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen-3-6-plus": { - id: "qwen-3-6-plus", - name: "Qwen 3.6 Plus", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-02", - last_updated: "2026-04-03", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 3, cache_read: 0.1 }, - limit: { context: 1000000, output: 64000 }, - }, - "minimax-m2-5": { - id: "minimax-m2-5", - name: "MiniMax-M2.5", - family: "minimax", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-09", - release_date: "2025-01-15", - last_updated: "2025-02-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2, cache_read: 0.03 }, - limit: { context: 192000, output: 8192 }, - }, - "gpt-4o": { - id: "gpt-4o", - name: "GPT-4o", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-09", - release_date: "2024-05-13", - last_updated: "2024-08-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10, cache_read: 1.25 }, - limit: { context: 128000, output: 16384 }, - }, - "claude-sonnet-4-5": { - id: "claude-sonnet-4-5", - name: "Claude Sonnet 4.5", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "gpt-oss-120b": { - id: "gpt-oss-120b", - name: "GPT OSS 120B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "1970-01-01", - last_updated: "1970-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 131072, output: 32768 }, - }, - "gemini-3-1-pro-preview": { - id: "gemini-3-1-pro-preview", - name: "Gemini 3.1 Pro Preview", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2026-01", - release_date: "2026-02-18", - last_updated: "2026-02-18", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2 }, - limit: { context: 1000000, output: 64000 }, - }, - "gpt-5-3-codex": { - id: "gpt-5-3-codex", - name: "GPT-5.3 Codex", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2026-01-31", - release_date: "2026-02-15", - last_updated: "2026-02-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, output: 128000 }, - }, - }, - }, - lmstudio: { - id: "lmstudio", - env: ["LMSTUDIO_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "http://127.0.0.1:1234/v1", - name: "LMStudio", - doc: "https://lmstudio.ai/models", - models: { - "openai/gpt-oss-20b": { - id: "openai/gpt-oss-20b", - name: "GPT OSS 20B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen/qwen3-coder-30b": { - id: "qwen/qwen3-coder-30b", - name: "Qwen3 Coder 30B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen/qwen3-30b-a3b-2507": { - id: "qwen/qwen3-30b-a3b-2507", - name: "Qwen3 30B A3B 2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-30", - last_updated: "2025-07-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 16384 }, - }, - }, - }, - lucidquery: { - id: "lucidquery", - env: ["LUCIDQUERY_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://lucidquery.com/api/v1", - name: "LucidQuery AI", - doc: "https://lucidquery.com/api/docs", - models: { - "lucidnova-rf1-100b": { - id: "lucidnova-rf1-100b", - name: "LucidNova RF1 100B", - family: "nova", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2025-09-16", - release_date: "2024-12-28", - last_updated: "2025-09-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 5 }, - limit: { context: 120000, output: 8000 }, - }, - "lucidquery-nexus-coder": { - id: "lucidquery-nexus-coder", - name: "LucidQuery Nexus Coder", - family: "lucid", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2025-08-01", - release_date: "2025-09-01", - last_updated: "2025-09-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 5 }, - limit: { context: 250000, output: 60000 }, - }, - }, - }, - "moonshotai-cn": { - id: "moonshotai-cn", - env: ["MOONSHOT_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.moonshot.cn/v1", - name: "Moonshot AI (China)", - doc: "https://platform.moonshot.cn/docs/api/chat", - models: { - "kimi-k2-thinking": { - id: "kimi-k2-thinking", - name: "Kimi K2 Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-08", - release_date: "2025-11-06", - last_updated: "2025-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.5, cache_read: 0.15 }, - limit: { context: 262144, output: 262144 }, - }, - "kimi-k2-0711-preview": { - id: "kimi-k2-0711-preview", - name: "Kimi K2 0711", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-07-14", - last_updated: "2025-07-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.5, cache_read: 0.15 }, - limit: { context: 131072, output: 16384 }, - }, - "kimi-k2-turbo-preview": { - id: "kimi-k2-turbo-preview", - name: "Kimi K2 Turbo", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2.4, output: 10, cache_read: 0.6 }, - limit: { context: 262144, output: 262144 }, - }, - "kimi-k2-thinking-turbo": { - id: "kimi-k2-thinking-turbo", - name: "Kimi K2 Thinking Turbo", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-08", - release_date: "2025-11-06", - last_updated: "2025-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.15, output: 8, cache_read: 0.15 }, - limit: { context: 262144, output: 262144 }, - }, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: false, - knowledge: "2025-01", - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3, cache_read: 0.1 }, - limit: { context: 262144, output: 262144 }, - }, - "kimi-k2-0905-preview": { - id: "kimi-k2-0905-preview", - name: "Kimi K2 0905", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.5, cache_read: 0.15 }, - limit: { context: 262144, output: 262144 }, - }, - }, - }, - "azure-cognitive-services": { - id: "azure-cognitive-services", - env: ["AZURE_COGNITIVE_SERVICES_RESOURCE_NAME", "AZURE_COGNITIVE_SERVICES_API_KEY"], - npm: "@ai-sdk/azure", - name: "Azure Cognitive Services", - doc: "https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models", - models: { - "claude-haiku-4-5": { - id: "claude-haiku-4-5", - name: "Claude Haiku 4.5", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-02-31", - release_date: "2025-11-18", - last_updated: "2025-11-18", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 200000, output: 64000 }, - provider: { - npm: "@ai-sdk/anthropic", - api: "https://${AZURE_COGNITIVE_SERVICES_RESOURCE_NAME}.services.ai.azure.com/anthropic/v1", - }, - }, - "claude-opus-4-1": { - id: "claude-opus-4-1", - name: "Claude Opus 4.1", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-18", - last_updated: "2025-11-18", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - provider: { - npm: "@ai-sdk/anthropic", - api: "https://${AZURE_COGNITIVE_SERVICES_RESOURCE_NAME}.services.ai.azure.com/anthropic/v1", - }, - }, - "gpt-5.4-mini": { - id: "gpt-5.4-mini", - name: "GPT-5.4 Mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.75, output: 4.5, cache_read: 0.075 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gpt-5.4-nano": { - id: "gpt-5.4-nano", - name: "GPT-5.4 Nano", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.25, cache_read: 0.02 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "claude-opus-4-5": { - id: "claude-opus-4-5", - name: "Claude Opus 4.5", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-24", - last_updated: "2025-08-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 64000 }, - provider: { - npm: "@ai-sdk/anthropic", - api: "https://${AZURE_COGNITIVE_SERVICES_RESOURCE_NAME}.services.ai.azure.com/anthropic/v1", - }, - }, - "claude-opus-4-6": { - id: "claude-opus-4-6", - name: "Claude Opus 4.6", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 5, - output: 25, - cache_read: 0.5, - cache_write: 6.25, - context_over_200k: { input: 10, output: 37.5, cache_read: 1, cache_write: 12.5 }, - }, - limit: { context: 200000, output: 128000 }, - provider: { - npm: "@ai-sdk/anthropic", - api: "https://${AZURE_COGNITIVE_SERVICES_RESOURCE_NAME}.services.ai.azure.com/anthropic/v1", - }, - }, - "claude-sonnet-4-5": { - id: "claude-sonnet-4-5", - name: "Claude Sonnet 4.5", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-11-18", - last_updated: "2025-11-18", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - provider: { - npm: "@ai-sdk/anthropic", - api: "https://${AZURE_COGNITIVE_SERVICES_RESOURCE_NAME}.services.ai.azure.com/anthropic/v1", - }, - }, - "mai-ds-r1": { - id: "mai-ds-r1", - name: "MAI-DS-R1", - family: "mai", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2024-06", - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.35, output: 5.4 }, - limit: { context: 128000, output: 8192 }, - }, - "grok-4-fast-non-reasoning": { - id: "grok-4-fast-non-reasoning", - name: "Grok 4 Fast (Non-Reasoning)", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-09-19", - last_updated: "2025-09-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - "grok-3": { - id: "grok-3", - name: "Grok 3", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.75 }, - limit: { context: 131072, output: 8192 }, - }, - "llama-4-maverick-17b-128e-instruct-fp8": { - id: "llama-4-maverick-17b-128e-instruct-fp8", - name: "Llama 4 Maverick 17B 128E Instruct FP8", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 1 }, - limit: { context: 128000, output: 8192 }, - }, - "codestral-2501": { - id: "codestral-2501", - name: "Codestral 25.01", - family: "codestral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-03", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.9 }, - limit: { context: 256000, output: 256000 }, - }, - "gpt-5.1-codex": { - id: "gpt-5.1-codex", - name: "GPT-5.1 Codex", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-14", - last_updated: "2025-11-14", - modalities: { input: ["text", "image", "audio"], output: ["text", "image", "audio"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "gpt-4.1-mini": { - id: "gpt-4.1-mini", - name: "GPT-4.1 mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-05", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.6, cache_read: 0.1 }, - limit: { context: 1047576, output: 32768 }, - }, - "kimi-k2-thinking": { - id: "kimi-k2-thinking", - name: "Kimi K2 Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-11-06", - last_updated: "2025-12-02", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.5, cache_read: 0.15 }, - limit: { context: 262144, output: 262144 }, - }, - "gpt-4.1": { - id: "gpt-4.1", - name: "GPT-4.1", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-05", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 1047576, output: 32768 }, - }, - "deepseek-r1-0528": { - id: "deepseek-r1-0528", - name: "DeepSeek-R1-0528", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-05-28", - last_updated: "2025-05-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.35, output: 5.4 }, - limit: { context: 163840, output: 163840 }, - }, - "gpt-3.5-turbo-instruct": { - id: "gpt-3.5-turbo-instruct", - name: "GPT-3.5 Turbo Instruct", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2021-08", - release_date: "2023-09-21", - last_updated: "2023-09-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.5, output: 2 }, - limit: { context: 4096, output: 4096 }, - }, - "mistral-medium-2505": { - id: "mistral-medium-2505", - name: "Mistral Medium 3", - family: "mistral-medium", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-05", - release_date: "2025-05-07", - last_updated: "2025-05-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2 }, - limit: { context: 128000, output: 128000 }, - }, - "phi-4-reasoning-plus": { - id: "phi-4-reasoning-plus", - name: "Phi-4-reasoning-plus", - family: "phi", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.125, output: 0.5 }, - limit: { context: 32000, output: 4096 }, - }, - "cohere-embed-v3-english": { - id: "cohere-embed-v3-english", - name: "Embed v3 English", - family: "cohere-embed", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2023-11-07", - last_updated: "2023-11-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0 }, - limit: { context: 512, output: 1024 }, - }, - "gpt-4-32k": { - id: "gpt-4-32k", - name: "GPT-4 32K", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-11", - release_date: "2023-03-14", - last_updated: "2023-03-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 60, output: 120 }, - limit: { context: 32768, output: 32768 }, - }, - "gpt-5": { - id: "gpt-5", - name: "GPT-5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.13 }, - limit: { context: 272000, output: 128000 }, - }, - "phi-4": { - id: "phi-4", - name: "Phi-4", - family: "phi", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.125, output: 0.5 }, - limit: { context: 128000, output: 4096 }, - }, - "cohere-command-r-plus-08-2024": { - id: "cohere-command-r-plus-08-2024", - name: "Command R+", - family: "command-r", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-06-01", - release_date: "2024-08-30", - last_updated: "2024-08-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2.5, output: 10 }, - limit: { context: 128000, output: 4000 }, - }, - "gpt-3.5-turbo-0613": { - id: "gpt-3.5-turbo-0613", - name: "GPT-3.5 Turbo 0613", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2021-08", - release_date: "2023-06-13", - last_updated: "2023-06-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 4 }, - limit: { context: 16384, output: 16384 }, - }, - "phi-3-medium-128k-instruct": { - id: "phi-3-medium-128k-instruct", - name: "Phi-3-medium-instruct (128k)", - family: "phi", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-04-23", - last_updated: "2024-04-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.17, output: 0.68 }, - limit: { context: 128000, output: 4096 }, - }, - "gpt-4o": { - id: "gpt-4o", - name: "GPT-4o", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-09", - release_date: "2024-05-13", - last_updated: "2024-05-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10, cache_read: 1.25 }, - limit: { context: 128000, output: 16384 }, - }, - "gpt-5-pro": { - id: "gpt-5-pro", - name: "GPT-5 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-10-06", - last_updated: "2025-10-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 120 }, - limit: { context: 400000, output: 272000 }, - }, - "deepseek-v3.2": { - id: "deepseek-v3.2", - name: "DeepSeek-V3.2", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.58, output: 1.68 }, - limit: { context: 128000, output: 128000 }, - }, - o3: { - id: "o3", - name: "o3", - family: "o", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05", - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 200000, output: 100000 }, - }, - "grok-3-mini": { - id: "grok-3-mini", - name: "Grok 3 Mini", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.5, reasoning: 0.5, cache_read: 0.075 }, - limit: { context: 131072, output: 8192 }, - }, - "gpt-4.1-nano": { - id: "gpt-4.1-nano", - name: "GPT-4.1 nano", - family: "gpt-nano", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-05", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.03 }, - limit: { context: 1047576, output: 32768 }, - }, - "phi-3-small-128k-instruct": { - id: "phi-3-small-128k-instruct", - name: "Phi-3-small-instruct (128k)", - family: "phi", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-04-23", - last_updated: "2024-04-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 128000, output: 4096 }, - }, - "gpt-3.5-turbo-0301": { - id: "gpt-3.5-turbo-0301", - name: "GPT-3.5 Turbo 0301", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2021-08", - release_date: "2023-03-01", - last_updated: "2023-03-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.5, output: 2 }, - limit: { context: 4096, output: 4096 }, - }, - "phi-4-mini": { - id: "phi-4-mini", - name: "Phi-4-mini", - family: "phi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.075, output: 0.3 }, - limit: { context: 128000, output: 4096 }, - }, - "gpt-5.4": { - id: "gpt-5.4", - name: "GPT-5.4", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 15, cache_read: 0.25 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gpt-5-codex": { - id: "gpt-5-codex", - name: "GPT-5-Codex", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-09-15", - last_updated: "2025-09-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.13 }, - limit: { context: 400000, output: 128000 }, - }, - "meta-llama-3-8b-instruct": { - id: "meta-llama-3-8b-instruct", - name: "Meta-Llama-3-8B-Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-12", - release_date: "2024-04-18", - last_updated: "2024-04-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.61 }, - limit: { context: 8192, output: 2048 }, - }, - "gpt-4": { - id: "gpt-4", - name: "GPT-4", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-11", - release_date: "2023-03-14", - last_updated: "2023-03-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 60, output: 120 }, - limit: { context: 8192, output: 8192 }, - }, - "phi-4-mini-reasoning": { - id: "phi-4-mini-reasoning", - name: "Phi-4-mini-reasoning", - family: "phi", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.075, output: 0.3 }, - limit: { context: 128000, output: 4096 }, - }, - "grok-4": { - id: "grok-4", - name: "Grok 4", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, reasoning: 15, cache_read: 0.75 }, - limit: { context: 256000, output: 64000 }, - }, - "meta-llama-3.1-70b-instruct": { - id: "meta-llama-3.1-70b-instruct", - name: "Meta-Llama-3.1-70B-Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2.68, output: 3.54 }, - limit: { context: 128000, output: 32768 }, - }, - "phi-3-mini-4k-instruct": { - id: "phi-3-mini-4k-instruct", - name: "Phi-3-mini-instruct (4k)", - family: "phi", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-04-23", - last_updated: "2024-04-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.13, output: 0.52 }, - limit: { context: 4096, output: 1024 }, - }, - "deepseek-v3.1": { - id: "deepseek-v3.1", - name: "DeepSeek-V3.1", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-08-21", - last_updated: "2025-08-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.56, output: 1.68 }, - limit: { context: 131072, output: 131072 }, - }, - "text-embedding-3-small": { - id: "text-embedding-3-small", - name: "text-embedding-3-small", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - release_date: "2024-01-25", - last_updated: "2024-01-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.02, output: 0 }, - limit: { context: 8191, output: 1536 }, - }, - "o3-mini": { - id: "o3-mini", - name: "o3-mini", - family: "o-mini", - attachment: false, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05", - release_date: "2024-12-20", - last_updated: "2025-01-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.55 }, - limit: { context: 200000, output: 100000 }, - }, - "gpt-3.5-turbo-1106": { - id: "gpt-3.5-turbo-1106", - name: "GPT-3.5 Turbo 1106", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2021-08", - release_date: "2023-11-06", - last_updated: "2023-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 2 }, - limit: { context: 16384, output: 16384 }, - }, - "model-router": { - id: "model-router", - name: "Model Router", - family: "model-router", - attachment: true, - reasoning: false, - tool_call: true, - release_date: "2025-05-19", - last_updated: "2025-11-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "gpt-5.4-pro": { - id: "gpt-5.4-pro", - name: "GPT-5.4 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 30, output: 180 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "mistral-small-2503": { - id: "mistral-small-2503", - name: "Mistral Small 3.1", - family: "mistral-small", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-09", - release_date: "2025-03-01", - last_updated: "2025-03-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 128000, output: 32768 }, - }, - o1: { - id: "o1", - name: "o1", - family: "o", - attachment: false, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2023-09", - release_date: "2024-12-05", - last_updated: "2024-12-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 60, cache_read: 7.5 }, - limit: { context: 200000, output: 100000 }, - }, - "grok-4-fast-reasoning": { - id: "grok-4-fast-reasoning", - name: "Grok 4 Fast (Reasoning)", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-09-19", - last_updated: "2025-09-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - "gpt-5.1": { - id: "gpt-5.1", - name: "GPT-5.1", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-14", - last_updated: "2025-11-14", - modalities: { input: ["text", "image", "audio"], output: ["text", "image", "audio"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 272000, output: 128000 }, - }, - "grok-code-fast-1": { - id: "grok-code-fast-1", - name: "Grok Code Fast 1", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2025-08-28", - last_updated: "2025-08-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.5, cache_read: 0.02 }, - limit: { context: 256000, output: 10000 }, - }, - "cohere-embed-v3-multilingual": { - id: "cohere-embed-v3-multilingual", - name: "Embed v3 Multilingual", - family: "cohere-embed", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2023-11-07", - last_updated: "2023-11-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0 }, - limit: { context: 512, output: 1024 }, - }, - "o1-preview": { - id: "o1-preview", - name: "o1-preview", - family: "o", - attachment: false, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2023-09", - release_date: "2024-09-12", - last_updated: "2024-09-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 16.5, output: 66, cache_read: 8.25 }, - limit: { context: 128000, output: 32768 }, - }, - "gpt-3.5-turbo-0125": { - id: "gpt-3.5-turbo-0125", - name: "GPT-3.5 Turbo 0125", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2021-08", - release_date: "2024-01-25", - last_updated: "2024-01-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 1.5 }, - limit: { context: 16384, output: 16384 }, - }, - "gpt-5.1-codex-mini": { - id: "gpt-5.1-codex-mini", - name: "GPT-5.1 Codex Mini", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-14", - last_updated: "2025-11-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.025 }, - limit: { context: 400000, output: 128000 }, - }, - "cohere-embed-v-4-0": { - id: "cohere-embed-v-4-0", - name: "Embed v4", - family: "cohere-embed", - attachment: true, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-04-15", - last_updated: "2025-04-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.12, output: 0 }, - limit: { context: 128000, output: 1536 }, - }, - "gpt-5.2-codex": { - id: "gpt-5.2-codex", - name: "GPT-5.2 Codex", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-01-14", - last_updated: "2026-01-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, output: 128000 }, - }, - "o4-mini": { - id: "o4-mini", - name: "o4-mini", - family: "o-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05", - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.28 }, - limit: { context: 200000, output: 100000 }, - }, - "gpt-4-turbo-vision": { - id: "gpt-4-turbo-vision", - name: "GPT-4 Turbo Vision", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-11", - release_date: "2023-11-06", - last_updated: "2024-04-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 10, output: 30 }, - limit: { context: 128000, output: 4096 }, - }, - "gpt-5.1-chat": { - id: "gpt-5.1-chat", - name: "GPT-5.1 Chat", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-14", - last_updated: "2025-11-14", - modalities: { input: ["text", "image", "audio"], output: ["text", "image", "audio"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 128000, output: 16384 }, - }, - "meta-llama-3.1-405b-instruct": { - id: "meta-llama-3.1-405b-instruct", - name: "Meta-Llama-3.1-405B-Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 5.33, output: 16 }, - limit: { context: 128000, output: 32768 }, - }, - "llama-3.2-11b-vision-instruct": { - id: "llama-3.2-11b-vision-instruct", - name: "Llama-3.2-11B-Vision-Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-09-25", - last_updated: "2024-09-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.37, output: 0.37 }, - limit: { context: 128000, output: 8192 }, - }, - "cohere-command-a": { - id: "cohere-command-a", - name: "Command A", - family: "command-a", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-06-01", - release_date: "2025-03-13", - last_updated: "2025-03-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2.5, output: 10 }, - limit: { context: 256000, output: 8000 }, - }, - "cohere-command-r-08-2024": { - id: "cohere-command-r-08-2024", - name: "Command R", - family: "command-r", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-06-01", - release_date: "2024-08-30", - last_updated: "2024-08-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 128000, output: 4000 }, - }, - "gpt-4o-mini": { - id: "gpt-4o-mini", - name: "GPT-4o mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-09", - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6, cache_read: 0.08 }, - limit: { context: 128000, output: 16384 }, - }, - "mistral-large-2411": { - id: "mistral-large-2411", - name: "Mistral Large 24.11", - family: "mistral-large", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-09", - release_date: "2024-11-01", - last_updated: "2024-11-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6 }, - limit: { context: 128000, output: 32768 }, - }, - "gpt-5.2": { - id: "gpt-5.2", - name: "GPT-5.2", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "deepseek-v3.2-speciale": { - id: "deepseek-v3.2-speciale", - name: "DeepSeek-V3.2-Speciale", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2024-07", - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.58, output: 1.68 }, - limit: { context: 128000, output: 128000 }, - }, - "deepseek-r1": { - id: "deepseek-r1", - name: "DeepSeek-R1", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2024-07", - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.35, output: 5.4 }, - limit: { context: 163840, output: 163840 }, - }, - "llama-3.2-90b-vision-instruct": { - id: "llama-3.2-90b-vision-instruct", - name: "Llama-3.2-90B-Vision-Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-09-25", - last_updated: "2024-09-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 2.04, output: 2.04 }, - limit: { context: 128000, output: 8192 }, - }, - "text-embedding-ada-002": { - id: "text-embedding-ada-002", - name: "text-embedding-ada-002", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - release_date: "2022-12-15", - last_updated: "2022-12-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0 }, - limit: { context: 8192, output: 1536 }, - }, - "gpt-4-turbo": { - id: "gpt-4-turbo", - name: "GPT-4 Turbo", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-11", - release_date: "2023-11-06", - last_updated: "2024-04-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 10, output: 30 }, - limit: { context: 128000, output: 4096 }, - }, - "gpt-5.3-codex": { - id: "gpt-5.3-codex", - name: "GPT-5.3 Codex", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-02-24", - last_updated: "2026-02-24", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, output: 128000 }, - }, - "phi-3-small-8k-instruct": { - id: "phi-3-small-8k-instruct", - name: "Phi-3-small-instruct (8k)", - family: "phi", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-04-23", - last_updated: "2024-04-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 8192, output: 2048 }, - }, - "meta-llama-3-70b-instruct": { - id: "meta-llama-3-70b-instruct", - name: "Meta-Llama-3-70B-Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-12", - release_date: "2024-04-18", - last_updated: "2024-04-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2.68, output: 3.54 }, - limit: { context: 8192, output: 2048 }, - }, - "gpt-5-nano": { - id: "gpt-5-nano", - name: "GPT-5 Nano", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.4, cache_read: 0.01 }, - limit: { context: 272000, output: 128000 }, - }, - "gpt-5-mini": { - id: "gpt-5-mini", - name: "GPT-5 Mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.03 }, - limit: { context: 272000, output: 128000 }, - }, - "phi-4-reasoning": { - id: "phi-4-reasoning", - name: "Phi-4-reasoning", - family: "phi", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.125, output: 0.5 }, - limit: { context: 32000, output: 4096 }, - }, - "phi-3-mini-128k-instruct": { - id: "phi-3-mini-128k-instruct", - name: "Phi-3-mini-instruct (128k)", - family: "phi", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-04-23", - last_updated: "2024-04-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.13, output: 0.52 }, - limit: { context: 128000, output: 4096 }, - }, - "text-embedding-3-large": { - id: "text-embedding-3-large", - name: "text-embedding-3-large", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - release_date: "2024-01-25", - last_updated: "2024-01-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.13, output: 0 }, - limit: { context: 8191, output: 3072 }, - }, - "o1-mini": { - id: "o1-mini", - name: "o1-mini", - family: "o-mini", - attachment: false, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2023-09", - release_date: "2024-09-12", - last_updated: "2024-09-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.55 }, - limit: { context: 128000, output: 65536 }, - }, - "phi-3.5-moe-instruct": { - id: "phi-3.5-moe-instruct", - name: "Phi-3.5-MoE-instruct", - family: "phi", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-08-20", - last_updated: "2024-08-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.16, output: 0.64 }, - limit: { context: 128000, output: 4096 }, - }, - "gpt-5-chat": { - id: "gpt-5-chat", - name: "GPT-5 Chat", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: false, - temperature: false, - knowledge: "2024-10-24", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.13 }, - limit: { context: 128000, output: 16384 }, - }, - "deepseek-v3-0324": { - id: "deepseek-v3-0324", - name: "DeepSeek-V3-0324", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-03-24", - last_updated: "2025-03-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.14, output: 4.56 }, - limit: { context: 131072, output: 131072 }, - }, - "llama-3.3-70b-instruct": { - id: "llama-3.3-70b-instruct", - name: "Llama-3.3-70B-Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.71, output: 0.71 }, - limit: { context: 128000, output: 32768 }, - }, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-02-06", - last_updated: "2026-02-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3 }, - limit: { context: 262144, output: 262144 }, - provider: { - npm: "@ai-sdk/openai-compatible", - api: "https://${AZURE_RESOURCE_NAME}.services.ai.azure.com/models", - shape: "completions", - }, - }, - "meta-llama-3.1-8b-instruct": { - id: "meta-llama-3.1-8b-instruct", - name: "Meta-Llama-3.1-8B-Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.61 }, - limit: { context: 128000, output: 32768 }, - }, - "ministral-3b": { - id: "ministral-3b", - name: "Ministral 3B", - family: "ministral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-03", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.04, output: 0.04 }, - limit: { context: 128000, output: 8192 }, - }, - "phi-3-medium-4k-instruct": { - id: "phi-3-medium-4k-instruct", - name: "Phi-3-medium-instruct (4k)", - family: "phi", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-04-23", - last_updated: "2024-04-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.17, output: 0.68 }, - limit: { context: 4096, output: 1024 }, - }, - "llama-4-scout-17b-16e-instruct": { - id: "llama-4-scout-17b-16e-instruct", - name: "Llama 4 Scout 17B 16E Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.78 }, - limit: { context: 128000, output: 8192 }, - }, - "phi-3.5-mini-instruct": { - id: "phi-3.5-mini-instruct", - name: "Phi-3.5-mini-instruct", - family: "phi", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-08-20", - last_updated: "2024-08-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.13, output: 0.52 }, - limit: { context: 128000, output: 4096 }, - }, - "phi-4-multimodal": { - id: "phi-4-multimodal", - name: "Phi-4-multimodal", - family: "phi", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text", "image", "audio"], output: ["text"] }, - open_weights: true, - cost: { input: 0.08, output: 0.32, input_audio: 4 }, - limit: { context: 128000, output: 4096 }, - }, - "codex-mini": { - id: "codex-mini", - name: "Codex Mini", - family: "gpt-codex-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-04", - release_date: "2025-05-16", - last_updated: "2025-05-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.5, output: 6, cache_read: 0.375 }, - limit: { context: 200000, output: 100000 }, - }, - "gpt-5.2-chat": { - id: "gpt-5.2-chat", - name: "GPT-5.2 Chat", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 128000, output: 16384 }, - }, - "mistral-nemo": { - id: "mistral-nemo", - name: "Mistral Nemo", - family: "mistral-nemo", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.15 }, - limit: { context: 128000, output: 128000 }, - }, - }, - }, - cohere: { - id: "cohere", - env: ["COHERE_API_KEY"], - npm: "@ai-sdk/cohere", - name: "Cohere", - doc: "https://docs.cohere.com/docs/models", - models: { - "command-a-reasoning-08-2025": { - id: "command-a-reasoning-08-2025", - name: "Command A Reasoning", - family: "command-a", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-06-01", - release_date: "2025-08-21", - last_updated: "2025-08-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2.5, output: 10 }, - limit: { context: 256000, output: 32000 }, - }, - "command-r7b-12-2024": { - id: "command-r7b-12-2024", - name: "Command R7B", - family: "command-r", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-06-01", - release_date: "2024-02-27", - last_updated: "2024-02-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.0375, output: 0.15 }, - limit: { context: 128000, output: 4000 }, - }, - "c4ai-aya-vision-8b": { - id: "c4ai-aya-vision-8b", - name: "Aya Vision 8B", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-03-04", - last_updated: "2025-05-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - limit: { context: 16000, output: 4000 }, - }, - "command-r-plus-08-2024": { - id: "command-r-plus-08-2024", - name: "Command R+", - family: "command-r", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-06-01", - release_date: "2024-08-30", - last_updated: "2024-08-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2.5, output: 10 }, - limit: { context: 128000, output: 4000 }, - }, - "c4ai-aya-expanse-8b": { - id: "c4ai-aya-expanse-8b", - name: "Aya Expanse 8B", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-10-24", - last_updated: "2024-10-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 8000, output: 4000 }, - }, - "command-r7b-arabic-02-2025": { - id: "command-r7b-arabic-02-2025", - name: "Command R7B Arabic", - family: "command-r", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-06-01", - release_date: "2025-02-27", - last_updated: "2025-02-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.0375, output: 0.15 }, - limit: { context: 128000, output: 4000 }, - }, - "command-a-vision-07-2025": { - id: "command-a-vision-07-2025", - name: "Command A Vision", - family: "command-a", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-06-01", - release_date: "2025-07-31", - last_updated: "2025-07-31", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 2.5, output: 10 }, - limit: { context: 128000, output: 8000 }, - }, - "c4ai-aya-vision-32b": { - id: "c4ai-aya-vision-32b", - name: "Aya Vision 32B", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-03-04", - last_updated: "2025-05-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - limit: { context: 16000, output: 4000 }, - }, - "command-a-translate-08-2025": { - id: "command-a-translate-08-2025", - name: "Command A Translate", - family: "command-a", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-06-01", - release_date: "2025-08-28", - last_updated: "2025-08-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2.5, output: 10 }, - limit: { context: 8000, output: 8000 }, - }, - "command-r-08-2024": { - id: "command-r-08-2024", - name: "Command R", - family: "command-r", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-06-01", - release_date: "2024-08-30", - last_updated: "2024-08-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 128000, output: 4000 }, - }, - "c4ai-aya-expanse-32b": { - id: "c4ai-aya-expanse-32b", - name: "Aya Expanse 32B", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-10-24", - last_updated: "2024-10-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 128000, output: 4000 }, - }, - "command-a-03-2025": { - id: "command-a-03-2025", - name: "Command A", - family: "command-a", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-06-01", - release_date: "2025-03-13", - last_updated: "2025-03-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2.5, output: 10 }, - limit: { context: 256000, output: 8000 }, - }, - }, - }, - "cloudferro-sherlock": { - id: "cloudferro-sherlock", - env: ["CLOUDFERRO_SHERLOCK_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api-sherlock.cloudferro.com/openai/v1/", - name: "CloudFerro Sherlock", - doc: "https://docs.sherlock.cloudferro.com/", - models: { - "meta-llama/Llama-3.3-70B-Instruct": { - id: "meta-llama/Llama-3.3-70B-Instruct", - name: "Llama 3.3 70B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10-09", - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2.92, output: 2.92 }, - limit: { context: 70000, output: 70000 }, - }, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "OpenAI GPT OSS 120B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-28", - last_updated: "2025-08-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2.92, output: 2.92 }, - limit: { context: 131000, output: 131000 }, - }, - "speakleash/Bielik-11B-v3.0-Instruct": { - id: "speakleash/Bielik-11B-v3.0-Instruct", - name: "Bielik 11B v3.0 Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-03", - release_date: "2025-03-13", - last_updated: "2025-03-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.67, output: 0.67 }, - limit: { context: 32000, output: 32000 }, - }, - "speakleash/Bielik-11B-v2.6-Instruct": { - id: "speakleash/Bielik-11B-v2.6-Instruct", - name: "Bielik 11B v2.6 Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-03", - release_date: "2025-03-13", - last_updated: "2025-03-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.67, output: 0.67 }, - limit: { context: 32000, output: 32000 }, - }, - "MiniMaxAI/MiniMax-M2.5": { - id: "MiniMaxAI/MiniMax-M2.5", - name: "MiniMax-M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2026-01", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 196000, output: 196000 }, - }, - }, - }, - "kuae-cloud-coding-plan": { - id: "kuae-cloud-coding-plan", - env: ["KUAE_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://coding-plan-endpoint.kuaecloud.net/v1", - name: "KUAE Cloud Coding Plan", - doc: "https://docs.mthreads.com/kuaecloud/kuaecloud-doc-online/coding_plan/", - models: { - "GLM-4.7": { - id: "GLM-4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - }, - }, - xai: { - id: "xai", - env: ["XAI_API_KEY"], - npm: "@ai-sdk/xai", - name: "xAI", - doc: "https://docs.x.ai/docs/models", - models: { - "grok-2-1212": { - id: "grok-2-1212", - name: "Grok 2 (1212)", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2024-12-12", - last_updated: "2024-12-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 10, cache_read: 2 }, - limit: { context: 131072, output: 8192 }, - }, - "grok-vision-beta": { - id: "grok-vision-beta", - name: "Grok Vision Beta", - family: "grok-vision", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2024-11-01", - last_updated: "2024-11-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 15, cache_read: 5 }, - limit: { context: 8192, output: 4096 }, - }, - "grok-3-mini-fast": { - id: "grok-3-mini-fast", - name: "Grok 3 Mini Fast", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 4, reasoning: 4, cache_read: 0.15 }, - limit: { context: 131072, output: 8192 }, - }, - "grok-3-mini-latest": { - id: "grok-3-mini-latest", - name: "Grok 3 Mini Latest", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.5, reasoning: 0.5, cache_read: 0.075 }, - limit: { context: 131072, output: 8192 }, - }, - "grok-3-fast": { - id: "grok-3-fast", - name: "Grok 3 Fast", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 1.25 }, - limit: { context: 131072, output: 8192 }, - }, - "grok-2-vision-latest": { - id: "grok-2-vision-latest", - name: "Grok 2 Vision Latest", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2024-08-20", - last_updated: "2024-12-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 10, cache_read: 2 }, - limit: { context: 8192, output: 4096 }, - }, - "grok-4.20-0309-reasoning": { - id: "grok-4.20-0309-reasoning", - name: "Grok 4.20 (Reasoning)", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-09", - last_updated: "2026-03-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6, cache_read: 0.2, context_over_200k: { input: 4, output: 12, cache_read: 0.4 } }, - limit: { context: 2000000, output: 30000 }, - }, - "grok-4-1-fast-non-reasoning": { - id: "grok-4-1-fast-non-reasoning", - name: "Grok 4.1 Fast (Non-Reasoning)", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-11-19", - last_updated: "2025-11-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - "grok-3-mini-fast-latest": { - id: "grok-3-mini-fast-latest", - name: "Grok 3 Mini Fast Latest", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 4, reasoning: 4, cache_read: 0.15 }, - limit: { context: 131072, output: 8192 }, - }, - "grok-4-fast": { - id: "grok-4-fast", - name: "Grok 4 Fast", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-09-19", - last_updated: "2025-09-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - "grok-3-latest": { - id: "grok-3-latest", - name: "Grok 3 Latest", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.75 }, - limit: { context: 131072, output: 8192 }, - }, - "grok-2": { - id: "grok-2", - name: "Grok 2", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2024-08-20", - last_updated: "2024-08-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 10, cache_read: 2 }, - limit: { context: 131072, output: 8192 }, - }, - "grok-code-fast-1": { - id: "grok-code-fast-1", - name: "Grok Code Fast 1", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2025-08-28", - last_updated: "2025-08-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.5, cache_read: 0.02 }, - limit: { context: 256000, output: 10000 }, - }, - "grok-4.20-0309-non-reasoning": { - id: "grok-4.20-0309-non-reasoning", - name: "Grok 4.20 (Non-Reasoning)", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2026-03-09", - last_updated: "2026-03-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6, cache_read: 0.2, context_over_200k: { input: 4, output: 12, cache_read: 0.4 } }, - limit: { context: 2000000, output: 30000 }, - }, - "grok-3-fast-latest": { - id: "grok-3-fast-latest", - name: "Grok 3 Fast Latest", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 1.25 }, - limit: { context: 131072, output: 8192 }, - }, - "grok-4.20-multi-agent-0309": { - id: "grok-4.20-multi-agent-0309", - name: "Grok 4.20 Multi-Agent", - family: "grok", - attachment: true, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2026-03-09", - last_updated: "2026-03-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6, cache_read: 0.2, context_over_200k: { input: 4, output: 12, cache_read: 0.4 } }, - limit: { context: 2000000, output: 30000 }, - }, - "grok-4": { - id: "grok-4", - name: "Grok 4", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, reasoning: 15, cache_read: 0.75 }, - limit: { context: 256000, output: 64000 }, - }, - "grok-2-latest": { - id: "grok-2-latest", - name: "Grok 2 Latest", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2024-08-20", - last_updated: "2024-12-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 10, cache_read: 2 }, - limit: { context: 131072, output: 8192 }, - }, - "grok-beta": { - id: "grok-beta", - name: "Grok Beta", - family: "grok-beta", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2024-11-01", - last_updated: "2024-11-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 15, cache_read: 5 }, - limit: { context: 131072, output: 4096 }, - }, - "grok-2-vision": { - id: "grok-2-vision", - name: "Grok 2 Vision", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2024-08-20", - last_updated: "2024-08-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 10, cache_read: 2 }, - limit: { context: 8192, output: 4096 }, - }, - "grok-4-1-fast": { - id: "grok-4-1-fast", - name: "Grok 4.1 Fast", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-11-19", - last_updated: "2025-11-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - "grok-3-mini": { - id: "grok-3-mini", - name: "Grok 3 Mini", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.5, reasoning: 0.5, cache_read: 0.075 }, - limit: { context: 131072, output: 8192 }, - }, - "grok-2-vision-1212": { - id: "grok-2-vision-1212", - name: "Grok 2 Vision (1212)", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2024-08-20", - last_updated: "2024-12-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 10, cache_read: 2 }, - limit: { context: 8192, output: 4096 }, - }, - "grok-3": { - id: "grok-3", - name: "Grok 3", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.75 }, - limit: { context: 131072, output: 8192 }, - }, - "grok-4-fast-non-reasoning": { - id: "grok-4-fast-non-reasoning", - name: "Grok 4 Fast (Non-Reasoning)", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-09-19", - last_updated: "2025-09-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - }, - }, - meganova: { - id: "meganova", - env: ["MEGANOVA_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.meganova.ai/v1", - name: "Meganova", - doc: "https://docs.meganova.ai", - models: { - "Qwen/Qwen3-235B-A22B-Instruct-2507": { - id: "Qwen/Qwen3-235B-A22B-Instruct-2507", - name: "Qwen3 235B A22B Instruct 2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.09, output: 0.6 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen3.5-Plus": { - id: "Qwen/Qwen3.5-Plus", - name: "Qwen3.5 Plus", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02", - last_updated: "2026-02", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2.4, reasoning: 2.4 }, - limit: { context: 1000000, output: 65536 }, - }, - "Qwen/Qwen2.5-VL-32B-Instruct": { - id: "Qwen/Qwen2.5-VL-32B-Instruct", - name: "Qwen2.5 VL 32B Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-03-24", - last_updated: "2025-03-24", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.6 }, - limit: { context: 16384, output: 16384 }, - }, - "zai-org/GLM-4.7": { - id: "zai-org/GLM-4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.8 }, - limit: { context: 202752, output: 131072 }, - }, - "zai-org/GLM-5": { - id: "zai-org/GLM-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.8, output: 2.56 }, - limit: { context: 202752, output: 131072 }, - }, - "zai-org/GLM-4.6": { - id: "zai-org/GLM-4.6", - name: "GLM-4.6", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.45, output: 1.9 }, - limit: { context: 202752, output: 131072 }, - }, - "mistralai/Mistral-Small-3.2-24B-Instruct-2506": { - id: "mistralai/Mistral-Small-3.2-24B-Instruct-2506", - name: "Mistral Small 3.2 24B Instruct", - family: "mistral-small", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-06-20", - last_updated: "2025-06-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 32768, output: 8192 }, - }, - "mistralai/Mistral-Nemo-Instruct-2407": { - id: "mistralai/Mistral-Nemo-Instruct-2407", - name: "Mistral Nemo Instruct 2407", - family: "mistral", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0.04 }, - limit: { context: 131072, output: 65536 }, - }, - "XiaomiMiMo/MiMo-V2-Flash": { - id: "XiaomiMiMo/MiMo-V2-Flash", - name: "MiMo V2 Flash", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-12-01", - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 262144, output: 32000 }, - }, - "meta-llama/Llama-3.3-70B-Instruct": { - id: "meta-llama/Llama-3.3-70B-Instruct", - name: "Llama 3.3 70B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 131072, output: 16384 }, - }, - "deepseek-ai/DeepSeek-V3.1": { - id: "deepseek-ai/DeepSeek-V3.1", - name: "DeepSeek V3.1", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-25", - last_updated: "2025-08-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 1 }, - limit: { context: 164000, output: 164000 }, - }, - "deepseek-ai/DeepSeek-V3-0324": { - id: "deepseek-ai/DeepSeek-V3-0324", - name: "DeepSeek V3 0324", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-03-24", - last_updated: "2025-03-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 0.88 }, - limit: { context: 163840, output: 163840 }, - }, - "deepseek-ai/DeepSeek-V3.2-Exp": { - id: "deepseek-ai/DeepSeek-V3.2-Exp", - name: "DeepSeek V3.2 Exp", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-10", - last_updated: "2025-10-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 0.4 }, - limit: { context: 164000, output: 164000 }, - }, - "deepseek-ai/DeepSeek-R1-0528": { - id: "deepseek-ai/DeepSeek-R1-0528", - name: "DeepSeek R1 0528", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: false, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-07", - release_date: "2025-05-28", - last_updated: "2025-05-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 2.15 }, - limit: { context: 163840, output: 64000 }, - }, - "deepseek-ai/DeepSeek-V3.2": { - id: "deepseek-ai/DeepSeek-V3.2", - name: "DeepSeek V3.2", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-03", - last_updated: "2025-12-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.26, output: 0.38 }, - limit: { context: 164000, output: 164000 }, - }, - "moonshotai/Kimi-K2-Thinking": { - id: "moonshotai/Kimi-K2-Thinking", - name: "Kimi K2 Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-08", - release_date: "2025-11-06", - last_updated: "2025-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.6 }, - limit: { context: 262144, output: 262144 }, - }, - "moonshotai/Kimi-K2.5": { - id: "moonshotai/Kimi-K2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2026-01", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.45, output: 2.8 }, - limit: { context: 262144, output: 262144 }, - }, - "MiniMaxAI/MiniMax-M2.5": { - id: "MiniMaxAI/MiniMax-M2.5", - name: "MiniMax M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 204800, output: 131072 }, - }, - "MiniMaxAI/MiniMax-M2.1": { - id: "MiniMaxAI/MiniMax-M2.1", - name: "MiniMax M2.1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.28, output: 1.2 }, - limit: { context: 196608, output: 131072 }, - }, - }, - }, - "google-vertex-anthropic": { - id: "google-vertex-anthropic", - env: ["GOOGLE_VERTEX_PROJECT", "GOOGLE_VERTEX_LOCATION", "GOOGLE_APPLICATION_CREDENTIALS"], - npm: "@ai-sdk/google-vertex/anthropic", - name: "Vertex (Anthropic)", - doc: "https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/claude", - models: { - "claude-haiku-4-5@20251001": { - id: "claude-haiku-4-5@20251001", - name: "Claude Haiku 4.5", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 200000, output: 64000 }, - }, - "claude-sonnet-4-6@default": { - id: "claude-sonnet-4-6@default", - name: "Claude Sonnet 4.6", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08", - release_date: "2026-02-17", - last_updated: "2026-02-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 3, - output: 15, - cache_read: 0.3, - cache_write: 3.75, - context_over_200k: { input: 6, output: 22.5, cache_read: 0.6, cache_write: 7.5 }, - }, - limit: { context: 200000, output: 64000 }, - }, - "claude-3-5-haiku@20241022": { - id: "claude-3-5-haiku@20241022", - name: "Claude Haiku 3.5", - family: "claude-haiku", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07-31", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 4, cache_read: 0.08, cache_write: 1 }, - limit: { context: 200000, output: 8192 }, - }, - "claude-3-5-sonnet@20241022": { - id: "claude-3-5-sonnet@20241022", - name: "Claude Sonnet 3.5 v2", - family: "claude-sonnet", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04-30", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 8192 }, - }, - "claude-opus-4-1@20250805": { - id: "claude-opus-4-1@20250805", - name: "Claude Opus 4.1", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "claude-sonnet-4@20250514": { - id: "claude-sonnet-4@20250514", - name: "Claude Sonnet 4", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "claude-3-7-sonnet@20250219": { - id: "claude-3-7-sonnet@20250219", - name: "Claude Sonnet 3.7", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10-31", - release_date: "2025-02-19", - last_updated: "2025-02-19", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "claude-opus-4@20250514": { - id: "claude-opus-4@20250514", - name: "Claude Opus 4", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "claude-opus-4-5@20251101": { - id: "claude-opus-4-5@20251101", - name: "Claude Opus 4.5", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-24", - last_updated: "2025-11-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 64000 }, - }, - "claude-sonnet-4-5@20250929": { - id: "claude-sonnet-4-5@20250929", - name: "Claude Sonnet 4.5", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "claude-opus-4-6@default": { - id: "claude-opus-4-6@default", - name: "Claude Opus 4.6", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 5, - output: 25, - cache_read: 0.5, - cache_write: 6.25, - context_over_200k: { input: 10, output: 37.5, cache_read: 1, cache_write: 12.5 }, - }, - limit: { context: 1000000, output: 128000 }, - }, - }, - }, - evroc: { - id: "evroc", - env: ["EVROC_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://models.think.evroc.com/v1", - name: "evroc", - doc: "https://docs.evroc.com/products/think/overview.html", - models: { - "Qwen/Qwen3-VL-30B-A3B-Instruct": { - id: "Qwen/Qwen3-VL-30B-A3B-Instruct", - name: "Qwen3 VL 30B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - release_date: "2025-07-30", - last_updated: "2025-07-30", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.24, output: 0.94 }, - limit: { context: 100000, output: 100000 }, - }, - "Qwen/Qwen3-Embedding-8B": { - id: "Qwen/Qwen3-Embedding-8B", - name: "Qwen3 Embedding 8B", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - release_date: "2025-07-30", - last_updated: "2025-07-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.12, output: 0.12 }, - limit: { context: 40960, output: 40960 }, - }, - "Qwen/Qwen3-30B-A3B-Instruct-2507-FP8": { - id: "Qwen/Qwen3-30B-A3B-Instruct-2507-FP8", - name: "Qwen3 30B 2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - release_date: "2025-07-30", - last_updated: "2025-07-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.35, output: 1.42 }, - limit: { context: 64000, output: 64000 }, - }, - "mistralai/devstral-small-2-24b-instruct-2512": { - id: "mistralai/devstral-small-2-24b-instruct-2512", - name: "Devstral Small 2 24B Instruct 2512", - family: "devstral", - attachment: false, - reasoning: false, - tool_call: true, - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.12, output: 0.47 }, - limit: { context: 32768, output: 32768 }, - }, - "mistralai/Voxtral-Small-24B-2507": { - id: "mistralai/Voxtral-Small-24B-2507", - name: "Voxtral Small 24B", - family: "voxtral", - attachment: false, - reasoning: false, - tool_call: false, - release_date: "2025-03-01", - last_updated: "2025-03-01", - modalities: { input: ["audio", "text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.00236, output: 0.00236, output_audio: 2.36 }, - limit: { context: 32000, output: 32000 }, - }, - "mistralai/Magistral-Small-2509": { - id: "mistralai/Magistral-Small-2509", - name: "Magistral Small 1.2 24B", - family: "magistral-small", - attachment: false, - reasoning: false, - tool_call: false, - release_date: "2025-06-01", - last_updated: "2025-06-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.59, output: 2.36 }, - limit: { context: 131072, output: 131072 }, - }, - "microsoft/Phi-4-multimodal-instruct": { - id: "microsoft/Phi-4-multimodal-instruct", - name: "Phi-4 15B", - family: "phi", - attachment: false, - reasoning: false, - tool_call: false, - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.24, output: 0.47 }, - limit: { context: 32000, output: 32000 }, - }, - "KBLab/kb-whisper-large": { - id: "KBLab/kb-whisper-large", - name: "KB Whisper", - family: "whisper", - attachment: false, - reasoning: false, - tool_call: false, - release_date: "2024-10-01", - last_updated: "2024-10-01", - modalities: { input: ["audio"], output: ["text"] }, - open_weights: true, - cost: { input: 0.00236, output: 0.00236, output_audio: 2.36 }, - limit: { context: 448, output: 448 }, - }, - "nvidia/Llama-3.3-70B-Instruct-FP8": { - id: "nvidia/Llama-3.3-70B-Instruct-FP8", - name: "Llama 3.3 70B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.18, output: 1.18 }, - limit: { context: 131072, output: 32768 }, - }, - "openai/whisper-large-v3": { - id: "openai/whisper-large-v3", - name: "Whisper 3 Large", - family: "whisper", - attachment: false, - reasoning: false, - tool_call: false, - release_date: "2024-10-01", - last_updated: "2024-10-01", - modalities: { input: ["audio"], output: ["text"] }, - open_weights: true, - cost: { input: 0.00236, output: 0.00236, output_audio: 2.36 }, - limit: { context: 448, output: 4096 }, - }, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "GPT OSS 120B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.24, output: 0.94 }, - limit: { context: 65536, output: 65536 }, - }, - "moonshotai/Kimi-K2.5": { - id: "moonshotai/Kimi-K2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 1.47, output: 5.9 }, - limit: { context: 262144, output: 262144 }, - }, - "intfloat/multilingual-e5-large-instruct": { - id: "intfloat/multilingual-e5-large-instruct", - name: "E5 Multi-Lingual Large Embeddings 0.6B", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - release_date: "2024-06-01", - last_updated: "2024-06-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.12, output: 0.12 }, - limit: { context: 512, output: 512 }, - }, - }, - }, - synthetic: { - id: "synthetic", - env: ["SYNTHETIC_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.synthetic.new/openai/v1", - name: "Synthetic", - doc: "https://synthetic.new/pricing", - models: { - "hf:meta-llama/Llama-3.1-405B-Instruct": { - id: "hf:meta-llama/Llama-3.1-405B-Instruct", - name: "Llama-3.1-405B-Instruct", - family: "llama", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 3, output: 3 }, - limit: { context: 128000, output: 32768 }, - }, - "hf:meta-llama/Llama-4-Scout-17B-16E-Instruct": { - id: "hf:meta-llama/Llama-4-Scout-17B-16E-Instruct", - name: "Llama-4-Scout-17B-16E-Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 328000, output: 4096 }, - }, - "hf:meta-llama/Llama-3.3-70B-Instruct": { - id: "hf:meta-llama/Llama-3.3-70B-Instruct", - name: "Llama-3.3-70B-Instruct", - family: "llama", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.9, output: 0.9 }, - limit: { context: 128000, output: 32768 }, - }, - "hf:meta-llama/Llama-3.1-8B-Instruct": { - id: "hf:meta-llama/Llama-3.1-8B-Instruct", - name: "Llama-3.1-8B-Instruct", - family: "llama", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 128000, output: 32768 }, - }, - "hf:meta-llama/Llama-3.1-70B-Instruct": { - id: "hf:meta-llama/Llama-3.1-70B-Instruct", - name: "Llama-3.1-70B-Instruct", - family: "llama", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.9, output: 0.9 }, - limit: { context: 128000, output: 32768 }, - }, - "hf:meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8": { - id: "hf:meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8", - name: "Llama-4-Maverick-17B-128E-Instruct-FP8", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.22, output: 0.88 }, - limit: { context: 524000, output: 4096 }, - }, - "hf:MiniMaxAI/MiniMax-M2": { - id: "hf:MiniMaxAI/MiniMax-M2", - name: "MiniMax-M2", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-10-27", - last_updated: "2025-10-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 2.19 }, - limit: { context: 196608, output: 131000 }, - }, - "hf:MiniMaxAI/MiniMax-M2.5": { - id: "hf:MiniMaxAI/MiniMax-M2.5", - name: "MiniMax-M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-02-07", - last_updated: "2026-02-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3, cache_read: 0.6 }, - limit: { context: 191488, output: 65536 }, - }, - "hf:MiniMaxAI/MiniMax-M2.1": { - id: "hf:MiniMaxAI/MiniMax-M2.1", - name: "MiniMax-M2.1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 2.19 }, - limit: { context: 204800, output: 131072 }, - }, - "hf:Qwen/Qwen3.5-397B-A17B": { - id: "hf:Qwen/Qwen3.5-397B-A17B", - name: "Qwen3.5-97B-A17B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3, cache_read: 0.6 }, - limit: { context: 262144, output: 65536 }, - status: "beta", - }, - "hf:Qwen/Qwen2.5-Coder-32B-Instruct": { - id: "hf:Qwen/Qwen2.5-Coder-32B-Instruct", - name: "Qwen2.5-Coder-32B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2024-11-11", - last_updated: "2024-11-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.8, output: 0.8 }, - limit: { context: 32768, output: 32768 }, - }, - "hf:Qwen/Qwen3-235B-A22B-Instruct-2507": { - id: "hf:Qwen/Qwen3-235B-A22B-Instruct-2507", - name: "Qwen 3 235B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04-28", - last_updated: "2025-07-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.6 }, - limit: { context: 256000, output: 32000 }, - }, - "hf:Qwen/Qwen3-235B-A22B-Thinking-2507": { - id: "hf:Qwen/Qwen3-235B-A22B-Thinking-2507", - name: "Qwen3 235B A22B Thinking 2507", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-25", - last_updated: "2025-07-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.65, output: 3 }, - limit: { context: 256000, output: 32000 }, - }, - "hf:Qwen/Qwen3-Coder-480B-A35B-Instruct": { - id: "hf:Qwen/Qwen3-Coder-480B-A35B-Instruct", - name: "Qwen 3 Coder 480B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2, output: 2 }, - limit: { context: 256000, output: 32000 }, - }, - "hf:deepseek-ai/DeepSeek-V3.1": { - id: "hf:deepseek-ai/DeepSeek-V3.1", - name: "DeepSeek V3.1", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-21", - last_updated: "2025-08-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.56, output: 1.68 }, - limit: { context: 128000, output: 128000 }, - }, - "hf:deepseek-ai/DeepSeek-V3-0324": { - id: "hf:deepseek-ai/DeepSeek-V3-0324", - name: "DeepSeek V3 (0324)", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-08-01", - last_updated: "2025-08-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.2, output: 1.2 }, - limit: { context: 128000, output: 128000 }, - }, - "hf:deepseek-ai/DeepSeek-V3": { - id: "hf:deepseek-ai/DeepSeek-V3", - name: "DeepSeek V3", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-01-20", - last_updated: "2025-05-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.25, output: 1.25 }, - limit: { context: 128000, output: 128000 }, - }, - "hf:deepseek-ai/DeepSeek-R1": { - id: "hf:deepseek-ai/DeepSeek-R1", - name: "DeepSeek R1", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 2.19 }, - limit: { context: 128000, output: 128000 }, - }, - "hf:deepseek-ai/DeepSeek-R1-0528": { - id: "hf:deepseek-ai/DeepSeek-R1-0528", - name: "DeepSeek R1 (0528)", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-01", - last_updated: "2025-08-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 8 }, - limit: { context: 128000, output: 128000 }, - }, - "hf:deepseek-ai/DeepSeek-V3.2": { - id: "hf:deepseek-ai/DeepSeek-V3.2", - name: "DeepSeek V3.2", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 0.4, cache_read: 0.27, cache_write: 0 }, - limit: { context: 162816, input: 162816, output: 8000 }, - }, - "hf:deepseek-ai/DeepSeek-V3.1-Terminus": { - id: "hf:deepseek-ai/DeepSeek-V3.1-Terminus", - name: "DeepSeek V3.1 Terminus", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-09-22", - last_updated: "2025-09-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.2, output: 1.2 }, - limit: { context: 128000, output: 128000 }, - }, - "hf:openai/gpt-oss-120b": { - id: "hf:openai/gpt-oss-120b", - name: "GPT OSS 120B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 128000, output: 32768 }, - }, - "hf:moonshotai/Kimi-K2-Thinking": { - id: "hf:moonshotai/Kimi-K2-Thinking", - name: "Kimi K2 Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-11", - release_date: "2025-11-07", - last_updated: "2025-11-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 2.19 }, - limit: { context: 262144, output: 262144 }, - }, - "hf:moonshotai/Kimi-K2-Instruct-0905": { - id: "hf:moonshotai/Kimi-K2-Instruct-0905", - name: "Kimi K2 0905", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.2, output: 1.2 }, - limit: { context: 262144, output: 32768 }, - }, - "hf:moonshotai/Kimi-K2.5": { - id: "hf:moonshotai/Kimi-K2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-01", - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 2.19 }, - limit: { context: 262144, output: 65536 }, - }, - "hf:nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4": { - id: "hf:nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4", - name: "Nemotron 3 Super 120B", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2026-03-11", - last_updated: "2026-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1, cache_read: 0.3 }, - limit: { context: 262144, output: 65536 }, - }, - "hf:nvidia/Kimi-K2.5-NVFP4": { - id: "hf:nvidia/Kimi-K2.5-NVFP4", - name: "Kimi K2.5 (NVFP4)", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-01", - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 2.19 }, - limit: { context: 262144, output: 65536 }, - }, - "hf:zai-org/GLM-4.7-Flash": { - id: "hf:zai-org/GLM-4.7-Flash", - name: "GLM-4.7-Flash", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-01-18", - last_updated: "2026-01-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.06, output: 0.4, cache_read: 0.06 }, - limit: { context: 196608, output: 65536 }, - }, - "hf:zai-org/GLM-4.7": { - id: "hf:zai-org/GLM-4.7", - name: "GLM 4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 2.19 }, - limit: { context: 200000, output: 64000 }, - }, - "hf:zai-org/GLM-5": { - id: "hf:zai-org/GLM-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-04-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3, cache_read: 1 }, - limit: { context: 196608, output: 65536 }, - }, - "hf:zai-org/GLM-4.6": { - id: "hf:zai-org/GLM-4.6", - name: "GLM 4.6", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 2.19 }, - limit: { context: 200000, output: 64000 }, - }, - }, - }, - nvidia: { - id: "nvidia", - env: ["NVIDIA_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://integrate.api.nvidia.com/v1", - name: "Nvidia", - doc: "https://docs.api.nvidia.com/nim/", - models: { - "black-forest-labs/flux.1-dev": { - id: "black-forest-labs/flux.1-dev", - name: "FLUX.1-dev", - family: "flux", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-08", - release_date: "2024-08-01", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 4096, output: 0 }, - }, - "stepfun-ai/step-3.5-flash": { - id: "stepfun-ai/step-3.5-flash", - name: "Step 3.5 Flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-02", - last_updated: "2026-02-02", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 256000, output: 16384 }, - }, - "mistralai/codestral-22b-instruct-v0.1": { - id: "mistralai/codestral-22b-instruct-v0.1", - name: "Codestral 22b Instruct V0.1", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-05-29", - last_updated: "2024-05-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "mistralai/mistral-large-3-675b-instruct-2512": { - id: "mistralai/mistral-large-3-675b-instruct-2512", - name: "Mistral Large 3 675B Instruct 2512", - family: "mistral-large", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-12-02", - last_updated: "2025-12-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 262144 }, - }, - "mistralai/devstral-2-123b-instruct-2512": { - id: "mistralai/devstral-2-123b-instruct-2512", - name: "Devstral-2-123B-Instruct-2512", - family: "devstral", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-12", - release_date: "2025-12-08", - last_updated: "2025-12-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 262144 }, - }, - "mistralai/ministral-14b-instruct-2512": { - id: "mistralai/ministral-14b-instruct-2512", - name: "Ministral 3 14B Instruct 2512", - family: "ministral", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-12", - release_date: "2025-12-01", - last_updated: "2025-12-08", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 262144 }, - }, - "mistralai/mistral-small-3.1-24b-instruct-2503": { - id: "mistralai/mistral-small-3.1-24b-instruct-2503", - name: "Mistral Small 3.1 24b Instruct 2503", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-03-11", - last_updated: "2025-03-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "mistralai/mamba-codestral-7b-v0.1": { - id: "mistralai/mamba-codestral-7b-v0.1", - name: "Mamba Codestral 7b V0.1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2024-07-16", - last_updated: "2024-07-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "mistralai/mistral-large-2-instruct": { - id: "mistralai/mistral-large-2-instruct", - name: "Mistral Large 2 Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-07-24", - last_updated: "2024-07-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "microsoft/phi-3-medium-4k-instruct": { - id: "microsoft/phi-3-medium-4k-instruct", - name: "Phi 3 Medium 4k Instruct", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-05-07", - last_updated: "2024-05-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 4000, output: 4096 }, - }, - "microsoft/phi-3.5-moe-instruct": { - id: "microsoft/phi-3.5-moe-instruct", - name: "Phi 3.5 Moe Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-08-17", - last_updated: "2024-08-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "microsoft/phi-4-mini-instruct": { - id: "microsoft/phi-4-mini-instruct", - name: "Phi-4-Mini", - family: "phi", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2024-12-01", - last_updated: "2025-09-05", - modalities: { input: ["text", "image", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 8192 }, - }, - "microsoft/phi-3-small-8k-instruct": { - id: "microsoft/phi-3-small-8k-instruct", - name: "Phi 3 Small 8k Instruct", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-05-07", - last_updated: "2024-05-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 8000, output: 4096 }, - }, - "microsoft/phi-3-vision-128k-instruct": { - id: "microsoft/phi-3-vision-128k-instruct", - name: "Phi 3 Vision 128k Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-05-19", - last_updated: "2024-05-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "microsoft/phi-3.5-vision-instruct": { - id: "microsoft/phi-3.5-vision-instruct", - name: "Phi 3.5 Vision Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-08-16", - last_updated: "2024-08-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "microsoft/phi-3-small-128k-instruct": { - id: "microsoft/phi-3-small-128k-instruct", - name: "Phi 3 Small 128k Instruct", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-05-07", - last_updated: "2024-05-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "microsoft/phi-3-medium-128k-instruct": { - id: "microsoft/phi-3-medium-128k-instruct", - name: "Phi 3 Medium 128k Instruct", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-05-07", - last_updated: "2024-05-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "nvidia/llama-3.3-nemotron-super-49b-v1": { - id: "nvidia/llama-3.3-nemotron-super-49b-v1", - name: "Llama 3.3 Nemotron Super 49b V1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-03-16", - last_updated: "2025-03-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "nvidia/nemotron-4-340b-instruct": { - id: "nvidia/nemotron-4-340b-instruct", - name: "Nemotron 4 340b Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-06-13", - last_updated: "2024-06-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "nvidia/llama3-chatqa-1.5-70b": { - id: "nvidia/llama3-chatqa-1.5-70b", - name: "Llama3 Chatqa 1.5 70b", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-04-28", - last_updated: "2024-04-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "nvidia/llama-3.3-nemotron-super-49b-v1.5": { - id: "nvidia/llama-3.3-nemotron-super-49b-v1.5", - name: "Llama 3.3 Nemotron Super 49b V1.5", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-03-16", - last_updated: "2025-03-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "nvidia/nemotron-3-super-120b-a12b": { - id: "nvidia/nemotron-3-super-120b-a12b", - name: "Nemotron 3 Super", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2026-03-11", - last_updated: "2026-03-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.8 }, - limit: { context: 262144, output: 262144 }, - }, - "nvidia/parakeet-tdt-0.6b-v2": { - id: "nvidia/parakeet-tdt-0.6b-v2", - name: "Parakeet TDT 0.6B v2", - family: "parakeet", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2024-01", - release_date: "2024-01-01", - last_updated: "2025-09-05", - modalities: { input: ["audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 0, output: 4096 }, - }, - "nvidia/nemotron-3-nano-30b-a3b": { - id: "nvidia/nemotron-3-nano-30b-a3b", - name: "nemotron-3-nano-30b-a3b", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-09", - release_date: "2024-12", - last_updated: "2024-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 131072 }, - }, - "nvidia/llama-3.1-nemotron-ultra-253b-v1": { - id: "nvidia/llama-3.1-nemotron-ultra-253b-v1", - name: "Llama-3.1-Nemotron-Ultra-253B-v1", - family: "llama", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2024-07-01", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 8192 }, - }, - "nvidia/nvidia-nemotron-nano-9b-v2": { - id: "nvidia/nvidia-nemotron-nano-9b-v2", - name: "nvidia-nemotron-nano-9b-v2", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-09", - release_date: "2025-08-18", - last_updated: "2025-08-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 131072 }, - }, - "nvidia/cosmos-nemotron-34b": { - id: "nvidia/cosmos-nemotron-34b", - name: "Cosmos Nemotron 34B", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2024-01", - release_date: "2024-01-01", - last_updated: "2025-09-05", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 8192 }, - }, - "nvidia/llama-embed-nemotron-8b": { - id: "nvidia/llama-embed-nemotron-8b", - name: "Llama Embed Nemotron 8B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2025-03", - release_date: "2025-03-18", - last_updated: "2025-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 32768, output: 2048 }, - }, - "nvidia/llama-3.1-nemotron-51b-instruct": { - id: "nvidia/llama-3.1-nemotron-51b-instruct", - name: "Llama 3.1 Nemotron 51b Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-09-22", - last_updated: "2024-09-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "nvidia/llama-3.1-nemotron-70b-instruct": { - id: "nvidia/llama-3.1-nemotron-70b-instruct", - name: "Llama 3.1 Nemotron 70b Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-10-12", - last_updated: "2024-10-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "nvidia/nemoretriever-ocr-v1": { - id: "nvidia/nemoretriever-ocr-v1", - name: "NeMo Retriever OCR v1", - family: "nemoretriever", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2024-01", - release_date: "2024-01-01", - last_updated: "2025-09-05", - modalities: { input: ["image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 0, output: 4096 }, - }, - "deepseek-ai/deepseek-r1": { - id: "deepseek-ai/deepseek-r1", - name: "Deepseek R1", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "deepseek-ai/deepseek-v3.1": { - id: "deepseek-ai/deepseek-v3.1", - name: "DeepSeek V3.1", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-08-20", - last_updated: "2025-08-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 8192 }, - }, - "deepseek-ai/deepseek-coder-6.7b-instruct": { - id: "deepseek-ai/deepseek-coder-6.7b-instruct", - name: "Deepseek Coder 6.7b Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2023-10-29", - last_updated: "2023-10-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "deepseek-ai/deepseek-v3.2": { - id: "deepseek-ai/deepseek-v3.2", - name: "DeepSeek V3.2", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 163840, output: 65536 }, - }, - "deepseek-ai/deepseek-r1-0528": { - id: "deepseek-ai/deepseek-r1-0528", - name: "Deepseek R1 0528", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-05-28", - last_updated: "2025-05-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "deepseek-ai/deepseek-v3.1-terminus": { - id: "deepseek-ai/deepseek-v3.1-terminus", - name: "DeepSeek V3.1 Terminus", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-09-22", - last_updated: "2025-09-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 8192 }, - }, - "openai/whisper-large-v3": { - id: "openai/whisper-large-v3", - name: "Whisper Large v3", - family: "whisper", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2023-09", - release_date: "2023-09-01", - last_updated: "2025-09-05", - modalities: { input: ["audio"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 0, output: 4096 }, - }, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "GPT-OSS-120B", - family: "gpt-oss", - attachment: true, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2025-08", - release_date: "2025-08-04", - last_updated: "2025-08-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 8192 }, - }, - "minimaxai/minimax-m2.1": { - id: "minimaxai/minimax-m2.1", - name: "MiniMax-M2.1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "minimaxai/minimax-m2.5": { - id: "minimaxai/minimax-m2.5", - name: "MiniMax-M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08", - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "z-ai/glm4.7": { - id: "z-ai/glm4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "z-ai/glm5": { - id: "z-ai/glm5", - name: "GLM5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 202752, output: 131000 }, - }, - "meta/llama-4-scout-17b-16e-instruct": { - id: "meta/llama-4-scout-17b-16e-instruct", - name: "Llama 4 Scout 17b 16e Instruct", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-02", - release_date: "2025-04-02", - last_updated: "2025-04-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "meta/llama-3.3-70b-instruct": { - id: "meta/llama-3.3-70b-instruct", - name: "Llama 3.3 70b Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-11-26", - last_updated: "2024-11-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "meta/llama3-8b-instruct": { - id: "meta/llama3-8b-instruct", - name: "Llama3 8b Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-04-17", - last_updated: "2024-04-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "meta/llama3-70b-instruct": { - id: "meta/llama3-70b-instruct", - name: "Llama3 70b Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-04-17", - last_updated: "2024-04-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "meta/codellama-70b": { - id: "meta/codellama-70b", - name: "Codellama 70b", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2024-01-29", - last_updated: "2024-01-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "meta/llama-3.2-11b-vision-instruct": { - id: "meta/llama-3.2-11b-vision-instruct", - name: "Llama 3.2 11b Vision Instruct", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-09-18", - last_updated: "2024-09-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "meta/llama-3.1-70b-instruct": { - id: "meta/llama-3.1-70b-instruct", - name: "Llama 3.1 70b Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-07-16", - last_updated: "2024-07-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "meta/llama-3.2-1b-instruct": { - id: "meta/llama-3.2-1b-instruct", - name: "Llama 3.2 1b Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-09-18", - last_updated: "2024-09-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "meta/llama-4-maverick-17b-128e-instruct": { - id: "meta/llama-4-maverick-17b-128e-instruct", - name: "Llama 4 Maverick 17b 128e Instruct", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-02", - release_date: "2025-04-01", - last_updated: "2025-04-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "meta/llama-3.1-405b-instruct": { - id: "meta/llama-3.1-405b-instruct", - name: "Llama 3.1 405b Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-07-16", - last_updated: "2024-07-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "qwen/qwen3-235b-a22b": { - id: "qwen/qwen3-235b-a22b", - name: "Qwen3-235B-A22B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2024-12-01", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen/qwen2.5-coder-7b-instruct": { - id: "qwen/qwen2.5-coder-7b-instruct", - name: "Qwen2.5 Coder 7b Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-09-17", - last_updated: "2024-09-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "qwen/qwen2.5-coder-32b-instruct": { - id: "qwen/qwen2.5-coder-32b-instruct", - name: "Qwen2.5 Coder 32b Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-11-06", - last_updated: "2024-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "qwen/qwen3.5-397b-a17b": { - id: "qwen/qwen3.5-397b-a17b", - name: "Qwen3.5-397B-A17B", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2026-01", - release_date: "2026-02-16", - last_updated: "2026-02-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 8192 }, - }, - "qwen/qwen3-next-80b-a3b-thinking": { - id: "qwen/qwen3-next-80b-a3b-thinking", - name: "Qwen3-Next-80B-A3B-Thinking", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2024-12-01", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 16384 }, - }, - "qwen/qwq-32b": { - id: "qwen/qwq-32b", - name: "Qwq 32b", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-03-05", - last_updated: "2025-03-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "qwen/qwen3-next-80b-a3b-instruct": { - id: "qwen/qwen3-next-80b-a3b-instruct", - name: "Qwen3-Next-80B-A3B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2024-12-01", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 16384 }, - }, - "qwen/qwen3-coder-480b-a35b-instruct": { - id: "qwen/qwen3-coder-480b-a35b-instruct", - name: "Qwen3 Coder 480B A35B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 66536 }, - }, - "google/gemma-2-2b-it": { - id: "google/gemma-2-2b-it", - name: "Gemma 2 2b It", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-07-16", - last_updated: "2024-07-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "google/gemma-3n-e4b-it": { - id: "google/gemma-3n-e4b-it", - name: "Gemma 3n E4b It", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-06", - release_date: "2025-06-03", - last_updated: "2025-06-03", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "google/gemma-3-1b-it": { - id: "google/gemma-3-1b-it", - name: "Gemma 3 1b It", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-03-10", - last_updated: "2025-03-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "google/codegemma-7b": { - id: "google/codegemma-7b", - name: "Codegemma 7b", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2024-03-21", - last_updated: "2024-03-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "google/gemma-4-31b-it": { - id: "google/gemma-4-31b-it", - name: "Gemma-4-31B-IT", - family: "gemma", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-04-02", - last_updated: "2026-04-02", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 256000, output: 16384 }, - }, - "google/gemma-3-12b-it": { - id: "google/gemma-3-12b-it", - name: "Gemma 3 12b It", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-03-01", - last_updated: "2025-03-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "google/codegemma-1.1-7b": { - id: "google/codegemma-1.1-7b", - name: "Codegemma 1.1 7b", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2024-04-30", - last_updated: "2024-04-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "google/gemma-3n-e2b-it": { - id: "google/gemma-3n-e2b-it", - name: "Gemma 3n E2b It", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-06", - release_date: "2025-06-12", - last_updated: "2025-06-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "google/gemma-2-27b-it": { - id: "google/gemma-2-27b-it", - name: "Gemma 2 27b It", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-06-24", - last_updated: "2024-06-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "google/gemma-3-27b-it": { - id: "google/gemma-3-27b-it", - name: "Gemma-3-27B-IT", - family: "gemma", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2024-12-01", - last_updated: "2025-09-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 8192 }, - }, - "moonshotai/kimi-k2.5": { - id: "moonshotai/kimi-k2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-07", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 262144 }, - }, - "moonshotai/kimi-k2-instruct": { - id: "moonshotai/kimi-k2-instruct", - name: "Kimi K2 Instruct", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-01", - release_date: "2025-01-01", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 8192 }, - }, - "moonshotai/kimi-k2-instruct-0905": { - id: "moonshotai/kimi-k2-instruct-0905", - name: "Kimi K2 0905", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 262144 }, - }, - "moonshotai/kimi-k2-thinking": { - id: "moonshotai/kimi-k2-thinking", - name: "Kimi K2 Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: true, - structured_output: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-11", - last_updated: "2025-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 262144, output: 262144 }, - }, - }, - }, - inference: { - id: "inference", - env: ["INFERENCE_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://inference.net/v1", - name: "Inference", - doc: "https://inference.net/models", - models: { - "mistral/mistral-nemo-12b-instruct": { - id: "mistral/mistral-nemo-12b-instruct", - name: "Mistral Nemo 12B Instruct", - family: "mistral-nemo", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.038, output: 0.1 }, - limit: { context: 16000, output: 4096 }, - }, - "meta/llama-3.2-11b-vision-instruct": { - id: "meta/llama-3.2-11b-vision-instruct", - name: "Llama 3.2 11B Vision Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.055, output: 0.055 }, - limit: { context: 16000, output: 4096 }, - }, - "meta/llama-3.2-1b-instruct": { - id: "meta/llama-3.2-1b-instruct", - name: "Llama 3.2 1B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.01, output: 0.01 }, - limit: { context: 16000, output: 4096 }, - }, - "meta/llama-3.2-3b-instruct": { - id: "meta/llama-3.2-3b-instruct", - name: "Llama 3.2 3B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0.02 }, - limit: { context: 16000, output: 4096 }, - }, - "meta/llama-3.1-8b-instruct": { - id: "meta/llama-3.1-8b-instruct", - name: "Llama 3.1 8B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.025, output: 0.025 }, - limit: { context: 16000, output: 4096 }, - }, - "qwen/qwen-2.5-7b-vision-instruct": { - id: "qwen/qwen-2.5-7b-vision-instruct", - name: "Qwen 2.5 7B Vision Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 125000, output: 4096 }, - }, - "qwen/qwen3-embedding-4b": { - id: "qwen/qwen3-embedding-4b", - name: "Qwen 3 Embedding 4B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2024-12", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.01, output: 0 }, - limit: { context: 32000, output: 2048 }, - }, - "google/gemma-3": { - id: "google/gemma-3", - name: "Google Gemma 3", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.3 }, - limit: { context: 125000, output: 4096 }, - }, - "osmosis/osmosis-structure-0.6b": { - id: "osmosis/osmosis-structure-0.6b", - name: "Osmosis Structure 0.6B", - family: "osmosis", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.5 }, - limit: { context: 4000, output: 2048 }, - }, - }, - }, - inception: { - id: "inception", - env: ["INCEPTION_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.inceptionlabs.ai/v1/", - name: "Inception", - doc: "https://platform.inceptionlabs.ai/docs", - models: { - "mercury-edit": { - id: "mercury-edit", - name: "Mercury Edit", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2026-02-24", - last_updated: "2026-02-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 0.75, cache_read: 0.025 }, - limit: { context: 128000, output: 8192 }, - }, - "mercury-2": { - id: "mercury-2", - name: "Mercury 2", - family: "mercury", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2026-02-24", - last_updated: "2026-02-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 0.75, cache_read: 0.025 }, - limit: { context: 128000, output: 50000 }, - }, - mercury: { - id: "mercury", - name: "Mercury", - family: "mercury", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2025-06-26", - last_updated: "2025-07-31", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1, cache_read: 0.25, cache_write: 1 }, - limit: { context: 128000, output: 16384 }, - }, - "mercury-coder": { - id: "mercury-coder", - name: "Mercury Coder", - family: "mercury", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2025-02-26", - last_updated: "2025-07-31", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1, cache_read: 0.25, cache_write: 1 }, - limit: { context: 128000, output: 16384 }, - }, - }, - }, - openai: { - id: "openai", - env: ["OPENAI_API_KEY"], - npm: "@ai-sdk/openai", - name: "OpenAI", - doc: "https://platform.openai.com/docs/models", - models: { - "gpt-5.1-codex-max": { - id: "gpt-5.1-codex-max", - name: "GPT-5.1 Codex Max", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gpt-4o-2024-05-13": { - id: "gpt-4o-2024-05-13", - name: "GPT-4o (2024-05-13)", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-09", - release_date: "2024-05-13", - last_updated: "2024-05-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 15 }, - limit: { context: 128000, output: 4096 }, - }, - "o1-mini": { - id: "o1-mini", - name: "o1-mini", - family: "o-mini", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: true, - temperature: false, - knowledge: "2023-09", - release_date: "2024-09-12", - last_updated: "2024-09-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.55 }, - limit: { context: 128000, output: 65536 }, - }, - "gpt-5.2-pro": { - id: "gpt-5.2-pro", - name: "GPT-5.2 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 21, output: 168 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "text-embedding-3-large": { - id: "text-embedding-3-large", - name: "text-embedding-3-large", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2024-01", - release_date: "2024-01-25", - last_updated: "2024-01-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.13, output: 0 }, - limit: { context: 8191, output: 3072 }, - }, - "gpt-5.3-chat-latest": { - id: "gpt-5.3-chat-latest", - name: "GPT-5.3 Chat (latest)", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-03-03", - last_updated: "2026-03-03", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 128000, output: 16384 }, - }, - "gpt-5-mini": { - id: "gpt-5-mini", - name: "GPT-5 Mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.025 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gpt-5-nano": { - id: "gpt-5-nano", - name: "GPT-5 Nano", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.4, cache_read: 0.005 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gpt-5.3-codex": { - id: "gpt-5.3-codex", - name: "GPT-5.3 Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gpt-4-turbo": { - id: "gpt-4-turbo", - name: "GPT-4 Turbo", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - knowledge: "2023-12", - release_date: "2023-11-06", - last_updated: "2024-04-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 10, output: 30 }, - limit: { context: 128000, output: 4096 }, - }, - "text-embedding-ada-002": { - id: "text-embedding-ada-002", - name: "text-embedding-ada-002", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2022-12", - release_date: "2022-12-15", - last_updated: "2022-12-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0 }, - limit: { context: 8192, output: 1536 }, - }, - "gpt-5.2": { - id: "gpt-5.2", - name: "GPT-5.2", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "o3-pro": { - id: "o3-pro", - name: "o3-pro", - family: "o-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05", - release_date: "2025-06-10", - last_updated: "2025-06-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 20, output: 80 }, - limit: { context: 200000, output: 100000 }, - }, - "gpt-4o-mini": { - id: "gpt-4o-mini", - name: "GPT-4o mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-09", - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6, cache_read: 0.08 }, - limit: { context: 128000, output: 16384 }, - }, - "o4-mini-deep-research": { - id: "o4-mini-deep-research", - name: "o4-mini-deep-research", - family: "o-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05", - release_date: "2024-06-26", - last_updated: "2024-06-26", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 200000, output: 100000 }, - }, - "gpt-5.4-mini": { - id: "gpt-5.4-mini", - name: "GPT-5.4 mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.75, output: 4.5, cache_read: 0.075 }, - limit: { context: 400000, input: 272000, output: 128000 }, - experimental: { - modes: { - fast: { - cost: { input: 1.5, output: 9, cache_read: 0.15 }, - provider: { body: { service_tier: "priority" } }, - }, - }, - }, - }, - "o4-mini": { - id: "o4-mini", - name: "o4-mini", - family: "o-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05", - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.28 }, - limit: { context: 200000, output: 100000 }, - }, - "gpt-5.4-nano": { - id: "gpt-5.4-nano", - name: "GPT-5.4 nano", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.25, cache_read: 0.02 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gpt-image-1": { - id: "gpt-image-1", - name: "gpt-image-1", - family: "gpt-image", - attachment: true, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-04-24", - last_updated: "2025-04-24", - modalities: { input: ["text", "image"], output: ["image"] }, - open_weights: false, - limit: { context: 0, input: 0, output: 0 }, - }, - "gpt-5.2-codex": { - id: "gpt-5.2-codex", - name: "GPT-5.2 Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gpt-5.2-chat-latest": { - id: "gpt-5.2-chat-latest", - name: "GPT-5.2 Chat", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 128000, output: 16384 }, - }, - "gpt-5.1-codex-mini": { - id: "gpt-5.1-codex-mini", - name: "GPT-5.1 Codex mini", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.025 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "o1-preview": { - id: "o1-preview", - name: "o1-preview", - family: "o", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2023-09", - release_date: "2024-09-12", - last_updated: "2024-09-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 60, cache_read: 7.5 }, - limit: { context: 128000, output: 32768 }, - }, - "gpt-4o-2024-08-06": { - id: "gpt-4o-2024-08-06", - name: "GPT-4o (2024-08-06)", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-09", - release_date: "2024-08-06", - last_updated: "2024-08-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10, cache_read: 1.25 }, - limit: { context: 128000, output: 16384 }, - }, - "gpt-5.1": { - id: "gpt-5.1", - name: "GPT-5.1", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.13 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gpt-image-1-mini": { - id: "gpt-image-1-mini", - name: "gpt-image-1-mini", - family: "gpt-image", - attachment: true, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-09-26", - last_updated: "2025-09-26", - modalities: { input: ["text", "image"], output: ["text", "image"] }, - open_weights: false, - limit: { context: 0, input: 0, output: 0 }, - }, - o1: { - id: "o1", - name: "o1", - family: "o", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2023-09", - release_date: "2024-12-05", - last_updated: "2024-12-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 60, cache_read: 7.5 }, - limit: { context: 200000, output: 100000 }, - }, - "gpt-5.4-pro": { - id: "gpt-5.4-pro", - name: "GPT-5.4 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 30, output: 180, context_over_200k: { input: 60, output: 270 } }, - limit: { context: 1050000, input: 922000, output: 128000 }, - }, - "gpt-3.5-turbo": { - id: "gpt-3.5-turbo", - name: "GPT-3.5-turbo", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - knowledge: "2021-09-01", - release_date: "2023-03-01", - last_updated: "2023-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 1.5, cache_read: 1.25 }, - limit: { context: 16385, output: 4096 }, - }, - "o3-deep-research": { - id: "o3-deep-research", - name: "o3-deep-research", - family: "o", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05", - release_date: "2024-06-26", - last_updated: "2024-06-26", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 10, output: 40, cache_read: 2.5 }, - limit: { context: 200000, output: 100000 }, - }, - "o3-mini": { - id: "o3-mini", - name: "o3-mini", - family: "o-mini", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05", - release_date: "2024-12-20", - last_updated: "2025-01-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.55 }, - limit: { context: 200000, output: 100000 }, - }, - "text-embedding-3-small": { - id: "text-embedding-3-small", - name: "text-embedding-3-small", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2024-01", - release_date: "2024-01-25", - last_updated: "2024-01-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.02, output: 0 }, - limit: { context: 8191, output: 1536 }, - }, - "o1-pro": { - id: "o1-pro", - name: "o1-pro", - family: "o-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2023-09", - release_date: "2025-03-19", - last_updated: "2025-03-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 150, output: 600 }, - limit: { context: 200000, output: 100000 }, - }, - "codex-mini-latest": { - id: "codex-mini-latest", - name: "Codex Mini", - family: "gpt-codex-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-04", - release_date: "2025-05-16", - last_updated: "2025-05-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.5, output: 6, cache_read: 0.375 }, - limit: { context: 200000, output: 100000 }, - }, - "gpt-4": { - id: "gpt-4", - name: "GPT-4", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - knowledge: "2023-11", - release_date: "2023-11-06", - last_updated: "2024-04-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 30, output: 60 }, - limit: { context: 8192, output: 8192 }, - }, - "gpt-5-codex": { - id: "gpt-5-codex", - name: "GPT-5-Codex", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-09-15", - last_updated: "2025-09-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gpt-5.4": { - id: "gpt-5.4", - name: "GPT-5.4", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 2.5, - output: 15, - cache_read: 0.25, - context_over_200k: { input: 5, output: 22.5, cache_read: 0.5 }, - }, - limit: { context: 1050000, input: 922000, output: 128000 }, - experimental: { - modes: { - fast: { cost: { input: 5, output: 30, cache_read: 0.5 }, provider: { body: { service_tier: "priority" } } }, - }, - }, - }, - "gpt-5.1-chat-latest": { - id: "gpt-5.1-chat-latest", - name: "GPT-5.1 Chat", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 128000, output: 16384 }, - }, - "gpt-5.3-codex-spark": { - id: "gpt-5.3-codex-spark", - name: "GPT-5.3 Codex Spark", - family: "gpt-codex-spark", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 128000, input: 100000, output: 32000 }, - }, - "chatgpt-image-latest": { - id: "chatgpt-image-latest", - name: "chatgpt-image-latest", - family: "gpt-image", - attachment: true, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-12-16", - last_updated: "2025-12-16", - modalities: { input: ["text", "image"], output: ["text", "image"] }, - open_weights: false, - limit: { context: 0, input: 0, output: 0 }, - }, - "gpt-4.1-nano": { - id: "gpt-4.1-nano", - name: "GPT-4.1 nano", - family: "gpt-nano", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.03 }, - limit: { context: 1047576, output: 32768 }, - }, - o3: { - id: "o3", - name: "o3", - family: "o", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05", - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 200000, output: 100000 }, - }, - "gpt-5-pro": { - id: "gpt-5-pro", - name: "GPT-5 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-10-06", - last_updated: "2025-10-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 120 }, - limit: { context: 400000, input: 272000, output: 272000 }, - }, - "gpt-4o": { - id: "gpt-4o", - name: "GPT-4o", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-09", - release_date: "2024-05-13", - last_updated: "2024-08-06", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10, cache_read: 1.25 }, - limit: { context: 128000, output: 16384 }, - }, - "gpt-5": { - id: "gpt-5", - name: "GPT-5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gpt-5-chat-latest": { - id: "gpt-5-chat-latest", - name: "GPT-5 Chat (latest)", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: true, - temperature: true, - knowledge: "2024-09-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gpt-image-1.5": { - id: "gpt-image-1.5", - name: "gpt-image-1.5", - family: "gpt-image", - attachment: true, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-11-25", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text", "image"] }, - open_weights: false, - limit: { context: 0, input: 0, output: 0 }, - }, - "gpt-4.1": { - id: "gpt-4.1", - name: "GPT-4.1", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 1047576, output: 32768 }, - }, - "gpt-4.1-mini": { - id: "gpt-4.1-mini", - name: "GPT-4.1 mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.6, cache_read: 0.1 }, - limit: { context: 1047576, output: 32768 }, - }, - "gpt-5.1-codex": { - id: "gpt-5.1-codex", - name: "GPT-5.1 Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gpt-4o-2024-11-20": { - id: "gpt-4o-2024-11-20", - name: "GPT-4o (2024-11-20)", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-09", - release_date: "2024-11-20", - last_updated: "2024-11-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10, cache_read: 1.25 }, - limit: { context: 128000, output: 16384 }, - }, - }, - }, - requesty: { - id: "requesty", - env: ["REQUESTY_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://router.requesty.ai/v1", - name: "Requesty", - doc: "https://requesty.ai/solution/llm-routing/models", - models: { - "xai/grok-4-fast": { - id: "xai/grok-4-fast", - name: "Grok 4 Fast", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-09-19", - last_updated: "2025-09-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05, cache_write: 0.2 }, - limit: { context: 2000000, output: 64000 }, - }, - "xai/grok-4": { - id: "xai/grok-4", - name: "Grok 4", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-09-09", - last_updated: "2025-09-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.75, cache_write: 3 }, - limit: { context: 256000, output: 64000 }, - }, - "openai/gpt-5.1-codex-max": { - id: "openai/gpt-5.1-codex-max", - name: "GPT-5.1-Codex-Max", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 9, cache_read: 0.11 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.2-chat": { - id: "openai/gpt-5.2-chat", - name: "GPT-5.2 Chat", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-5-chat": { - id: "openai/gpt-5-chat", - name: "GPT-5 Chat (latest)", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: true, - temperature: true, - knowledge: "2024-09-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.2-pro": { - id: "openai/gpt-5.2-pro", - name: "GPT-5.2 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 21, output: 168 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5-mini": { - id: "openai/gpt-5-mini", - name: "GPT-5 Mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.03 }, - limit: { context: 128000, output: 32000 }, - }, - "openai/gpt-5-nano": { - id: "openai/gpt-5-nano", - name: "GPT-5 Nano", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.4, cache_read: 0.01 }, - limit: { context: 16000, output: 4000 }, - }, - "openai/gpt-5.3-codex": { - id: "openai/gpt-5.3-codex", - name: "GPT-5.3-Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-02-24", - last_updated: "2026-02-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.2": { - id: "openai/gpt-5.2", - name: "GPT-5.2", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-4o-mini": { - id: "openai/gpt-4o-mini", - name: "GPT-4o Mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6, cache_read: 0.08 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-5.1-chat": { - id: "openai/gpt-5.1-chat", - name: "GPT-5.1 Chat", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/o4-mini": { - id: "openai/o4-mini", - name: "o4 Mini", - family: "o-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-06", - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.28 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-5.2-codex": { - id: "openai/gpt-5.2-codex", - name: "GPT-5.2-Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-01-14", - last_updated: "2026-01-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.1-codex-mini": { - id: "openai/gpt-5.1-codex-mini", - name: "GPT-5.1-Codex-Mini", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.025 }, - limit: { context: 400000, output: 100000 }, - }, - "openai/gpt-5-image": { - id: "openai/gpt-5-image", - name: "GPT-5 Image", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-10-01", - release_date: "2025-10-14", - last_updated: "2025-10-14", - modalities: { input: ["text", "image", "pdf"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 5, output: 10, cache_read: 1.25 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.1": { - id: "openai/gpt-5.1", - name: "GPT-5.1", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.4-pro": { - id: "openai/gpt-5.4-pro", - name: "GPT-5.4 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 30, output: 180, cache_read: 30 }, - limit: { context: 1050000, input: 922000, output: 128000 }, - }, - "openai/gpt-5-codex": { - id: "openai/gpt-5-codex", - name: "GPT-5 Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-10-01", - release_date: "2025-09-15", - last_updated: "2025-09-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.4": { - id: "openai/gpt-5.4", - name: "GPT-5.4", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 2.5, - output: 15, - cache_read: 0.25, - context_over_200k: { input: 5, output: 22.5, cache_read: 0.5 }, - }, - limit: { context: 1050000, input: 922000, output: 128000 }, - }, - "openai/gpt-5-pro": { - id: "openai/gpt-5-pro", - name: "GPT-5 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-10-06", - last_updated: "2025-10-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 120 }, - limit: { context: 400000, output: 272000 }, - }, - "openai/gpt-5": { - id: "openai/gpt-5", - name: "GPT-5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "audio", "image", "video"], output: ["text", "audio", "image"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.13 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-4.1": { - id: "openai/gpt-4.1", - name: "GPT-4.1", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 1047576, output: 32768 }, - }, - "openai/gpt-4.1-mini": { - id: "openai/gpt-4.1-mini", - name: "GPT-4.1 Mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.6, cache_read: 0.1 }, - limit: { context: 1047576, output: 32768 }, - }, - "openai/gpt-5.1-codex": { - id: "openai/gpt-5.1-codex", - name: "GPT-5.1-Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "google/gemini-3-flash-preview": { - id: "google/gemini-3-flash-preview", - name: "Gemini 3 Flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 3, cache_read: 0.05, cache_write: 1 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-3-pro-preview": { - id: "google/gemini-3-pro-preview", - name: "Gemini 3 Pro", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-11-18", - last_updated: "2025-11-18", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2, cache_write: 4.5 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-2.5-pro": { - id: "google/gemini-2.5-pro", - name: "Gemini 2.5 Pro", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.31, cache_write: 2.375 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-2.5-flash": { - id: "google/gemini-2.5-flash", - name: "Gemini 2.5 Flash", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5, cache_read: 0.075, cache_write: 0.55 }, - limit: { context: 1048576, output: 65536 }, - }, - "anthropic/claude-haiku-4-5": { - id: "anthropic/claude-haiku-4-5", - name: "Claude Haiku 4.5", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-02-01", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 200000, output: 62000 }, - }, - "anthropic/claude-sonnet-4-6": { - id: "anthropic/claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-17", - last_updated: "2026-02-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { - input: 3, - output: 15, - cache_read: 0.3, - cache_write: 3.75, - context_over_200k: { input: 6, output: 22.5, cache_read: 0.6, cache_write: 7.5 }, - }, - limit: { context: 1000000, output: 128000 }, - }, - "anthropic/claude-3-7-sonnet": { - id: "anthropic/claude-3-7-sonnet", - name: "Claude Sonnet 3.7", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-01", - release_date: "2025-02-19", - last_updated: "2025-02-19", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-opus-4-1": { - id: "anthropic/claude-opus-4-1", - name: "Claude Opus 4.1", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "anthropic/claude-sonnet-4": { - id: "anthropic/claude-sonnet-4", - name: "Claude Sonnet 4", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-opus-4-5": { - id: "anthropic/claude-opus-4-5", - name: "Claude Opus 4.5", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-24", - last_updated: "2025-11-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-opus-4": { - id: "anthropic/claude-opus-4", - name: "Claude Opus 4", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "anthropic/claude-opus-4-6": { - id: "anthropic/claude-opus-4-6", - name: "Claude Opus 4.6", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-05-30", - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 5, - output: 25, - cache_read: 0.5, - cache_write: 6.25, - context_over_200k: { input: 10, output: 37.5, cache_read: 1, cache_write: 12.5 }, - }, - limit: { context: 1000000, output: 128000 }, - }, - "anthropic/claude-sonnet-4-5": { - id: "anthropic/claude-sonnet-4-5", - name: "Claude Sonnet 4.5", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 1000000, output: 64000 }, - }, - }, - }, - vultr: { - id: "vultr", - env: ["VULTR_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.vultrinference.com/v1", - name: "Vultr", - doc: "https://api.vultrinference.com/", - models: { - "MiniMax-M2.5": { - id: "MiniMax-M2.5", - name: "MiniMax M2.5", - family: "minimax", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-09", - release_date: "2025-02-11", - last_updated: "2025-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 194000, output: 4096 }, - }, - "GLM-5-FP8": { - id: "GLM-5-FP8", - name: "GLM 5 FP8", - family: "glm", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.85, output: 3.1 }, - limit: { context: 200000, output: 131072 }, - }, - "DeepSeek-V3.2": { - id: "DeepSeek-V3.2", - name: "DeepSeek V3.2", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 1.65 }, - limit: { context: 127000, output: 4096 }, - }, - "gpt-oss-120b": { - id: "gpt-oss-120b", - name: "GPT OSS 120B", - family: "gpt-oss", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-06", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 129000, output: 4096 }, - }, - "Kimi-K2.5": { - id: "Kimi-K2.5", - name: "Kimi K2 Instruct", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 2.75 }, - limit: { context: 254000, output: 32768 }, - }, - }, - }, - "alibaba-coding-plan-cn": { - id: "alibaba-coding-plan-cn", - env: ["ALIBABA_CODING_PLAN_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://coding.dashscope.aliyuncs.com/v1", - name: "Alibaba Coding Plan (China)", - doc: "https://help.aliyun.com/zh/model-studio/coding-plan", - models: { - "qwen3-coder-plus": { - id: "qwen3-coder-plus", - name: "Qwen3 Coder Plus", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 1000000, output: 65536 }, - }, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-01", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 262144, output: 32768 }, - }, - "glm-4.7": { - id: "glm-4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 202752, output: 16384 }, - }, - "glm-5": { - id: "glm-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 202752, output: 16384 }, - }, - "MiniMax-M2.5": { - id: "MiniMax-M2.5", - name: "MiniMax-M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 196608, output: 24576 }, - }, - "qwen3.6-plus": { - id: "qwen3.6-plus", - name: "Qwen3.6 Plus", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-04-02", - last_updated: "2026-04-02", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 1000000, output: 65536 }, - }, - "qwen3-max-2026-01-23": { - id: "qwen3-max-2026-01-23", - name: "Qwen3 Max", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-01-23", - last_updated: "2026-01-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 262144, output: 32768 }, - }, - "qwen3-coder-next": { - id: "qwen3-coder-next", - name: "Qwen3 Coder Next", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-03", - last_updated: "2026-02-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen3.5-plus": { - id: "qwen3.5-plus", - name: "Qwen3.5 Plus", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02-16", - last_updated: "2026-02-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 1000000, output: 65536 }, - }, - }, - }, - mistral: { - id: "mistral", - env: ["MISTRAL_API_KEY"], - npm: "@ai-sdk/mistral", - name: "Mistral", - doc: "https://docs.mistral.ai/getting-started/models/", - models: { - "mistral-small-latest": { - id: "mistral-small-latest", - name: "Mistral Small (latest)", - family: "mistral-small", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2026-03-16", - last_updated: "2026-03-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 256000, output: 256000 }, - }, - "mistral-nemo": { - id: "mistral-nemo", - name: "Mistral Nemo", - family: "mistral-nemo", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2024-07-01", - last_updated: "2024-07-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.15 }, - limit: { context: 128000, output: 128000 }, - }, - "mistral-large-2512": { - id: "mistral-large-2512", - name: "Mistral Large 3", - family: "mistral-large", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2024-11-01", - last_updated: "2025-12-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 1.5 }, - limit: { context: 262144, output: 262144 }, - }, - "labs-devstral-small-2512": { - id: "labs-devstral-small-2512", - name: "Devstral Small 2", - family: "devstral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-12", - release_date: "2025-12-09", - last_updated: "2025-12-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 256000, output: 256000 }, - }, - "devstral-2512": { - id: "devstral-2512", - name: "Devstral 2", - family: "devstral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-12", - release_date: "2025-12-09", - last_updated: "2025-12-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 2 }, - limit: { context: 262144, output: 262144 }, - }, - "magistral-medium-latest": { - id: "magistral-medium-latest", - name: "Magistral Medium (latest)", - family: "magistral-medium", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-03-17", - last_updated: "2025-03-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2, output: 5 }, - limit: { context: 128000, output: 16384 }, - }, - "open-mixtral-8x7b": { - id: "open-mixtral-8x7b", - name: "Mixtral 8x7B", - family: "mixtral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-01", - release_date: "2023-12-11", - last_updated: "2023-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.7, output: 0.7 }, - limit: { context: 32000, output: 32000 }, - }, - "pixtral-large-latest": { - id: "pixtral-large-latest", - name: "Pixtral Large (latest)", - family: "pixtral", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2024-11-01", - last_updated: "2024-11-04", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 2, output: 6 }, - limit: { context: 128000, output: 128000 }, - }, - "mistral-large-2411": { - id: "mistral-large-2411", - name: "Mistral Large 2.1", - family: "mistral-large", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2024-11-01", - last_updated: "2024-11-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2, output: 6 }, - limit: { context: 131072, output: 16384 }, - }, - "codestral-latest": { - id: "codestral-latest", - name: "Codestral (latest)", - family: "codestral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-05-29", - last_updated: "2025-01-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.9 }, - limit: { context: 256000, output: 4096 }, - }, - "mistral-large-latest": { - id: "mistral-large-latest", - name: "Mistral Large (latest)", - family: "mistral-large", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2024-11-01", - last_updated: "2025-12-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 1.5 }, - limit: { context: 262144, output: 262144 }, - }, - "mistral-small-2506": { - id: "mistral-small-2506", - name: "Mistral Small 3.2", - family: "mistral-small", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-03", - release_date: "2025-06-20", - last_updated: "2025-06-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 128000, output: 16384 }, - }, - "pixtral-12b": { - id: "pixtral-12b", - name: "Pixtral 12B", - family: "pixtral", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-09", - release_date: "2024-09-01", - last_updated: "2024-09-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.15 }, - limit: { context: 128000, output: 128000 }, - }, - "ministral-8b-latest": { - id: "ministral-8b-latest", - name: "Ministral 8B (latest)", - family: "ministral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-10-01", - last_updated: "2024-10-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 128000, output: 128000 }, - }, - "mistral-embed": { - id: "mistral-embed", - name: "Mistral Embed", - family: "mistral-embed", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2023-12-11", - last_updated: "2023-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0 }, - limit: { context: 8000, output: 3072 }, - }, - "magistral-small": { - id: "magistral-small", - name: "Magistral Small", - family: "magistral-small", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-03-17", - last_updated: "2025-03-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 1.5 }, - limit: { context: 128000, output: 128000 }, - }, - "mistral-small-2603": { - id: "mistral-small-2603", - name: "Mistral Small 4", - family: "mistral-small", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2026-03-16", - last_updated: "2026-03-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 256000, output: 256000 }, - }, - "ministral-3b-latest": { - id: "ministral-3b-latest", - name: "Ministral 3B (latest)", - family: "ministral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-10-01", - last_updated: "2024-10-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.04, output: 0.04 }, - limit: { context: 128000, output: 128000 }, - }, - "open-mixtral-8x22b": { - id: "open-mixtral-8x22b", - name: "Mixtral 8x22B", - family: "mixtral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-04-17", - last_updated: "2024-04-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2, output: 6 }, - limit: { context: 64000, output: 64000 }, - }, - "devstral-small-2505": { - id: "devstral-small-2505", - name: "Devstral Small 2505", - family: "devstral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-05", - release_date: "2025-05-07", - last_updated: "2025-05-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 128000, output: 128000 }, - }, - "devstral-medium-2507": { - id: "devstral-medium-2507", - name: "Devstral Medium", - family: "devstral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-05", - release_date: "2025-07-10", - last_updated: "2025-07-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 2 }, - limit: { context: 128000, output: 128000 }, - }, - "mistral-medium-latest": { - id: "mistral-medium-latest", - name: "Mistral Medium (latest)", - family: "mistral-medium", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-05", - release_date: "2025-05-07", - last_updated: "2025-05-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 2 }, - limit: { context: 128000, output: 16384 }, - }, - "open-mistral-7b": { - id: "open-mistral-7b", - name: "Mistral 7B", - family: "mistral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2023-09-27", - last_updated: "2023-09-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 0.25 }, - limit: { context: 8000, output: 8000 }, - }, - "devstral-medium-latest": { - id: "devstral-medium-latest", - name: "Devstral 2 (latest)", - family: "devstral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-12", - release_date: "2025-12-02", - last_updated: "2025-12-02", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 2 }, - limit: { context: 262144, output: 262144 }, - }, - "mistral-medium-2505": { - id: "mistral-medium-2505", - name: "Mistral Medium 3", - family: "mistral-medium", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-05", - release_date: "2025-05-07", - last_updated: "2025-05-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2 }, - limit: { context: 131072, output: 131072 }, - }, - "devstral-small-2507": { - id: "devstral-small-2507", - name: "Devstral Small", - family: "devstral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-05", - release_date: "2025-07-10", - last_updated: "2025-07-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 128000, output: 128000 }, - }, - "mistral-medium-2508": { - id: "mistral-medium-2508", - name: "Mistral Medium 3.1", - family: "mistral-medium", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-05", - release_date: "2025-08-12", - last_updated: "2025-08-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2 }, - limit: { context: 262144, output: 262144 }, - }, - }, - }, - ovhcloud: { - id: "ovhcloud", - env: ["OVHCLOUD_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://oai.endpoints.kepler.ai.cloud.ovh.net/v1", - name: "OVHcloud AI Endpoints", - doc: "https://www.ovhcloud.com/en/public-cloud/ai-endpoints/catalog//", - models: { - "mixtral-8x7b-instruct-v0.1": { - id: "mixtral-8x7b-instruct-v0.1", - name: "Mixtral-8x7B-Instruct-v0.1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-04-01", - last_updated: "2025-04-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.7, output: 0.7 }, - limit: { context: 32768, output: 32768 }, - }, - "qwen2.5-coder-32b-instruct": { - id: "qwen2.5-coder-32b-instruct", - name: "Qwen2.5-Coder-32B-Instruct", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-03-24", - last_updated: "2025-03-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.96, output: 0.96 }, - limit: { context: 32768, output: 32768 }, - }, - "meta-llama-3_3-70b-instruct": { - id: "meta-llama-3_3-70b-instruct", - name: "Meta-Llama-3_3-70B-Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-01", - last_updated: "2025-04-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.74, output: 0.74 }, - limit: { context: 131072, output: 131072 }, - }, - "mistral-7b-instruct-v0.3": { - id: "mistral-7b-instruct-v0.3", - name: "Mistral-7B-Instruct-v0.3", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-01", - last_updated: "2025-04-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.11, output: 0.11 }, - limit: { context: 65536, output: 65536 }, - }, - "deepseek-r1-distill-llama-70b": { - id: "deepseek-r1-distill-llama-70b", - name: "DeepSeek-R1-Distill-Llama-70B", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-01-30", - last_updated: "2025-01-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.74, output: 0.74 }, - limit: { context: 131072, output: 131072 }, - }, - "qwen3-32b": { - id: "qwen3-32b", - name: "Qwen3-32B", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-16", - last_updated: "2025-07-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.09, output: 0.25 }, - limit: { context: 32768, output: 32768 }, - }, - "qwen2.5-vl-72b-instruct": { - id: "qwen2.5-vl-72b-instruct", - name: "Qwen2.5-VL-72B-Instruct", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-03-31", - last_updated: "2025-03-31", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 1.01, output: 1.01 }, - limit: { context: 32768, output: 32768 }, - }, - "qwen3-coder-30b-a3b-instruct": { - id: "qwen3-coder-30b-a3b-instruct", - name: "Qwen3-Coder-30B-A3B-Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-28", - last_updated: "2025-10-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.26 }, - limit: { context: 262144, output: 262144 }, - }, - "gpt-oss-20b": { - id: "gpt-oss-20b", - name: "gpt-oss-20b", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-08-28", - last_updated: "2025-08-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.18 }, - limit: { context: 131072, output: 131072 }, - }, - "mistral-small-3.2-24b-instruct-2506": { - id: "mistral-small-3.2-24b-instruct-2506", - name: "Mistral-Small-3.2-24B-Instruct-2506", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-16", - last_updated: "2025-07-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.31 }, - limit: { context: 131072, output: 131072 }, - }, - "gpt-oss-120b": { - id: "gpt-oss-120b", - name: "gpt-oss-120b", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-08-28", - last_updated: "2025-08-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.09, output: 0.47 }, - limit: { context: 131072, output: 131072 }, - }, - "mistral-nemo-instruct-2407": { - id: "mistral-nemo-instruct-2407", - name: "Mistral-Nemo-Instruct-2407", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-11-20", - last_updated: "2024-11-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.14, output: 0.14 }, - limit: { context: 65536, output: 65536 }, - }, - "llama-3.1-8b-instruct": { - id: "llama-3.1-8b-instruct", - name: "Llama-3.1-8B-Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-06-11", - last_updated: "2025-06-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.11, output: 0.11 }, - limit: { context: 131072, output: 131072 }, - }, - }, - }, - friendli: { - id: "friendli", - env: ["FRIENDLI_TOKEN"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.friendli.ai/serverless/v1", - name: "Friendli", - doc: "https://friendli.ai/docs/guides/serverless_endpoints/introduction", - models: { - "Qwen/Qwen3-235B-A22B-Instruct-2507": { - id: "Qwen/Qwen3-235B-A22B-Instruct-2507", - name: "Qwen3 235B A22B Instruct 2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-29", - last_updated: "2026-01-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.8 }, - limit: { context: 262144, output: 262144 }, - }, - "zai-org/GLM-5.1": { - id: "zai-org/GLM-5.1", - name: "GLM-5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-04-07", - last_updated: "2026-04-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.4, output: 4.4 }, - limit: { context: 202752, output: 202752 }, - }, - "zai-org/GLM-5": { - id: "zai-org/GLM-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3.2 }, - limit: { context: 202752, output: 202752 }, - }, - "meta-llama/Llama-3.3-70B-Instruct": { - id: "meta-llama/Llama-3.3-70B-Instruct", - name: "Llama 3.3 70B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-08-01", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 0.6 }, - limit: { context: 131072, output: 131072 }, - }, - "meta-llama/Llama-3.1-8B-Instruct": { - id: "meta-llama/Llama-3.1-8B-Instruct", - name: "Llama 3.1 8B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-08-01", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 131072, output: 8000 }, - }, - "MiniMaxAI/MiniMax-M2.5": { - id: "MiniMaxAI/MiniMax-M2.5", - name: "MiniMax-M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 196608, output: 196608 }, - }, - }, - }, - cortecs: { - id: "cortecs", - env: ["CORTECS_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.cortecs.ai/v1", - name: "Cortecs", - doc: "https://api.cortecs.ai/v1/models", - models: { - "claude-haiku-4-5": { - id: "claude-haiku-4-5", - name: "Claude Haiku 4.5", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.09, output: 5.43 }, - limit: { context: 200000, output: 200000 }, - }, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Kimi K2.5", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-01", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 2.76 }, - limit: { context: 256000, output: 256000 }, - }, - "deepseek-v3-0324": { - id: "deepseek-v3-0324", - name: "DeepSeek V3 0324", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-03-24", - last_updated: "2025-03-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.551, output: 1.654 }, - limit: { context: 128000, output: 128000 }, - }, - "glm-4.7": { - id: "glm-4.7", - name: "GLM 4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.45, output: 2.23 }, - limit: { context: 198000, output: 198000 }, - }, - "glm-5": { - id: "glm-5", - name: "GLM 5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.08, output: 3.44 }, - limit: { context: 202752, output: 202752 }, - }, - "nova-pro-v1": { - id: "nova-pro-v1", - name: "Nova Pro 1.0", - family: "nova-pro", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-12-03", - last_updated: "2024-12-03", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.016, output: 4.061 }, - limit: { context: 300000, output: 5000 }, - }, - "devstral-2512": { - id: "devstral-2512", - name: "Devstral 2 2512", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-12", - release_date: "2025-12-09", - last_updated: "2025-12-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262000, output: 262000 }, - }, - "qwen3-32b": { - id: "qwen3-32b", - name: "Qwen3 32B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-04-29", - last_updated: "2025-04-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.099, output: 0.33 }, - limit: { context: 16384, output: 16384 }, - }, - "claude-4-5-sonnet": { - id: "claude-4-5-sonnet", - name: "Claude 4.5 Sonnet", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3.259, output: 16.296 }, - limit: { context: 200000, output: 200000 }, - }, - "kimi-k2-instruct": { - id: "kimi-k2-instruct", - name: "Kimi K2 Instruct", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-07-11", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.551, output: 2.646 }, - limit: { context: 131000, output: 131000 }, - }, - "minimax-m2": { - id: "minimax-m2", - name: "MiniMax-M2", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-11", - release_date: "2025-10-27", - last_updated: "2025-10-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.39, output: 1.57 }, - limit: { context: 400000, output: 400000 }, - }, - "gemini-2.5-pro": { - id: "gemini-2.5-pro", - name: "Gemini 2.5 Pro", - family: "gemini-pro", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-03-20", - last_updated: "2025-06-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.654, output: 11.024 }, - limit: { context: 1048576, output: 65535 }, - }, - "claude-opus4-6": { - id: "claude-opus4-6", - name: "Claude Opus 4.6", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-02-05", - last_updated: "2026-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5.98, output: 29.89 }, - limit: { context: 1000000, output: 1000000 }, - }, - "devstral-small-2512": { - id: "devstral-small-2512", - name: "Devstral Small 2 2512", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-12", - release_date: "2025-12-09", - last_updated: "2025-12-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262000, output: 262000 }, - }, - "minimax-m2.1": { - id: "minimax-m2.1", - name: "MiniMax-M2.1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.34, output: 1.34 }, - limit: { context: 196000, output: 196000 }, - }, - "glm-4.5": { - id: "glm-4.5", - name: "GLM 4.5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-29", - last_updated: "2025-07-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.67, output: 2.46 }, - limit: { context: 131072, output: 131072 }, - }, - "claude-opus4-5": { - id: "claude-opus4-5", - name: "Claude Opus 4.5", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-24", - last_updated: "2025-11-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5.98, output: 29.89 }, - limit: { context: 200000, output: 200000 }, - }, - "claude-sonnet-4": { - id: "claude-sonnet-4", - name: "Claude Sonnet 4", - family: "claude-sonnet", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-03", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3.307, output: 16.536 }, - limit: { context: 200000, output: 64000 }, - }, - "qwen3-next-80b-a3b-thinking": { - id: "qwen3-next-80b-a3b-thinking", - name: "Qwen3 Next 80B A3B Thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-11", - last_updated: "2025-09-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.164, output: 1.311 }, - limit: { context: 128000, output: 128000 }, - }, - "glm-4.5-air": { - id: "glm-4.5-air", - name: "GLM 4.5 Air", - family: "glm-air", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-08-01", - last_updated: "2025-08-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.22, output: 1.34 }, - limit: { context: 131072, output: 131072 }, - }, - "qwen3-coder-next": { - id: "qwen3-coder-next", - name: "Qwen3 Coder Next 80B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02-04", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.158, output: 0.84 }, - limit: { context: 256000, output: 65536 }, - }, - "claude-4-6-sonnet": { - id: "claude-4-6-sonnet", - name: "Claude Sonnet 4.6", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08", - release_date: "2026-02-17", - last_updated: "2026-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3.59, output: 17.92 }, - limit: { context: 1000000, output: 1000000 }, - }, - "qwen3-coder-480b-a35b-instruct": { - id: "qwen3-coder-480b-a35b-instruct", - name: "Qwen3 Coder 480B A35B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-07-25", - last_updated: "2025-07-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.441, output: 1.984 }, - limit: { context: 262000, output: 262000 }, - }, - "minimax-m2.5": { - id: "minimax-m2.5", - name: "MiniMax-M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.32, output: 1.18 }, - limit: { context: 196608, output: 196608 }, - }, - "intellect-3": { - id: "intellect-3", - name: "INTELLECT 3", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-11", - release_date: "2025-11-26", - last_updated: "2025-11-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.219, output: 1.202 }, - limit: { context: 128000, output: 128000 }, - }, - "glm-4.7-flash": { - id: "glm-4.7-flash", - name: "GLM-4.7-Flash", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-08-08", - last_updated: "2025-08-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.09, output: 0.53 }, - limit: { context: 203000, output: 203000 }, - }, - "gpt-oss-120b": { - id: "gpt-oss-120b", - name: "GPT Oss 120b", - family: "gpt-oss", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-01", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 128000 }, - }, - "gpt-4.1": { - id: "gpt-4.1", - name: "GPT 4.1", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-06", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.354, output: 9.417 }, - limit: { context: 1047576, output: 32768 }, - }, - "kimi-k2-thinking": { - id: "kimi-k2-thinking", - name: "Kimi K2 Thinking", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-12", - release_date: "2025-12-08", - last_updated: "2025-12-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.656, output: 2.731 }, - limit: { context: 262000, output: 262000 }, - }, - "llama-3.1-405b-instruct": { - id: "llama-3.1-405b-instruct", - name: "Llama 3.1 405B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 128000 }, - }, - }, - }, - siliconflow: { - id: "siliconflow", - env: ["SILICONFLOW_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.siliconflow.com/v1", - name: "SiliconFlow", - doc: "https://cloud.siliconflow.com/models", - models: { - "nex-agi/DeepSeek-V3.1-Nex-N1": { - id: "nex-agi/DeepSeek-V3.1-Nex-N1", - name: "nex-agi/DeepSeek-V3.1-Nex-N1", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-01-01", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 2 }, - limit: { context: 131000, output: 131000 }, - }, - "Qwen/Qwen2.5-VL-72B-Instruct": { - id: "Qwen/Qwen2.5-VL-72B-Instruct", - name: "Qwen/Qwen2.5-VL-72B-Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-01-28", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.59, output: 0.59 }, - limit: { context: 131000, output: 4000 }, - }, - "Qwen/Qwen3-VL-32B-Thinking": { - id: "Qwen/Qwen3-VL-32B-Thinking", - name: "Qwen/Qwen3-VL-32B-Thinking", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-21", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.5 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen3-30B-A3B-Thinking-2507": { - id: "Qwen/Qwen3-30B-A3B-Thinking-2507", - name: "Qwen/Qwen3-30B-A3B-Thinking-2507", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-31", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.09, output: 0.3 }, - limit: { context: 262000, output: 131000 }, - }, - "Qwen/Qwen3-VL-235B-A22B-Thinking": { - id: "Qwen/Qwen3-VL-235B-A22B-Thinking", - name: "Qwen/Qwen3-VL-235B-A22B-Thinking", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-04", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.45, output: 3.5 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen2.5-7B-Instruct": { - id: "Qwen/Qwen2.5-7B-Instruct", - name: "Qwen/Qwen2.5-7B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-09-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.05 }, - limit: { context: 33000, output: 4000 }, - }, - "Qwen/Qwen2.5-Coder-32B-Instruct": { - id: "Qwen/Qwen2.5-Coder-32B-Instruct", - name: "Qwen/Qwen2.5-Coder-32B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-11-11", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.18, output: 0.18 }, - limit: { context: 33000, output: 4000 }, - }, - "Qwen/Qwen3-VL-30B-A3B-Instruct": { - id: "Qwen/Qwen3-VL-30B-A3B-Instruct", - name: "Qwen/Qwen3-VL-30B-A3B-Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-05", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.29, output: 1 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/QwQ-32B": { - id: "Qwen/QwQ-32B", - name: "Qwen/QwQ-32B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-03-06", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.58 }, - limit: { context: 131000, output: 131000 }, - }, - "Qwen/Qwen3-235B-A22B": { - id: "Qwen/Qwen3-235B-A22B", - name: "Qwen/Qwen3-235B-A22B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-30", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.35, output: 1.42 }, - limit: { context: 131000, output: 131000 }, - }, - "Qwen/Qwen3-Omni-30B-A3B-Captioner": { - id: "Qwen/Qwen3-Omni-30B-A3B-Captioner", - name: "Qwen/Qwen3-Omni-30B-A3B-Captioner", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-04", - last_updated: "2025-11-25", - modalities: { input: ["audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 66000, output: 66000 }, - }, - "Qwen/Qwen3-VL-8B-Thinking": { - id: "Qwen/Qwen3-VL-8B-Thinking", - name: "Qwen/Qwen3-VL-8B-Thinking", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-15", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.18, output: 2 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen2.5-VL-7B-Instruct": { - id: "Qwen/Qwen2.5-VL-7B-Instruct", - name: "Qwen/Qwen2.5-VL-7B-Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-01-28", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.05 }, - limit: { context: 33000, output: 4000 }, - }, - "Qwen/Qwen3-Next-80B-A3B-Instruct": { - id: "Qwen/Qwen3-Next-80B-A3B-Instruct", - name: "Qwen/Qwen3-Next-80B-A3B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 1.4 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen3-8B": { - id: "Qwen/Qwen3-8B", - name: "Qwen/Qwen3-8B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-30", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.06, output: 0.06 }, - limit: { context: 131000, output: 131000 }, - }, - "Qwen/Qwen3-30B-A3B-Instruct-2507": { - id: "Qwen/Qwen3-30B-A3B-Instruct-2507", - name: "Qwen/Qwen3-30B-A3B-Instruct-2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-30", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.09, output: 0.3 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen3-235B-A22B-Instruct-2507": { - id: "Qwen/Qwen3-235B-A22B-Instruct-2507", - name: "Qwen/Qwen3-235B-A22B-Instruct-2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-23", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.09, output: 0.6 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen3-32B": { - id: "Qwen/Qwen3-32B", - name: "Qwen/Qwen3-32B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-30", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.57 }, - limit: { context: 131000, output: 131000 }, - }, - "Qwen/Qwen3-Coder-30B-A3B-Instruct": { - id: "Qwen/Qwen3-Coder-30B-A3B-Instruct", - name: "Qwen/Qwen3-Coder-30B-A3B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-01", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.07, output: 0.28 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen3-Omni-30B-A3B-Instruct": { - id: "Qwen/Qwen3-Omni-30B-A3B-Instruct", - name: "Qwen/Qwen3-Omni-30B-A3B-Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-04", - last_updated: "2025-11-25", - modalities: { input: ["text", "image", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 66000, output: 66000 }, - }, - "Qwen/Qwen3-14B": { - id: "Qwen/Qwen3-14B", - name: "Qwen/Qwen3-14B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-30", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.07, output: 0.28 }, - limit: { context: 131000, output: 131000 }, - }, - "Qwen/Qwen2.5-72B-Instruct-128K": { - id: "Qwen/Qwen2.5-72B-Instruct-128K", - name: "Qwen/Qwen2.5-72B-Instruct-128K", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-09-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.59, output: 0.59 }, - limit: { context: 131000, output: 4000 }, - }, - "Qwen/Qwen2.5-32B-Instruct": { - id: "Qwen/Qwen2.5-32B-Instruct", - name: "Qwen/Qwen2.5-32B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-09-19", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.18, output: 0.18 }, - limit: { context: 33000, output: 4000 }, - }, - "Qwen/Qwen3-235B-A22B-Thinking-2507": { - id: "Qwen/Qwen3-235B-A22B-Thinking-2507", - name: "Qwen/Qwen3-235B-A22B-Thinking-2507", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-28", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.13, output: 0.6 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen3-Omni-30B-A3B-Thinking": { - id: "Qwen/Qwen3-Omni-30B-A3B-Thinking", - name: "Qwen/Qwen3-Omni-30B-A3B-Thinking", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-04", - last_updated: "2025-11-25", - modalities: { input: ["text", "image", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 66000, output: 66000 }, - }, - "Qwen/Qwen2.5-VL-32B-Instruct": { - id: "Qwen/Qwen2.5-VL-32B-Instruct", - name: "Qwen/Qwen2.5-VL-32B-Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-03-24", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 0.27 }, - limit: { context: 131000, output: 131000 }, - }, - "Qwen/Qwen3-Next-80B-A3B-Thinking": { - id: "Qwen/Qwen3-Next-80B-A3B-Thinking", - name: "Qwen/Qwen3-Next-80B-A3B-Thinking", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-25", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.57 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen3-VL-235B-A22B-Instruct": { - id: "Qwen/Qwen3-VL-235B-A22B-Instruct", - name: "Qwen/Qwen3-VL-235B-A22B-Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-04", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.5 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen2.5-14B-Instruct": { - id: "Qwen/Qwen2.5-14B-Instruct", - name: "Qwen/Qwen2.5-14B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-09-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 33000, output: 4000 }, - }, - "Qwen/Qwen3-VL-30B-A3B-Thinking": { - id: "Qwen/Qwen3-VL-30B-A3B-Thinking", - name: "Qwen/Qwen3-VL-30B-A3B-Thinking", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-11", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.29, output: 1 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen3-VL-32B-Instruct": { - id: "Qwen/Qwen3-VL-32B-Instruct", - name: "Qwen/Qwen3-VL-32B-Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-21", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.6 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen3-VL-8B-Instruct": { - id: "Qwen/Qwen3-VL-8B-Instruct", - name: "Qwen/Qwen3-VL-8B-Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-15", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.18, output: 0.68 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen3-Coder-480B-A35B-Instruct": { - id: "Qwen/Qwen3-Coder-480B-A35B-Instruct", - name: "Qwen/Qwen3-Coder-480B-A35B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-31", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen2.5-72B-Instruct": { - id: "Qwen/Qwen2.5-72B-Instruct", - name: "Qwen/Qwen2.5-72B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-09-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.59, output: 0.59 }, - limit: { context: 33000, output: 4000 }, - }, - "stepfun-ai/Step-3.5-Flash": { - id: "stepfun-ai/Step-3.5-Flash", - name: "stepfun-ai/Step-3.5-Flash", - family: "step", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 262000, output: 262000 }, - }, - "zai-org/GLM-4.5": { - id: "zai-org/GLM-4.5", - name: "zai-org/GLM-4.5", - family: "glm", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-28", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2 }, - limit: { context: 131000, output: 131000 }, - }, - "zai-org/GLM-5V-Turbo": { - id: "zai-org/GLM-5V-Turbo", - name: "zai-org/GLM-5V-Turbo", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-04-01", - last_updated: "2026-04-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.2, output: 4, cache_write: 0 }, - limit: { context: 200000, output: 131072 }, - }, - "zai-org/GLM-4.7": { - id: "zai-org/GLM-4.7", - name: "zai-org/GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 2.2 }, - limit: { context: 205000, output: 205000 }, - }, - "zai-org/GLM-5.1": { - id: "zai-org/GLM-5.1", - name: "zai-org/GLM-5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-04-08", - last_updated: "2026-04-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.4, output: 4.4, cache_write: 0 }, - limit: { context: 205000, output: 205000 }, - }, - "zai-org/GLM-4.5-Air": { - id: "zai-org/GLM-4.5-Air", - name: "zai-org/GLM-4.5-Air", - family: "glm-air", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-28", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.86 }, - limit: { context: 131000, output: 131000 }, - }, - "zai-org/GLM-5": { - id: "zai-org/GLM-5", - name: "zai-org/GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3.2 }, - limit: { context: 205000, output: 205000 }, - }, - "zai-org/GLM-4.6V": { - id: "zai-org/GLM-4.6V", - name: "zai-org/GLM-4.6V", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-12-07", - last_updated: "2025-12-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.9 }, - limit: { context: 131000, output: 131000 }, - }, - "zai-org/GLM-4.6": { - id: "zai-org/GLM-4.6", - name: "zai-org/GLM-4.6", - family: "glm", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-04", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 1.9 }, - limit: { context: 205000, output: 205000 }, - }, - "zai-org/GLM-4.5V": { - id: "zai-org/GLM-4.5V", - name: "zai-org/GLM-4.5V", - family: "glm", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-13", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.86 }, - limit: { context: 66000, output: 66000 }, - }, - "meta-llama/Meta-Llama-3.1-8B-Instruct": { - id: "meta-llama/Meta-Llama-3.1-8B-Instruct", - name: "meta-llama/Meta-Llama-3.1-8B-Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-23", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.06, output: 0.06 }, - limit: { context: 33000, output: 4000 }, - }, - "inclusionAI/Ring-flash-2.0": { - id: "inclusionAI/Ring-flash-2.0", - name: "inclusionAI/Ring-flash-2.0", - family: "ring", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-29", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.57 }, - limit: { context: 131000, output: 131000 }, - }, - "inclusionAI/Ling-mini-2.0": { - id: "inclusionAI/Ling-mini-2.0", - name: "inclusionAI/Ling-mini-2.0", - family: "ling", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-10", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.07, output: 0.28 }, - limit: { context: 131000, output: 131000 }, - }, - "inclusionAI/Ling-flash-2.0": { - id: "inclusionAI/Ling-flash-2.0", - name: "inclusionAI/Ling-flash-2.0", - family: "ling", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.57 }, - limit: { context: 131000, output: 131000 }, - }, - "tencent/Hunyuan-A13B-Instruct": { - id: "tencent/Hunyuan-A13B-Instruct", - name: "tencent/Hunyuan-A13B-Instruct", - family: "hunyuan", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-06-30", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.57 }, - limit: { context: 131000, output: 131000 }, - }, - "tencent/Hunyuan-MT-7B": { - id: "tencent/Hunyuan-MT-7B", - name: "tencent/Hunyuan-MT-7B", - family: "hunyuan", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 33000, output: 33000 }, - }, - "deepseek-ai/DeepSeek-V3.1": { - id: "deepseek-ai/DeepSeek-V3.1", - name: "deepseek-ai/DeepSeek-V3.1", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-25", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 1 }, - limit: { context: 164000, output: 164000 }, - }, - "deepseek-ai/deepseek-vl2": { - id: "deepseek-ai/deepseek-vl2", - name: "deepseek-ai/deepseek-vl2", - family: "deepseek", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-12-13", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.15 }, - limit: { context: 4000, output: 4000 }, - }, - "deepseek-ai/DeepSeek-V3": { - id: "deepseek-ai/DeepSeek-V3", - name: "deepseek-ai/DeepSeek-V3", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-12-26", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1 }, - limit: { context: 164000, output: 164000 }, - }, - "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B": { - id: "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B", - name: "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-01-20", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.18, output: 0.18 }, - limit: { context: 131000, output: 131000 }, - }, - "deepseek-ai/DeepSeek-R1": { - id: "deepseek-ai/DeepSeek-R1", - name: "deepseek-ai/DeepSeek-R1", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-05-28", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 2.18 }, - limit: { context: 164000, output: 164000 }, - }, - "deepseek-ai/DeepSeek-R1-Distill-Qwen-14B": { - id: "deepseek-ai/DeepSeek-R1-Distill-Qwen-14B", - name: "deepseek-ai/DeepSeek-R1-Distill-Qwen-14B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-01-20", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 131000, output: 131000 }, - }, - "deepseek-ai/DeepSeek-V3.2-Exp": { - id: "deepseek-ai/DeepSeek-V3.2-Exp", - name: "deepseek-ai/DeepSeek-V3.2-Exp", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-10", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 0.41 }, - limit: { context: 164000, output: 164000 }, - }, - "deepseek-ai/DeepSeek-V3.2": { - id: "deepseek-ai/DeepSeek-V3.2", - name: "deepseek-ai/DeepSeek-V3.2", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-03", - last_updated: "2025-12-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 0.42 }, - limit: { context: 164000, output: 164000 }, - }, - "deepseek-ai/DeepSeek-V3.1-Terminus": { - id: "deepseek-ai/DeepSeek-V3.1-Terminus", - name: "deepseek-ai/DeepSeek-V3.1-Terminus", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-29", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 1 }, - limit: { context: 164000, output: 164000 }, - }, - "openai/gpt-oss-20b": { - id: "openai/gpt-oss-20b", - name: "openai/gpt-oss-20b", - family: "gpt-oss", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-13", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.04, output: 0.18 }, - limit: { context: 131000, output: 8000 }, - }, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "openai/gpt-oss-120b", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-13", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.45 }, - limit: { context: 131000, output: 8000 }, - }, - "baidu/ERNIE-4.5-300B-A47B": { - id: "baidu/ERNIE-4.5-300B-A47B", - name: "baidu/ERNIE-4.5-300B-A47B", - family: "ernie", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-02", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.28, output: 1.1 }, - limit: { context: 131000, output: 131000 }, - }, - "THUDM/GLM-Z1-9B-0414": { - id: "THUDM/GLM-Z1-9B-0414", - name: "THUDM/GLM-Z1-9B-0414", - family: "glm-z", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.086, output: 0.086 }, - limit: { context: 131000, output: 131000 }, - }, - "THUDM/GLM-4-9B-0414": { - id: "THUDM/GLM-4-9B-0414", - name: "THUDM/GLM-4-9B-0414", - family: "glm", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.086, output: 0.086 }, - limit: { context: 33000, output: 33000 }, - }, - "THUDM/GLM-4-32B-0414": { - id: "THUDM/GLM-4-32B-0414", - name: "THUDM/GLM-4-32B-0414", - family: "glm", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 0.27 }, - limit: { context: 33000, output: 33000 }, - }, - "THUDM/GLM-Z1-32B-0414": { - id: "THUDM/GLM-Z1-32B-0414", - name: "THUDM/GLM-Z1-32B-0414", - family: "glm-z", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.57 }, - limit: { context: 131000, output: 131000 }, - }, - "moonshotai/Kimi-K2-Thinking": { - id: "moonshotai/Kimi-K2-Thinking", - name: "moonshotai/Kimi-K2-Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-11-07", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.55, output: 2.5 }, - limit: { context: 262000, output: 262000 }, - }, - "moonshotai/Kimi-K2-Instruct": { - id: "moonshotai/Kimi-K2-Instruct", - name: "moonshotai/Kimi-K2-Instruct", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-13", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.58, output: 2.29 }, - limit: { context: 131000, output: 131000 }, - }, - "moonshotai/Kimi-K2-Instruct-0905": { - id: "moonshotai/Kimi-K2-Instruct-0905", - name: "moonshotai/Kimi-K2-Instruct-0905", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-08", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2 }, - limit: { context: 262000, output: 262000 }, - }, - "moonshotai/Kimi-K2.5": { - id: "moonshotai/Kimi-K2.5", - name: "moonshotai/Kimi-K2.5", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.55, output: 3 }, - limit: { context: 262000, output: 262000 }, - }, - "MiniMaxAI/MiniMax-M2.5": { - id: "MiniMaxAI/MiniMax-M2.5", - name: "MiniMaxAI/MiniMax-M2.5", - family: "minimax", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2026-02-15", - last_updated: "2026-02-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 197000, output: 131000 }, - }, - "MiniMaxAI/MiniMax-M2.1": { - id: "MiniMaxAI/MiniMax-M2.1", - name: "MiniMaxAI/MiniMax-M2.1", - family: "minimax", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 197000, output: 131000 }, - }, - "ByteDance-Seed/Seed-OSS-36B-Instruct": { - id: "ByteDance-Seed/Seed-OSS-36B-Instruct", - name: "ByteDance-Seed/Seed-OSS-36B-Instruct", - family: "seed", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-04", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.21, output: 0.57 }, - limit: { context: 262000, output: 262000 }, - }, - }, - }, - vercel: { - id: "vercel", - env: ["AI_GATEWAY_API_KEY"], - npm: "@ai-sdk/gateway", - name: "Vercel AI Gateway", - doc: "https://github.com/vercel/ai/tree/5eb85cc45a259553501f535b8ac79a77d0e79223/packages/gateway", - models: { - "alibaba/qwen3-coder-plus": { - id: "alibaba/qwen3-coder-plus", - name: "Qwen3 Coder Plus", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 5 }, - limit: { context: 1000000, output: 1000000 }, - }, - "alibaba/qwen3-embedding-8b": { - id: "alibaba/qwen3-embedding-8b", - name: "Qwen3 Embedding 8B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-06-05", - last_updated: "2025-06-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0 }, - limit: { context: 32768, output: 32768 }, - }, - "alibaba/qwen-3-30b": { - id: "alibaba/qwen-3-30b", - name: "Qwen3-30B-A3B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.08, output: 0.29 }, - limit: { context: 40960, output: 16384 }, - }, - "alibaba/qwen-3-235b": { - id: "alibaba/qwen-3-235b", - name: "Qwen3 235B A22B Instruct 2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.13, output: 0.6 }, - limit: { context: 40960, output: 16384 }, - }, - "alibaba/qwen3.5-flash": { - id: "alibaba/qwen3.5-flash", - name: "Qwen 3.5 Flash", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-24", - last_updated: "2026-02-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.001, cache_write: 0.125 }, - limit: { context: 1000000, output: 64000 }, - }, - "alibaba/qwen3.6-plus": { - id: "alibaba/qwen3.6-plus", - name: "Qwen 3.6 Plus", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-02", - last_updated: "2026-04-03", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 3, cache_read: 0.09999999999999999, cache_write: 0.625 }, - limit: { context: 1000000, output: 64000 }, - }, - "alibaba/qwen3-max": { - id: "alibaba/qwen3-max", - name: "Qwen3 Max", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-23", - last_updated: "2025-09-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.2, output: 6 }, - limit: { context: 262144, output: 32768 }, - }, - "alibaba/qwen3-embedding-0.6b": { - id: "alibaba/qwen3-embedding-0.6b", - name: "Qwen3 Embedding 0.6B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-11-14", - last_updated: "2025-11-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.01, output: 0 }, - limit: { context: 32768, output: 32768 }, - }, - "alibaba/qwen-3-32b": { - id: "alibaba/qwen-3-32b", - name: "Qwen 3.32B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 40960, output: 16384 }, - }, - "alibaba/qwen3-next-80b-a3b-thinking": { - id: "alibaba/qwen3-next-80b-a3b-thinking", - name: "Qwen3 Next 80B A3B Thinking", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-09", - release_date: "2025-09-12", - last_updated: "2025-09-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 1.5 }, - limit: { context: 131072, output: 65536 }, - }, - "alibaba/qwen3-vl-thinking": { - id: "alibaba/qwen3-vl-thinking", - name: "Qwen3 VL Thinking", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-09", - release_date: "2025-09-24", - last_updated: "2025-09-24", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.7, output: 8.4 }, - limit: { context: 131072, output: 129024 }, - }, - "alibaba/qwen3-235b-a22b-thinking": { - id: "alibaba/qwen3-235b-a22b-thinking", - name: "Qwen3 235B A22B Thinking 2507", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.9 }, - limit: { context: 262114, output: 262114 }, - }, - "alibaba/qwen3-next-80b-a3b-instruct": { - id: "alibaba/qwen3-next-80b-a3b-instruct", - name: "Qwen3 Next 80B A3B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-12", - last_updated: "2025-09-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.09, output: 1.1 }, - limit: { context: 262144, output: 32768 }, - }, - "alibaba/qwen3-coder-next": { - id: "alibaba/qwen3-coder-next", - name: "Qwen3 Coder Next", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-07-22", - last_updated: "2026-02-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 1.2 }, - limit: { context: 256000, output: 256000 }, - }, - "alibaba/qwen3-embedding-4b": { - id: "alibaba/qwen3-embedding-4b", - name: "Qwen3 Embedding 4B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-06-05", - last_updated: "2025-06-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.02, output: 0 }, - limit: { context: 32768, output: 32768 }, - }, - "alibaba/qwen3-max-thinking": { - id: "alibaba/qwen3-max-thinking", - name: "Qwen 3 Max Thinking", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-01", - last_updated: "2025-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.2, output: 6, cache_read: 0.24 }, - limit: { context: 256000, output: 65536 }, - }, - "alibaba/qwen3-coder": { - id: "alibaba/qwen3-coder", - name: "Qwen3 Coder 480B A35B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.38, output: 1.53 }, - limit: { context: 262144, output: 66536 }, - }, - "alibaba/qwen3-max-preview": { - id: "alibaba/qwen3-max-preview", - name: "Qwen3 Max Preview", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-23", - last_updated: "2025-09-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.2, output: 6, cache_read: 0.24 }, - limit: { context: 262144, output: 32768 }, - }, - "alibaba/qwen3.5-plus": { - id: "alibaba/qwen3.5-plus", - name: "Qwen 3.5 Plus", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-16", - last_updated: "2026-02-19", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2.4, cache_read: 0.04, cache_write: 0.5 }, - limit: { context: 1000000, output: 64000 }, - }, - "alibaba/qwen-3-14b": { - id: "alibaba/qwen-3-14b", - name: "Qwen3-14B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.06, output: 0.24 }, - limit: { context: 40960, output: 16384 }, - }, - "alibaba/qwen3-vl-instruct": { - id: "alibaba/qwen3-vl-instruct", - name: "Qwen3 VL Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-24", - last_updated: "2025-09-24", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.7, output: 2.8 }, - limit: { context: 131072, output: 129024 }, - }, - "alibaba/qwen3-coder-30b-a3b": { - id: "alibaba/qwen3-coder-30b-a3b", - name: "Qwen 3 Coder 30B A3B Instruct", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.07, output: 0.27 }, - limit: { context: 160000, output: 32768 }, - }, - "perplexity/sonar-pro": { - id: "perplexity/sonar-pro", - name: "Sonar Pro", - family: "sonar-pro", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-09", - release_date: "2025-02-19", - last_updated: "2025-02-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 200000, output: 8000 }, - }, - "perplexity/sonar": { - id: "perplexity/sonar", - name: "Sonar", - family: "sonar", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-02", - release_date: "2025-02-19", - last_updated: "2025-02-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 1 }, - limit: { context: 127000, output: 8000 }, - }, - "perplexity/sonar-reasoning": { - id: "perplexity/sonar-reasoning", - name: "Sonar Reasoning", - family: "sonar-reasoning", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2025-09", - release_date: "2025-02-19", - last_updated: "2025-02-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5 }, - limit: { context: 127000, output: 8000 }, - }, - "perplexity/sonar-reasoning-pro": { - id: "perplexity/sonar-reasoning-pro", - name: "Sonar Reasoning Pro", - family: "sonar-reasoning", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2025-09", - release_date: "2025-02-19", - last_updated: "2025-02-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8 }, - limit: { context: 127000, output: 8000 }, - }, - "deepseek/deepseek-v3.2-thinking": { - id: "deepseek/deepseek-v3.2-thinking", - name: "DeepSeek V3.2 Thinking", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.28, output: 0.42, cache_read: 0.03 }, - limit: { context: 128000, output: 64000 }, - }, - "deepseek/deepseek-v3.2-exp": { - id: "deepseek/deepseek-v3.2-exp", - name: "DeepSeek V3.2 Exp", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-09", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 0.4 }, - limit: { context: 163840, output: 163840 }, - }, - "deepseek/deepseek-v3.1": { - id: "deepseek/deepseek-v3.1", - name: "DeepSeek-V3.1", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-08-21", - last_updated: "2025-08-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1 }, - limit: { context: 163840, output: 128000 }, - }, - "deepseek/deepseek-v3.2": { - id: "deepseek/deepseek-v3.2", - name: "DeepSeek V3.2", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-07", - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 0.4, cache_read: 0.22 }, - limit: { context: 163842, output: 8000 }, - }, - "deepseek/deepseek-v3": { - id: "deepseek/deepseek-v3", - name: "DeepSeek V3 0324", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2024-12-26", - last_updated: "2024-12-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.77, output: 0.77 }, - limit: { context: 163840, output: 16384 }, - }, - "deepseek/deepseek-v3.1-terminus": { - id: "deepseek/deepseek-v3.1-terminus", - name: "DeepSeek V3.1 Terminus", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-09-22", - last_updated: "2025-09-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 1 }, - limit: { context: 131072, output: 65536 }, - }, - "deepseek/deepseek-r1": { - id: "deepseek/deepseek-r1", - name: "DeepSeek-R1", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-01-20", - last_updated: "2025-05-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.35, output: 5.4 }, - limit: { context: 128000, output: 32768 }, - }, - "arcee-ai/trinity-mini": { - id: "arcee-ai/trinity-mini", - name: "Trinity Mini", - family: "trinity", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2025-12", - last_updated: "2025-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.15 }, - limit: { context: 131072, output: 131072 }, - }, - "arcee-ai/trinity-large-thinking": { - id: "arcee-ai/trinity-large-thinking", - name: "Trinity Large Thinking", - family: "trinity", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-01", - last_updated: "2026-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 0.8999999999999999 }, - limit: { context: 262100, output: 80000 }, - }, - "arcee-ai/trinity-large-preview": { - id: "arcee-ai/trinity-large-preview", - name: "Trinity Large Preview", - family: "trinity", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-01", - last_updated: "2025-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1 }, - limit: { context: 131000, output: 131000 }, - }, - "recraft/recraft-v3": { - id: "recraft/recraft-v3", - name: "Recraft V3", - family: "recraft", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-10", - last_updated: "2024-10", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 512, output: 0 }, - }, - "recraft/recraft-v2": { - id: "recraft/recraft-v2", - name: "Recraft V2", - family: "recraft", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-03", - last_updated: "2024-03", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 512, output: 0 }, - }, - "voyage/voyage-3-large": { - id: "voyage/voyage-3-large", - name: "voyage-3-large", - family: "voyage", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-09", - last_updated: "2024-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.18, output: 0 }, - limit: { context: 8192, output: 1536 }, - }, - "voyage/voyage-4-large": { - id: "voyage/voyage-4-large", - name: "voyage-4-large", - family: "voyage", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2026-03-06", - last_updated: "2026-03-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 32000, output: 0 }, - }, - "voyage/voyage-3.5-lite": { - id: "voyage/voyage-3.5-lite", - name: "voyage-3.5-lite", - family: "voyage", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-05-20", - last_updated: "2025-05-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.02, output: 0 }, - limit: { context: 8192, output: 1536 }, - }, - "voyage/voyage-code-3": { - id: "voyage/voyage-code-3", - name: "voyage-code-3", - family: "voyage", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-09", - last_updated: "2024-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.18, output: 0 }, - limit: { context: 8192, output: 1536 }, - }, - "voyage/voyage-finance-2": { - id: "voyage/voyage-finance-2", - name: "voyage-finance-2", - family: "voyage", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-03", - last_updated: "2024-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.12, output: 0 }, - limit: { context: 8192, output: 1536 }, - }, - "voyage/voyage-4-lite": { - id: "voyage/voyage-4-lite", - name: "voyage-4-lite", - family: "voyage", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2026-03-06", - last_updated: "2026-03-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 32000, output: 0 }, - }, - "voyage/voyage-4": { - id: "voyage/voyage-4", - name: "voyage-4", - family: "voyage", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2026-03-06", - last_updated: "2026-03-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 32000, output: 0 }, - }, - "voyage/voyage-code-2": { - id: "voyage/voyage-code-2", - name: "voyage-code-2", - family: "voyage", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-01", - last_updated: "2024-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.12, output: 0 }, - limit: { context: 8192, output: 1536 }, - }, - "voyage/voyage-law-2": { - id: "voyage/voyage-law-2", - name: "voyage-law-2", - family: "voyage", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-03", - last_updated: "2024-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.12, output: 0 }, - limit: { context: 8192, output: 1536 }, - }, - "voyage/voyage-3.5": { - id: "voyage/voyage-3.5", - name: "voyage-3.5", - family: "voyage", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-05-20", - last_updated: "2025-05-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.06, output: 0 }, - limit: { context: 8192, output: 1536 }, - }, - "morph/morph-v3-large": { - id: "morph/morph-v3-large", - name: "Morph v3 Large", - family: "morph", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-08-15", - last_updated: "2024-08-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.9, output: 1.9 }, - limit: { context: 32000, output: 32000 }, - }, - "morph/morph-v3-fast": { - id: "morph/morph-v3-fast", - name: "Morph v3 Fast", - family: "morph", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-08-15", - last_updated: "2024-08-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 1.2 }, - limit: { context: 16000, output: 16000 }, - }, - "zai/glm-5v-turbo": { - id: "zai/glm-5v-turbo", - name: "GLM 5V Turbo", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-01", - last_updated: "2026-04-03", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.2, output: 4, cache_read: 0.24 }, - limit: { context: 200000, output: 128000 }, - }, - "zai/glm-4.7": { - id: "zai/glm-4.7", - name: "GLM 4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.43, output: 1.75, cache_read: 0.08 }, - limit: { context: 202752, output: 120000 }, - }, - "zai/glm-5": { - id: "zai/glm-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3.2, cache_read: 0.2 }, - limit: { context: 202800, output: 131072 }, - }, - "zai/glm-4.7-flashx": { - id: "zai/glm-4.7-flashx", - name: "GLM 4.7 FlashX", - family: "glm-flash", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-01", - last_updated: "2025-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.06, output: 0.4, cache_read: 0.01 }, - limit: { context: 200000, output: 128000 }, - }, - "zai/glm-4.6v-flash": { - id: "zai/glm-4.6v-flash", - name: "GLM-4.6V-Flash", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 24000 }, - }, - "zai/glm-4.5": { - id: "zai/glm-4.5", - name: "GLM 4.5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2 }, - limit: { context: 131072, output: 131072 }, - }, - "zai/glm-4.5-air": { - id: "zai/glm-4.5-air", - name: "GLM 4.5 Air", - family: "glm-air", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 1.1 }, - limit: { context: 128000, output: 96000 }, - }, - "zai/glm-5-turbo": { - id: "zai/glm-5-turbo", - name: "GLM 5 Turbo", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-15", - last_updated: "2026-03-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.2, output: 4, cache_read: 0.24 }, - limit: { context: 202800, output: 131100 }, - }, - "zai/glm-4.5v": { - id: "zai/glm-4.5v", - name: "GLM 4.5V", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08", - release_date: "2025-08-11", - last_updated: "2025-08-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 1.8 }, - limit: { context: 66000, output: 66000 }, - }, - "zai/glm-4.6": { - id: "zai/glm-4.6", - name: "GLM 4.6", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.45, output: 1.8 }, - limit: { context: 200000, output: 96000 }, - }, - "zai/glm-4.6v": { - id: "zai/glm-4.6v", - name: "GLM-4.6V", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.9, cache_read: 0.05 }, - limit: { context: 128000, output: 24000 }, - }, - "zai/glm-4.7-flash": { - id: "zai/glm-4.7-flash", - name: "GLM 4.7 Flash", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-13", - last_updated: "2026-03-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.07, output: 0.39999999999999997 }, - limit: { context: 200000, output: 131000 }, - }, - "cohere/command-a": { - id: "cohere/command-a", - name: "Command A", - family: "command", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-03-13", - last_updated: "2025-03-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10 }, - limit: { context: 256000, output: 8000 }, - }, - "cohere/embed-v4.0": { - id: "cohere/embed-v4.0", - name: "Embed v4.0", - family: "cohere-embed", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-04-15", - last_updated: "2025-04-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.12, output: 0 }, - limit: { context: 8192, output: 1536 }, - }, - "prime-intellect/intellect-3": { - id: "prime-intellect/intellect-3", - name: "INTELLECT 3", - family: "intellect", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-11-26", - last_updated: "2025-11-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.1 }, - limit: { context: 131072, output: 131072 }, - }, - "xai/grok-4.20-non-reasoning": { - id: "xai/grok-4.20-non-reasoning", - name: "Grok 4.20 Non-Reasoning", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2026-03-09", - last_updated: "2026-03-23", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6, cache_read: 0.19999999999999998 }, - limit: { context: 2000000, output: 2000000 }, - }, - "xai/grok-4.20-non-reasoning-beta": { - id: "xai/grok-4.20-non-reasoning-beta", - name: "Grok 4.20 Beta Non-Reasoning", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2026-03-11", - last_updated: "2026-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6, cache_read: 0.19999999999999998 }, - limit: { context: 2000000, output: 2000000 }, - }, - "xai/grok-4.20-reasoning": { - id: "xai/grok-4.20-reasoning", - name: "Grok 4.20 Reasoning", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-09", - last_updated: "2026-03-23", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6, cache_read: 0.19999999999999998 }, - limit: { context: 2000000, output: 2000000 }, - }, - "xai/grok-imagine-image": { - id: "xai/grok-imagine-image", - name: "Grok Imagine Image", - family: "grok", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2026-01-28", - last_updated: "2026-02-19", - modalities: { input: ["text"], output: ["text", "image"] }, - open_weights: false, - limit: { context: 0, output: 0 }, - }, - "xai/grok-4.20-multi-agent-beta": { - id: "xai/grok-4.20-multi-agent-beta", - name: "Grok 4.20 Multi Agent Beta", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-11", - last_updated: "2026-03-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6, cache_read: 0.19999999999999998 }, - limit: { context: 2000000, output: 2000000 }, - }, - "xai/grok-imagine-image-pro": { - id: "xai/grok-imagine-image-pro", - name: "Grok Imagine Image Pro", - family: "grok", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2026-01-28", - last_updated: "2026-02-19", - modalities: { input: ["text"], output: ["text", "image"] }, - open_weights: false, - limit: { context: 0, output: 0 }, - }, - "xai/grok-4-fast-reasoning": { - id: "xai/grok-4-fast-reasoning", - name: "Grok 4 Fast Reasoning", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 256000 }, - }, - "xai/grok-4.1-fast-non-reasoning": { - id: "xai/grok-4.1-fast-non-reasoning", - name: "Grok 4.1 Fast Non-Reasoning", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - "xai/grok-4.20-reasoning-beta": { - id: "xai/grok-4.20-reasoning-beta", - name: "Grok 4.20 Beta Reasoning", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-11", - last_updated: "2026-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6, cache_read: 0.19999999999999998 }, - limit: { context: 2000000, output: 2000000 }, - }, - "xai/grok-4.20-multi-agent": { - id: "xai/grok-4.20-multi-agent", - name: "Grok 4.20 Multi-Agent", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-09", - last_updated: "2026-03-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6, cache_read: 0.19999999999999998 }, - limit: { context: 2000000, output: 2000000 }, - }, - "xai/grok-4.1-fast-reasoning": { - id: "xai/grok-4.1-fast-reasoning", - name: "Grok 4.1 Fast Reasoning", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - "xai/grok-4-fast-non-reasoning": { - id: "xai/grok-4-fast-non-reasoning", - name: "Grok 4 Fast (Non-Reasoning)", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-09-19", - last_updated: "2025-09-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - "xai/grok-3": { - id: "xai/grok-3", - name: "Grok 3", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.75 }, - limit: { context: 131072, output: 8192 }, - }, - "xai/grok-3-mini": { - id: "xai/grok-3-mini", - name: "Grok 3 Mini", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.5, reasoning: 0.5, cache_read: 0.075 }, - limit: { context: 131072, output: 8192 }, - }, - "xai/grok-2-vision": { - id: "xai/grok-2-vision", - name: "Grok 2 Vision", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2024-08-20", - last_updated: "2024-08-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 10, cache_read: 2 }, - limit: { context: 8192, output: 4096 }, - }, - "xai/grok-4": { - id: "xai/grok-4", - name: "Grok 4", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, reasoning: 15, cache_read: 0.75 }, - limit: { context: 256000, output: 64000 }, - }, - "xai/grok-code-fast-1": { - id: "xai/grok-code-fast-1", - name: "Grok Code Fast 1", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2025-08-28", - last_updated: "2025-08-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.5, cache_read: 0.02 }, - limit: { context: 256000, output: 10000 }, - }, - "xai/grok-3-fast": { - id: "xai/grok-3-fast", - name: "Grok 3 Fast", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 1.25 }, - limit: { context: 131072, output: 8192 }, - }, - "xai/grok-3-mini-fast": { - id: "xai/grok-3-mini-fast", - name: "Grok 3 Mini Fast", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 4, reasoning: 4, cache_read: 0.15 }, - limit: { context: 131072, output: 8192 }, - }, - "nvidia/nemotron-3-super-120b-a12b": { - id: "nvidia/nemotron-3-super-120b-a12b", - name: "NVIDIA Nemotron 3 Super 120B A12B", - family: "nemotron", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.65 }, - limit: { context: 256000, output: 32000 }, - }, - "nvidia/nemotron-3-nano-30b-a3b": { - id: "nvidia/nemotron-3-nano-30b-a3b", - name: "Nemotron 3 Nano 30B A3B", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2024-12", - last_updated: "2024-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.06, output: 0.24 }, - limit: { context: 262144, output: 262144 }, - }, - "nvidia/nemotron-nano-12b-v2-vl": { - id: "nvidia/nemotron-nano-12b-v2-vl", - name: "Nvidia Nemotron Nano 12B V2 VL", - family: "nemotron", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-12", - last_updated: "2024-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.6 }, - limit: { context: 131072, output: 131072 }, - }, - "nvidia/nemotron-nano-9b-v2": { - id: "nvidia/nemotron-nano-9b-v2", - name: "Nvidia Nemotron Nano 9B V2", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-08-18", - last_updated: "2025-08-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.04, output: 0.16 }, - limit: { context: 131072, output: 131072 }, - }, - "inception/mercury-2": { - id: "inception/mercury-2", - name: "Mercury 2", - family: "mercury", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-24", - last_updated: "2026-03-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 0.75, cache_read: 0.024999999999999998 }, - limit: { context: 128000, output: 128000 }, - }, - "inception/mercury-coder-small": { - id: "inception/mercury-coder-small", - name: "Mercury Coder Small Beta", - family: "mercury", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-02-26", - last_updated: "2025-02-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1 }, - limit: { context: 32000, output: 16384 }, - }, - "openai/gpt-5.1-codex-max": { - id: "openai/gpt-5.1-codex-max", - name: "GPT 5.1 Codex Max", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.13 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "openai/gpt-5.2-chat": { - id: "openai/gpt-5.2-chat", - name: "GPT-5.2 Chat", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.18 }, - limit: { context: 128000, input: 111616, output: 16384 }, - }, - "openai/gpt-4o-mini-search-preview": { - id: "openai/gpt-4o-mini-search-preview", - name: "GPT 4o Mini Search Preview", - family: "gpt-mini", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - knowledge: "2023-09", - release_date: "2025-01", - last_updated: "2025-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 128000, input: 111616, output: 16384 }, - }, - "openai/codex-mini": { - id: "openai/codex-mini", - name: "Codex Mini", - family: "gpt-codex-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-05-16", - last_updated: "2025-05-16", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.5, output: 6, cache_read: 0.38 }, - limit: { context: 200000, input: 100000, output: 100000 }, - }, - "openai/gpt-5-chat": { - id: "openai/gpt-5-chat", - name: "GPT-5 Chat", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image", "pdf"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.13 }, - limit: { context: 128000, input: 111616, output: 16384 }, - }, - "openai/gpt-5.3-chat": { - id: "openai/gpt-5.3-chat", - name: "GPT-5.3 Chat", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-03", - last_updated: "2026-03-06", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 128000, input: 111616, output: 16384 }, - }, - "openai/gpt-5.2-pro": { - id: "openai/gpt-5.2-pro", - name: "GPT 5.2 ", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 21, output: 168 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "openai/text-embedding-3-large": { - id: "openai/text-embedding-3-large", - name: "text-embedding-3-large", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-01-25", - last_updated: "2024-01-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.13, output: 0 }, - limit: { context: 8192, input: 6656, output: 1536 }, - }, - "openai/gpt-5.3-codex": { - id: "openai/gpt-5.3-codex", - name: "GPT 5.3 Codex", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-24", - last_updated: "2026-02-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "openai/text-embedding-ada-002": { - id: "openai/text-embedding-ada-002", - name: "text-embedding-ada-002", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2022-12-15", - last_updated: "2022-12-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0 }, - limit: { context: 8192, input: 6656, output: 1536 }, - }, - "openai/gpt-5.2": { - id: "openai/gpt-5.2", - name: "GPT-5.2", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.18 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "openai/o3-pro": { - id: "openai/o3-pro", - name: "o3 Pro", - family: "o-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-10", - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 20, output: 80 }, - limit: { context: 200000, input: 100000, output: 100000 }, - }, - "openai/gpt-5.4-mini": { - id: "openai/gpt-5.4-mini", - name: "GPT 5.4 Mini", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.75, output: 4.5, cache_read: 0.075 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "openai/gpt-5.4-nano": { - id: "openai/gpt-5.4-nano", - name: "GPT 5.4 Nano", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.19999999999999998, output: 1.25, cache_read: 0.02 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "openai/gpt-5.2-codex": { - id: "openai/gpt-5.2-codex", - name: "GPT-5.2-Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-12", - last_updated: "2025-12", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "openai/gpt-5.1-codex-mini": { - id: "openai/gpt-5.1-codex-mini", - name: "GPT-5.1 Codex mini", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-05-16", - last_updated: "2025-05-16", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.03 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "openai/gpt-5.1-thinking": { - id: "openai/gpt-5.1-thinking", - name: "GPT 5.1 Thinking", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-10", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image", "pdf"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.13 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "openai/gpt-5.4-pro": { - id: "openai/gpt-5.4-pro", - name: "GPT 5.4 Pro", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-05", - last_updated: "2026-03-06", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 30, output: 180 }, - limit: { context: 1050000, input: 922000, output: 128000 }, - }, - "openai/gpt-3.5-turbo": { - id: "openai/gpt-3.5-turbo", - name: "GPT-3.5 Turbo", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2021-09", - release_date: "2023-03-01", - last_updated: "2023-03-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 1.5 }, - limit: { context: 16385, input: 12289, output: 4096 }, - }, - "openai/o3-deep-research": { - id: "openai/o3-deep-research", - name: "o3-deep-research", - family: "o", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-10", - release_date: "2024-06-26", - last_updated: "2024-06-26", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 10, output: 40, cache_read: 2.5 }, - limit: { context: 200000, input: 100000, output: 100000 }, - }, - "openai/text-embedding-3-small": { - id: "openai/text-embedding-3-small", - name: "text-embedding-3-small", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-01-25", - last_updated: "2024-01-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.02, output: 0 }, - limit: { context: 8192, input: 6656, output: 1536 }, - }, - "openai/gpt-5.4": { - id: "openai/gpt-5.4", - name: "GPT 5.4", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-05", - last_updated: "2026-03-06", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 15, cache_read: 0.25 }, - limit: { context: 1050000, input: 922000, output: 128000 }, - }, - "openai/gpt-oss-20b": { - id: "openai/gpt-oss-20b", - name: "GPT OSS 20B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.3 }, - limit: { context: 131072, input: 98304, output: 32768 }, - }, - "openai/gpt-5-pro": { - id: "openai/gpt-5-pro", - name: "GPT-5 pro", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image", "pdf"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 15, output: 120 }, - limit: { context: 400000, input: 128000, output: 272000 }, - }, - "openai/gpt-oss-safeguard-20b": { - id: "openai/gpt-oss-safeguard-20b", - name: "gpt-oss-safeguard-20b", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.08, output: 0.3, cache_read: 0.04 }, - limit: { context: 131072, input: 65536, output: 65536 }, - }, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "GPT OSS 120B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.5 }, - limit: { context: 131072, output: 131072 }, - }, - "openai/gpt-3.5-turbo-instruct": { - id: "openai/gpt-3.5-turbo-instruct", - name: "GPT-3.5 Turbo Instruct", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2021-09", - release_date: "2023-03-01", - last_updated: "2023-03-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.5, output: 2 }, - limit: { context: 8192, input: 4096, output: 4096 }, - }, - "openai/gpt-5.1-instant": { - id: "openai/gpt-5.1-instant", - name: "GPT-5.1 Instant", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image", "pdf"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.13 }, - limit: { context: 128000, input: 111616, output: 16384 }, - }, - "openai/gpt-5.1-codex": { - id: "openai/gpt-5.1-codex", - name: "GPT-5.1-Codex", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.13 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "openai/gpt-4.1-mini": { - id: "openai/gpt-4.1-mini", - name: "GPT-4.1 mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.6, cache_read: 0.1 }, - limit: { context: 1047576, output: 32768 }, - }, - "openai/gpt-4.1": { - id: "openai/gpt-4.1", - name: "GPT-4.1", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 1047576, output: 32768 }, - }, - "openai/gpt-5": { - id: "openai/gpt-5", - name: "GPT-5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "openai/gpt-4o": { - id: "openai/gpt-4o", - name: "GPT-4o", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-09", - release_date: "2024-05-13", - last_updated: "2024-08-06", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10, cache_read: 1.25 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/o3": { - id: "openai/o3", - name: "o3", - family: "o", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05", - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-4.1-nano": { - id: "openai/gpt-4.1-nano", - name: "GPT-4.1 nano", - family: "gpt-nano", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.03 }, - limit: { context: 1047576, output: 32768 }, - }, - "openai/gpt-5-codex": { - id: "openai/gpt-5-codex", - name: "GPT-5-Codex", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-09-15", - last_updated: "2025-09-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "openai/o3-mini": { - id: "openai/o3-mini", - name: "o3-mini", - family: "o-mini", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05", - release_date: "2024-12-20", - last_updated: "2025-01-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.55 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/o1": { - id: "openai/o1", - name: "o1", - family: "o", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2023-09", - release_date: "2024-12-05", - last_updated: "2024-12-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 60, cache_read: 7.5 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/o4-mini": { - id: "openai/o4-mini", - name: "o4-mini", - family: "o-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05", - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.28 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-4o-mini": { - id: "openai/gpt-4o-mini", - name: "GPT-4o mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-09", - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6, cache_read: 0.08 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-4-turbo": { - id: "openai/gpt-4-turbo", - name: "GPT-4 Turbo", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - knowledge: "2023-12", - release_date: "2023-11-06", - last_updated: "2024-04-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 10, output: 30 }, - limit: { context: 128000, output: 4096 }, - }, - "openai/gpt-5-nano": { - id: "openai/gpt-5-nano", - name: "GPT-5 Nano", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.4, cache_read: 0.005 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "openai/gpt-5-mini": { - id: "openai/gpt-5-mini", - name: "GPT-5 Mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.025 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "amazon/titan-embed-text-v2": { - id: "amazon/titan-embed-text-v2", - name: "Titan Text Embeddings V2", - family: "titan-embed", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-04", - last_updated: "2024-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.02, output: 0 }, - limit: { context: 8192, output: 1536 }, - }, - "amazon/nova-2-lite": { - id: "amazon/nova-2-lite", - name: "Nova 2 Lite", - family: "nova", - attachment: true, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5 }, - limit: { context: 1000000, output: 1000000 }, - }, - "amazon/nova-pro": { - id: "amazon/nova-pro", - name: "Nova Pro", - family: "nova-pro", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-12-03", - last_updated: "2024-12-03", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 3.2, cache_read: 0.2 }, - limit: { context: 300000, output: 8192 }, - }, - "amazon/nova-lite": { - id: "amazon/nova-lite", - name: "Nova Lite", - family: "nova-lite", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-12-03", - last_updated: "2024-12-03", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.06, output: 0.24, cache_read: 0.015 }, - limit: { context: 300000, output: 8192 }, - }, - "amazon/nova-micro": { - id: "amazon/nova-micro", - name: "Nova Micro", - family: "nova-micro", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-12-03", - last_updated: "2024-12-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.035, output: 0.14, cache_read: 0.00875 }, - limit: { context: 128000, output: 8192 }, - }, - "mistral/mistral-nemo": { - id: "mistral/mistral-nemo", - name: "Mistral Nemo", - family: "mistral-nemo", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-07-01", - last_updated: "2024-07-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.04, output: 0.17 }, - limit: { context: 60288, output: 16000 }, - }, - "mistral/ministral-14b": { - id: "mistral/ministral-14b", - name: "Ministral 14B", - family: "ministral", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 256000, output: 256000 }, - }, - "mistral/codestral-embed": { - id: "mistral/codestral-embed", - name: "Codestral Embed", - family: "codestral-embed", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-05-28", - last_updated: "2025-05-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0 }, - limit: { context: 8192, output: 1536 }, - }, - "mistral/mistral-medium": { - id: "mistral/mistral-medium", - name: "Mistral Medium 3.1", - family: "mistral-medium", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-05-07", - last_updated: "2025-05-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2 }, - limit: { context: 128000, output: 64000 }, - }, - "mistral/mistral-embed": { - id: "mistral/mistral-embed", - name: "Mistral Embed", - family: "mistral-embed", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2023-12-11", - last_updated: "2023-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0 }, - limit: { context: 8192, output: 1536 }, - }, - "mistral/devstral-2": { - id: "mistral/devstral-2", - name: "Devstral 2", - family: "devstral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-12-09", - last_updated: "2025-12-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 256000, output: 256000 }, - }, - "mistral/mistral-large-3": { - id: "mistral/mistral-large-3", - name: "Mistral Large 3", - family: "mistral-large", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2025-12-02", - last_updated: "2025-12-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 1.5 }, - limit: { context: 256000, output: 256000 }, - }, - "mistral/devstral-small-2": { - id: "mistral/devstral-small-2", - name: "Devstral Small 2", - family: "devstral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-05-07", - last_updated: "2025-05-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 256000, output: 256000 }, - }, - "mistral/devstral-small": { - id: "mistral/devstral-small", - name: "Devstral Small 1.1", - family: "devstral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-05-07", - last_updated: "2025-05-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 128000, output: 64000 }, - }, - "mistral/ministral-8b": { - id: "mistral/ministral-8b", - name: "Ministral 8B (latest)", - family: "ministral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-10-01", - last_updated: "2024-10-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 128000, output: 128000 }, - }, - "mistral/magistral-medium": { - id: "mistral/magistral-medium", - name: "Magistral Medium (latest)", - family: "magistral-medium", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-03-17", - last_updated: "2025-03-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2, output: 5 }, - limit: { context: 128000, output: 16384 }, - }, - "mistral/mistral-small": { - id: "mistral/mistral-small", - name: "Mistral Small (latest)", - family: "mistral-small", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2026-03-16", - last_updated: "2026-03-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 256000, output: 256000 }, - }, - "mistral/magistral-small": { - id: "mistral/magistral-small", - name: "Magistral Small", - family: "magistral-small", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-03-17", - last_updated: "2025-03-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 1.5 }, - limit: { context: 128000, output: 128000 }, - }, - "mistral/pixtral-12b": { - id: "mistral/pixtral-12b", - name: "Pixtral 12B", - family: "pixtral", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-09", - release_date: "2024-09-01", - last_updated: "2024-09-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.15 }, - limit: { context: 128000, output: 128000 }, - }, - "mistral/mixtral-8x22b-instruct": { - id: "mistral/mixtral-8x22b-instruct", - name: "Mixtral 8x22B", - family: "mixtral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-04-17", - last_updated: "2024-04-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2, output: 6 }, - limit: { context: 64000, output: 64000 }, - }, - "mistral/pixtral-large": { - id: "mistral/pixtral-large", - name: "Pixtral Large (latest)", - family: "pixtral", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2024-11-01", - last_updated: "2024-11-04", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 2, output: 6 }, - limit: { context: 128000, output: 128000 }, - }, - "mistral/ministral-3b": { - id: "mistral/ministral-3b", - name: "Ministral 3B (latest)", - family: "ministral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-10-01", - last_updated: "2024-10-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.04, output: 0.04 }, - limit: { context: 128000, output: 128000 }, - }, - "mistral/codestral": { - id: "mistral/codestral", - name: "Codestral (latest)", - family: "codestral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-05-29", - last_updated: "2025-01-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.9 }, - limit: { context: 256000, output: 4096 }, - }, - "meta/llama-3.2-1b": { - id: "meta/llama-3.2-1b", - name: "Llama 3.2 1B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-12", - release_date: "2024-09-18", - last_updated: "2024-09-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 128000, output: 8192 }, - }, - "meta/llama-3.1-8b": { - id: "meta/llama-3.1-8b", - name: "Llama 3.1 8B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.03, output: 0.05 }, - limit: { context: 131072, output: 16384 }, - }, - "meta/llama-3.2-90b": { - id: "meta/llama-3.2-90b", - name: "Llama 3.2 90B Vision Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-09-25", - last_updated: "2024-09-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.72, output: 0.72 }, - limit: { context: 128000, output: 8192 }, - }, - "meta/llama-3.2-3b": { - id: "meta/llama-3.2-3b", - name: "Llama 3.2 3B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-12", - release_date: "2024-09-18", - last_updated: "2024-09-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.15 }, - limit: { context: 128000, output: 8192 }, - }, - "meta/llama-3.2-11b": { - id: "meta/llama-3.2-11b", - name: "Llama 3.2 11B Vision Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-09-25", - last_updated: "2024-09-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.16, output: 0.16 }, - limit: { context: 128000, output: 8192 }, - }, - "meta/llama-3.1-70b": { - id: "meta/llama-3.1-70b", - name: "Llama 3.1 70B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 0.4 }, - limit: { context: 131072, output: 16384 }, - }, - "meta/llama-3.3-70b": { - id: "meta/llama-3.3-70b", - name: "Llama-3.3-70B-Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "meta/llama-4-maverick": { - id: "meta/llama-4-maverick", - name: "Llama-4-Maverick-17B-128E-Instruct-FP8", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "meta/llama-4-scout": { - id: "meta/llama-4-scout", - name: "Llama-4-Scout-17B-16E-Instruct-FP8", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "vercel/v0-1.5-md": { - id: "vercel/v0-1.5-md", - name: "v0-1.5-md", - family: "v0", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-06-09", - last_updated: "2025-06-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 128000, output: 32000 }, - }, - "vercel/v0-1.0-md": { - id: "vercel/v0-1.0-md", - name: "v0-1.0-md", - family: "v0", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 128000, output: 32000 }, - }, - "minimax/minimax-m2.7": { - id: "minimax/minimax-m2.7", - name: "Minimax M2.7", - family: "minimax", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.06, cache_write: 0.375 }, - limit: { context: 204800, output: 131000 }, - }, - "minimax/minimax-m2.7-highspeed": { - id: "minimax/minimax-m2.7-highspeed", - name: "MiniMax M2.7 High Speed", - family: "minimax", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.4, cache_read: 0.06, cache_write: 0.375 }, - limit: { context: 204800, output: 131100 }, - }, - "minimax/minimax-m2": { - id: "minimax/minimax-m2", - name: "MiniMax M2", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-10-27", - last_updated: "2025-10-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 1.15, cache_read: 0.03, cache_write: 0.38 }, - limit: { context: 262114, output: 262114 }, - }, - "minimax/minimax-m2.1": { - id: "minimax/minimax-m2.1", - name: "MiniMax M2.1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-10-27", - last_updated: "2025-10-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2, cache_read: 0.03, cache_write: 0.38 }, - limit: { context: 204800, output: 131072 }, - }, - "minimax/minimax-m2.1-lightning": { - id: "minimax/minimax-m2.1-lightning", - name: "MiniMax M2.1 Lightning", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-10-27", - last_updated: "2025-10-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.4, cache_read: 0.03, cache_write: 0.38 }, - limit: { context: 204800, output: 131072 }, - }, - "minimax/minimax-m2.5": { - id: "minimax/minimax-m2.5", - name: "MiniMax M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2, cache_read: 0.03, cache_write: 0.375 }, - limit: { context: 204800, output: 131000 }, - }, - "minimax/minimax-m2.5-highspeed": { - id: "minimax/minimax-m2.5-highspeed", - name: "MiniMax M2.5 High Speed", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-03-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 2.4, cache_read: 0.03, cache_write: 0.375 }, - limit: { context: 0, output: 0 }, - }, - "kwaipilot/kat-coder-pro-v1": { - id: "kwaipilot/kat-coder-pro-v1", - name: "KAT-Coder-Pro V1", - family: "kat-coder", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2025-10-24", - last_updated: "2025-10-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 256000, output: 32000 }, - }, - "kwaipilot/kat-coder-pro-v2": { - id: "kwaipilot/kat-coder-pro-v2", - name: "Kat Coder Pro V2", - family: "kat-coder", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-27", - last_updated: "2026-03-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2, cache_read: 0.06 }, - limit: { context: 256000, output: 256000 }, - }, - "google/gemini-2.5-flash-lite-preview-09-2025": { - id: "google/gemini-2.5-flash-lite-preview-09-2025", - name: "Gemini 2.5 Flash Lite Preview 09-25", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.01 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-3.1-flash-lite-preview": { - id: "google/gemini-3.1-flash-lite-preview", - name: "Gemini 3.1 Flash Lite Preview", - family: "gemini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-03", - last_updated: "2026-03-06", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1.5, cache_read: 0.025, cache_write: 1 }, - limit: { context: 1000000, output: 65000 }, - }, - "google/gemini-3-pro-image": { - id: "google/gemini-3-pro-image", - name: "Nano Banana Pro (Gemini 3 Pro Image)", - family: "gemini-pro", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-03", - release_date: "2025-09", - last_updated: "2025-09", - modalities: { input: ["text"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 2, output: 120 }, - limit: { context: 65536, output: 32768 }, - }, - "google/gemini-3.1-pro-preview": { - id: "google/gemini-3.1-pro-preview", - name: "Gemini 3.1 Pro Preview", - family: "gemini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-19", - last_updated: "2026-02-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2 }, - limit: { context: 1000000, output: 64000 }, - }, - "google/gemini-3-pro-preview": { - id: "google/gemini-3-pro-preview", - name: "Gemini 3 Pro Preview", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-11-18", - last_updated: "2025-11-18", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2, context_over_200k: { input: 4, output: 18, cache_read: 0.4 } }, - limit: { context: 1000000, output: 64000 }, - }, - "google/imagen-4.0-ultra-generate-001": { - id: "google/imagen-4.0-ultra-generate-001", - name: "Imagen 4 Ultra", - family: "imagen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-05-24", - last_updated: "2025-05-24", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 480, output: 0 }, - }, - "google/gemini-embedding-001": { - id: "google/gemini-embedding-001", - name: "Gemini Embedding 001", - family: "gemini-embedding", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-05-20", - last_updated: "2025-05-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0 }, - limit: { context: 8192, output: 1536 }, - }, - "google/gemma-4-31b-it": { - id: "google/gemma-4-31b-it", - name: "Gemma 4 31B IT", - family: "gemma", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-02", - last_updated: "2026-04-03", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.39999999999999997 }, - limit: { context: 262144, output: 131072 }, - }, - "google/gemini-2.5-flash-image": { - id: "google/gemini-2.5-flash-image", - name: "Nano Banana (Gemini 2.5 Flash Image)", - family: "gemini-flash", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-01", - release_date: "2025-03-20", - last_updated: "2025-03-20", - modalities: { input: ["text"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5 }, - limit: { context: 32768, output: 32768 }, - }, - "google/text-embedding-005": { - id: "google/text-embedding-005", - name: "Text Embedding 005", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-08", - last_updated: "2024-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.03, output: 0 }, - limit: { context: 8192, output: 1536 }, - }, - "google/text-multilingual-embedding-002": { - id: "google/text-multilingual-embedding-002", - name: "Text Multilingual Embedding 002", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-03", - last_updated: "2024-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.03, output: 0 }, - limit: { context: 8192, output: 1536 }, - }, - "google/gemini-3.1-flash-image-preview": { - id: "google/gemini-3.1-flash-image-preview", - name: "Gemini 3.1 Flash Image Preview (Nano Banana 2)", - family: "gemini", - attachment: true, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2026-02-26", - last_updated: "2026-03-06", - modalities: { input: ["text", "image"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 0.5, output: 3 }, - limit: { context: 131072, output: 32768 }, - }, - "google/gemini-3-flash": { - id: "google/gemini-3-flash", - name: "Gemini 3 Flash", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03", - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 3, cache_read: 0.05 }, - limit: { context: 1000000, output: 64000 }, - }, - "google/imagen-4.0-generate-001": { - id: "google/imagen-4.0-generate-001", - name: "Imagen 4", - family: "imagen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 480, output: 0 }, - }, - "google/gemini-2.5-flash-preview-09-2025": { - id: "google/gemini-2.5-flash-preview-09-2025", - name: "Gemini 2.5 Flash Preview 09-25", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5, cache_read: 0.03, cache_write: 0.383 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-embedding-2": { - id: "google/gemini-embedding-2", - name: "Gemini Embedding 2", - family: "gemini-embedding", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2026-03-10", - last_updated: "2026-03-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 0, output: 0 }, - }, - "google/gemma-4-26b-a4b-it": { - id: "google/gemma-4-26b-a4b-it", - name: "Gemma 4 26B A4B IT", - family: "gemma", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-02", - last_updated: "2026-04-03", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.13, output: 0.39999999999999997 }, - limit: { context: 262144, output: 131072 }, - }, - "google/imagen-4.0-fast-generate-001": { - id: "google/imagen-4.0-fast-generate-001", - name: "Imagen 4 Fast", - family: "imagen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-06", - last_updated: "2025-06", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 480, output: 0 }, - }, - "google/gemini-2.5-flash-lite": { - id: "google/gemini-2.5-flash-lite", - name: "Gemini 2.5 Flash Lite", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.01 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-2.5-flash-image-preview": { - id: "google/gemini-2.5-flash-image-preview", - name: "Nano Banana Preview (Gemini 2.5 Flash Image Preview)", - family: "gemini-flash", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-01", - release_date: "2025-03-20", - last_updated: "2025-03-20", - modalities: { input: ["text"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5 }, - limit: { context: 32768, output: 32768 }, - }, - "google/gemini-2.0-flash-lite": { - id: "google/gemini-2.0-flash-lite", - name: "Gemini 2.0 Flash Lite", - family: "gemini-flash-lite", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-06", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.075, output: 0.3 }, - limit: { context: 1048576, output: 8192 }, - }, - "google/gemini-2.5-flash": { - id: "google/gemini-2.5-flash", - name: "Gemini 2.5 Flash", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-03-20", - last_updated: "2025-06-05", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5, cache_read: 0.075, input_audio: 1 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-2.5-pro": { - id: "google/gemini-2.5-pro", - name: "Gemini 2.5 Pro", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-03-20", - last_updated: "2025-06-05", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.31 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-2.0-flash": { - id: "google/gemini-2.0-flash", - name: "Gemini 2.0 Flash", - family: "gemini-flash", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-06", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.025 }, - limit: { context: 1048576, output: 8192 }, - }, - "moonshotai/kimi-k2-turbo": { - id: "moonshotai/kimi-k2-turbo", - name: "Kimi K2 Turbo", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.4, output: 10 }, - limit: { context: 256000, output: 16384 }, - }, - "moonshotai/kimi-k2.5": { - id: "moonshotai/kimi-k2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-01-26", - last_updated: "2026-01-26", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 1.2 }, - limit: { context: 262144, output: 262144 }, - }, - "moonshotai/kimi-k2-thinking-turbo": { - id: "moonshotai/kimi-k2-thinking-turbo", - name: "Kimi K2 Thinking Turbo", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-11-06", - last_updated: "2025-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.15, output: 8, cache_read: 0.15 }, - limit: { context: 262114, output: 262114 }, - }, - "moonshotai/kimi-k2-0905": { - id: "moonshotai/kimi-k2-0905", - name: "Kimi K2 0905", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 2.5 }, - limit: { context: 131072, output: 16384 }, - }, - "moonshotai/kimi-k2-thinking": { - id: "moonshotai/kimi-k2-thinking", - name: "Kimi K2 Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-11-06", - last_updated: "2025-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.47, output: 2, cache_read: 0.14 }, - limit: { context: 216144, output: 216144 }, - }, - "moonshotai/kimi-k2": { - id: "moonshotai/kimi-k2", - name: "Kimi K2 Instruct", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-07-14", - last_updated: "2025-07-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3 }, - limit: { context: 131072, output: 16384 }, - status: "deprecated", - }, - "anthropic/claude-3.5-sonnet-20240620": { - id: "anthropic/claude-3.5-sonnet-20240620", - name: "Claude 3.5 Sonnet (2024-06-20)", - family: "claude-sonnet", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-06-20", - last_updated: "2024-06-20", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 200000, output: 8192 }, - }, - "anthropic/claude-opus-4.6": { - id: "anthropic/claude-opus-4.6", - name: "Claude Opus 4.6", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-02", - last_updated: "2026-02", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 1000000, output: 128000 }, - }, - "anthropic/claude-opus-4.5": { - id: "anthropic/claude-opus-4.5", - name: "Claude Opus 4.5", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-24", - last_updated: "2025-11-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 18.75 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-haiku-4.5": { - id: "anthropic/claude-haiku-4.5", - name: "Claude Haiku 4.5", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-sonnet-4.6": { - id: "anthropic/claude-sonnet-4.6", - name: "Claude Sonnet 4.6", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: true, - temperature: true, - knowledge: "2025-08", - release_date: "2026-02-17", - last_updated: "2026-02-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 3, - output: 15, - cache_read: 0.3, - cache_write: 3.75, - context_over_200k: { input: 6, output: 22.5, cache_read: 0.6, cache_write: 7.5 }, - }, - limit: { context: 1000000, output: 128000 }, - }, - "anthropic/claude-3-opus": { - id: "anthropic/claude-3-opus", - name: "Claude Opus 3", - family: "claude-opus", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-08-31", - release_date: "2024-02-29", - last_updated: "2024-02-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 4096 }, - }, - "anthropic/claude-3.5-haiku": { - id: "anthropic/claude-3.5-haiku", - name: "Claude Haiku 3.5", - family: "claude-haiku", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07-31", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 4, cache_read: 0.08, cache_write: 1 }, - limit: { context: 200000, output: 8192 }, - }, - "anthropic/claude-opus-4": { - id: "anthropic/claude-opus-4", - name: "Claude Opus 4", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "anthropic/claude-3-haiku": { - id: "anthropic/claude-3-haiku", - name: "Claude Haiku 3", - family: "claude-haiku", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-08-31", - release_date: "2024-03-13", - last_updated: "2024-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1.25, cache_read: 0.03, cache_write: 0.3 }, - limit: { context: 200000, output: 4096 }, - }, - "anthropic/claude-sonnet-4.5": { - id: "anthropic/claude-sonnet-4.5", - name: "Claude Sonnet 4.5", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-sonnet-4": { - id: "anthropic/claude-sonnet-4", - name: "Claude Sonnet 4", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-3.5-sonnet": { - id: "anthropic/claude-3.5-sonnet", - name: "Claude Sonnet 3.5 v2", - family: "claude-sonnet", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04-30", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 8192 }, - }, - "anthropic/claude-3.7-sonnet": { - id: "anthropic/claude-3.7-sonnet", - name: "Claude Sonnet 3.7", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10-31", - release_date: "2025-02-19", - last_updated: "2025-02-19", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-opus-4.1": { - id: "anthropic/claude-opus-4.1", - name: "Claude Opus 4", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "xiaomi/mimo-v2-pro": { - id: "xiaomi/mimo-v2-pro", - name: "MiMo V2 Pro", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 3, cache_read: 0.19999999999999998 }, - limit: { context: 1000000, output: 128000 }, - }, - "xiaomi/mimo-v2-flash": { - id: "xiaomi/mimo-v2-flash", - name: "MiMo V2 Flash", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.29 }, - limit: { context: 262144, output: 32000 }, - }, - "bytedance/seed-1.6": { - id: "bytedance/seed-1.6", - name: "Seed 1.6", - family: "seed", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-09", - last_updated: "2025-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.05 }, - limit: { context: 256000, output: 32000 }, - }, - "bytedance/seed-1.8": { - id: "bytedance/seed-1.8", - name: "Seed 1.8", - family: "seed", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-10", - last_updated: "2025-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.05 }, - limit: { context: 256000, output: 64000 }, - }, - "meituan/longcat-flash-chat": { - id: "meituan/longcat-flash-chat", - name: "LongCat Flash Chat", - family: "longcat", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-08-30", - last_updated: "2025-08-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 8192 }, - }, - "meituan/longcat-flash-thinking": { - id: "meituan/longcat-flash-thinking", - name: "LongCat Flash Thinking", - family: "longcat", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-09-23", - last_updated: "2025-09-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 1.5 }, - limit: { context: 128000, output: 8192 }, - }, - "meituan/longcat-flash-thinking-2601": { - id: "meituan/longcat-flash-thinking-2601", - name: "LongCat Flash Thinking 2601", - family: "longcat", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2026-03-13", - last_updated: "2026-03-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 32768, output: 32768 }, - }, - "bfl/flux-pro-1.0-fill": { - id: "bfl/flux-pro-1.0-fill", - name: "FLUX.1 Fill [pro]", - family: "flux", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-10", - last_updated: "2024-10", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 512, output: 0 }, - }, - "bfl/flux-pro-1.1": { - id: "bfl/flux-pro-1.1", - name: "FLUX1.1 [pro]", - family: "flux", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-10", - last_updated: "2024-10", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 512, output: 0 }, - }, - "bfl/flux-kontext-pro": { - id: "bfl/flux-kontext-pro", - name: "FLUX.1 Kontext Pro", - family: "flux", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-06", - last_updated: "2025-06", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 512, output: 0 }, - }, - "bfl/flux-kontext-max": { - id: "bfl/flux-kontext-max", - name: "FLUX.1 Kontext Max", - family: "flux", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-06", - last_updated: "2025-06", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 512, output: 0 }, - }, - "bfl/flux-pro-1.1-ultra": { - id: "bfl/flux-pro-1.1-ultra", - name: "FLUX1.1 [pro] Ultra", - family: "flux", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-11", - last_updated: "2024-11", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 512, output: 0 }, - }, - }, - }, - minimax: { - id: "minimax", - env: ["MINIMAX_API_KEY"], - npm: "@ai-sdk/anthropic", - api: "https://api.minimax.io/anthropic/v1", - name: "MiniMax (minimax.io)", - doc: "https://platform.minimax.io/docs/guides/quickstart", - models: { - "MiniMax-M2": { - id: "MiniMax-M2", - name: "MiniMax-M2", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-10-27", - last_updated: "2025-10-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 196608, output: 128000 }, - }, - "MiniMax-M2.5": { - id: "MiniMax-M2.5", - name: "MiniMax-M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.03, cache_write: 0.375 }, - limit: { context: 204800, output: 131072 }, - }, - "MiniMax-M2.7": { - id: "MiniMax-M2.7", - name: "MiniMax-M2.7", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.06, cache_write: 0.375 }, - limit: { context: 204800, output: 131072 }, - }, - "MiniMax-M2.7-highspeed": { - id: "MiniMax-M2.7-highspeed", - name: "MiniMax-M2.7-highspeed", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.4, cache_read: 0.06, cache_write: 0.375 }, - limit: { context: 204800, output: 131072 }, - }, - "MiniMax-M2.1": { - id: "MiniMax-M2.1", - name: "MiniMax-M2.1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 204800, output: 131072 }, - }, - "MiniMax-M2.5-highspeed": { - id: "MiniMax-M2.5-highspeed", - name: "MiniMax-M2.5-highspeed", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-13", - last_updated: "2026-02-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.4, cache_read: 0.06, cache_write: 0.375 }, - limit: { context: 204800, output: 131072 }, - }, - }, - }, - llmgateway: { - id: "llmgateway", - env: ["LLMGATEWAY_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.llmgateway.io/v1", - name: "LLM Gateway", - doc: "https://llmgateway.io/docs", - models: { - "minimax-m2.7": { - id: "minimax-m2.7", - name: "MiniMax M2.7", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-01-01", - last_updated: "2024-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.06 }, - limit: { context: 204800, output: 131100 }, - }, - "gpt-4o-mini-search-preview": { - id: "gpt-4o-mini-search-preview", - name: "GPT-4o Mini Search Preview", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2024-10-01", - last_updated: "2024-10-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 128000, output: 16384 }, - }, - "grok-4-1-fast-reasoning": { - id: "grok-4-1-fast-reasoning", - name: "Grok 4.1 Fast Reasoning", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-11-19", - last_updated: "2025-11-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - "grok-4-20-beta-0309-non-reasoning": { - id: "grok-4-20-beta-0309-non-reasoning", - name: "Grok 4.20 Beta Non-Reasoning (0309)", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-03-09", - last_updated: "2026-03-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6, cache_read: 0.2 }, - limit: { context: 2000000, output: 30000 }, - }, - "qwen3-coder-plus": { - id: "qwen3-coder-plus", - name: "Qwen3 Coder Plus", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-23", - last_updated: "2025-09-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 6, output: 60 }, - limit: { context: 1000000, output: 66000 }, - }, - "claude-haiku-4-5": { - id: "claude-haiku-4-5", - name: "Claude Haiku 4.5", - family: "claude", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1 }, - limit: { context: 200000, output: 64000 }, - }, - "claude-opus-4-5-20251101": { - id: "claude-opus-4-5-20251101", - name: "Claude Opus 4.5", - family: "claude", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-11-24", - last_updated: "2025-11-24", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5 }, - limit: { context: 200000, output: 32000 }, - }, - "gemini-2.5-flash-lite-preview-09-2025": { - id: "gemini-2.5-flash-lite-preview-09-2025", - name: "Gemini 2.5 Flash Lite Preview (09-2025)", - family: "gemini", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.01 }, - limit: { context: 1048576, output: 65535 }, - }, - "qwen3-235b-a22b-instruct-2507": { - id: "qwen3-235b-a22b-instruct-2507", - name: "Qwen3 235B A22B Instruct 2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-21", - last_updated: "2025-07-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.6 }, - limit: { context: 262000, output: 8192 }, - status: "beta", - }, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01-26", - last_updated: "2026-01-26", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 3, cache_read: 0.1 }, - limit: { context: 262144, output: 32768 }, - }, - "llama-3.3-70b-instruct": { - id: "llama-3.3-70b-instruct", - name: "Llama 3.3 70B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.13, output: 0.4 }, - limit: { context: 128000, output: 16384 }, - }, - "mistral-large-2512": { - id: "mistral-large-2512", - name: "Mistral Large 3", - family: "mistral", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-12-02", - last_updated: "2025-12-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 1.5 }, - limit: { context: 262144, output: 16384 }, - }, - "llama-4-scout": { - id: "llama-4-scout", - name: "Llama 4 Scout", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.18, output: 0.59 }, - limit: { context: 32768, output: 16384 }, - status: "beta", - }, - "glm-4.7": { - id: "glm-4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 2.2, cache_read: 0.11 }, - limit: { context: 200000, output: 128000 }, - }, - "minimax-m2.7-highspeed": { - id: "minimax-m2.7-highspeed", - name: "MiniMax M2.7 Highspeed", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2024-01-01", - last_updated: "2024-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.4, cache_read: 0.06 }, - limit: { context: 204800, output: 131100 }, - }, - "hermes-2-pro-llama-3-8b": { - id: "hermes-2-pro-llama-3-8b", - name: "Hermes 2 Pro Llama 3 8B", - family: "nousresearch", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2024-05-27", - last_updated: "2024-05-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.14, output: 0.14 }, - limit: { context: 8192, output: 8192 }, - status: "beta", - }, - "qwen-coder-plus": { - id: "qwen-coder-plus", - name: "Qwen Coder Plus", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-09-18", - last_updated: "2024-09-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 5 }, - limit: { context: 131072, output: 8192 }, - }, - auto: { - id: "auto", - name: "Auto Route", - family: "auto", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-01-01", - last_updated: "2024-01-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "gemma-3n-e4b-it": { - id: "gemma-3n-e4b-it", - name: "Gemma 3n E4B IT", - family: "gemma", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-06-26", - last_updated: "2025-06-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.08, output: 0.3 }, - limit: { context: 1000000, output: 16384 }, - }, - "claude-3-5-sonnet-20241022": { - id: "claude-3-5-sonnet-20241022", - name: "Claude 3.5 Sonnet (2024-10-22)", - family: "claude", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3 }, - limit: { context: 200000, output: 8192 }, - status: "deprecated", - }, - "gpt-5.2-pro": { - id: "gpt-5.2-pro", - name: "GPT-5.2 Pro", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 21, output: 168 }, - limit: { context: 400000, output: 272000 }, - }, - "qwq-plus": { - id: "qwq-plus", - name: "QwQ Plus", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-03-06", - last_updated: "2025-03-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.8, output: 2.4 }, - limit: { context: 131072, output: 8192 }, - }, - "glm-4.6v-flashx": { - id: "glm-4.6v-flashx", - name: "GLM-4.6V FlashX", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-08", - last_updated: "2025-12-08", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.04, output: 0.4, cache_read: 0 }, - limit: { context: 128000, output: 16000 }, - }, - "gemini-3.1-flash-lite-preview": { - id: "gemini-3.1-flash-lite-preview", - name: "Gemini 3.1 Flash Lite (Preview)", - family: "gemini", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-03-03", - last_updated: "2026-03-03", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1.5, cache_read: 0.03 }, - limit: { context: 1048576, output: 65536 }, - }, - "qwen-vl-plus": { - id: "qwen-vl-plus", - name: "Qwen VL Plus", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-02-05", - last_updated: "2025-02-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.21, output: 0.64 }, - limit: { context: 131072, output: 32000 }, - }, - "gemma-2-27b-it-together": { - id: "gemma-2-27b-it-together", - name: "Gemma 2 27B IT", - family: "gemma", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2024-06-27", - last_updated: "2024-06-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.08, output: 0.08 }, - limit: { context: 8192, output: 16384 }, - }, - "glm-5": { - id: "glm-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-15", - last_updated: "2026-02-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 3.2, cache_read: 0.2 }, - limit: { context: 202800, output: 131100 }, - }, - "devstral-2512": { - id: "devstral-2512", - name: "Devstral 2", - family: "mistral", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-12-09", - last_updated: "2025-12-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 2 }, - limit: { context: 262144, output: 16384 }, - }, - "qwen3-32b": { - id: "qwen3-32b", - name: "Qwen3 32B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-28", - last_updated: "2025-04-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 32768, output: 8192 }, - }, - "codestral-2508": { - id: "codestral-2508", - name: "Codestral", - family: "mistral", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-07-30", - last_updated: "2025-07-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.9 }, - limit: { context: 256000, output: 16384 }, - }, - "claude-sonnet-4-6": { - id: "claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - family: "claude", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-17", - last_updated: "2026-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3 }, - limit: { context: 200000, output: 64000 }, - }, - "glm-4.7-flashx": { - id: "glm-4.7-flashx", - name: "GLM-4.7 FlashX", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.07, output: 0.4, cache_read: 0.01 }, - limit: { context: 200000, output: 128000 }, - }, - "gemma-3-1b-it": { - id: "gemma-3-1b-it", - name: "Gemma 3 1B IT", - family: "gemma", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-03-12", - last_updated: "2025-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.08, output: 0.3 }, - limit: { context: 1000000, output: 16384 }, - }, - "gemini-3.1-pro-preview": { - id: "gemini-3.1-pro-preview", - name: "Gemini 3.1 Pro (Preview)", - family: "gemini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-19", - last_updated: "2026-02-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2 }, - limit: { context: 1048576, output: 65536 }, - }, - "qwen35-397b-a17b": { - id: "qwen35-397b-a17b", - name: "Qwen3.5 397B A17B", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-16", - last_updated: "2026-02-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3.6 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen-max": { - id: "qwen-max", - name: "Qwen Max", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 1.6, output: 6.4 }, - limit: { context: 131072, output: 32000 }, - }, - "gpt-5.3-chat-latest": { - id: "gpt-5.3-chat-latest", - name: "GPT-5.3 Chat", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2026-03-03", - last_updated: "2026-03-03", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.18 }, - limit: { context: 128000, output: 16384 }, - }, - "gemini-2.0-flash": { - id: "gemini-2.0-flash", - name: "Gemini 2.0 Flash", - family: "gemini", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-02-05", - last_updated: "2025-02-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.03 }, - limit: { context: 1048576, output: 8192 }, - status: "deprecated", - }, - "gemini-3-flash-preview": { - id: "gemini-3-flash-preview", - name: "Gemini 3 Flash (Preview)", - family: "gemini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 3, cache_read: 0.05 }, - limit: { context: 1048576, output: 65535 }, - }, - "qwen-plus": { - id: "qwen-plus", - name: "Qwen Plus", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-01-25", - last_updated: "2025-01-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 1.2, cache_read: 0.08 }, - limit: { context: 131072, output: 32000 }, - }, - "glm-4-32b-0414-128k": { - id: "glm-4-32b-0414-128k", - name: "GLM-4 32B (0414-128k)", - family: "glm", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 128000, output: 16384 }, - }, - "seed-1-6-flash-250715": { - id: "seed-1-6-flash-250715", - name: "Seed 1.6 Flash (250715)", - family: "seed", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-26", - last_updated: "2025-07-26", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.07, output: 0.3, cache_read: 0.02 }, - limit: { context: 256000, output: 16384 }, - }, - "qwen-omni-turbo": { - id: "qwen-omni-turbo", - name: "Qwen Omni Turbo", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-03-26", - last_updated: "2025-03-26", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.8 }, - limit: { context: 32768, output: 8192 }, - }, - "gpt-5-mini": { - id: "gpt-5-mini", - name: "GPT-5 Mini", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-01", - last_updated: "2025-08-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.03 }, - limit: { context: 400000, output: 128000 }, - }, - "gpt-5-nano": { - id: "gpt-5-nano", - name: "GPT-5 Nano", - family: "gpt", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-01", - last_updated: "2025-08-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.4, cache_read: 0.01 }, - limit: { context: 400000, output: 128000 }, - }, - "claude-3-haiku-20240307": { - id: "claude-3-haiku-20240307", - name: "Claude 3 Haiku (2024-03-07)", - family: "claude", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2024-03-04", - last_updated: "2024-03-04", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1.25, cache_read: 0.03 }, - limit: { context: 200000, output: 4096 }, - }, - "seed-1-6-250615": { - id: "seed-1-6-250615", - name: "Seed 1.6 (250615)", - family: "seed", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-06-25", - last_updated: "2025-06-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.05 }, - limit: { context: 256000, output: 16384 }, - }, - "qwen3-vl-235b-a22b-thinking": { - id: "qwen3-vl-235b-a22b-thinking", - name: "Qwen3 VL 235B A22B Thinking", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-09-23", - last_updated: "2025-09-23", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 2 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen3-vl-30b-a3b-thinking": { - id: "qwen3-vl-30b-a3b-thinking", - name: "Qwen3 VL 30B A3B Thinking", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-11", - last_updated: "2025-10-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 1 }, - limit: { context: 131072, output: 32768 }, - }, - "gpt-5.3-codex": { - id: "gpt-5.3-codex", - name: "GPT-5.3 Codex", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-24", - last_updated: "2026-02-24", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.18 }, - limit: { context: 400000, output: 128000 }, - }, - "minimax-m2": { - id: "minimax-m2", - name: "MiniMax M2", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-27", - last_updated: "2025-10-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 1, cache_read: 0.03 }, - limit: { context: 196608, output: 131072 }, - }, - "claude-sonnet-4-5-20250929": { - id: "claude-sonnet-4-5-20250929", - name: "Claude Sonnet 4.5 (2025-09-29)", - family: "claude", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3 }, - limit: { context: 200000, output: 64000 }, - }, - "qwen-flash": { - id: "qwen-flash", - name: "Qwen Flash", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-09-09", - last_updated: "2024-09-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.4, cache_read: 0.01 }, - limit: { context: 1000000, output: 32000 }, - }, - "gpt-4-turbo": { - id: "gpt-4-turbo", - name: "GPT-4 Turbo", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2023-11-06", - last_updated: "2023-11-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 10, output: 30 }, - limit: { context: 128000, output: 16384 }, - }, - "cogview-4": { - id: "cogview-4", - name: "CogView-4", - family: "glm", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-03-04", - last_updated: "2025-03-04", - modalities: { input: ["text"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 2000, output: 4096 }, - }, - "qwen2-5-vl-32b-instruct": { - id: "qwen2-5-vl-32b-instruct", - name: "Qwen2.5 VL 32B Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-02-19", - last_updated: "2025-02-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 1.4, output: 4.2 }, - limit: { context: 131072, output: 32768 }, - }, - "gemini-2.5-pro": { - id: "gemini-2.5-pro", - name: "Gemini 2.5 Pro", - family: "gemini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-03-25", - last_updated: "2025-03-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.13 }, - limit: { context: 1048576, output: 65536 }, - }, - "grok-4-1-fast-non-reasoning": { - id: "grok-4-1-fast-non-reasoning", - name: "Grok 4.1 Fast Non-Reasoning", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-11-19", - last_updated: "2025-11-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - "sonar-pro": { - id: "sonar-pro", - name: "Sonar Pro", - family: "sonar", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-03-07", - last_updated: "2025-03-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 200000, output: 16384 }, - }, - "pixtral-large-latest": { - id: "pixtral-large-latest", - name: "Pixtral Large Latest", - family: "mistral", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2024-11-18", - last_updated: "2024-11-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 4, output: 12 }, - limit: { context: 128000, output: 16384 }, - }, - "gpt-5.2": { - id: "gpt-5.2", - name: "GPT-5.2", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.18 }, - limit: { context: 400000, output: 128000 }, - }, - "qwen3-vl-8b-instruct": { - id: "qwen3-vl-8b-instruct", - name: "Qwen3 VL 8B Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-10-14", - last_updated: "2025-10-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.08, output: 0.5 }, - limit: { context: 131072, output: 8192 }, - }, - "claude-3-7-sonnet": { - id: "claude-3-7-sonnet", - name: "Claude 3.7 Sonnet", - family: "claude", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-02-24", - last_updated: "2025-02-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3 }, - limit: { context: 200000, output: 8192 }, - }, - "grok-4-20-beta-0309-reasoning": { - id: "grok-4-20-beta-0309-reasoning", - name: "Grok 4.20 Beta Reasoning (0309)", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-03-09", - last_updated: "2026-03-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6, cache_read: 0.2 }, - limit: { context: 2000000, output: 30000 }, - }, - "grok-imagine-image": { - id: "grok-imagine-image", - name: "Grok Imagine Image", - family: "grok", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2026-03-02", - last_updated: "2026-03-02", - modalities: { input: ["text", "image"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 2000, output: 4096 }, - }, - "gpt-4o-mini": { - id: "gpt-4o-mini", - name: "GPT-4o Mini", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6, cache_read: 0.08 }, - limit: { context: 128000, output: 16384 }, - }, - "gemini-pro-latest": { - id: "gemini-pro-latest", - name: "Gemini Pro Latest", - family: "gemini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-27", - last_updated: "2026-02-27", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2 }, - limit: { context: 1048576, output: 65536 }, - }, - "gpt-5.4-mini": { - id: "gpt-5.4-mini", - name: "GPT-5.4 Mini", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.75, output: 4.5, cache_read: 0.08 }, - limit: { context: 400000, output: 128000 }, - }, - "claude-3-5-haiku": { - id: "claude-3-5-haiku", - name: "Claude 3.5 Haiku", - family: "claude", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 4, cache_read: 0.08 }, - limit: { context: 200000, output: 8192 }, - status: "deprecated", - }, - "qwen3-max": { - id: "qwen3-max", - name: "Qwen3 Max", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-24", - last_updated: "2025-09-24", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 3, output: 15, cache_read: 0.6 }, - limit: { context: 256000, output: 32800 }, - }, - "minimax-m2.1": { - id: "minimax-m2.1", - name: "MiniMax M2.1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 1.1 }, - limit: { context: 196608, output: 131072 }, - }, - "gemini-3-pro-image-preview": { - id: "gemini-3-pro-image-preview", - name: "Gemini 3 Pro Image (Preview)", - family: "gemini", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-11-20", - last_updated: "2025-11-20", - modalities: { input: ["text", "image"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2 }, - limit: { context: 65536, output: 32768 }, - }, - "mixtral-8x7b-instruct-together": { - id: "mixtral-8x7b-instruct-together", - name: "Mixtral 8x7B Instruct", - family: "mistral", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2023-12-10", - last_updated: "2023-12-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.06, output: 0.06 }, - limit: { context: 32768, output: 16384 }, - }, - "qwen-max-latest": { - id: "qwen-max-latest", - name: "Qwen Max Latest", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-01-25", - last_updated: "2025-01-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 1.6, output: 6.4 }, - limit: { context: 131072, output: 32000 }, - }, - "o4-mini": { - id: "o4-mini", - name: "o4 Mini", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.28 }, - limit: { context: 200000, output: 16384 }, - }, - "glm-4.6v-flash": { - id: "glm-4.6v-flash", - name: "GLM-4.6V Flash", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-08", - last_updated: "2025-12-08", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 16000 }, - }, - "gpt-5.4-nano": { - id: "gpt-5.4-nano", - name: "GPT-5.4 Nano", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.25, cache_read: 0.02 }, - limit: { context: 400000, output: 128000 }, - }, - "gemini-2.5-flash-image": { - id: "gemini-2.5-flash-image", - name: "Gemini 2.5 Flash Image", - family: "gemini", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-10-02", - last_updated: "2025-10-02", - modalities: { input: ["text", "image"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 0.3, output: 30, cache_read: 0.03 }, - limit: { context: 32768, output: 32768 }, - }, - "glm-4.5": { - id: "glm-4.5", - name: "GLM-4.5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 2.2, cache_read: 0.11 }, - limit: { context: 128000, output: 16384 }, - }, - "mistral-large-latest": { - id: "mistral-large-latest", - name: "Mistral Large Latest", - family: "mistral", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-12-02", - last_updated: "2025-12-02", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 4, output: 12 }, - limit: { context: 128000, output: 16384 }, - }, - "mistral-small-2506": { - id: "mistral-small-2506", - name: "Mistral Small 3.2", - family: "mistral", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-06-20", - last_updated: "2025-06-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 128000, output: 16384 }, - }, - "gemma-3-12b-it": { - id: "gemma-3-12b-it", - name: "Gemma 3 12B IT", - family: "gemma", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-03-10", - last_updated: "2025-03-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.08, output: 0.3 }, - limit: { context: 1000000, output: 16384 }, - }, - "seedream-4-0": { - id: "seedream-4-0", - name: "Seedream 4.0", - family: "seed", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-09-16", - last_updated: "2025-09-16", - modalities: { input: ["text"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 2000, output: 4096 }, - }, - "qwen3-30b-a3b-instruct-2507": { - id: "qwen3-30b-a3b-instruct-2507", - name: "Qwen3 30B A3B Instruct 2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-30", - last_updated: "2025-07-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 262000, output: 8192 }, - }, - "gpt-5.2-codex": { - id: "gpt-5.2-codex", - name: "GPT-5.2 Codex", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01-14", - last_updated: "2026-01-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.18 }, - limit: { context: 400000, output: 128000 }, - }, - "minimax-text-01": { - id: "minimax-text-01", - name: "MiniMax Text 01", - family: "minimax", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-01-15", - last_updated: "2025-01-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 1.1 }, - limit: { context: 1000000, output: 131072 }, - }, - "qwen3-32b-fp8": { - id: "qwen3-32b-fp8", - name: "Qwen3 32B FP8", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-04-28", - last_updated: "2025-04-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.45 }, - limit: { context: 40960, output: 20000 }, - }, - "gemini-2.5-flash": { - id: "gemini-2.5-flash", - name: "Gemini 2.5 Flash", - family: "gemini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-26", - last_updated: "2025-08-26", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5, cache_read: 0.03 }, - limit: { context: 1048576, output: 65535 }, - }, - "llama-4-scout-17b-instruct": { - id: "llama-4-scout-17b-instruct", - name: "Llama 4 Scout 17B Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.17, output: 0.66 }, - limit: { context: 8192, output: 2048 }, - status: "beta", - }, - "gpt-5.2-chat-latest": { - id: "gpt-5.2-chat-latest", - name: "GPT-5.2 Chat", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.18 }, - limit: { context: 128000, output: 16400 }, - }, - "qwen3-4b-fp8": { - id: "qwen3-4b-fp8", - name: "Qwen3 4B FP8", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-04-28", - last_updated: "2025-04-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.03 }, - limit: { context: 128000, output: 20000 }, - }, - "veo-3.1-generate-preview": { - id: "veo-3.1-generate-preview", - name: "Veo 3.1", - family: "gemini", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2026-03-14", - last_updated: "2026-03-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 32768, output: 1 }, - status: "beta", - }, - "llama-guard-4-12b": { - id: "llama-guard-4-12b", - name: "Llama Guard 4 12B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-04-30", - last_updated: "2025-04-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 131072, output: 16384 }, - }, - "gemma-3n-e2b-it": { - id: "gemma-3n-e2b-it", - name: "Gemma 3n E2B IT", - family: "gemma", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-06-26", - last_updated: "2025-06-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.08, output: 0.3 }, - limit: { context: 1000000, output: 16384 }, - }, - "gpt-5.1-codex-mini": { - id: "gpt-5.1-codex-mini", - name: "GPT-5.1 Codex mini", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-11-12", - last_updated: "2025-11-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.03 }, - limit: { context: 400000, output: 128000 }, - }, - "gemini-3.1-flash-image-preview": { - id: "gemini-3.1-flash-image-preview", - name: "Gemini 3.1 Flash Image (Preview)", - family: "gemini", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2026-02-26", - last_updated: "2026-02-26", - modalities: { input: ["text", "image"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 0.25, output: 1.5 }, - limit: { context: 65536, output: 65536 }, - }, - "ministral-8b-2512": { - id: "ministral-8b-2512", - name: "Ministral 8B", - family: "mistral", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-12-02", - last_updated: "2025-12-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.15 }, - limit: { context: 262144, output: 16384 }, - }, - "grok-4-fast": { - id: "grok-4-fast", - name: "Grok 4 Fast", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - "gemma-3-27b": { - id: "gemma-3-27b", - name: "Gemma 3 27B", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-03-12", - last_updated: "2025-03-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 0.27 }, - limit: { context: 128000, output: 16384 }, - }, - "grok-imagine-image-pro": { - id: "grok-imagine-image-pro", - name: "Grok Imagine Image Pro", - family: "grok", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2026-03-02", - last_updated: "2026-03-02", - modalities: { input: ["text", "image"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 2000, output: 4096 }, - }, - "qwen3-vl-flash": { - id: "qwen3-vl-flash", - name: "Qwen3 VL Flash", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.4, cache_read: 0.01 }, - limit: { context: 262144, output: 32768 }, - }, - "llama-3.1-70b-instruct": { - id: "llama-3.1-70b-instruct", - name: "Llama 3.1 70B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.72, output: 0.72 }, - limit: { context: 128000, output: 2048 }, - status: "beta", - }, - "seed-1-8-251228": { - id: "seed-1-8-251228", - name: "Seed 1.8 (251228)", - family: "seed", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-18", - last_updated: "2025-12-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.05 }, - limit: { context: 256000, output: 16384 }, - }, - "qwen3-235b-a22b-thinking-2507": { - id: "qwen3-235b-a22b-thinking-2507", - name: "Qwen3 235B A22B Thinking 2507", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-25", - last_updated: "2025-07-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.6 }, - limit: { context: 262000, output: 8192 }, - status: "beta", - }, - "qwen3-next-80b-a3b-thinking": { - id: "qwen3-next-80b-a3b-thinking", - name: "Qwen3 Next 80B A3B Thinking", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-09-10", - last_updated: "2025-09-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 6 }, - limit: { context: 131072, output: 32768 }, - status: "beta", - }, - "seed-1-6-250915": { - id: "seed-1-6-250915", - name: "Seed 1.6 (250915)", - family: "seed", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-15", - last_updated: "2025-09-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.05 }, - limit: { context: 256000, output: 16384 }, - }, - "grok-code-fast-1": { - id: "grok-code-fast-1", - name: "Grok Code Fast 1", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-28", - last_updated: "2025-08-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.5 }, - limit: { context: 256000, output: 10000 }, - }, - "glm-4.5-x": { - id: "glm-4.5-x", - name: "GLM-4.5 X", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.2, output: 8.9, cache_read: 0.45 }, - limit: { context: 128000, output: 16384 }, - status: "beta", - }, - "veo-3.1-fast-generate-preview": { - id: "veo-3.1-fast-generate-preview", - name: "Veo 3.1 Fast", - family: "gemini", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2026-03-14", - last_updated: "2026-03-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 32768, output: 1 }, - status: "beta", - }, - "gpt-5.1": { - id: "gpt-5.1", - name: "GPT-5.1", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-11-01", - last_updated: "2025-11-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.13 }, - limit: { context: 400000, output: 128000 }, - }, - "gemma-3-4b-it": { - id: "gemma-3-4b-it", - name: "Gemma 3 4B IT", - family: "gemma", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-03-10", - last_updated: "2025-03-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.08, output: 0.3 }, - limit: { context: 1000000, output: 16384 }, - }, - "kimi-k2-thinking-turbo": { - id: "kimi-k2-thinking-turbo", - name: "Kimi K2 Thinking Turbo", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-11-06", - last_updated: "2025-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.15, output: 8, cache_read: 0.15 }, - limit: { context: 262144, output: 262144 }, - }, - "qwen-image-max": { - id: "qwen-image-max", - name: "Qwen Image Max", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-08-04", - last_updated: "2025-08-04", - modalities: { input: ["text"], output: ["text", "image"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 2000, output: 4096 }, - }, - "qwen3-30b-a3b-thinking-2507": { - id: "qwen3-30b-a3b-thinking-2507", - name: "Qwen3 30B A3B Thinking 2507", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-30", - last_updated: "2025-07-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 262000, output: 8192 }, - }, - "grok-4-fast-reasoning": { - id: "grok-4-fast-reasoning", - name: "Grok 4 Fast Reasoning", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - o1: { - id: "o1", - name: "o1", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2024-09-12", - last_updated: "2024-09-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 60, cache_read: 7.5 }, - limit: { context: 200000, output: 16384 }, - }, - "glm-4.5-air": { - id: "glm-4.5-air", - name: "GLM-4.5 Air", - family: "glm", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-25", - last_updated: "2025-07-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.1, cache_read: 0.03 }, - limit: { context: 128000, output: 16384 }, - }, - "gpt-5.4-pro": { - id: "gpt-5.4-pro", - name: "GPT-5.4 Pro", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-03-01", - last_updated: "2026-03-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 30, output: 180 }, - limit: { context: 1050000, output: 128000 }, - }, - "claude-3-5-sonnet": { - id: "claude-3-5-sonnet", - name: "Claude 3.5 Sonnet", - family: "claude", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2024-06-20", - last_updated: "2024-06-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3 }, - limit: { context: 200000, output: 16384 }, - }, - "gpt-3.5-turbo": { - id: "gpt-3.5-turbo", - name: "GPT-3.5 Turbo", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2022-11-30", - last_updated: "2022-11-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 1.5 }, - limit: { context: 16385, output: 16384 }, - }, - "o3-mini": { - id: "o3-mini", - name: "o3 Mini", - family: "gpt", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-06-01", - last_updated: "2025-06-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.55 }, - limit: { context: 200000, output: 16384 }, - }, - "qwen-image-max-2025-12-30": { - id: "qwen-image-max-2025-12-30", - name: "Qwen Image Max 2025-12-30", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-12-31", - last_updated: "2025-12-31", - modalities: { input: ["text"], output: ["text", "image"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 2000, output: 4096 }, - }, - "qwen-vl-max": { - id: "qwen-vl-max", - name: "Qwen VL Max", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-02-01", - last_updated: "2025-02-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.8, output: 3.2 }, - limit: { context: 131072, output: 32000 }, - }, - sonar: { - id: "sonar", - name: "Sonar", - family: "sonar", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 1 }, - limit: { context: 130000, output: 16384 }, - }, - "qwen3-coder-flash": { - id: "qwen3-coder-flash", - name: "Qwen3 Coder Flash", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-22", - last_updated: "2025-07-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.5, cache_read: 0.06 }, - limit: { context: 1000000, output: 65536 }, - }, - "deepseek-v3.1": { - id: "deepseek-v3.1", - name: "DeepSeek V3.1", - family: "deepseek", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-21", - last_updated: "2025-08-21", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.56, output: 1.68, cache_read: 0.11 }, - limit: { context: 128000, output: 32768 }, - }, - "ministral-3b-2512": { - id: "ministral-3b-2512", - name: "Ministral 3B", - family: "mistral", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-12-02", - last_updated: "2025-12-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 131072, output: 16384 }, - }, - "grok-4-20-multi-agent-beta-0309": { - id: "grok-4-20-multi-agent-beta-0309", - name: "Grok 4.20 Multi-Agent Beta (0309)", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-03-09", - last_updated: "2026-03-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6, cache_read: 0.2 }, - limit: { context: 2000000, output: 30000 }, - }, - "qwen-plus-latest": { - id: "qwen-plus-latest", - name: "Qwen Plus Latest", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-09-09", - last_updated: "2024-09-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 1.2, cache_read: 0.08 }, - limit: { context: 1000000, output: 32000 }, - }, - "glm-4.5v": { - id: "glm-4.5v", - name: "GLM-4.5V", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-11", - last_updated: "2025-08-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 1.8, cache_read: 0.11 }, - limit: { context: 128000, output: 16000 }, - }, - "seedream-4-5": { - id: "seedream-4-5", - name: "Seedream 4.5", - family: "seed", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-12-03", - last_updated: "2025-12-03", - modalities: { input: ["text"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 2000, output: 4096 }, - }, - "llama-3.1-nemotron-ultra-253b": { - id: "llama-3.1-nemotron-ultra-253b", - name: "Llama 3.1 Nemotron Ultra 253B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-04-07", - last_updated: "2025-04-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 1.8 }, - limit: { context: 128000, output: 16384 }, - }, - "grok-4": { - id: "grok-4", - name: "Grok 4", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 256000, output: 256000 }, - }, - "llama-4-maverick-17b-instruct": { - id: "llama-4-maverick-17b-instruct", - name: "Llama 4 Maverick 17B Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.24, output: 0.97 }, - limit: { context: 8192, output: 2048 }, - status: "beta", - }, - "grok-4-0709": { - id: "grok-4-0709", - name: "Grok 4 (0709)", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 256000, output: 256000 }, - }, - "qwen3-next-80b-a3b-instruct": { - id: "qwen3-next-80b-a3b-instruct", - name: "Qwen3 Next 80B A3B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-10", - last_updated: "2025-09-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 2 }, - limit: { context: 129024, output: 32768 }, - }, - "gpt-4": { - id: "gpt-4", - name: "GPT-4", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2023-03-14", - last_updated: "2023-03-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 30, output: 60 }, - limit: { context: 8192, output: 8192 }, - }, - "qwen-image": { - id: "qwen-image", - name: "Qwen Image", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-08-04", - last_updated: "2025-08-04", - modalities: { input: ["text"], output: ["text", "image"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 2000, output: 4096 }, - }, - "qwen-image-edit-plus": { - id: "qwen-image-edit-plus", - name: "Qwen Image Edit Plus", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-08-19", - last_updated: "2025-08-19", - modalities: { input: ["text", "image"], output: ["text", "image"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 2000, output: 4096 }, - }, - "glm-4.6": { - id: "glm-4.6", - name: "GLM-4.6", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 2.2, cache_read: 0.11 }, - limit: { context: 200000, output: 16384 }, - }, - "qwen3-30b-a3b-fp8": { - id: "qwen3-30b-a3b-fp8", - name: "Qwen3 30B A3B FP8", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-04-28", - last_updated: "2025-04-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.09, output: 0.45 }, - limit: { context: 40960, output: 20000 }, - }, - "minimax-m2.1-lightning": { - id: "minimax-m2.1-lightning", - name: "MiniMax M2.1 Lightning", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.12, output: 0.48 }, - limit: { context: 196608, output: 131072 }, - }, - "claude-3-haiku": { - id: "claude-3-haiku", - name: "Claude 3 Haiku", - family: "claude", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2024-03-04", - last_updated: "2024-03-04", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1.25, cache_read: 0.03 }, - limit: { context: 200000, output: 4096 }, - }, - "glm-image": { - id: "glm-image", - name: "GLM-Image", - family: "glm", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-01-14", - last_updated: "2025-01-14", - modalities: { input: ["text"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 2000, output: 4096 }, - }, - "glm-4.6v": { - id: "glm-4.6v", - name: "GLM-4.6V", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-08", - last_updated: "2025-12-08", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.9, cache_read: 0.05 }, - limit: { context: 128000, output: 16000 }, - }, - "qwen3-max-2026-01-23": { - id: "qwen3-max-2026-01-23", - name: "Qwen3 Max 2026-01-23", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01-23", - last_updated: "2026-01-23", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 1.2, output: 6, cache_read: 0.24 }, - limit: { context: 262144, output: 65536 }, - }, - "claude-opus-4-1-20250805": { - id: "claude-opus-4-1-20250805", - name: "Claude Opus 4.1", - family: "claude", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5 }, - limit: { context: 200000, output: 32000 }, - }, - "gpt-5.4": { - id: "gpt-5.4", - name: "GPT-5.4", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-03-06", - last_updated: "2026-03-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 15, cache_read: 0.25 }, - limit: { context: 1050000, output: 128000 }, - }, - "claude-haiku-4-5-20251001": { - id: "claude-haiku-4-5-20251001", - name: "Claude Haiku 4.5 (2025-10-01)", - family: "claude", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1 }, - limit: { context: 200000, output: 64000 }, - }, - "qwen-image-edit-max": { - id: "qwen-image-edit-max", - name: "Qwen Image Edit Max", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2026-01-16", - last_updated: "2026-01-16", - modalities: { input: ["text", "image"], output: ["text", "image"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 2000, output: 4096 }, - }, - "glm-4.5-flash": { - id: "glm-4.5-flash", - name: "GLM-4.5 Flash", - family: "glm", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-13", - last_updated: "2025-08-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "llama-3.2-3b-instruct": { - id: "llama-3.2-3b-instruct", - name: "Llama 3.2 3B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2024-09-18", - last_updated: "2024-09-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.05 }, - limit: { context: 32768, output: 32000 }, - status: "beta", - }, - "qwen3-coder-next": { - id: "qwen3-coder-next", - name: "Qwen3 Coder Next", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-01-01", - last_updated: "2024-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.11, output: 0.68, cache_read: 0.06 }, - limit: { context: 262144, output: 262144 }, - }, - "qwen-image-plus": { - id: "qwen-image-plus", - name: "Qwen Image Plus", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-08-04", - last_updated: "2025-08-04", - modalities: { input: ["text"], output: ["text", "image"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 2000, output: 4096 }, - }, - "qwen3-vl-plus": { - id: "qwen3-vl-plus", - name: "Qwen3 VL Plus", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-09-23", - last_updated: "2025-09-23", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 1.6, cache_read: 0.04 }, - limit: { context: 262144, output: 32768 }, - }, - "grok-4-1-fast": { - id: "grok-4-1-fast", - name: "Grok 4.1 Fast", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-11-19", - last_updated: "2025-11-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - "claude-sonnet-4-20250514": { - id: "claude-sonnet-4-20250514", - name: "Claude Sonnet 4 (2025-05-14)", - family: "claude", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-05-14", - last_updated: "2025-05-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3 }, - limit: { context: 200000, output: 16384 }, - }, - "qwen3-coder-480b-a35b-instruct": { - id: "qwen3-coder-480b-a35b-instruct", - name: "Qwen3 Coder 480B A35B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-01-31", - last_updated: "2025-01-31", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 1.8 }, - limit: { context: 262000, output: 8192 }, - }, - "claude-opus-4-6": { - id: "claude-opus-4-6", - name: "Claude Opus 4.6", - family: "claude", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5 }, - limit: { context: 1000000, output: 128000 }, - }, - "gpt-4o-search-preview": { - id: "gpt-4o-search-preview", - name: "GPT-4o Search Preview", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2024-10-01", - last_updated: "2024-10-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10 }, - limit: { context: 128000, output: 16384 }, - }, - custom: { - id: "custom", - name: "Custom Model", - family: "auto", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-01-01", - last_updated: "2024-01-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "gpt-4.1-nano": { - id: "gpt-4.1-nano", - name: "GPT-4.1 Nano", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.03 }, - limit: { context: 1000000, output: 16384 }, - }, - "claude-3-7-sonnet-20250219": { - id: "claude-3-7-sonnet-20250219", - name: "Claude 3.7 Sonnet (2025-02-19)", - family: "claude", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-02-19", - last_updated: "2025-02-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3 }, - limit: { context: 200000, output: 8192 }, - }, - "qwen3-vl-30b-a3b-instruct": { - id: "qwen3-vl-30b-a3b-instruct", - name: "Qwen3 VL 30B A3B Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-10-05", - last_updated: "2025-10-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.7 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen3-coder-30b-a3b-instruct": { - id: "qwen3-coder-30b-a3b-instruct", - name: "Qwen3 Coder 30B A3B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-31", - last_updated: "2025-07-31", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 262000, output: 8192 }, - }, - "minimax-m2.5": { - id: "minimax-m2.5", - name: "MiniMax M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-15", - last_updated: "2026-02-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.03 }, - limit: { context: 204800, output: 131100 }, - }, - o3: { - id: "o3", - name: "o3", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-06-01", - last_updated: "2025-06-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 200000, output: 16384 }, - }, - "deepseek-v3.2": { - id: "deepseek-v3.2", - name: "DeepSeek V3.2", - family: "deepseek", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.28, output: 0.42, cache_read: 0.03 }, - limit: { context: 163840, output: 16384 }, - }, - "qwen3-235b-a22b-fp8": { - id: "qwen3-235b-a22b-fp8", - name: "Qwen3 235B A22B FP8", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-04-28", - last_updated: "2025-04-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.8 }, - limit: { context: 40960, output: 20000 }, - }, - "gpt-oss-20b": { - id: "gpt-oss-20b", - name: "GPT OSS 20B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.5 }, - limit: { context: 131072, output: 32766 }, - }, - "gpt-5-pro": { - id: "gpt-5-pro", - name: "GPT-5 Pro", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-01", - last_updated: "2025-08-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 120 }, - limit: { context: 400000, output: 272000 }, - }, - "gpt-4o": { - id: "gpt-4o", - name: "GPT-4o", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-05-13", - last_updated: "2024-05-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10, cache_read: 1.25 }, - limit: { context: 128000, output: 16384 }, - }, - "minimax-m2.5-highspeed": { - id: "minimax-m2.5-highspeed", - name: "MiniMax M2.5 Highspeed", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2024-01-01", - last_updated: "2024-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.4, cache_read: 0.03 }, - limit: { context: 204800, output: 131100 }, - }, - "qwen-turbo": { - id: "qwen-turbo", - name: "Qwen Turbo", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-02-01", - last_updated: "2025-02-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.2 }, - limit: { context: 1000000, output: 8192 }, - }, - "kimi-k2": { - id: "kimi-k2", - name: "Kimi K2", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-11", - last_updated: "2025-07-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 3, cache_read: 0.5 }, - limit: { context: 131072, output: 16384 }, - }, - "llama-3-8b-instruct": { - id: "llama-3-8b-instruct", - name: "Llama 3 8B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.04, output: 0.04 }, - limit: { context: 8192, output: 8192 }, - }, - "claude-sonnet-4-5": { - id: "claude-sonnet-4-5", - name: "Claude Sonnet 4.5", - family: "claude", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3 }, - limit: { context: 200000, output: 64000 }, - }, - "qwen3-vl-235b-a22b-instruct": { - id: "qwen3-vl-235b-a22b-instruct", - name: "Qwen3 VL 235B A22B Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-23", - last_updated: "2025-09-23", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 2 }, - limit: { context: 131072, output: 32768 }, - }, - "gemini-2.5-flash-lite": { - id: "gemini-2.5-flash-lite", - name: "Gemini 2.5 Flash Lite", - family: "gemini", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-22", - last_updated: "2025-07-22", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.01 }, - limit: { context: 1048576, output: 65535 }, - }, - "gpt-5": { - id: "gpt-5", - name: "GPT-5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-01", - last_updated: "2025-08-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.13 }, - limit: { context: 400000, output: 128000 }, - }, - "glm-4.7-flash": { - id: "glm-4.7-flash", - name: "GLM-4.7 Flash", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 200000, output: 128000 }, - }, - "gemini-2.5-flash-image-preview": { - id: "gemini-2.5-flash-image-preview", - name: "Gemini 2.5 Flash Image (Preview)", - family: "gemini", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-10-02", - last_updated: "2025-10-02", - modalities: { input: ["text", "image"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5 }, - limit: { context: 32768, output: 32768 }, - }, - "gpt-oss-120b": { - id: "gpt-oss-120b", - name: "GPT OSS 120B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.75 }, - limit: { context: 131072, output: 32766 }, - }, - "gpt-5-chat-latest": { - id: "gpt-5-chat-latest", - name: "GPT-5 Chat Latest", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-08-01", - last_updated: "2025-08-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.13 }, - limit: { context: 400000, output: 128000 }, - }, - "claude-opus-4-20250514": { - id: "claude-opus-4-20250514", - name: "Claude Opus 4 (2025-05-14)", - family: "claude", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5 }, - limit: { context: 200000, output: 16384 }, - }, - "qwen2-5-vl-72b-instruct": { - id: "qwen2-5-vl-72b-instruct", - name: "Qwen2.5 VL 72B Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-01-26", - last_updated: "2025-01-26", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.13, output: 0.4 }, - limit: { context: 32768, output: 8192 }, - }, - "qwen25-coder-7b": { - id: "qwen25-coder-7b", - name: "Qwen2.5 Coder 7B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2024-09-19", - last_updated: "2024-09-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.01, output: 0.03 }, - limit: { context: 32768, output: 8192 }, - }, - "llama-3.1-8b-instruct": { - id: "llama-3.1-8b-instruct", - name: "Llama 3.1 8B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.22, output: 0.22 }, - limit: { context: 128000, output: 2048 }, - status: "beta", - }, - "llama-3-70b-instruct": { - id: "llama-3-70b-instruct", - name: "Llama 3 70B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2024-04-18", - last_updated: "2024-04-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.51, output: 0.74 }, - limit: { context: 8192, output: 8000 }, - }, - "deepseek-r1-0528": { - id: "deepseek-r1-0528", - name: "DeepSeek R1 (0528)", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-05-28", - last_updated: "2025-05-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.8, output: 2.4 }, - limit: { context: 64000, output: 16384 }, - status: "beta", - }, - "glm-4.5-airx": { - id: "glm-4.5-airx", - name: "GLM-4.5 AirX", - family: "glm", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.5, cache_read: 0.22 }, - limit: { context: 128000, output: 16384 }, - }, - "gpt-4.1": { - id: "gpt-4.1", - name: "GPT-4.1", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 1000000, output: 16384 }, - }, - "devstral-small-2507": { - id: "devstral-small-2507", - name: "Devstral Small 1.1", - family: "mistral", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-07-21", - last_updated: "2025-07-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 131072, output: 16384 }, - }, - "kimi-k2-thinking": { - id: "kimi-k2-thinking", - name: "Kimi K2 Thinking", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-11-06", - last_updated: "2025-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 2.5, cache_read: 0.15 }, - limit: { context: 262144, output: 262144 }, - }, - "gemini-2.0-flash-lite": { - id: "gemini-2.0-flash-lite", - name: "Gemini 2.0 Flash Lite", - family: "gemini", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-02-25", - last_updated: "2025-02-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.08, output: 0.3 }, - limit: { context: 1048576, output: 8192 }, - status: "deprecated", - }, - "gpt-4.1-mini": { - id: "gpt-4.1-mini", - name: "GPT-4.1 Mini", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.6, cache_read: 0.1 }, - limit: { context: 1000000, output: 16384 }, - }, - "gpt-5.1-codex": { - id: "gpt-5.1-codex", - name: "GPT-5.1 Codex", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 400000, output: 272000 }, - }, - "grok-3": { - id: "grok-3", - name: "Grok-3", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 131072, output: 16384 }, - }, - "grok-4-fast-non-reasoning": { - id: "grok-4-fast-non-reasoning", - name: "Grok 4 Fast Non-Reasoning", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-10", - last_updated: "2025-10-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - "ministral-14b-2512": { - id: "ministral-14b-2512", - name: "Ministral 14B", - family: "mistral", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-12-02", - last_updated: "2025-12-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 262144, output: 16384 }, - }, - "llama-3.2-11b-instruct": { - id: "llama-3.2-11b-instruct", - name: "Llama 3.2 11B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2024-09-25", - last_updated: "2024-09-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.33 }, - limit: { context: 128000, output: 16384 }, - status: "beta", - }, - "sonar-reasoning-pro": { - id: "sonar-reasoning-pro", - name: "Sonar Reasoning Pro", - family: "sonar", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-03-07", - last_updated: "2025-03-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8 }, - limit: { context: 128000, output: 16384 }, - }, - "claude-3-opus": { - id: "claude-3-opus", - name: "Claude 3 Opus", - family: "claude", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2024-03-04", - last_updated: "2024-03-04", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5 }, - limit: { context: 200000, output: 4096 }, - }, - }, - }, - "google-vertex": { - id: "google-vertex", - env: ["GOOGLE_VERTEX_PROJECT", "GOOGLE_VERTEX_LOCATION", "GOOGLE_APPLICATION_CREDENTIALS"], - npm: "@ai-sdk/google-vertex", - name: "Vertex", - doc: "https://cloud.google.com/vertex-ai/generative-ai/docs/models", - models: { - "gemini-flash-lite-latest": { - id: "gemini-flash-lite-latest", - name: "Gemini Flash-Lite Latest", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.025 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-2.5-pro-preview-05-06": { - id: "gemini-2.5-pro-preview-05-06", - name: "Gemini 2.5 Pro Preview 05-06", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-05-06", - last_updated: "2025-05-06", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.31 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-3.1-pro-preview-customtools": { - id: "gemini-3.1-pro-preview-customtools", - name: "Gemini 3.1 Pro Preview Custom Tools", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-02-19", - last_updated: "2026-02-19", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2, context_over_200k: { input: 4, output: 18, cache_read: 0.4 } }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-2.5-flash-lite-preview-09-2025": { - id: "gemini-2.5-flash-lite-preview-09-2025", - name: "Gemini 2.5 Flash Lite Preview 09-25", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.025 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-3.1-pro-preview": { - id: "gemini-3.1-pro-preview", - name: "Gemini 3.1 Pro Preview", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-02-19", - last_updated: "2026-02-19", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2, context_over_200k: { input: 4, output: 18, cache_read: 0.4 } }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-2.0-flash": { - id: "gemini-2.0-flash", - name: "Gemini 2.0 Flash", - family: "gemini-flash", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-06", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6, cache_read: 0.025 }, - limit: { context: 1048576, output: 8192 }, - }, - "gemini-3-flash-preview": { - id: "gemini-3-flash-preview", - name: "Gemini 3 Flash Preview", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 0.5, - output: 3, - cache_read: 0.05, - context_over_200k: { input: 0.5, output: 3, cache_read: 0.05 }, - }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-3-pro-preview": { - id: "gemini-3-pro-preview", - name: "Gemini 3 Pro Preview", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-11-18", - last_updated: "2025-11-18", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2, context_over_200k: { input: 4, output: 18, cache_read: 0.4 } }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-2.5-flash-preview-05-20": { - id: "gemini-2.5-flash-preview-05-20", - name: "Gemini 2.5 Flash Preview 05-20", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-05-20", - last_updated: "2025-05-20", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6, cache_read: 0.0375 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-embedding-001": { - id: "gemini-embedding-001", - name: "Gemini Embedding 001", - family: "gemini", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2025-05", - release_date: "2025-05-20", - last_updated: "2025-05-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0 }, - limit: { context: 2048, output: 3072 }, - }, - "gemini-2.5-pro": { - id: "gemini-2.5-pro", - name: "Gemini 2.5 Pro", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-03-20", - last_updated: "2025-06-05", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.31 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-flash-latest": { - id: "gemini-flash-latest", - name: "Gemini Flash Latest", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5, cache_read: 0.075, cache_write: 0.383 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-2.5-pro-preview-06-05": { - id: "gemini-2.5-pro-preview-06-05", - name: "Gemini 2.5 Pro Preview 06-05", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-06-05", - last_updated: "2025-06-05", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.31 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-2.5-flash-lite-preview-06-17": { - id: "gemini-2.5-flash-lite-preview-06-17", - name: "Gemini 2.5 Flash Lite Preview 06-17", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.025 }, - limit: { context: 65536, output: 65536 }, - }, - "gemini-2.5-flash": { - id: "gemini-2.5-flash", - name: "Gemini 2.5 Flash", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5, cache_read: 0.075, cache_write: 0.383 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-2.5-flash-preview-04-17": { - id: "gemini-2.5-flash-preview-04-17", - name: "Gemini 2.5 Flash Preview 04-17", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-04-17", - last_updated: "2025-04-17", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6, cache_read: 0.0375 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-2.5-flash-preview-09-2025": { - id: "gemini-2.5-flash-preview-09-2025", - name: "Gemini 2.5 Flash Preview 09-25", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5, cache_read: 0.075, cache_write: 0.383 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-2.5-flash-lite": { - id: "gemini-2.5-flash-lite", - name: "Gemini 2.5 Flash Lite", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.025 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-2.0-flash-lite": { - id: "gemini-2.0-flash-lite", - name: "Gemini 2.0 Flash Lite", - family: "gemini-flash-lite", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-06", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.075, output: 0.3 }, - limit: { context: 1048576, output: 8192 }, - }, - "zai-org/glm-5-maas": { - id: "zai-org/glm-5-maas", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3.2, cache_read: 0.1 }, - limit: { context: 202752, output: 131072 }, - provider: { - npm: "@ai-sdk/openai-compatible", - api: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}/endpoints/openapi", - }, - }, - "zai-org/glm-4.7-maas": { - id: "zai-org/glm-4.7-maas", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-01-06", - last_updated: "2026-01-06", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2 }, - limit: { context: 200000, output: 128000 }, - provider: { - npm: "@ai-sdk/openai-compatible", - api: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}/endpoints/openapi", - }, - }, - "deepseek-ai/deepseek-v3.2-maas": { - id: "deepseek-ai/deepseek-v3.2-maas", - name: "DeepSeek V3.2", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-17", - last_updated: "2026-04-04", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: true, - cost: { input: 0.56, output: 1.68, cache_read: 0.056 }, - limit: { context: 163840, output: 65536 }, - provider: { - npm: "@ai-sdk/openai-compatible", - api: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}/endpoints/openapi", - }, - }, - "deepseek-ai/deepseek-v3.1-maas": { - id: "deepseek-ai/deepseek-v3.1-maas", - name: "DeepSeek V3.1", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-28", - last_updated: "2025-08-28", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 1.7 }, - limit: { context: 163840, output: 32768 }, - provider: { - npm: "@ai-sdk/openai-compatible", - api: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}/endpoints/openapi", - }, - }, - "openai/gpt-oss-120b-maas": { - id: "openai/gpt-oss-120b-maas", - name: "GPT OSS 120B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.09, output: 0.36 }, - limit: { context: 131072, output: 32768 }, - }, - "openai/gpt-oss-20b-maas": { - id: "openai/gpt-oss-20b-maas", - name: "GPT OSS 20B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.25 }, - limit: { context: 131072, output: 32768 }, - }, - "meta/llama-3.3-70b-instruct-maas": { - id: "meta/llama-3.3-70b-instruct-maas", - name: "Llama 3.3 70B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-12", - release_date: "2025-04-29", - last_updated: "2025-04-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.72, output: 0.72 }, - limit: { context: 128000, output: 8192 }, - provider: { - npm: "@ai-sdk/openai-compatible", - api: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}/endpoints/openapi", - }, - }, - "meta/llama-4-maverick-17b-128e-instruct-maas": { - id: "meta/llama-4-maverick-17b-128e-instruct-maas", - name: "Llama 4 Maverick 17B 128E Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-04-29", - last_updated: "2025-04-29", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.35, output: 1.15 }, - limit: { context: 524288, output: 8192 }, - provider: { - npm: "@ai-sdk/openai-compatible", - api: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}/endpoints/openapi", - }, - }, - "qwen/qwen3-235b-a22b-instruct-2507-maas": { - id: "qwen/qwen3-235b-a22b-instruct-2507-maas", - name: "Qwen3 235B A22B Instruct", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-13", - last_updated: "2025-08-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.22, output: 0.88 }, - limit: { context: 262144, output: 16384 }, - provider: { - npm: "@ai-sdk/openai-compatible", - api: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}/endpoints/openapi", - }, - }, - "moonshotai/kimi-k2-thinking-maas": { - id: "moonshotai/kimi-k2-thinking-maas", - name: "Kimi K2 Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.5 }, - limit: { context: 262144, output: 262144 }, - provider: { - npm: "@ai-sdk/openai-compatible", - api: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}/endpoints/openapi", - }, - }, - }, - }, - "cloudflare-workers-ai": { - id: "cloudflare-workers-ai", - env: ["CLOUDFLARE_ACCOUNT_ID", "CLOUDFLARE_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1", - name: "Cloudflare Workers AI", - doc: "https://developers.cloudflare.com/workers-ai/models/", - models: { - "@cf/zai-org/glm-4.7-flash": { - id: "@cf/zai-org/glm-4.7-flash", - name: "GLM-4.7-Flash", - family: "glm-flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-01-19", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.06, output: 0.4 }, - limit: { context: 131072, output: 131072 }, - }, - "@cf/nvidia/nemotron-3-120b-a12b": { - id: "@cf/nvidia/nemotron-3-120b-a12b", - name: "Nemotron 3 Super 120B", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-03-11", - last_updated: "2026-03-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 1.5 }, - limit: { context: 256000, output: 256000 }, - }, - "@cf/openai/gpt-oss-20b": { - id: "@cf/openai/gpt-oss-20b", - name: "GPT OSS 20B", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.3 }, - limit: { context: 128000, output: 16384 }, - }, - "@cf/openai/gpt-oss-120b": { - id: "@cf/openai/gpt-oss-120b", - name: "GPT OSS 120B", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.35, output: 0.75 }, - limit: { context: 128000, output: 16384 }, - }, - "@cf/meta/llama-4-scout-17b-16e-instruct": { - id: "@cf/meta/llama-4-scout-17b-16e-instruct", - name: "Llama 4 Scout 17B 16E Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 0.85 }, - limit: { context: 128000, output: 16384 }, - }, - "@cf/google/gemma-4-26b-a4b-it": { - id: "@cf/google/gemma-4-26b-a4b-it", - name: "Gemma 4 26B A4B IT", - family: "gemma", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-12-15", - last_updated: "2025-12-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 256000, output: 16384 }, - }, - "@cf/moonshotai/kimi-k2.5": { - id: "@cf/moonshotai/kimi-k2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3, cache_read: 0.1 }, - limit: { context: 256000, output: 256000 }, - }, - }, - }, - groq: { - id: "groq", - env: ["GROQ_API_KEY"], - npm: "@ai-sdk/groq", - name: "Groq", - doc: "https://console.groq.com/docs/models", - models: { - "gemma2-9b-it": { - id: "gemma2-9b-it", - name: "Gemma 2 9B", - family: "gemma", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-06", - release_date: "2024-06-27", - last_updated: "2024-06-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 8192, output: 8192 }, - status: "deprecated", - }, - "mistral-saba-24b": { - id: "mistral-saba-24b", - name: "Mistral Saba 24B", - family: "mistral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-02-06", - last_updated: "2025-02-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.79, output: 0.79 }, - limit: { context: 32768, output: 32768 }, - status: "deprecated", - }, - "deepseek-r1-distill-llama-70b": { - id: "deepseek-r1-distill-llama-70b", - name: "DeepSeek R1 Distill Llama 70B", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.75, output: 0.99 }, - limit: { context: 131072, output: 8192 }, - status: "deprecated", - }, - "llama-guard-3-8b": { - id: "llama-guard-3-8b", - name: "Llama Guard 3 8B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 8192, output: 8192 }, - status: "deprecated", - }, - "llama-3.3-70b-versatile": { - id: "llama-3.3-70b-versatile", - name: "Llama 3.3 70B Versatile", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.59, output: 0.79 }, - limit: { context: 131072, output: 32768 }, - }, - "allam-2-7b": { - id: "allam-2-7b", - name: "ALLaM-2-7b", - family: "allam", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-09", - release_date: "2024-09", - last_updated: "2024-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 4096, output: 4096 }, - }, - "whisper-large-v3": { - id: "whisper-large-v3", - name: "Whisper Large V3", - family: "whisper", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-09", - release_date: "2023-09-01", - last_updated: "2025-09-05", - modalities: { input: ["audio"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 448, output: 448 }, - }, - "llama-3.1-8b-instant": { - id: "llama-3.1-8b-instant", - name: "Llama 3.1 8B Instant", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.08 }, - limit: { context: 131072, output: 131072 }, - }, - "llama3-70b-8192": { - id: "llama3-70b-8192", - name: "Llama 3 70B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-03", - release_date: "2024-04-18", - last_updated: "2024-04-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.59, output: 0.79 }, - limit: { context: 8192, output: 8192 }, - status: "deprecated", - }, - "qwen-qwq-32b": { - id: "qwen-qwq-32b", - name: "Qwen QwQ 32B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-09", - release_date: "2024-11-27", - last_updated: "2024-11-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.29, output: 0.39 }, - limit: { context: 131072, output: 16384 }, - status: "deprecated", - }, - "whisper-large-v3-turbo": { - id: "whisper-large-v3-turbo", - name: "Whisper Large v3 Turbo", - family: "whisper", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2024-10-01", - last_updated: "2024-10-01", - modalities: { input: ["audio"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 448, output: 448 }, - }, - "llama3-8b-8192": { - id: "llama3-8b-8192", - name: "Llama 3 8B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-03", - release_date: "2024-04-18", - last_updated: "2024-04-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.08 }, - limit: { context: 8192, output: 8192 }, - status: "deprecated", - }, - "canopylabs/orpheus-arabic-saudi": { - id: "canopylabs/orpheus-arabic-saudi", - name: "Orpheus Arabic Saudi", - family: "canopylabs", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-12-16", - release_date: "2025-12-16", - last_updated: "2025-12-16", - modalities: { input: ["text"], output: ["audio"] }, - open_weights: false, - cost: { input: 40, output: 0 }, - limit: { context: 4000, output: 50000 }, - }, - "canopylabs/orpheus-v1-english": { - id: "canopylabs/orpheus-v1-english", - name: "Orpheus V1 English", - family: "canopylabs", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-12-19", - release_date: "2025-12-19", - last_updated: "2025-12-19", - modalities: { input: ["text"], output: ["audio"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 4000, output: 50000 }, - }, - "meta-llama/llama-4-scout-17b-16e-instruct": { - id: "meta-llama/llama-4-scout-17b-16e-instruct", - name: "Llama 4 Scout 17B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.11, output: 0.34 }, - limit: { context: 131072, output: 8192 }, - }, - "meta-llama/llama-prompt-guard-2-22m": { - id: "meta-llama/llama-prompt-guard-2-22m", - name: "Llama Prompt Guard 2 22M", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2024-10-01", - last_updated: "2024-10-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.03 }, - limit: { context: 512, output: 512 }, - }, - "meta-llama/llama-guard-4-12b": { - id: "meta-llama/llama-guard-4-12b", - name: "Llama Guard 4 12B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 131072, output: 1024 }, - status: "deprecated", - }, - "meta-llama/llama-4-maverick-17b-128e-instruct": { - id: "meta-llama/llama-4-maverick-17b-128e-instruct", - name: "Llama 4 Maverick 17B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.6 }, - limit: { context: 131072, output: 8192 }, - status: "deprecated", - }, - "meta-llama/llama-prompt-guard-2-86m": { - id: "meta-llama/llama-prompt-guard-2-86m", - name: "Llama Prompt Guard 2 86M", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2024-10-01", - last_updated: "2024-10-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.04, output: 0.04 }, - limit: { context: 512, output: 512 }, - }, - "openai/gpt-oss-20b": { - id: "openai/gpt-oss-20b", - name: "GPT OSS 20B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.075, output: 0.3 }, - limit: { context: 131072, output: 65536 }, - }, - "openai/gpt-oss-safeguard-20b": { - id: "openai/gpt-oss-safeguard-20b", - name: "Safety GPT OSS 20B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-03-05", - last_updated: "2025-03-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.075, output: 0.3, cache_read: 0.037 }, - limit: { context: 131072, output: 65536 }, - }, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "GPT OSS 120B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 131072, output: 65536 }, - }, - "qwen/qwen3-32b": { - id: "qwen/qwen3-32b", - name: "Qwen3 32B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-11-08", - release_date: "2024-12-23", - last_updated: "2024-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.29, output: 0.59 }, - limit: { context: 131072, output: 40960 }, - }, - "groq/compound": { - id: "groq/compound", - name: "Compound", - family: "groq", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-09-04", - release_date: "2025-09-04", - last_updated: "2025-09-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 8192 }, - }, - "groq/compound-mini": { - id: "groq/compound-mini", - name: "Compound Mini", - family: "groq", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-09-04", - release_date: "2025-09-04", - last_updated: "2025-09-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 8192 }, - }, - "moonshotai/kimi-k2-instruct": { - id: "moonshotai/kimi-k2-instruct", - name: "Kimi K2 Instruct", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-07-14", - last_updated: "2025-07-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3 }, - limit: { context: 131072, output: 16384 }, - status: "deprecated", - }, - "moonshotai/kimi-k2-instruct-0905": { - id: "moonshotai/kimi-k2-instruct-0905", - name: "Kimi K2 Instruct 0905", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3 }, - limit: { context: 262144, output: 16384 }, - }, - }, - }, - azure: { - id: "azure", - env: ["AZURE_RESOURCE_NAME", "AZURE_API_KEY"], - npm: "@ai-sdk/azure", - name: "Azure", - doc: "https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models", - models: { - "mistral-nemo": { - id: "mistral-nemo", - name: "Mistral Nemo", - family: "mistral-nemo", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.15 }, - limit: { context: 128000, output: 128000 }, - }, - "gpt-5.1-codex-max": { - id: "gpt-5.1-codex-max", - name: "GPT-5.1 Codex Max", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "gpt-5.2-chat": { - id: "gpt-5.2-chat", - name: "GPT-5.2 Chat", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 128000, output: 16384 }, - }, - "codex-mini": { - id: "codex-mini", - name: "Codex Mini", - family: "gpt-codex-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-04", - release_date: "2025-05-16", - last_updated: "2025-05-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.5, output: 6, cache_read: 0.375 }, - limit: { context: 200000, output: 100000 }, - }, - "phi-4-multimodal": { - id: "phi-4-multimodal", - name: "Phi-4-multimodal", - family: "phi", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text", "image", "audio"], output: ["text"] }, - open_weights: true, - cost: { input: 0.08, output: 0.32, input_audio: 4 }, - limit: { context: 128000, output: 4096 }, - }, - "phi-3.5-mini-instruct": { - id: "phi-3.5-mini-instruct", - name: "Phi-3.5-mini-instruct", - family: "phi", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-08-20", - last_updated: "2024-08-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.13, output: 0.52 }, - limit: { context: 128000, output: 4096 }, - }, - "llama-4-scout-17b-16e-instruct": { - id: "llama-4-scout-17b-16e-instruct", - name: "Llama 4 Scout 17B 16E Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.78 }, - limit: { context: 128000, output: 8192 }, - }, - "grok-4-1-fast-reasoning": { - id: "grok-4-1-fast-reasoning", - name: "Grok 4.1 Fast (Reasoning)", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-06-27", - last_updated: "2025-06-27", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 128000, input: 128000, output: 8192 }, - status: "beta", - }, - "phi-3-medium-4k-instruct": { - id: "phi-3-medium-4k-instruct", - name: "Phi-3-medium-instruct (4k)", - family: "phi", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-04-23", - last_updated: "2024-04-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.17, output: 0.68 }, - limit: { context: 4096, output: 1024 }, - }, - "ministral-3b": { - id: "ministral-3b", - name: "Ministral 3B", - family: "ministral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-03", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.04, output: 0.04 }, - limit: { context: 128000, output: 8192 }, - }, - "claude-haiku-4-5": { - id: "claude-haiku-4-5", - name: "Claude Haiku 4.5", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-02-31", - release_date: "2025-11-18", - last_updated: "2025-11-18", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 200000, output: 64000 }, - provider: { - npm: "@ai-sdk/anthropic", - api: "https://${AZURE_RESOURCE_NAME}.services.ai.azure.com/anthropic/v1", - }, - }, - "meta-llama-3.1-8b-instruct": { - id: "meta-llama-3.1-8b-instruct", - name: "Meta-Llama-3.1-8B-Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.61 }, - limit: { context: 128000, output: 32768 }, - }, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-02-06", - last_updated: "2026-02-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3 }, - limit: { context: 262144, output: 262144 }, - provider: { - npm: "@ai-sdk/openai-compatible", - api: "https://${AZURE_RESOURCE_NAME}.services.ai.azure.com/models", - shape: "completions", - }, - }, - "llama-3.3-70b-instruct": { - id: "llama-3.3-70b-instruct", - name: "Llama-3.3-70B-Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.71, output: 0.71 }, - limit: { context: 128000, output: 32768 }, - }, - "deepseek-v3-0324": { - id: "deepseek-v3-0324", - name: "DeepSeek-V3-0324", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-03-24", - last_updated: "2025-03-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.14, output: 4.56 }, - limit: { context: 131072, output: 131072 }, - }, - "gpt-5-chat": { - id: "gpt-5-chat", - name: "GPT-5 Chat", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: false, - temperature: false, - knowledge: "2024-10-24", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.13 }, - limit: { context: 128000, output: 16384 }, - }, - "phi-3.5-moe-instruct": { - id: "phi-3.5-moe-instruct", - name: "Phi-3.5-MoE-instruct", - family: "phi", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-08-20", - last_updated: "2024-08-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.16, output: 0.64 }, - limit: { context: 128000, output: 4096 }, - }, - "gpt-5.3-chat": { - id: "gpt-5.3-chat", - name: "GPT-5.3 Chat", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-03", - last_updated: "2026-03-03", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 128000, output: 16384 }, - }, - "o1-mini": { - id: "o1-mini", - name: "o1-mini", - family: "o-mini", - attachment: false, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2023-09", - release_date: "2024-09-12", - last_updated: "2024-09-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.55 }, - limit: { context: 128000, output: 65536 }, - }, - "text-embedding-3-large": { - id: "text-embedding-3-large", - name: "text-embedding-3-large", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - release_date: "2024-01-25", - last_updated: "2024-01-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.13, output: 0 }, - limit: { context: 8191, output: 3072 }, - }, - "phi-3-mini-128k-instruct": { - id: "phi-3-mini-128k-instruct", - name: "Phi-3-mini-instruct (128k)", - family: "phi", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-04-23", - last_updated: "2024-04-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.13, output: 0.52 }, - limit: { context: 128000, output: 4096 }, - }, - "phi-4-reasoning": { - id: "phi-4-reasoning", - name: "Phi-4-reasoning", - family: "phi", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.125, output: 0.5 }, - limit: { context: 32000, output: 4096 }, - }, - "gpt-5-mini": { - id: "gpt-5-mini", - name: "GPT-5 Mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.03 }, - limit: { context: 272000, output: 128000 }, - }, - "gpt-5-nano": { - id: "gpt-5-nano", - name: "GPT-5 Nano", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.4, cache_read: 0.01 }, - limit: { context: 272000, output: 128000 }, - }, - "meta-llama-3-70b-instruct": { - id: "meta-llama-3-70b-instruct", - name: "Meta-Llama-3-70B-Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-12", - release_date: "2024-04-18", - last_updated: "2024-04-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2.68, output: 3.54 }, - limit: { context: 8192, output: 2048 }, - }, - "phi-3-small-8k-instruct": { - id: "phi-3-small-8k-instruct", - name: "Phi-3-small-instruct (8k)", - family: "phi", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-04-23", - last_updated: "2024-04-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 8192, output: 2048 }, - }, - "gpt-5.3-codex": { - id: "gpt-5.3-codex", - name: "GPT-5.3 Codex", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-02-24", - last_updated: "2026-02-24", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, output: 128000 }, - }, - "gpt-4-turbo": { - id: "gpt-4-turbo", - name: "GPT-4 Turbo", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-11", - release_date: "2023-11-06", - last_updated: "2024-04-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 10, output: 30 }, - limit: { context: 128000, output: 4096 }, - }, - "text-embedding-ada-002": { - id: "text-embedding-ada-002", - name: "text-embedding-ada-002", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - release_date: "2022-12-15", - last_updated: "2022-12-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0 }, - limit: { context: 8192, output: 1536 }, - }, - "llama-3.2-90b-vision-instruct": { - id: "llama-3.2-90b-vision-instruct", - name: "Llama-3.2-90B-Vision-Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-09-25", - last_updated: "2024-09-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 2.04, output: 2.04 }, - limit: { context: 128000, output: 8192 }, - }, - "deepseek-r1": { - id: "deepseek-r1", - name: "DeepSeek-R1", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2024-07", - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.35, output: 5.4 }, - limit: { context: 163840, output: 163840 }, - }, - "grok-4-1-fast-non-reasoning": { - id: "grok-4-1-fast-non-reasoning", - name: "Grok 4.1 Fast (Non-Reasoning)", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-06-27", - last_updated: "2025-06-27", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 128000, input: 128000, output: 8192 }, - status: "beta", - }, - "deepseek-v3.2-speciale": { - id: "deepseek-v3.2-speciale", - name: "DeepSeek-V3.2-Speciale", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2024-07", - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.58, output: 1.68 }, - limit: { context: 128000, output: 128000 }, - }, - "gpt-5.2": { - id: "gpt-5.2", - name: "GPT-5.2", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "mistral-large-2411": { - id: "mistral-large-2411", - name: "Mistral Large 24.11", - family: "mistral-large", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-09", - release_date: "2024-11-01", - last_updated: "2024-11-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6 }, - limit: { context: 128000, output: 32768 }, - }, - "claude-opus-4-1": { - id: "claude-opus-4-1", - name: "Claude Opus 4.1", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-18", - last_updated: "2025-11-18", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - provider: { - npm: "@ai-sdk/anthropic", - api: "https://${AZURE_RESOURCE_NAME}.services.ai.azure.com/anthropic/v1", - }, - }, - "gpt-4o-mini": { - id: "gpt-4o-mini", - name: "GPT-4o mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-09", - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6, cache_read: 0.08 }, - limit: { context: 128000, output: 16384 }, - }, - "gpt-5.4-mini": { - id: "gpt-5.4-mini", - name: "GPT-5.4 Mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.75, output: 4.5, cache_read: 0.075 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "cohere-command-r-08-2024": { - id: "cohere-command-r-08-2024", - name: "Command R", - family: "command-r", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-06-01", - release_date: "2024-08-30", - last_updated: "2024-08-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 128000, output: 4000 }, - }, - "cohere-command-a": { - id: "cohere-command-a", - name: "Command A", - family: "command-a", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-06-01", - release_date: "2025-03-13", - last_updated: "2025-03-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2.5, output: 10 }, - limit: { context: 256000, output: 8000 }, - }, - "llama-3.2-11b-vision-instruct": { - id: "llama-3.2-11b-vision-instruct", - name: "Llama-3.2-11B-Vision-Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-09-25", - last_updated: "2024-09-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.37, output: 0.37 }, - limit: { context: 128000, output: 8192 }, - }, - "meta-llama-3.1-405b-instruct": { - id: "meta-llama-3.1-405b-instruct", - name: "Meta-Llama-3.1-405B-Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 5.33, output: 16 }, - limit: { context: 128000, output: 32768 }, - }, - "gpt-5.1-chat": { - id: "gpt-5.1-chat", - name: "GPT-5.1 Chat", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-14", - last_updated: "2025-11-14", - modalities: { input: ["text", "image", "audio"], output: ["text", "image", "audio"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 128000, output: 16384 }, - }, - "gpt-4-turbo-vision": { - id: "gpt-4-turbo-vision", - name: "GPT-4 Turbo Vision", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-11", - release_date: "2023-11-06", - last_updated: "2024-04-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 10, output: 30 }, - limit: { context: 128000, output: 4096 }, - }, - "o4-mini": { - id: "o4-mini", - name: "o4-mini", - family: "o-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05", - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.28 }, - limit: { context: 200000, output: 100000 }, - }, - "gpt-5.4-nano": { - id: "gpt-5.4-nano", - name: "GPT-5.4 Nano", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.25, cache_read: 0.02 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gpt-5.2-codex": { - id: "gpt-5.2-codex", - name: "GPT-5.2 Codex", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-01-14", - last_updated: "2026-01-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, output: 128000 }, - }, - "cohere-embed-v-4-0": { - id: "cohere-embed-v-4-0", - name: "Embed v4", - family: "cohere-embed", - attachment: true, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-04-15", - last_updated: "2025-04-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.12, output: 0 }, - limit: { context: 128000, output: 1536 }, - }, - "gpt-5.1-codex-mini": { - id: "gpt-5.1-codex-mini", - name: "GPT-5.1 Codex Mini", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-14", - last_updated: "2025-11-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.025 }, - limit: { context: 400000, output: 128000 }, - }, - "gpt-3.5-turbo-0125": { - id: "gpt-3.5-turbo-0125", - name: "GPT-3.5 Turbo 0125", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2021-08", - release_date: "2024-01-25", - last_updated: "2024-01-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 1.5 }, - limit: { context: 16384, output: 16384 }, - }, - "o1-preview": { - id: "o1-preview", - name: "o1-preview", - family: "o", - attachment: false, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2023-09", - release_date: "2024-09-12", - last_updated: "2024-09-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 16.5, output: 66, cache_read: 8.25 }, - limit: { context: 128000, output: 32768 }, - }, - "cohere-embed-v3-multilingual": { - id: "cohere-embed-v3-multilingual", - name: "Embed v3 Multilingual", - family: "cohere-embed", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2023-11-07", - last_updated: "2023-11-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0 }, - limit: { context: 512, output: 1024 }, - }, - "grok-4-20-non-reasoning": { - id: "grok-4-20-non-reasoning", - name: "Grok 4.20 (Non-Reasoning)", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-09", - release_date: "2026-04-08", - last_updated: "2026-04-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6 }, - limit: { context: 262000, output: 8192 }, - status: "beta", - }, - "grok-code-fast-1": { - id: "grok-code-fast-1", - name: "Grok Code Fast 1", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2025-08-28", - last_updated: "2025-08-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.5, cache_read: 0.02 }, - limit: { context: 256000, output: 10000 }, - }, - "gpt-5.1": { - id: "gpt-5.1", - name: "GPT-5.1", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-14", - last_updated: "2025-11-14", - modalities: { input: ["text", "image", "audio"], output: ["text", "image", "audio"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 272000, output: 128000 }, - }, - "grok-4-fast-reasoning": { - id: "grok-4-fast-reasoning", - name: "Grok 4 Fast (Reasoning)", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-09-19", - last_updated: "2025-09-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - o1: { - id: "o1", - name: "o1", - family: "o", - attachment: false, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2023-09", - release_date: "2024-12-05", - last_updated: "2024-12-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 60, cache_read: 7.5 }, - limit: { context: 200000, output: 100000 }, - }, - "mistral-small-2503": { - id: "mistral-small-2503", - name: "Mistral Small 3.1", - family: "mistral-small", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-09", - release_date: "2025-03-01", - last_updated: "2025-03-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 128000, output: 32768 }, - }, - "gpt-5.4-pro": { - id: "gpt-5.4-pro", - name: "GPT-5.4 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 30, output: 180 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "model-router": { - id: "model-router", - name: "Model Router", - family: "model-router", - attachment: true, - reasoning: false, - tool_call: true, - release_date: "2025-05-19", - last_updated: "2025-11-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "gpt-3.5-turbo-1106": { - id: "gpt-3.5-turbo-1106", - name: "GPT-3.5 Turbo 1106", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2021-08", - release_date: "2023-11-06", - last_updated: "2023-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 2 }, - limit: { context: 16384, output: 16384 }, - }, - "o3-mini": { - id: "o3-mini", - name: "o3-mini", - family: "o-mini", - attachment: false, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05", - release_date: "2024-12-20", - last_updated: "2025-01-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.55 }, - limit: { context: 200000, output: 100000 }, - }, - "text-embedding-3-small": { - id: "text-embedding-3-small", - name: "text-embedding-3-small", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - release_date: "2024-01-25", - last_updated: "2024-01-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.02, output: 0 }, - limit: { context: 8191, output: 1536 }, - }, - "deepseek-v3.1": { - id: "deepseek-v3.1", - name: "DeepSeek-V3.1", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-08-21", - last_updated: "2025-08-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.56, output: 1.68 }, - limit: { context: 131072, output: 131072 }, - }, - "claude-opus-4-5": { - id: "claude-opus-4-5", - name: "Claude Opus 4.5", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-24", - last_updated: "2025-08-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 64000 }, - provider: { - npm: "@ai-sdk/anthropic", - api: "https://${AZURE_RESOURCE_NAME}.services.ai.azure.com/anthropic/v1", - }, - }, - "phi-3-mini-4k-instruct": { - id: "phi-3-mini-4k-instruct", - name: "Phi-3-mini-instruct (4k)", - family: "phi", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-04-23", - last_updated: "2024-04-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.13, output: 0.52 }, - limit: { context: 4096, output: 1024 }, - }, - "meta-llama-3.1-70b-instruct": { - id: "meta-llama-3.1-70b-instruct", - name: "Meta-Llama-3.1-70B-Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2.68, output: 3.54 }, - limit: { context: 128000, output: 32768 }, - }, - "grok-4": { - id: "grok-4", - name: "Grok 4", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, reasoning: 15, cache_read: 0.75 }, - limit: { context: 256000, output: 64000 }, - }, - "phi-4-mini-reasoning": { - id: "phi-4-mini-reasoning", - name: "Phi-4-mini-reasoning", - family: "phi", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.075, output: 0.3 }, - limit: { context: 128000, output: 4096 }, - }, - "gpt-4": { - id: "gpt-4", - name: "GPT-4", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-11", - release_date: "2023-03-14", - last_updated: "2023-03-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 60, output: 120 }, - limit: { context: 8192, output: 8192 }, - }, - "meta-llama-3-8b-instruct": { - id: "meta-llama-3-8b-instruct", - name: "Meta-Llama-3-8B-Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-12", - release_date: "2024-04-18", - last_updated: "2024-04-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.61 }, - limit: { context: 8192, output: 2048 }, - }, - "gpt-5-codex": { - id: "gpt-5-codex", - name: "GPT-5-Codex", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-09-15", - last_updated: "2025-09-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.13 }, - limit: { context: 400000, output: 128000 }, - }, - "gpt-5.4": { - id: "gpt-5.4", - name: "GPT-5.4", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 15, cache_read: 0.25 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "phi-4-mini": { - id: "phi-4-mini", - name: "Phi-4-mini", - family: "phi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.075, output: 0.3 }, - limit: { context: 128000, output: 4096 }, - }, - "grok-4-20-reasoning": { - id: "grok-4-20-reasoning", - name: "Grok 4.20 (Reasoning)", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-09", - release_date: "2026-04-08", - last_updated: "2026-04-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6 }, - limit: { context: 262000, output: 8192 }, - status: "beta", - }, - "gpt-3.5-turbo-0301": { - id: "gpt-3.5-turbo-0301", - name: "GPT-3.5 Turbo 0301", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2021-08", - release_date: "2023-03-01", - last_updated: "2023-03-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.5, output: 2 }, - limit: { context: 4096, output: 4096 }, - }, - "claude-opus-4-6": { - id: "claude-opus-4-6", - name: "Claude Opus 4.6", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 5, - output: 25, - cache_read: 0.5, - cache_write: 6.25, - context_over_200k: { input: 10, output: 37.5, cache_read: 1, cache_write: 12.5 }, - }, - limit: { context: 200000, output: 128000 }, - provider: { - npm: "@ai-sdk/anthropic", - api: "https://${AZURE_RESOURCE_NAME}.services.ai.azure.com/anthropic/v1", - }, - }, - "phi-3-small-128k-instruct": { - id: "phi-3-small-128k-instruct", - name: "Phi-3-small-instruct (128k)", - family: "phi", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-04-23", - last_updated: "2024-04-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 128000, output: 4096 }, - }, - "gpt-4.1-nano": { - id: "gpt-4.1-nano", - name: "GPT-4.1 nano", - family: "gpt-nano", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-05", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.03 }, - limit: { context: 1047576, output: 32768 }, - }, - "grok-3-mini": { - id: "grok-3-mini", - name: "Grok 3 Mini", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.5, reasoning: 0.5, cache_read: 0.075 }, - limit: { context: 131072, output: 8192 }, - }, - o3: { - id: "o3", - name: "o3", - family: "o", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05", - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 200000, output: 100000 }, - }, - "deepseek-v3.2": { - id: "deepseek-v3.2", - name: "DeepSeek-V3.2", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.58, output: 1.68 }, - limit: { context: 128000, output: 128000 }, - }, - "gpt-5-pro": { - id: "gpt-5-pro", - name: "GPT-5 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-10-06", - last_updated: "2025-10-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 120 }, - limit: { context: 400000, output: 272000 }, - }, - "gpt-4o": { - id: "gpt-4o", - name: "GPT-4o", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-09", - release_date: "2024-05-13", - last_updated: "2024-05-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10, cache_read: 1.25 }, - limit: { context: 128000, output: 16384 }, - }, - "phi-3-medium-128k-instruct": { - id: "phi-3-medium-128k-instruct", - name: "Phi-3-medium-instruct (128k)", - family: "phi", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-04-23", - last_updated: "2024-04-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.17, output: 0.68 }, - limit: { context: 128000, output: 4096 }, - }, - "gpt-3.5-turbo-0613": { - id: "gpt-3.5-turbo-0613", - name: "GPT-3.5 Turbo 0613", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2021-08", - release_date: "2023-06-13", - last_updated: "2023-06-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 4 }, - limit: { context: 16384, output: 16384 }, - }, - "cohere-command-r-plus-08-2024": { - id: "cohere-command-r-plus-08-2024", - name: "Command R+", - family: "command-r", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-06-01", - release_date: "2024-08-30", - last_updated: "2024-08-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2.5, output: 10 }, - limit: { context: 128000, output: 4000 }, - }, - "claude-sonnet-4-5": { - id: "claude-sonnet-4-5", - name: "Claude Sonnet 4.5", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-11-18", - last_updated: "2025-11-18", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - provider: { - npm: "@ai-sdk/anthropic", - api: "https://${AZURE_RESOURCE_NAME}.services.ai.azure.com/anthropic/v1", - }, - }, - "phi-4": { - id: "phi-4", - name: "Phi-4", - family: "phi", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.125, output: 0.5 }, - limit: { context: 128000, output: 4096 }, - }, - "gpt-5": { - id: "gpt-5", - name: "GPT-5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.13 }, - limit: { context: 272000, output: 128000 }, - }, - "gpt-4-32k": { - id: "gpt-4-32k", - name: "GPT-4 32K", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-11", - release_date: "2023-03-14", - last_updated: "2023-03-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 60, output: 120 }, - limit: { context: 32768, output: 32768 }, - }, - "cohere-embed-v3-english": { - id: "cohere-embed-v3-english", - name: "Embed v3 English", - family: "cohere-embed", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2023-11-07", - last_updated: "2023-11-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0 }, - limit: { context: 512, output: 1024 }, - }, - "phi-4-reasoning-plus": { - id: "phi-4-reasoning-plus", - name: "Phi-4-reasoning-plus", - family: "phi", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.125, output: 0.5 }, - limit: { context: 32000, output: 4096 }, - }, - "mistral-medium-2505": { - id: "mistral-medium-2505", - name: "Mistral Medium 3", - family: "mistral-medium", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-05", - release_date: "2025-05-07", - last_updated: "2025-05-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2 }, - limit: { context: 128000, output: 128000 }, - }, - "gpt-3.5-turbo-instruct": { - id: "gpt-3.5-turbo-instruct", - name: "GPT-3.5 Turbo Instruct", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2021-08", - release_date: "2023-09-21", - last_updated: "2023-09-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.5, output: 2 }, - limit: { context: 4096, output: 4096 }, - }, - "deepseek-r1-0528": { - id: "deepseek-r1-0528", - name: "DeepSeek-R1-0528", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-05-28", - last_updated: "2025-05-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.35, output: 5.4 }, - limit: { context: 163840, output: 163840 }, - }, - "gpt-4.1": { - id: "gpt-4.1", - name: "GPT-4.1", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-05", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 1047576, output: 32768 }, - }, - "kimi-k2-thinking": { - id: "kimi-k2-thinking", - name: "Kimi K2 Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-11-06", - last_updated: "2025-12-02", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.5, cache_read: 0.15 }, - limit: { context: 262144, output: 262144 }, - }, - "gpt-4.1-mini": { - id: "gpt-4.1-mini", - name: "GPT-4.1 mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-05", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.6, cache_read: 0.1 }, - limit: { context: 1047576, output: 32768 }, - }, - "gpt-5.1-codex": { - id: "gpt-5.1-codex", - name: "GPT-5.1 Codex", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-14", - last_updated: "2025-11-14", - modalities: { input: ["text", "image", "audio"], output: ["text", "image", "audio"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "codestral-2501": { - id: "codestral-2501", - name: "Codestral 25.01", - family: "codestral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-03", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.9 }, - limit: { context: 256000, output: 256000 }, - }, - "llama-4-maverick-17b-128e-instruct-fp8": { - id: "llama-4-maverick-17b-128e-instruct-fp8", - name: "Llama 4 Maverick 17B 128E Instruct FP8", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 1 }, - limit: { context: 128000, output: 8192 }, - }, - "grok-3": { - id: "grok-3", - name: "Grok 3", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.75 }, - limit: { context: 131072, output: 8192 }, - }, - "grok-4-fast-non-reasoning": { - id: "grok-4-fast-non-reasoning", - name: "Grok 4 Fast (Non-Reasoning)", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-09-19", - last_updated: "2025-09-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - "mai-ds-r1": { - id: "mai-ds-r1", - name: "MAI-DS-R1", - family: "mai", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2024-06", - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.35, output: 5.4 }, - limit: { context: 128000, output: 8192 }, - }, - }, - }, - fastrouter: { - id: "fastrouter", - env: ["FASTROUTER_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://go.fastrouter.ai/api/v1", - name: "FastRouter", - doc: "https://fastrouter.ai/models", - models: { - "x-ai/grok-4": { - id: "x-ai/grok-4", - name: "Grok 4", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.75, cache_write: 15 }, - limit: { context: 256000, output: 64000 }, - }, - "deepseek-ai/deepseek-r1-distill-llama-70b": { - id: "deepseek-ai/deepseek-r1-distill-llama-70b", - name: "DeepSeek R1 Distill Llama 70B", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2025-01-23", - last_updated: "2025-01-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.14 }, - limit: { context: 131072, output: 131072 }, - }, - "openai/gpt-5-mini": { - id: "openai/gpt-5-mini", - name: "GPT-5 Mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10-01", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.025 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5-nano": { - id: "openai/gpt-5-nano", - name: "GPT-5 Nano", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10-01", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.4, cache_read: 0.005 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-oss-20b": { - id: "openai/gpt-oss-20b", - name: "GPT OSS 20B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.2 }, - limit: { context: 131072, output: 65536 }, - }, - "openai/gpt-5": { - id: "openai/gpt-5", - name: "GPT-5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10-01", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "GPT OSS 120B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 131072, output: 32768 }, - }, - "openai/gpt-4.1": { - id: "openai/gpt-4.1", - name: "GPT-4.1", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 1047576, output: 32768 }, - }, - "z-ai/glm-5": { - id: "z-ai/glm-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.95, output: 3.15 }, - limit: { context: 204800, output: 131072 }, - }, - "qwen/qwen3-coder": { - id: "qwen/qwen3-coder", - name: "Qwen3 Coder", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 262144, output: 66536 }, - }, - "google/gemini-2.5-pro": { - id: "google/gemini-2.5-pro", - name: "Gemini 2.5 Pro", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.31 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-2.5-flash": { - id: "google/gemini-2.5-flash", - name: "Gemini 2.5 Flash", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5, cache_read: 0.0375 }, - limit: { context: 1048576, output: 65536 }, - }, - "moonshotai/kimi-k2": { - id: "moonshotai/kimi-k2", - name: "Kimi K2", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-07-11", - last_updated: "2025-07-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 2.2 }, - limit: { context: 131072, output: 32768 }, - }, - "anthropic/claude-opus-4.1": { - id: "anthropic/claude-opus-4.1", - name: "Claude Opus 4.1", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "anthropic/claude-sonnet-4": { - id: "anthropic/claude-sonnet-4", - name: "Claude Sonnet 4", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - }, - }, - stackit: { - id: "stackit", - env: ["STACKIT_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.openai-compat.model-serving.eu01.onstackit.cloud/v1", - name: "STACKIT", - doc: "https://docs.stackit.cloud/products/data-and-ai/ai-model-serving/basics/available-shared-models", - models: { - "Qwen/Qwen3-VL-Embedding-8B": { - id: "Qwen/Qwen3-VL-Embedding-8B", - name: "Qwen3-VL Embedding 8B", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: false, - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.09, output: 0.09 }, - limit: { context: 32000, output: 4096 }, - }, - "Qwen/Qwen3-VL-235B-A22B-Instruct-FP8": { - id: "Qwen/Qwen3-VL-235B-A22B-Instruct-FP8", - name: "Qwen3-VL 235B", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2024-11-01", - last_updated: "2024-11-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 1.64, output: 1.91 }, - limit: { context: 218000, output: 8192 }, - }, - "neuralmagic/Meta-Llama-3.1-8B-Instruct-FP8": { - id: "neuralmagic/Meta-Llama-3.1-8B-Instruct-FP8", - name: "Llama 3.1 8B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.16, output: 0.27 }, - limit: { context: 128000, output: 8192 }, - }, - "neuralmagic/Mistral-Nemo-Instruct-2407-FP8": { - id: "neuralmagic/Mistral-Nemo-Instruct-2407-FP8", - name: "Mistral Nemo", - family: "mistral", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2024-07-01", - last_updated: "2024-07-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.49, output: 0.71 }, - limit: { context: 128000, output: 8192 }, - }, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "GPT-OSS 120B", - family: "gpt", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.49, output: 0.71 }, - limit: { context: 131000, output: 8192 }, - }, - "cortecs/Llama-3.3-70B-Instruct-FP8-Dynamic": { - id: "cortecs/Llama-3.3-70B-Instruct-FP8-Dynamic", - name: "Llama 3.3 70B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2024-12-05", - last_updated: "2024-12-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.49, output: 0.71 }, - limit: { context: 128000, output: 8192 }, - }, - "google/gemma-3-27b-it": { - id: "google/gemma-3-27b-it", - name: "Gemma 3 27B", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-05-17", - last_updated: "2025-05-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.49, output: 0.71 }, - limit: { context: 37000, output: 8192 }, - }, - "intfloat/e5-mistral-7b-instruct": { - id: "intfloat/e5-mistral-7b-instruct", - name: "E5 Mistral 7B", - family: "mistral", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: false, - release_date: "2023-12-11", - last_updated: "2023-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0.02 }, - limit: { context: 4096, output: 4096 }, - }, - }, - }, - "tencent-coding-plan": { - id: "tencent-coding-plan", - env: ["TENCENT_CODING_PLAN_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.lkeap.cloud.tencent.com/coding/v3", - name: "Tencent Coding Plan (China)", - doc: "https://cloud.tencent.com/document/product/1772/128947", - models: { - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Kimi-K2.5", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-01", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 262144, output: 32768 }, - }, - "glm-5": { - id: "glm-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 202752, output: 16384 }, - }, - "hunyuan-turbos": { - id: "hunyuan-turbos", - name: "Hunyuan-TurboS", - family: "hunyuan", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2026-03-08", - last_updated: "2026-03-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 131072, output: 16384 }, - }, - "hunyuan-t1": { - id: "hunyuan-t1", - name: "Hunyuan-T1", - family: "hunyuan", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-03-08", - last_updated: "2026-03-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 131072, output: 16384 }, - }, - "hunyuan-2.0-instruct": { - id: "hunyuan-2.0-instruct", - name: "Tencent HY 2.0 Instruct", - family: "hunyuan", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2026-03-08", - last_updated: "2026-03-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 131072, output: 16384 }, - }, - "minimax-m2.5": { - id: "minimax-m2.5", - name: "MiniMax-M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 204800, output: 32768 }, - }, - "tc-code-latest": { - id: "tc-code-latest", - name: "Auto", - family: "auto", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2026-03-08", - last_updated: "2026-03-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 131072, output: 16384 }, - }, - "hunyuan-2.0-thinking": { - id: "hunyuan-2.0-thinking", - name: "Tencent HY 2.0 Think", - family: "hunyuan", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-03-08", - last_updated: "2026-03-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 131072, output: 16384 }, - }, - }, - }, - "privatemode-ai": { - id: "privatemode-ai", - env: ["PRIVATEMODE_API_KEY", "PRIVATEMODE_ENDPOINT"], - npm: "@ai-sdk/openai-compatible", - api: "http://localhost:8080/v1", - name: "Privatemode AI", - doc: "https://docs.privatemode.ai/api/overview", - models: { - "gemma-3-27b": { - id: "gemma-3-27b", - name: "Gemma 3 27B", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-03-12", - last_updated: "2025-03-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 8192 }, - }, - "whisper-large-v3": { - id: "whisper-large-v3", - name: "Whisper large-v3", - family: "whisper", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - knowledge: "2023-09", - release_date: "2023-09-01", - last_updated: "2023-09-01", - modalities: { input: ["audio"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 0, output: 4096 }, - }, - "qwen3-embedding-4b": { - id: "qwen3-embedding-4b", - name: "Qwen3-Embedding 4B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - knowledge: "2025-06", - release_date: "2025-06-06", - last_updated: "2025-06-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 32000, output: 2560 }, - }, - "gpt-oss-120b": { - id: "gpt-oss-120b", - name: "gpt-oss-120b", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-08", - release_date: "2025-08-04", - last_updated: "2025-08-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 128000 }, - }, - "qwen3-coder-30b-a3b": { - id: "qwen3-coder-30b-a3b", - name: "Qwen3-Coder 30B-A3B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 32768 }, - }, - }, - }, - google: { - id: "google", - env: ["GOOGLE_GENERATIVE_AI_API_KEY", "GEMINI_API_KEY"], - npm: "@ai-sdk/google", - name: "Google", - doc: "https://ai.google.dev/gemini-api/docs/pricing", - models: { - "gemini-flash-lite-latest": { - id: "gemini-flash-lite-latest", - name: "Gemini Flash-Lite Latest", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.025 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-2.5-pro-preview-05-06": { - id: "gemini-2.5-pro-preview-05-06", - name: "Gemini 2.5 Pro Preview 05-06", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-05-06", - last_updated: "2025-05-06", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.31 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-live-2.5-flash-preview-native-audio": { - id: "gemini-live-2.5-flash-preview-native-audio", - name: "Gemini Live 2.5 Flash Preview Native Audio", - family: "gemini-flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2025-01", - release_date: "2025-06-17", - last_updated: "2025-09-18", - modalities: { input: ["text", "audio", "video"], output: ["text", "audio"] }, - open_weights: false, - cost: { input: 0.5, output: 2, input_audio: 3, output_audio: 12 }, - limit: { context: 131072, output: 65536 }, - }, - "gemini-3.1-pro-preview-customtools": { - id: "gemini-3.1-pro-preview-customtools", - name: "Gemini 3.1 Pro Preview Custom Tools", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-02-19", - last_updated: "2026-02-19", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2, context_over_200k: { input: 4, output: 18, cache_read: 0.4 } }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-2.5-flash-lite-preview-09-2025": { - id: "gemini-2.5-flash-lite-preview-09-2025", - name: "Gemini 2.5 Flash Lite Preview 09-25", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.025 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-1.5-flash": { - id: "gemini-1.5-flash", - name: "Gemini 1.5 Flash", - family: "gemini-flash", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-05-14", - last_updated: "2024-05-14", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.075, output: 0.3, cache_read: 0.01875 }, - limit: { context: 1000000, output: 8192 }, - }, - "gemini-1.5-pro": { - id: "gemini-1.5-pro", - name: "Gemini 1.5 Pro", - family: "gemini-pro", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-02-15", - last_updated: "2024-02-15", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 5, cache_read: 0.3125 }, - limit: { context: 1000000, output: 8192 }, - }, - "gemma-3n-e4b-it": { - id: "gemma-3n-e4b-it", - name: "Gemma 3n 4B", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2025-05-20", - last_updated: "2025-05-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 8192, output: 2000 }, - }, - "gemini-3.1-flash-lite-preview": { - id: "gemini-3.1-flash-lite-preview", - name: "Gemini 3.1 Flash Lite Preview", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-03-03", - last_updated: "2026-03-03", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1.5, cache_read: 0.025, cache_write: 1 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-3.1-pro-preview": { - id: "gemini-3.1-pro-preview", - name: "Gemini 3.1 Pro Preview", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-02-19", - last_updated: "2026-02-19", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2, context_over_200k: { input: 4, output: 18, cache_read: 0.4 } }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-2.0-flash": { - id: "gemini-2.0-flash", - name: "Gemini 2.0 Flash", - family: "gemini-flash", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-06", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.025 }, - limit: { context: 1048576, output: 8192 }, - }, - "gemini-3-flash-preview": { - id: "gemini-3-flash-preview", - name: "Gemini 3 Flash Preview", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 0.5, - output: 3, - cache_read: 0.05, - context_over_200k: { input: 0.5, output: 3, cache_read: 0.05 }, - }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-2.5-flash-preview-tts": { - id: "gemini-2.5-flash-preview-tts", - name: "Gemini 2.5 Flash Preview TTS", - family: "gemini-flash", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2025-01", - release_date: "2025-05-01", - last_updated: "2025-05-01", - modalities: { input: ["text"], output: ["audio"] }, - open_weights: false, - cost: { input: 0.5, output: 10 }, - limit: { context: 8000, output: 16000 }, - }, - "gemini-3-pro-preview": { - id: "gemini-3-pro-preview", - name: "Gemini 3 Pro Preview", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-11-18", - last_updated: "2025-11-18", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2, context_over_200k: { input: 4, output: 18, cache_read: 0.4 } }, - limit: { context: 1000000, output: 64000 }, - }, - "gemini-2.5-flash-preview-05-20": { - id: "gemini-2.5-flash-preview-05-20", - name: "Gemini 2.5 Flash Preview 05-20", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-05-20", - last_updated: "2025-05-20", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6, cache_read: 0.0375 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-embedding-001": { - id: "gemini-embedding-001", - name: "Gemini Embedding 001", - family: "gemini", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2025-05", - release_date: "2025-05-20", - last_updated: "2025-05-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0 }, - limit: { context: 2048, output: 3072 }, - }, - "gemini-2.5-pro": { - id: "gemini-2.5-pro", - name: "Gemini 2.5 Pro", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-03-20", - last_updated: "2025-06-05", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.31 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-flash-latest": { - id: "gemini-flash-latest", - name: "Gemini Flash Latest", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5, cache_read: 0.075, input_audio: 1 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemma-4-31b-it": { - id: "gemma-4-31b-it", - name: "Gemma 4 31B", - family: "gemma", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-04-02", - last_updated: "2026-04-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - limit: { context: 256000, output: 8192 }, - }, - "gemini-2.5-pro-preview-06-05": { - id: "gemini-2.5-pro-preview-06-05", - name: "Gemini 2.5 Pro Preview 06-05", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-06-05", - last_updated: "2025-06-05", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.31 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-2.5-flash-image": { - id: "gemini-2.5-flash-image", - name: "Gemini 2.5 Flash Image", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2025-06", - release_date: "2025-08-26", - last_updated: "2025-08-26", - modalities: { input: ["text", "image"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 0.3, output: 30, cache_read: 0.075 }, - limit: { context: 32768, output: 32768 }, - }, - "gemini-2.5-flash-lite-preview-06-17": { - id: "gemini-2.5-flash-lite-preview-06-17", - name: "Gemini 2.5 Flash Lite Preview 06-17", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.025, input_audio: 0.3 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemma-3-12b-it": { - id: "gemma-3-12b-it", - name: "Gemma 3 12B", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-03-13", - last_updated: "2025-03-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 32768, output: 8192 }, - }, - "gemini-2.5-flash": { - id: "gemini-2.5-flash", - name: "Gemini 2.5 Flash", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-03-20", - last_updated: "2025-06-05", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5, cache_read: 0.075, input_audio: 1 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemma-3n-e2b-it": { - id: "gemma-3n-e2b-it", - name: "Gemma 3n 2B", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 8192, output: 2000 }, - }, - "gemini-3.1-flash-image-preview": { - id: "gemini-3.1-flash-image-preview", - name: "Gemini 3.1 Flash Image (Preview)", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2025-01", - release_date: "2026-02-26", - last_updated: "2026-02-26", - modalities: { input: ["text", "image", "pdf"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 0.25, output: 60 }, - limit: { context: 131072, output: 32768 }, - }, - "gemma-3-4b-it": { - id: "gemma-3-4b-it", - name: "Gemma 3 4B", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2025-03-13", - last_updated: "2025-03-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 32768, output: 8192 }, - }, - "gemini-2.5-flash-preview-04-17": { - id: "gemini-2.5-flash-preview-04-17", - name: "Gemini 2.5 Flash Preview 04-17", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-04-17", - last_updated: "2025-04-17", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6, cache_read: 0.0375 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-2.5-pro-preview-tts": { - id: "gemini-2.5-pro-preview-tts", - name: "Gemini 2.5 Pro Preview TTS", - family: "gemini-flash", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2025-01", - release_date: "2025-05-01", - last_updated: "2025-05-01", - modalities: { input: ["text"], output: ["audio"] }, - open_weights: false, - cost: { input: 1, output: 20 }, - limit: { context: 8000, output: 16000 }, - }, - "gemini-2.5-flash-preview-09-2025": { - id: "gemini-2.5-flash-preview-09-2025", - name: "Gemini 2.5 Flash Preview 09-25", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5, cache_read: 0.075, input_audio: 1 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemma-3-27b-it": { - id: "gemma-3-27b-it", - name: "Gemma 3 27B", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-03-12", - last_updated: "2025-03-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 8192 }, - }, - "gemini-2.5-flash-lite": { - id: "gemini-2.5-flash-lite", - name: "Gemini 2.5 Flash Lite", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.025 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemma-4-26b-it": { - id: "gemma-4-26b-it", - name: "Gemma 4 26B", - family: "gemma", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-04-02", - last_updated: "2026-04-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - limit: { context: 256000, output: 8192 }, - }, - "gemini-2.5-flash-image-preview": { - id: "gemini-2.5-flash-image-preview", - name: "Gemini 2.5 Flash Image (Preview)", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2025-06", - release_date: "2025-08-26", - last_updated: "2025-08-26", - modalities: { input: ["text", "image"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 0.3, output: 30, cache_read: 0.075 }, - limit: { context: 32768, output: 32768 }, - }, - "gemini-1.5-flash-8b": { - id: "gemini-1.5-flash-8b", - name: "Gemini 1.5 Flash-8B", - family: "gemini-flash", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-10-03", - last_updated: "2024-10-03", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.0375, output: 0.15, cache_read: 0.01 }, - limit: { context: 1000000, output: 8192 }, - }, - "gemini-live-2.5-flash": { - id: "gemini-live-2.5-flash", - name: "Gemini Live 2.5 Flash", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-09-01", - last_updated: "2025-09-01", - modalities: { input: ["text", "image", "audio", "video"], output: ["text", "audio"] }, - open_weights: false, - cost: { input: 0.5, output: 2, input_audio: 3, output_audio: 12 }, - limit: { context: 128000, output: 8000 }, - }, - "gemini-2.0-flash-lite": { - id: "gemini-2.0-flash-lite", - name: "Gemini 2.0 Flash Lite", - family: "gemini-flash-lite", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-06", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.075, output: 0.3 }, - limit: { context: 1048576, output: 8192 }, - }, - }, - }, - drun: { - id: "drun", - env: ["DRUN_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://chat.d.run/v1", - name: "D.Run (China)", - doc: "https://www.d.run", - models: { - "public/deepseek-r1": { - id: "public/deepseek-r1", - name: "DeepSeek R1", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 2.2 }, - limit: { context: 131072, output: 32000 }, - }, - "public/minimax-m25": { - id: "public/minimax-m25", - name: "MiniMax M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_details" }, - temperature: true, - release_date: "2025-03-01", - last_updated: "2025-03-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.29, output: 1.16 }, - limit: { context: 204800, output: 131072 }, - }, - "public/deepseek-v3": { - id: "public/deepseek-v3", - name: "DeepSeek V3", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2024-12-26", - last_updated: "2024-12-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.28, output: 1.1 }, - limit: { context: 131072, output: 8192 }, - }, - }, - }, - moonshotai: { - id: "moonshotai", - env: ["MOONSHOT_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.moonshot.ai/v1", - name: "Moonshot AI", - doc: "https://platform.moonshot.ai/docs/api/chat", - models: { - "kimi-k2-0905-preview": { - id: "kimi-k2-0905-preview", - name: "Kimi K2 0905", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.5, cache_read: 0.15 }, - limit: { context: 262144, output: 262144 }, - }, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: false, - knowledge: "2025-01", - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3, cache_read: 0.1 }, - limit: { context: 262144, output: 262144 }, - }, - "kimi-k2-thinking-turbo": { - id: "kimi-k2-thinking-turbo", - name: "Kimi K2 Thinking Turbo", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-08", - release_date: "2025-11-06", - last_updated: "2025-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.15, output: 8, cache_read: 0.15 }, - limit: { context: 262144, output: 262144 }, - }, - "kimi-k2-turbo-preview": { - id: "kimi-k2-turbo-preview", - name: "Kimi K2 Turbo", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2.4, output: 10, cache_read: 0.6 }, - limit: { context: 262144, output: 262144 }, - }, - "kimi-k2-0711-preview": { - id: "kimi-k2-0711-preview", - name: "Kimi K2 0711", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-07-14", - last_updated: "2025-07-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.5, cache_read: 0.15 }, - limit: { context: 131072, output: 16384 }, - }, - "kimi-k2-thinking": { - id: "kimi-k2-thinking", - name: "Kimi K2 Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-08", - release_date: "2025-11-06", - last_updated: "2025-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.5, cache_read: 0.15 }, - limit: { context: 262144, output: 262144 }, - }, - }, - }, - berget: { - id: "berget", - env: ["BERGET_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.berget.ai/v1", - name: "Berget.AI", - doc: "https://api.berget.ai", - models: { - "zai-org/GLM-4.7": { - id: "zai-org/GLM-4.7", - name: "GLM 4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-12", - release_date: "2026-01-19", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.7, output: 2.3 }, - limit: { context: 128000, output: 8192 }, - }, - "BAAI/bge-reranker-v2-m3": { - id: "BAAI/bge-reranker-v2-m3", - name: "bge-reranker-v2-m3", - family: "bge", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2025-04", - release_date: "2025-04-23", - last_updated: "2025-04-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 512, output: 512 }, - }, - "mistralai/Mistral-Small-3.2-24B-Instruct-2506": { - id: "mistralai/Mistral-Small-3.2-24B-Instruct-2506", - name: "Mistral Small 3.2 24B Instruct 2506", - family: "mistral-small", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-09", - release_date: "2025-10-01", - last_updated: "2025-10-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.3 }, - limit: { context: 32000, output: 8192 }, - }, - "meta-llama/Llama-3.3-70B-Instruct": { - id: "meta-llama/Llama-3.3-70B-Instruct", - name: "Llama 3.3 70B Instruct", - family: "llama", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-12", - release_date: "2025-04-27", - last_updated: "2025-04-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.9, output: 0.9 }, - limit: { context: 128000, output: 8192 }, - }, - "KBLab/kb-whisper-large": { - id: "KBLab/kb-whisper-large", - name: "KB-Whisper-Large", - family: "whisper", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2025-04", - release_date: "2025-04-27", - last_updated: "2025-04-27", - modalities: { input: ["audio"], output: ["text"] }, - open_weights: true, - cost: { input: 3, output: 3 }, - limit: { context: 480000, output: 4800 }, - }, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "GPT-OSS-120B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-08", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.9 }, - limit: { context: 128000, output: 8192 }, - }, - "intfloat/multilingual-e5-large-instruct": { - id: "intfloat/multilingual-e5-large-instruct", - name: "Multilingual-E5-large-instruct", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2025-04", - release_date: "2025-04-27", - last_updated: "2025-04-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0 }, - limit: { context: 512, output: 1024 }, - }, - "intfloat/multilingual-e5-large": { - id: "intfloat/multilingual-e5-large", - name: "Multilingual-E5-large", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2025-09", - release_date: "2025-09-11", - last_updated: "2025-09-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0 }, - limit: { context: 512, output: 1024 }, - }, - }, - }, - "github-models": { - id: "github-models", - env: ["GITHUB_TOKEN"], - npm: "@ai-sdk/openai-compatible", - api: "https://models.github.ai/inference", - name: "GitHub Models", - doc: "https://docs.github.com/en/github-models", - models: { - "deepseek/deepseek-v3-0324": { - id: "deepseek/deepseek-v3-0324", - name: "DeepSeek-V3-0324", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-06", - release_date: "2025-03-24", - last_updated: "2025-03-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 8192 }, - }, - "deepseek/deepseek-r1": { - id: "deepseek/deepseek-r1", - name: "DeepSeek-R1", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-06", - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 65536, output: 8192 }, - }, - "deepseek/deepseek-r1-0528": { - id: "deepseek/deepseek-r1-0528", - name: "DeepSeek-R1-0528", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-06", - release_date: "2025-05-28", - last_updated: "2025-05-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 65536, output: 8192 }, - }, - "ai21-labs/ai21-jamba-1.5-mini": { - id: "ai21-labs/ai21-jamba-1.5-mini", - name: "AI21 Jamba 1.5 Mini", - family: "jamba", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-03", - release_date: "2024-08-29", - last_updated: "2024-08-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 256000, output: 4096 }, - }, - "ai21-labs/ai21-jamba-1.5-large": { - id: "ai21-labs/ai21-jamba-1.5-large", - name: "AI21 Jamba 1.5 Large", - family: "jamba", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-03", - release_date: "2024-08-29", - last_updated: "2024-08-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 256000, output: 4096 }, - }, - "microsoft/phi-3.5-mini-instruct": { - id: "microsoft/phi-3.5-mini-instruct", - name: "Phi-3.5-mini instruct (128k)", - family: "phi", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-08-20", - last_updated: "2024-08-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "microsoft/phi-3-medium-4k-instruct": { - id: "microsoft/phi-3-medium-4k-instruct", - name: "Phi-3-medium instruct (4k)", - family: "phi", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-04-23", - last_updated: "2024-04-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 4096, output: 1024 }, - }, - "microsoft/phi-3.5-moe-instruct": { - id: "microsoft/phi-3.5-moe-instruct", - name: "Phi-3.5-MoE instruct (128k)", - family: "phi", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-08-20", - last_updated: "2024-08-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "microsoft/phi-3-mini-128k-instruct": { - id: "microsoft/phi-3-mini-128k-instruct", - name: "Phi-3-mini instruct (128k)", - family: "phi", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-04-23", - last_updated: "2024-04-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "microsoft/phi-4-mini-instruct": { - id: "microsoft/phi-4-mini-instruct", - name: "Phi-4-mini-instruct", - family: "phi", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "microsoft/phi-4-reasoning": { - id: "microsoft/phi-4-reasoning", - name: "Phi-4-Reasoning", - family: "phi", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "microsoft/phi-3-small-8k-instruct": { - id: "microsoft/phi-3-small-8k-instruct", - name: "Phi-3-small instruct (8k)", - family: "phi", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-04-23", - last_updated: "2024-04-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 8192, output: 2048 }, - }, - "microsoft/phi-3.5-vision-instruct": { - id: "microsoft/phi-3.5-vision-instruct", - name: "Phi-3.5-vision instruct (128k)", - family: "phi", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-08-20", - last_updated: "2024-08-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "microsoft/phi-3-mini-4k-instruct": { - id: "microsoft/phi-3-mini-4k-instruct", - name: "Phi-3-mini instruct (4k)", - family: "phi", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-04-23", - last_updated: "2024-04-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 4096, output: 1024 }, - }, - "microsoft/phi-4-mini-reasoning": { - id: "microsoft/phi-4-mini-reasoning", - name: "Phi-4-mini-reasoning", - family: "phi", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "microsoft/phi-3-small-128k-instruct": { - id: "microsoft/phi-3-small-128k-instruct", - name: "Phi-3-small instruct (128k)", - family: "phi", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-04-23", - last_updated: "2024-04-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "microsoft/phi-3-medium-128k-instruct": { - id: "microsoft/phi-3-medium-128k-instruct", - name: "Phi-3-medium instruct (128k)", - family: "phi", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-04-23", - last_updated: "2024-04-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "microsoft/phi-4": { - id: "microsoft/phi-4", - name: "Phi-4", - family: "phi", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 16000, output: 4096 }, - }, - "microsoft/phi-4-multimodal-instruct": { - id: "microsoft/phi-4-multimodal-instruct", - name: "Phi-4-multimodal-instruct", - family: "phi", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text", "image", "audio"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "microsoft/mai-ds-r1": { - id: "microsoft/mai-ds-r1", - name: "MAI-DS-R1", - family: "mai", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-06", - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 65536, output: 8192 }, - }, - "cohere/cohere-command-r-08-2024": { - id: "cohere/cohere-command-r-08-2024", - name: "Cohere Command R 08-2024", - family: "command-r", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-03", - release_date: "2024-08-01", - last_updated: "2024-08-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "cohere/cohere-command-a": { - id: "cohere/cohere-command-a", - name: "Cohere Command A", - family: "command-a", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-03", - release_date: "2024-11-01", - last_updated: "2024-11-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "cohere/cohere-command-r-plus": { - id: "cohere/cohere-command-r-plus", - name: "Cohere Command R+", - family: "command-r", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-03", - release_date: "2024-04-04", - last_updated: "2024-08-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "cohere/cohere-command-r": { - id: "cohere/cohere-command-r", - name: "Cohere Command R", - family: "command-r", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-03", - release_date: "2024-03-11", - last_updated: "2024-08-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "cohere/cohere-command-r-plus-08-2024": { - id: "cohere/cohere-command-r-plus-08-2024", - name: "Cohere Command R+ 08-2024", - family: "command-r", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-03", - release_date: "2024-08-01", - last_updated: "2024-08-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "xai/grok-3-mini": { - id: "xai/grok-3-mini", - name: "Grok 3 Mini", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-12-09", - last_updated: "2024-12-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 8192 }, - }, - "xai/grok-3": { - id: "xai/grok-3", - name: "Grok 3", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-12-09", - last_updated: "2024-12-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 8192 }, - }, - "openai/o1-mini": { - id: "openai/o1-mini", - name: "OpenAI o1-mini", - family: "o-mini", - attachment: false, - reasoning: true, - tool_call: false, - temperature: false, - knowledge: "2023-10", - release_date: "2024-09-12", - last_updated: "2024-12-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 65536 }, - }, - "openai/gpt-4o-mini": { - id: "openai/gpt-4o-mini", - name: "GPT-4o mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text", "image", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/o4-mini": { - id: "openai/o4-mini", - name: "OpenAI o4-mini", - family: "o-mini", - attachment: false, - reasoning: true, - tool_call: false, - temperature: false, - knowledge: "2024-04", - release_date: "2025-01-31", - last_updated: "2025-01-31", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/o1-preview": { - id: "openai/o1-preview", - name: "OpenAI o1-preview", - family: "o", - attachment: false, - reasoning: true, - tool_call: false, - temperature: false, - knowledge: "2023-10", - release_date: "2024-09-12", - last_updated: "2024-09-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 32768 }, - }, - "openai/o1": { - id: "openai/o1", - name: "OpenAI o1", - family: "o", - attachment: false, - reasoning: true, - tool_call: false, - temperature: false, - knowledge: "2023-10", - release_date: "2024-09-12", - last_updated: "2024-12-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/o3-mini": { - id: "openai/o3-mini", - name: "OpenAI o3-mini", - family: "o-mini", - attachment: false, - reasoning: true, - tool_call: false, - temperature: false, - knowledge: "2024-04", - release_date: "2025-01-31", - last_updated: "2025-01-31", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-4.1-nano": { - id: "openai/gpt-4.1-nano", - name: "GPT-4.1-nano", - family: "gpt-nano", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/o3": { - id: "openai/o3", - name: "OpenAI o3", - family: "o", - attachment: false, - reasoning: true, - tool_call: false, - temperature: false, - knowledge: "2024-04", - release_date: "2025-01-31", - last_updated: "2025-01-31", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-4o": { - id: "openai/gpt-4o", - name: "GPT-4o", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-05-13", - last_updated: "2024-05-13", - modalities: { input: ["text", "image", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-4.1": { - id: "openai/gpt-4.1", - name: "GPT-4.1", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-4.1-mini": { - id: "openai/gpt-4.1-mini", - name: "GPT-4.1-mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "meta/llama-4-scout-17b-16e-instruct": { - id: "meta/llama-4-scout-17b-16e-instruct", - name: "Llama 4 Scout 17B 16E Instruct", - family: "llama", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-01-31", - last_updated: "2025-01-31", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 8192 }, - }, - "meta/meta-llama-3.1-8b-instruct": { - id: "meta/meta-llama-3.1-8b-instruct", - name: "Meta-Llama-3.1-8B-Instruct", - family: "llama", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 32768 }, - }, - "meta/llama-3.3-70b-instruct": { - id: "meta/llama-3.3-70b-instruct", - name: "Llama-3.3-70B-Instruct", - family: "llama", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 32768 }, - }, - "meta/meta-llama-3-70b-instruct": { - id: "meta/meta-llama-3-70b-instruct", - name: "Meta-Llama-3-70B-Instruct", - family: "llama", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-04-18", - last_updated: "2024-04-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 8192, output: 2048 }, - }, - "meta/llama-3.2-90b-vision-instruct": { - id: "meta/llama-3.2-90b-vision-instruct", - name: "Llama-3.2-90B-Vision-Instruct", - family: "llama", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-09-25", - last_updated: "2024-09-25", - modalities: { input: ["text", "image", "audio"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 8192 }, - }, - "meta/llama-3.2-11b-vision-instruct": { - id: "meta/llama-3.2-11b-vision-instruct", - name: "Llama-3.2-11B-Vision-Instruct", - family: "llama", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-09-25", - last_updated: "2024-09-25", - modalities: { input: ["text", "image", "audio"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 8192 }, - }, - "meta/meta-llama-3.1-405b-instruct": { - id: "meta/meta-llama-3.1-405b-instruct", - name: "Meta-Llama-3.1-405B-Instruct", - family: "llama", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 32768 }, - }, - "meta/meta-llama-3.1-70b-instruct": { - id: "meta/meta-llama-3.1-70b-instruct", - name: "Meta-Llama-3.1-70B-Instruct", - family: "llama", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 32768 }, - }, - "meta/meta-llama-3-8b-instruct": { - id: "meta/meta-llama-3-8b-instruct", - name: "Meta-Llama-3-8B-Instruct", - family: "llama", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-04-18", - last_updated: "2024-04-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 8192, output: 2048 }, - }, - "meta/llama-4-maverick-17b-128e-instruct-fp8": { - id: "meta/llama-4-maverick-17b-128e-instruct-fp8", - name: "Llama 4 Maverick 17B 128E Instruct FP8", - family: "llama", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-01-31", - last_updated: "2025-01-31", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 8192 }, - }, - "core42/jais-30b-chat": { - id: "core42/jais-30b-chat", - name: "JAIS 30b Chat", - family: "jais", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-03", - release_date: "2023-08-30", - last_updated: "2023-08-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 8192, output: 2048 }, - }, - "mistral-ai/mistral-nemo": { - id: "mistral-ai/mistral-nemo", - name: "Mistral Nemo", - family: "mistral-nemo", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-03", - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 8192 }, - }, - "mistral-ai/ministral-3b": { - id: "mistral-ai/ministral-3b", - name: "Ministral 3B", - family: "ministral", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-03", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 8192 }, - }, - "mistral-ai/mistral-large-2411": { - id: "mistral-ai/mistral-large-2411", - name: "Mistral Large 24.11", - family: "mistral-large", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-09", - release_date: "2024-11-01", - last_updated: "2024-11-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 32768 }, - }, - "mistral-ai/mistral-small-2503": { - id: "mistral-ai/mistral-small-2503", - name: "Mistral Small 3.1", - family: "mistral-small", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-09", - release_date: "2025-03-01", - last_updated: "2025-03-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 32768 }, - }, - "mistral-ai/mistral-medium-2505": { - id: "mistral-ai/mistral-medium-2505", - name: "Mistral Medium 3 (25.05)", - family: "mistral-medium", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-09", - release_date: "2025-05-01", - last_updated: "2025-05-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 32768 }, - }, - "mistral-ai/codestral-2501": { - id: "mistral-ai/codestral-2501", - name: "Codestral 25.01", - family: "codestral", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-03", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 32000, output: 8192 }, - }, - }, - }, - togetherai: { - id: "togetherai", - env: ["TOGETHER_API_KEY"], - npm: "@ai-sdk/togetherai", - name: "Together AI", - doc: "https://docs.together.ai/docs/serverless-models", - models: { - "essentialai/Rnj-1-Instruct": { - id: "essentialai/Rnj-1-Instruct", - name: "Rnj-1 Instruct", - family: "rnj", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-12-05", - last_updated: "2025-12-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.15 }, - limit: { context: 32768, output: 32768 }, - }, - "Qwen/Qwen3.5-397B-A17B": { - id: "Qwen/Qwen3.5-397B-A17B", - name: "Qwen3.5 397B A17B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-16", - last_updated: "2026-02-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3.6 }, - limit: { context: 262144, output: 130000 }, - }, - "Qwen/Qwen3-Coder-Next-FP8": { - id: "Qwen/Qwen3-Coder-Next-FP8", - name: "Qwen3 Coder Next FP8", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2026-02-03", - release_date: "2026-02-03", - last_updated: "2026-02-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 1.2 }, - limit: { context: 262144, output: 262144 }, - }, - "Qwen/Qwen3-235B-A22B-Instruct-2507-tput": { - id: "Qwen/Qwen3-235B-A22B-Instruct-2507-tput", - name: "Qwen3 235B A22B Instruct 2507 FP8", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-07-25", - last_updated: "2025-07-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.6 }, - limit: { context: 262144, output: 262144 }, - }, - "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8": { - id: "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8", - name: "Qwen3 Coder 480B A35B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2, output: 2 }, - limit: { context: 262144, output: 262144 }, - }, - "zai-org/GLM-5.1": { - id: "zai-org/GLM-5.1", - name: "GLM-5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-11", - release_date: "2026-04-07", - last_updated: "2026-04-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.4, output: 4.4 }, - limit: { context: 202752, output: 131072 }, - }, - "meta-llama/Llama-3.3-70B-Instruct-Turbo": { - id: "meta-llama/Llama-3.3-70B-Instruct-Turbo", - name: "Llama 3.3 70B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.88, output: 0.88 }, - limit: { context: 131072, output: 131072 }, - }, - "deepseek-ai/DeepSeek-V3": { - id: "deepseek-ai/DeepSeek-V3", - name: "DeepSeek V3", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-01-20", - last_updated: "2025-05-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.25, output: 1.25 }, - limit: { context: 131072, output: 131072 }, - }, - "deepseek-ai/DeepSeek-R1": { - id: "deepseek-ai/DeepSeek-R1", - name: "DeepSeek R1", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2024-07", - release_date: "2024-12-26", - last_updated: "2025-03-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 3, output: 7 }, - limit: { context: 163839, output: 163839 }, - }, - "deepseek-ai/DeepSeek-V3-1": { - id: "deepseek-ai/DeepSeek-V3-1", - name: "DeepSeek V3.1", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08", - release_date: "2025-08-21", - last_updated: "2025-08-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 1.7 }, - limit: { context: 131072, output: 131072 }, - }, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "GPT OSS 120B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 131072, output: 131072 }, - }, - "google/gemma-4-31B-it": { - id: "google/gemma-4-31B-it", - name: "Gemma 4 31B Instruct", - family: "gemma", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-04-07", - last_updated: "2026-04-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.5 }, - limit: { context: 262144, output: 131072 }, - }, - "moonshotai/Kimi-K2.5": { - id: "moonshotai/Kimi-K2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: true, - temperature: true, - knowledge: "2026-01", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 2.8 }, - limit: { context: 262144, output: 262144 }, - }, - "MiniMaxAI/MiniMax-M2.5": { - id: "MiniMaxAI/MiniMax-M2.5", - name: "MiniMax-M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.06 }, - limit: { context: 204800, output: 131072 }, - }, - }, - }, - "qihang-ai": { - id: "qihang-ai", - env: ["QIHANG_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.qhaigc.net/v1", - name: "QiHang", - doc: "https://www.qhaigc.net/docs", - models: { - "claude-opus-4-5-20251101": { - id: "claude-opus-4-5-20251101", - name: "Claude Opus 4.5", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03", - release_date: "2025-11-01", - last_updated: "2025-11-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.71, output: 3.57 }, - limit: { context: 200000, output: 32000 }, - }, - "gemini-3-flash-preview": { - id: "gemini-3-flash-preview", - name: "Gemini 3 Flash Preview", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.07, output: 0.43, context_over_200k: { input: 0.07, output: 0.43 } }, - limit: { context: 1048576, output: 65536 }, - }, - "gpt-5-mini": { - id: "gpt-5-mini", - name: "GPT-5-Mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-09-30", - release_date: "2025-09-15", - last_updated: "2025-09-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.04, output: 0.29 }, - limit: { context: 200000, output: 64000 }, - }, - "gemini-3-pro-preview": { - id: "gemini-3-pro-preview", - name: "Gemini 3 Pro Preview", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-11", - release_date: "2025-11-19", - last_updated: "2025-11-19", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.57, output: 3.43 }, - limit: { context: 1000000, output: 65000 }, - }, - "claude-sonnet-4-5-20250929": { - id: "claude-sonnet-4-5-20250929", - name: "Claude Sonnet 4.5", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.43, output: 2.14 }, - limit: { context: 200000, output: 64000 }, - }, - "gpt-5.2": { - id: "gpt-5.2", - name: "GPT-5.2", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gpt-5.2-codex": { - id: "gpt-5.2-codex", - name: "GPT-5.2 Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 1.14 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gemini-2.5-flash": { - id: "gemini-2.5-flash", - name: "Gemini 2.5 Flash", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.09, output: 0.71, context_over_200k: { input: 0.09, output: 0.71 } }, - limit: { context: 1048576, output: 65536 }, - }, - "claude-haiku-4-5-20251001": { - id: "claude-haiku-4-5-20251001", - name: "Claude Haiku 4.5", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-10-01", - last_updated: "2025-10-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.71 }, - limit: { context: 200000, output: 64000 }, - }, - }, - }, - anthropic: { - id: "anthropic", - env: ["ANTHROPIC_API_KEY"], - npm: "@ai-sdk/anthropic", - name: "Anthropic", - doc: "https://docs.anthropic.com/en/docs/about-claude/models", - models: { - "claude-3-sonnet-20240229": { - id: "claude-3-sonnet-20240229", - name: "Claude Sonnet 3", - family: "claude-sonnet", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-08-31", - release_date: "2024-03-04", - last_updated: "2024-03-04", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 0.3 }, - limit: { context: 200000, output: 4096 }, - }, - "claude-haiku-4-5": { - id: "claude-haiku-4-5", - name: "Claude Haiku 4.5 (latest)", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 200000, output: 64000 }, - }, - "claude-opus-4-5-20251101": { - id: "claude-opus-4-5-20251101", - name: "Claude Opus 4.5", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-01", - last_updated: "2025-11-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 64000 }, - }, - "claude-3-opus-20240229": { - id: "claude-3-opus-20240229", - name: "Claude Opus 3", - family: "claude-opus", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-08-31", - release_date: "2024-02-29", - last_updated: "2024-02-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 4096 }, - }, - "claude-3-5-haiku-20241022": { - id: "claude-3-5-haiku-20241022", - name: "Claude Haiku 3.5", - family: "claude-haiku", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07-31", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 4, cache_read: 0.08, cache_write: 1 }, - limit: { context: 200000, output: 8192 }, - }, - "claude-3-5-sonnet-20241022": { - id: "claude-3-5-sonnet-20241022", - name: "Claude Sonnet 3.5 v2", - family: "claude-sonnet", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04-30", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 8192 }, - }, - "claude-sonnet-4-6": { - id: "claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08", - release_date: "2026-02-17", - last_updated: "2026-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 1000000, output: 64000 }, - }, - "claude-opus-4-0": { - id: "claude-opus-4-0", - name: "Claude Opus 4 (latest)", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "claude-3-haiku-20240307": { - id: "claude-3-haiku-20240307", - name: "Claude Haiku 3", - family: "claude-haiku", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-08-31", - release_date: "2024-03-13", - last_updated: "2024-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1.25, cache_read: 0.03, cache_write: 0.3 }, - limit: { context: 200000, output: 4096 }, - }, - "claude-sonnet-4-5-20250929": { - id: "claude-sonnet-4-5-20250929", - name: "Claude Sonnet 4.5", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "claude-3-5-haiku-latest": { - id: "claude-3-5-haiku-latest", - name: "Claude Haiku 3.5 (latest)", - family: "claude-haiku", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07-31", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 4, cache_read: 0.08, cache_write: 1 }, - limit: { context: 200000, output: 8192 }, - }, - "claude-opus-4-1": { - id: "claude-opus-4-1", - name: "Claude Opus 4.1 (latest)", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "claude-sonnet-4-0": { - id: "claude-sonnet-4-0", - name: "Claude Sonnet 4 (latest)", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "claude-3-5-sonnet-20240620": { - id: "claude-3-5-sonnet-20240620", - name: "Claude Sonnet 3.5", - family: "claude-sonnet", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04-30", - release_date: "2024-06-20", - last_updated: "2024-06-20", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 8192 }, - }, - "claude-opus-4-5": { - id: "claude-opus-4-5", - name: "Claude Opus 4.5 (latest)", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-24", - last_updated: "2025-11-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 64000 }, - }, - "claude-opus-4-1-20250805": { - id: "claude-opus-4-1-20250805", - name: "Claude Opus 4.1", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "claude-haiku-4-5-20251001": { - id: "claude-haiku-4-5-20251001", - name: "Claude Haiku 4.5", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 200000, output: 64000 }, - }, - "claude-sonnet-4-20250514": { - id: "claude-sonnet-4-20250514", - name: "Claude Sonnet 4", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "claude-opus-4-6": { - id: "claude-opus-4-6", - name: "Claude Opus 4.6", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-02-05", - last_updated: "2026-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 1000000, output: 128000 }, - experimental: { - modes: { - fast: { - cost: { input: 30, output: 150, cache_read: 3, cache_write: 37.5 }, - provider: { body: { speed: "fast" }, headers: { "anthropic-beta": "fast-mode-2026-02-01" } }, - }, - }, - }, - }, - "claude-3-7-sonnet-20250219": { - id: "claude-3-7-sonnet-20250219", - name: "Claude Sonnet 3.7", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10-31", - release_date: "2025-02-19", - last_updated: "2025-02-19", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "claude-sonnet-4-5": { - id: "claude-sonnet-4-5", - name: "Claude Sonnet 4.5 (latest)", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "claude-opus-4-20250514": { - id: "claude-opus-4-20250514", - name: "Claude Opus 4", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - }, - }, - modelscope: { - id: "modelscope", - env: ["MODELSCOPE_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api-inference.modelscope.cn/v1", - name: "ModelScope", - doc: "https://modelscope.cn/docs/model-service/API-Inference/intro", - models: { - "Qwen/Qwen3-30B-A3B-Thinking-2507": { - id: "Qwen/Qwen3-30B-A3B-Thinking-2507", - name: "Qwen3 30B A3B Thinking 2507", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-30", - last_updated: "2025-07-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 32768 }, - }, - "Qwen/Qwen3-30B-A3B-Instruct-2507": { - id: "Qwen/Qwen3-30B-A3B-Instruct-2507", - name: "Qwen3 30B A3B Instruct 2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-30", - last_updated: "2025-07-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 16384 }, - }, - "Qwen/Qwen3-235B-A22B-Instruct-2507": { - id: "Qwen/Qwen3-235B-A22B-Instruct-2507", - name: "Qwen3 235B A22B Instruct 2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04-28", - last_updated: "2025-07-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 131072 }, - }, - "Qwen/Qwen3-Coder-30B-A3B-Instruct": { - id: "Qwen/Qwen3-Coder-30B-A3B-Instruct", - name: "Qwen3 Coder 30B A3B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-31", - last_updated: "2025-07-31", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 65536 }, - }, - "Qwen/Qwen3-235B-A22B-Thinking-2507": { - id: "Qwen/Qwen3-235B-A22B-Thinking-2507", - name: "Qwen3-235B-A22B-Thinking-2507", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-25", - last_updated: "2025-07-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 131072 }, - }, - "ZhipuAI/GLM-4.5": { - id: "ZhipuAI/GLM-4.5", - name: "GLM-4.5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 98304 }, - }, - "ZhipuAI/GLM-4.6": { - id: "ZhipuAI/GLM-4.6", - name: "GLM-4.6", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 202752, output: 98304 }, - }, - }, - }, - gitlab: { - id: "gitlab", - env: ["GITLAB_TOKEN"], - npm: "gitlab-ai-provider", - name: "GitLab Duo", - doc: "https://docs.gitlab.com/user/duo_agent_platform/", - models: { - "duo-chat-gpt-5-4-nano": { - id: "duo-chat-gpt-5-4-nano", - name: "Agentic Chat (GPT-5.4 Nano)", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "duo-chat-gpt-5-mini": { - id: "duo-chat-gpt-5-mini", - name: "Agentic Chat (GPT-5 Mini)", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2026-01-22", - last_updated: "2026-01-22", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "duo-chat-sonnet-4-6": { - id: "duo-chat-sonnet-4-6", - name: "Agentic Chat (Claude Sonnet 4.6)", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-02-17", - last_updated: "2026-02-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 1000000, output: 64000 }, - }, - "duo-chat-gpt-5-2": { - id: "duo-chat-gpt-5-2", - name: "Agentic Chat (GPT-5.2)", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-01-23", - last_updated: "2026-01-23", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "duo-chat-gpt-5-codex": { - id: "duo-chat-gpt-5-codex", - name: "Agentic Chat (GPT-5 Codex)", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2026-01-22", - last_updated: "2026-01-22", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "duo-chat-gpt-5-1": { - id: "duo-chat-gpt-5-1", - name: "Agentic Chat (GPT-5.1)", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2026-01-22", - last_updated: "2026-01-22", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "duo-chat-gpt-5-2-codex": { - id: "duo-chat-gpt-5-2-codex", - name: "Agentic Chat (GPT-5.2 Codex)", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-01-22", - last_updated: "2026-01-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "duo-chat-sonnet-4-5": { - id: "duo-chat-sonnet-4-5", - name: "Agentic Chat (Claude Sonnet 4.5)", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2026-01-08", - last_updated: "2026-01-08", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 200000, output: 64000 }, - }, - "duo-chat-gpt-5-4": { - id: "duo-chat-gpt-5-4", - name: "Agentic Chat (GPT-5.4)", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 1050000, input: 922000, output: 128000 }, - }, - "duo-chat-haiku-4-5": { - id: "duo-chat-haiku-4-5", - name: "Agentic Chat (Claude Haiku 4.5)", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2026-01-08", - last_updated: "2026-01-08", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 200000, output: 64000 }, - }, - "duo-chat-gpt-5-3-codex": { - id: "duo-chat-gpt-5-3-codex", - name: "Agentic Chat (GPT-5.3 Codex)", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "duo-chat-gpt-5-4-mini": { - id: "duo-chat-gpt-5-4-mini", - name: "Agentic Chat (GPT-5.4 Mini)", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "duo-chat-opus-4-5": { - id: "duo-chat-opus-4-5", - name: "Agentic Chat (Claude Opus 4.5)", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2026-01-08", - last_updated: "2026-01-08", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 200000, output: 64000 }, - }, - "duo-chat-opus-4-6": { - id: "duo-chat-opus-4-6", - name: "Agentic Chat (Claude Opus 4.6)", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 1000000, output: 64000 }, - }, - }, - }, - xiaomi: { - id: "xiaomi", - env: ["XIAOMI_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.xiaomimimo.com/v1", - name: "Xiaomi", - doc: "https://platform.xiaomimimo.com/#/docs", - models: { - "mimo-v2-omni": { - id: "mimo-v2-omni", - name: "MiMo-V2-Omni", - family: "mimo", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 2, cache_read: 0.08 }, - limit: { context: 256000, output: 128000 }, - }, - "mimo-v2-pro": { - id: "mimo-v2-pro", - name: "MiMo-V2-Pro", - family: "mimo", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3, cache_read: 0.2 }, - limit: { context: 1000000, output: 128000 }, - }, - "mimo-v2-flash": { - id: "mimo-v2-flash", - name: "MiMo-V2-Flash", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12-01", - release_date: "2025-12-16", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3, cache_read: 0.01 }, - limit: { context: 256000, output: 64000 }, - }, - }, - }, - clarifai: { - id: "clarifai", - env: ["CLARIFAI_PAT"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.clarifai.com/v2/ext/openai/v1", - name: "Clarifai", - doc: "https://docs.clarifai.com/compute/inference/", - models: { - "arcee_ai/AFM/models/trinity-mini": { - id: "arcee_ai/AFM/models/trinity-mini", - name: "Trinity Mini", - family: "trinity-mini", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-12", - last_updated: "2026-02-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.045, output: 0.15 }, - limit: { context: 131072, output: 131072 }, - }, - "mistralai/completion/models/Ministral-3-14B-Reasoning-2512": { - id: "mistralai/completion/models/Ministral-3-14B-Reasoning-2512", - name: "Ministral 3 14B Reasoning 2512", - family: "ministral", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-12", - release_date: "2025-12-01", - last_updated: "2025-12-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 2.5, output: 1.7 }, - limit: { context: 262144, output: 262144 }, - }, - "mistralai/completion/models/Ministral-3-3B-Reasoning-2512": { - id: "mistralai/completion/models/Ministral-3-3B-Reasoning-2512", - name: "Ministral 3 3B Reasoning 2512", - family: "ministral", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-12", - last_updated: "2026-02-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 1.039, output: 0.54825 }, - limit: { context: 262144, output: 262144 }, - }, - "deepseek-ai/deepseek-ocr/models/DeepSeek-OCR": { - id: "deepseek-ai/deepseek-ocr/models/DeepSeek-OCR", - name: "DeepSeek OCR", - family: "deepseek", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-10-20", - last_updated: "2026-02-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.7 }, - limit: { context: 8192, output: 8192 }, - }, - "openai/chat-completion/models/gpt-oss-20b": { - id: "openai/chat-completion/models/gpt-oss-20b", - name: "GPT OSS 20B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-12-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.045, output: 0.18 }, - limit: { context: 131072, output: 16384 }, - }, - "openai/chat-completion/models/gpt-oss-120b-high-throughput": { - id: "openai/chat-completion/models/gpt-oss-120b-high-throughput", - name: "GPT OSS 120B High Throughput", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2026-02-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.09, output: 0.36 }, - limit: { context: 131072, output: 16384 }, - }, - "minimaxai/chat-completion/models/MiniMax-M2_5-high-throughput": { - id: "minimaxai/chat-completion/models/MiniMax-M2_5-high-throughput", - name: "MiniMax-M2.5 High Throughput", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 204800, output: 131072 }, - }, - "qwen/qwenCoder/models/Qwen3-Coder-30B-A3B-Instruct": { - id: "qwen/qwenCoder/models/Qwen3-Coder-30B-A3B-Instruct", - name: "Qwen3 Coder 30B A3B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-31", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.11458, output: 0.74812 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen/qwenLM/models/Qwen3-30B-A3B-Thinking-2507": { - id: "qwen/qwenLM/models/Qwen3-30B-A3B-Thinking-2507", - name: "Qwen3 30B A3B Thinking 2507", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-31", - last_updated: "2026-02-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.36, output: 1.3 }, - limit: { context: 262144, output: 131072 }, - }, - "qwen/qwenLM/models/Qwen3-30B-A3B-Instruct-2507": { - id: "qwen/qwenLM/models/Qwen3-30B-A3B-Instruct-2507", - name: "Qwen3 30B A3B Instruct 2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-30", - last_updated: "2026-02-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.5 }, - limit: { context: 262144, output: 262144 }, - }, - "clarifai/main/models/mm-poly-8b": { - id: "clarifai/main/models/mm-poly-8b", - name: "MM Poly 8B", - family: "mm-poly", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-06", - last_updated: "2026-02-25", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.658, output: 1.11 }, - limit: { context: 32768, output: 4096 }, - }, - }, - }, - "minimax-cn": { - id: "minimax-cn", - env: ["MINIMAX_API_KEY"], - npm: "@ai-sdk/anthropic", - api: "https://api.minimaxi.com/anthropic/v1", - name: "MiniMax (minimaxi.com)", - doc: "https://platform.minimaxi.com/docs/guides/quickstart", - models: { - "MiniMax-M2": { - id: "MiniMax-M2", - name: "MiniMax-M2", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-10-27", - last_updated: "2025-10-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 196608, output: 128000 }, - }, - "MiniMax-M2.5": { - id: "MiniMax-M2.5", - name: "MiniMax-M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.03, cache_write: 0.375 }, - limit: { context: 204800, output: 131072 }, - }, - "MiniMax-M2.7": { - id: "MiniMax-M2.7", - name: "MiniMax-M2.7", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.06, cache_write: 0.375 }, - limit: { context: 204800, output: 131072 }, - }, - "MiniMax-M2.7-highspeed": { - id: "MiniMax-M2.7-highspeed", - name: "MiniMax-M2.7-highspeed", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.4, cache_read: 0.06, cache_write: 0.375 }, - limit: { context: 204800, output: 131072 }, - }, - "MiniMax-M2.1": { - id: "MiniMax-M2.1", - name: "MiniMax-M2.1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 204800, output: 131072 }, - }, - "MiniMax-M2.5-highspeed": { - id: "MiniMax-M2.5-highspeed", - name: "MiniMax-M2.5-highspeed", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-13", - last_updated: "2026-02-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.4, cache_read: 0.06, cache_write: 0.375 }, - limit: { context: 204800, output: 131072 }, - }, - }, - }, - "xiaomi-token-plan-ams": { - id: "xiaomi-token-plan-ams", - env: ["XIAOMI_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://token-plan-ams.xiaomimimo.com/v1", - name: "Xiaomi Token Plan (Europe)", - doc: "https://platform.xiaomimimo.com/#/docs", - models: { - "mimo-v2-pro": { - id: "mimo-v2-pro", - name: "MiMo-V2-Pro", - family: "mimo", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0 }, - limit: { context: 1000000, output: 128000 }, - }, - "mimo-v2-tts": { - id: "mimo-v2-tts", - name: "MiMo-V2-TTS", - family: "mimo", - attachment: false, - reasoning: false, - tool_call: false, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["audio"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 8000, output: 16000 }, - }, - "mimo-v2-omni": { - id: "mimo-v2-omni", - name: "MiMo-V2-Omni", - family: "mimo", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0 }, - limit: { context: 256000, output: 128000 }, - }, - }, - }, - zhipuai: { - id: "zhipuai", - env: ["ZHIPU_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://open.bigmodel.cn/api/paas/v4", - name: "Zhipu AI", - doc: "https://docs.z.ai/guides/overview/pricing", - models: { - "glm-5v-turbo": { - id: "glm-5v-turbo", - name: "glm-5v-turbo", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-04-01", - last_updated: "2026-04-01", - modalities: { input: ["text", "image", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 22, cache_read: 1.2, cache_write: 0 }, - limit: { context: 200000, output: 131072 }, - }, - "glm-5": { - id: "glm-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3.2, cache_read: 0.2, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "glm-5.1": { - id: "glm-5.1", - name: "GLM-5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-03-27", - last_updated: "2026-03-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 6, output: 24, cache_read: 1.3, cache_write: 0 }, - limit: { context: 200000, output: 131072 }, - }, - "glm-4.7-flash": { - id: "glm-4.7-flash", - name: "GLM-4.7-Flash", - family: "glm-flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-01-19", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 200000, output: 131072 }, - }, - "glm-4.5-flash": { - id: "glm-4.5-flash", - name: "GLM-4.5-Flash", - family: "glm-flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 131072, output: 98304 }, - }, - "glm-4.6v": { - id: "glm-4.6v", - name: "GLM-4.6V", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-08", - last_updated: "2025-12-08", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.9 }, - limit: { context: 128000, output: 32768 }, - }, - "glm-4.6": { - id: "glm-4.6", - name: "GLM-4.6", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2, cache_read: 0.11, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "glm-4.5v": { - id: "glm-4.5v", - name: "GLM-4.5V", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-08-11", - last_updated: "2025-08-11", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 1.8 }, - limit: { context: 64000, output: 16384 }, - }, - "glm-4.5-air": { - id: "glm-4.5-air", - name: "GLM-4.5-Air", - family: "glm-air", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 1.1, cache_read: 0.03, cache_write: 0 }, - limit: { context: 131072, output: 98304 }, - }, - "glm-4.5": { - id: "glm-4.5", - name: "GLM-4.5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2, cache_read: 0.11, cache_write: 0 }, - limit: { context: 131072, output: 98304 }, - }, - "glm-4.7-flashx": { - id: "glm-4.7-flashx", - name: "GLM-4.7-FlashX", - family: "glm-flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-01-19", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.4, cache_read: 0.01, cache_write: 0 }, - limit: { context: 200000, output: 131072 }, - }, - "glm-4.7": { - id: "glm-4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2, cache_read: 0.11, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - }, - }, - nova: { - id: "nova", - env: ["NOVA_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.nova.amazon.com/v1", - name: "Nova", - doc: "https://nova.amazon.com/dev/documentation", - models: { - "nova-2-lite-v1": { - id: "nova-2-lite-v1", - name: "Nova 2 Lite", - family: "nova-lite", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text", "image", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, reasoning: 0 }, - limit: { context: 1000000, output: 64000 }, - }, - "nova-2-pro-v1": { - id: "nova-2-pro-v1", - name: "Nova 2 Pro", - family: "nova-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-12-03", - last_updated: "2026-01-03", - modalities: { input: ["text", "image", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, reasoning: 0 }, - limit: { context: 1000000, output: 64000 }, - }, - }, - }, -} From ae17b416b8da910f43b8dca5356de41ef72d2685 Mon Sep 17 00:00:00 2001 From: Goni Zahavy Date: Mon, 13 Apr 2026 04:37:57 +0300 Subject: [PATCH 010/300] fix(cli): auth login now asks for api key in handlePluginAuth (#21641) Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> --- packages/opencode/src/cli/cmd/providers.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/providers.ts b/packages/opencode/src/cli/cmd/providers.ts index 1ab0ecc7bc..52da441904 100644 --- a/packages/opencode/src/cli/cmd/providers.ts +++ b/packages/opencode/src/cli/cmd/providers.ts @@ -148,6 +148,12 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string, } if (method.type === "api") { + const key = await prompts.password({ + message: "Enter your API key", + validate: (x) => (x && x.length > 0 ? undefined : "Required"), + }) + if (prompts.isCancel(key)) throw new UI.CancelledError() + if (method.authorize) { const result = await method.authorize(inputs) if (result.type === "failed") { @@ -157,7 +163,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string, const saveProvider = result.provider ?? provider await Auth.set(saveProvider, { type: "api", - key: result.key, + key: result.key ?? key, }) prompts.log.success("Login successful") } From 26d35583c5b9e75b7986f332cfc68813ea3a6e06 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Mon, 13 Apr 2026 09:39:53 +0800 Subject: [PATCH 011/300] sdk: throw error if response has text/html content type (#21289) --- packages/sdk/js/src/v2/client.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/sdk/js/src/v2/client.ts b/packages/sdk/js/src/v2/client.ts index 67fe1de32f..2d71d8446d 100644 --- a/packages/sdk/js/src/v2/client.ts +++ b/packages/sdk/js/src/v2/client.ts @@ -77,6 +77,12 @@ export function createOpencodeClient(config?: Config & { directory?: string; exp workspace: config?.experimental_workspaceID, }), ) - const result = new OpencodeClient({ client }) - return result + client.interceptors.response.use((response) => { + const contentType = response.headers.get("content-type") + if (contentType === "text/html") + throw new Error("Request is not supported by this version of OpenCode Server (Server responded with text/html)") + + return response + }) + return new OpencodeClient({ client }) } From a915fe74be24d4df9caf4c5b0e0f60133367b00d Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sun, 12 Apr 2026 21:39:06 -0500 Subject: [PATCH 012/300] tweak: adjust session getUsage function to use more up to date LanguageModelUsage instead of LanguageModelV2Usage (#22224) --- packages/opencode/src/session/index.ts | 16 ++-- .../opencode/test/session/compaction.test.ts | 96 +++++++++++++++++-- 2 files changed, 99 insertions(+), 13 deletions(-) diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 3d49035881..b43b724a00 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -4,7 +4,7 @@ import { BusEvent } from "@/bus/bus-event" import { Bus } from "@/bus" import { Decimal } from "decimal.js" import z from "zod" -import { type ProviderMetadata } from "ai" +import { type ProviderMetadata, type LanguageModelUsage } from "ai" import { Flag } from "../flag/flag" import { Installation } from "../installation" @@ -28,7 +28,6 @@ import { SessionID, MessageID, PartID } from "./schema" import type { Provider } from "@/provider/provider" import { Permission } from "@/permission" import { Global } from "@/global" -import type { LanguageModelV2Usage } from "@ai-sdk/provider" import { Effect, Layer, Option, Context } from "effect" import { makeRuntime } from "@/effect/run-service" @@ -240,7 +239,7 @@ export namespace Session { export const getUsage = (input: { model: Provider.Model - usage: LanguageModelV2Usage + usage: LanguageModelUsage metadata?: ProviderMetadata }) => { const safe = (value: number) => { @@ -249,11 +248,14 @@ export namespace Session { } const inputTokens = safe(input.usage.inputTokens ?? 0) const outputTokens = safe(input.usage.outputTokens ?? 0) - const reasoningTokens = safe(input.usage.reasoningTokens ?? 0) + const reasoningTokens = safe(input.usage.outputTokenDetails?.reasoningTokens ?? input.usage.reasoningTokens ?? 0) - const cacheReadInputTokens = safe(input.usage.cachedInputTokens ?? 0) + const cacheReadInputTokens = safe( + input.usage.inputTokenDetails?.cacheReadTokens ?? input.usage.cachedInputTokens ?? 0, + ) const cacheWriteInputTokens = safe( - (input.metadata?.["anthropic"]?.["cacheCreationInputTokens"] ?? + (input.usage.inputTokenDetails?.cacheWriteTokens ?? + input.metadata?.["anthropic"]?.["cacheCreationInputTokens"] ?? // google-vertex-anthropic returns metadata under "vertex" key // (AnthropicMessagesLanguageModel custom provider key from 'vertex.anthropic.messages') input.metadata?.["vertex"]?.["cacheCreationInputTokens"] ?? @@ -274,7 +276,7 @@ export namespace Session { const tokens = { total, input: adjustedInputTokens, - output: outputTokens - reasoningTokens, + output: safe(outputTokens - reasoningTokens), reasoning: reasoningTokens, cache: { write: cacheWriteInputTokens, diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index 76a83c34da..61b47df34a 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/packages/opencode/test/session/compaction.test.ts @@ -1005,6 +1005,15 @@ describe("session.getUsage", () => { inputTokens: 1000, outputTokens: 500, totalTokens: 1500, + inputTokenDetails: { + noCacheTokens: undefined, + cacheReadTokens: undefined, + cacheWriteTokens: undefined, + }, + outputTokenDetails: { + textTokens: undefined, + reasoningTokens: undefined, + }, }, }) @@ -1023,7 +1032,15 @@ describe("session.getUsage", () => { inputTokens: 1000, outputTokens: 500, totalTokens: 1500, - cachedInputTokens: 200, + inputTokenDetails: { + noCacheTokens: 800, + cacheReadTokens: 200, + cacheWriteTokens: undefined, + }, + outputTokenDetails: { + textTokens: undefined, + reasoningTokens: undefined, + }, }, }) @@ -1039,6 +1056,15 @@ describe("session.getUsage", () => { inputTokens: 1000, outputTokens: 500, totalTokens: 1500, + inputTokenDetails: { + noCacheTokens: undefined, + cacheReadTokens: undefined, + cacheWriteTokens: undefined, + }, + outputTokenDetails: { + textTokens: undefined, + reasoningTokens: undefined, + }, }, metadata: { anthropic: { @@ -1059,7 +1085,15 @@ describe("session.getUsage", () => { inputTokens: 1000, outputTokens: 500, totalTokens: 1500, - cachedInputTokens: 200, + inputTokenDetails: { + noCacheTokens: 800, + cacheReadTokens: 200, + cacheWriteTokens: undefined, + }, + outputTokenDetails: { + textTokens: undefined, + reasoningTokens: undefined, + }, }, metadata: { anthropic: {}, @@ -1078,7 +1112,15 @@ describe("session.getUsage", () => { inputTokens: 1000, outputTokens: 500, totalTokens: 1500, - reasoningTokens: 100, + inputTokenDetails: { + noCacheTokens: undefined, + cacheReadTokens: undefined, + cacheWriteTokens: undefined, + }, + outputTokenDetails: { + textTokens: 400, + reasoningTokens: 100, + }, }, }) @@ -1104,7 +1146,15 @@ describe("session.getUsage", () => { inputTokens: 0, outputTokens: 1_000_000, totalTokens: 1_000_000, - reasoningTokens: 250_000, + inputTokenDetails: { + noCacheTokens: undefined, + cacheReadTokens: undefined, + cacheWriteTokens: undefined, + }, + outputTokenDetails: { + textTokens: 750_000, + reasoningTokens: 250_000, + }, }, }) @@ -1121,6 +1171,15 @@ describe("session.getUsage", () => { inputTokens: 0, outputTokens: 0, totalTokens: 0, + inputTokenDetails: { + noCacheTokens: undefined, + cacheReadTokens: undefined, + cacheWriteTokens: undefined, + }, + outputTokenDetails: { + textTokens: undefined, + reasoningTokens: undefined, + }, }, }) @@ -1148,6 +1207,15 @@ describe("session.getUsage", () => { inputTokens: 1_000_000, outputTokens: 100_000, totalTokens: 1_100_000, + inputTokenDetails: { + noCacheTokens: undefined, + cacheReadTokens: undefined, + cacheWriteTokens: undefined, + }, + outputTokenDetails: { + textTokens: undefined, + reasoningTokens: undefined, + }, }, }) @@ -1163,7 +1231,15 @@ describe("session.getUsage", () => { inputTokens: 1000, outputTokens: 500, totalTokens: 1500, - cachedInputTokens: 200, + inputTokenDetails: { + noCacheTokens: 800, + cacheReadTokens: 200, + cacheWriteTokens: undefined, + }, + outputTokenDetails: { + textTokens: undefined, + reasoningTokens: undefined, + }, } if (npm === "@ai-sdk/amazon-bedrock") { const result = Session.getUsage({ @@ -1214,7 +1290,15 @@ describe("session.getUsage", () => { inputTokens: 1000, outputTokens: 500, totalTokens: 1500, - cachedInputTokens: 200, + inputTokenDetails: { + noCacheTokens: 800, + cacheReadTokens: 200, + cacheWriteTokens: undefined, + }, + outputTokenDetails: { + textTokens: undefined, + reasoningTokens: undefined, + }, }, metadata: { vertex: { From 7230cd26838a133e93699497d2d27eb59ccf8460 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Mon, 13 Apr 2026 00:08:07 -0500 Subject: [PATCH 013/300] feat: add alibaba pkg and cache support (#22248) --- bun.lock | 5 +++++ packages/opencode/package.json | 1 + packages/opencode/src/provider/provider.ts | 2 ++ packages/opencode/src/provider/transform.ts | 3 +++ 4 files changed, 11 insertions(+) diff --git a/bun.lock b/bun.lock index 88d9635491..7ff8a3072f 100644 --- a/bun.lock +++ b/bun.lock @@ -319,6 +319,7 @@ "@actions/core": "1.11.1", "@actions/github": "6.0.1", "@agentclientprotocol/sdk": "0.16.1", + "@ai-sdk/alibaba": "1.0.17", "@ai-sdk/amazon-bedrock": "4.0.93", "@ai-sdk/anthropic": "3.0.67", "@ai-sdk/azure": "3.0.49", @@ -707,6 +708,8 @@ "@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.16.1", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-1ad+Sc/0sCtZGHthxxvgEUo5Wsbw16I+aF+YwdiLnPwkZG8KAGUEAPK6LM6Pf69lCyJPt1Aomk1d+8oE3C4ZEw=="], + "@ai-sdk/alibaba": ["@ai-sdk/alibaba@1.0.17", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZbE+U5bWz2JBc5DERLowx5+TKbjGBE93LqKZAWvuEn7HOSQMraxFMZuc0ST335QZJAyfBOzh7m1mPQ+y7EaaoA=="], + "@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.93", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.69", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-hcXDU8QDwpAzLVTuY932TQVlIij9+iaVTxc5mPGY6yb//JMAAC5hMVhg93IrxlrxWLvMgjezNgoZGwquR+SGnw=="], "@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rwLi/Rsuj2pYniQXIrvClHvXDzgM4UQHHnvHTWEF14efnlKclG/1ghpNC+adsRujAbCTr6gRsSbDE2vEqriV7g=="], @@ -5003,6 +5006,8 @@ "@actions/http-client/undici": ["undici@6.24.1", "", {}, "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA=="], + "@ai-sdk/alibaba/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="], + "@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.69", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-LshR7X3pFugY0o41G2VKTmg1XoGpSl7uoYWfzk6zjVZLhCfeFiwgpOga+eTV4XY1VVpZwKVqRnkDbIL7K2eH5g=="], "@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.12", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA=="], diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 60d63f8403..f5cc0e0a9b 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -76,6 +76,7 @@ "@actions/core": "1.11.1", "@actions/github": "6.0.1", "@agentclientprotocol/sdk": "0.16.1", + "@ai-sdk/alibaba": "1.0.17", "@ai-sdk/amazon-bedrock": "4.0.93", "@ai-sdk/anthropic": "3.0.67", "@ai-sdk/azure": "3.0.49", diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index e401a067c7..ef822739dd 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -46,6 +46,7 @@ import { createTogetherAI } from "@ai-sdk/togetherai" import { createPerplexity } from "@ai-sdk/perplexity" import { createVercel } from "@ai-sdk/vercel" import { createVenice } from "venice-ai-sdk-provider" +import { createAlibaba } from "@ai-sdk/alibaba" import { createGitLab, VERSION as GITLAB_PROVIDER_VERSION, @@ -145,6 +146,7 @@ export namespace Provider { "@ai-sdk/togetherai": createTogetherAI, "@ai-sdk/perplexity": createPerplexity, "@ai-sdk/vercel": createVercel, + "@ai-sdk/alibaba": createAlibaba, "gitlab-ai-provider": createGitLab, "@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible, "venice-ai-sdk-provider": createVenice, diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index dea8cf936a..99aaad6c9c 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -209,6 +209,9 @@ export namespace ProviderTransform { copilot: { copilot_cache_control: { type: "ephemeral" }, }, + alibaba: { + cacheControl: { type: "ephemeral" }, + }, } for (const msg of unique([...system, ...final])) { From 0b4fe14b0a0ab0b38d890706d4071a407d16674f Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Mon, 13 Apr 2026 00:39:12 -0500 Subject: [PATCH 014/300] fix: forgot to put alibaba case in last commit (#22249) --- packages/opencode/src/provider/transform.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 99aaad6c9c..8cdc48e243 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -288,7 +288,8 @@ export namespace ProviderTransform { model.api.id.includes("claude") || model.id.includes("anthropic") || model.id.includes("claude") || - model.api.npm === "@ai-sdk/anthropic") && + model.api.npm === "@ai-sdk/anthropic" || + model.api.npm === "@ai-sdk/alibaba") && model.api.npm !== "@ai-sdk/gateway" ) { msgs = applyCaching(msgs, model) From 34f5bdbc9967fb67ef75c645443c638ce852aa09 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Mon, 13 Apr 2026 13:55:33 +0800 Subject: [PATCH 015/300] app: fix scroll to bottom light mode style (#22250) --- packages/app/src/pages/session/message-timeline.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx index fe6447c2e8..eac425fa6c 100644 --- a/packages/app/src/pages/session/message-timeline.tsx +++ b/packages/app/src/pages/session/message-timeline.tsx @@ -642,10 +642,10 @@ export function MessageTimeline(props: { onClick={props.onResumeScroll} >
From a6b9f0dac1a67ab669543ac946dcaace2031e2ec Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Mon, 13 Apr 2026 13:58:35 +0800 Subject: [PATCH 016/300] app: align workspace load more button (#22251) --- packages/app/src/pages/layout/sidebar-workspace.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/pages/layout/sidebar-workspace.tsx b/packages/app/src/pages/layout/sidebar-workspace.tsx index 68e36ff77a..878b6e5fa2 100644 --- a/packages/app/src/pages/layout/sidebar-workspace.tsx +++ b/packages/app/src/pages/layout/sidebar-workspace.tsx @@ -274,7 +274,7 @@ const WorkspaceSessionList = (props: {
From c98f61638535c9cc57a2b710decc780f7289fc2f Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Wed, 15 Apr 2026 15:29:36 +0800 Subject: [PATCH 133/300] ui: update accordion styles and session review component (#22582) --- packages/ui/src/styles/base.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/styles/base.css b/packages/ui/src/styles/base.css index b5604ad619..a032f9ea2d 100644 --- a/packages/ui/src/styles/base.css +++ b/packages/ui/src/styles/base.css @@ -82,7 +82,7 @@ a { cursor: default; } -*[data-tauri-drag-region] { +#root:not([aria-hidden]) *[data-tauri-drag-region] { app-region: drag; } From d7718d41d465cc1e84bc4d6c2e81af8baf46a23e Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Wed, 15 Apr 2026 17:21:04 +0800 Subject: [PATCH 134/300] refactor(electron): update store configuration (#22597) --- packages/desktop-electron/src/main/store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/desktop-electron/src/main/store.ts b/packages/desktop-electron/src/main/store.ts index cf2d25b110..709e820e25 100644 --- a/packages/desktop-electron/src/main/store.ts +++ b/packages/desktop-electron/src/main/store.ts @@ -7,7 +7,7 @@ const cache = new Map() export function getStore(name = SETTINGS_STORE) { const cached = cache.get(name) if (cached) return cached - const next = new Store({ name, fileExtension: "" }) + const next = new Store({ name, fileExtension: "", accessPropertiesByDotNotation: false }) cache.set(name, next) return next } From 405b0b037c7597448e6b36438425042c8b0cf772 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 15 Apr 2026 14:29:09 +0200 Subject: [PATCH 135/300] handle non-throwing requests (#22604) --- packages/opencode/src/cli/cmd/tui/context/sync.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index 498db99a1b..772b5d9a0c 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -336,7 +336,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ case "lsp.updated": { const workspace = project.workspace.current() - sdk.client.lsp.status({ workspace }).then((x) => setStore("lsp", x.data!)) + sdk.client.lsp.status({ workspace }).then((x) => setStore("lsp", x.data ?? [])) break } @@ -419,14 +419,14 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ ...(args.continue ? [] : [sessionListPromise.then((sessions) => setStore("session", reconcile(sessions)))]), consoleStatePromise.then((consoleState) => setStore("console_state", reconcile(consoleState))), sdk.client.command.list({ workspace }).then((x) => setStore("command", reconcile(x.data ?? []))), - sdk.client.lsp.status({ workspace }).then((x) => setStore("lsp", reconcile(x.data!))), - sdk.client.mcp.status({ workspace }).then((x) => setStore("mcp", reconcile(x.data!))), + sdk.client.lsp.status({ workspace }).then((x) => setStore("lsp", reconcile(x.data ?? []))), + sdk.client.mcp.status({ workspace }).then((x) => setStore("mcp", reconcile(x.data ?? {}))), sdk.client.experimental.resource .list({ workspace }) .then((x) => setStore("mcp_resource", reconcile(x.data ?? {}))), - sdk.client.formatter.status({ workspace }).then((x) => setStore("formatter", reconcile(x.data!))), + sdk.client.formatter.status({ workspace }).then((x) => setStore("formatter", reconcile(x.data ?? []))), sdk.client.session.status({ workspace }).then((x) => { - setStore("session_status", reconcile(x.data!)) + setStore("session_status", reconcile(x.data ?? {})) }), sdk.client.provider.auth({ workspace }).then((x) => setStore("provider_auth", reconcile(x.data ?? {}))), sdk.client.vcs.get({ workspace }).then((x) => setStore("vcs", reconcile(x.data))), From 004a9284afb09b31105cb2bc26d993af0726585b Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 15 Apr 2026 09:12:09 -0400 Subject: [PATCH 136/300] sync --- packages/web/src/content/docs/ar/go.mdx | 16 +++++++++++----- packages/web/src/content/docs/bs/go.mdx | 16 +++++++++++----- packages/web/src/content/docs/da/go.mdx | 16 +++++++++++----- packages/web/src/content/docs/de/go.mdx | 16 +++++++++++----- packages/web/src/content/docs/es/go.mdx | 16 +++++++++++----- packages/web/src/content/docs/fr/go.mdx | 16 +++++++++++----- packages/web/src/content/docs/it/go.mdx | 16 +++++++++++----- packages/web/src/content/docs/ja/go.mdx | 16 +++++++++++----- packages/web/src/content/docs/ko/go.mdx | 16 +++++++++++----- packages/web/src/content/docs/nb/go.mdx | 16 +++++++++++----- packages/web/src/content/docs/pl/go.mdx | 16 +++++++++++----- packages/web/src/content/docs/pt-br/go.mdx | 16 +++++++++++----- packages/web/src/content/docs/ru/go.mdx | 16 +++++++++++----- packages/web/src/content/docs/th/go.mdx | 16 +++++++++++----- packages/web/src/content/docs/tr/go.mdx | 16 +++++++++++----- packages/web/src/content/docs/zh-cn/go.mdx | 16 +++++++++++----- packages/web/src/content/docs/zh-tw/go.mdx | 16 +++++++++++----- 17 files changed, 187 insertions(+), 85 deletions(-) diff --git a/packages/web/src/content/docs/ar/go.mdx b/packages/web/src/content/docs/ar/go.mdx index fe95dc3dd2..655749eeae 100644 --- a/packages/web/src/content/docs/ar/go.mdx +++ b/packages/web/src/content/docs/ar/go.mdx @@ -79,11 +79,17 @@ OpenCode Go حاليًا في المرحلة التجريبية. يوضح الجدول أدناه عددًا تقديريًا للطلبات بناءً على أنماط استخدام Go المعتادة: -| | GLM-5.1 | GLM-5 | Kimi K2.5 | MiMo-V2-Pro | MiMo-V2-Omni | MiniMax M2.7 | MiniMax M2.5 | Qwen3.6 Plus | Qwen3.5 Plus | -| ------------------- | ------- | ----- | --------- | ----------- | ------------ | ------------ | ------------ | ------------ | ------------ | -| الطلبات لكل 5 ساعات | 880 | 1,150 | 1,850 | 1,290 | 2,150 | 14,000 | 20,000 | 3,300 | 10,200 | -| الطلبات في الأسبوع | 2,150 | 2,880 | 4,630 | 3,225 | 5,450 | 35,000 | 50,000 | 8,200 | 25,200 | -| الطلبات في الشهر | 4,300 | 5,750 | 9,250 | 6,450 | 10,900 | 70,000 | 100,000 | 20,500 | 50,500 | +| Model | الطلبات لكل 5 ساعات | الطلبات في الأسبوع | الطلبات في الشهر | +| ------------ | ------------------- | ------------------ | ---------------- | +| GLM-5.1 | 880 | 2,150 | 4,300 | +| GLM-5 | 1,150 | 2,880 | 5,750 | +| Kimi K2.5 | 1,850 | 4,630 | 9,250 | +| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | +| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | +| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | +| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | +| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | تستند التقديرات إلى متوسطات أنماط الطلبات المرصودة: diff --git a/packages/web/src/content/docs/bs/go.mdx b/packages/web/src/content/docs/bs/go.mdx index f777d8d99e..3dabedf6a3 100644 --- a/packages/web/src/content/docs/bs/go.mdx +++ b/packages/web/src/content/docs/bs/go.mdx @@ -89,11 +89,17 @@ Ograničenja su definisana u dolarskoj vrijednosti. To znači da vaš stvarni br Tabela ispod pruža procijenjeni broj zahtjeva na osnovu tipičnih obrazaca korištenja Go pretplate: -| | GLM-5.1 | GLM-5 | Kimi K2.5 | MiMo-V2-Pro | MiMo-V2-Omni | MiniMax M2.7 | MiniMax M2.5 | Qwen3.6 Plus | Qwen3.5 Plus | -| ------------------ | ------- | ----- | --------- | ----------- | ------------ | ------------ | ------------ | ------------ | ------------ | -| zahtjeva na 5 sati | 880 | 1,150 | 1,850 | 1,290 | 2,150 | 14,000 | 20,000 | 3,300 | 10,200 | -| zahtjeva sedmično | 2,150 | 2,880 | 4,630 | 3,225 | 5,450 | 35,000 | 50,000 | 8,200 | 25,200 | -| zahtjeva mjesečno | 4,300 | 5,750 | 9,250 | 6,450 | 10,900 | 70,000 | 100,000 | 20,500 | 50,500 | +| Model | zahtjeva na 5 sati | zahtjeva sedmično | zahtjeva mjesečno | +| ------------ | ------------------ | ----------------- | ----------------- | +| GLM-5.1 | 880 | 2,150 | 4,300 | +| GLM-5 | 1,150 | 2,880 | 5,750 | +| Kimi K2.5 | 1,850 | 4,630 | 9,250 | +| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | +| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | +| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | +| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | +| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | Procjene se zasnivaju na zapaženim prosječnim obrascima zahtjeva: diff --git a/packages/web/src/content/docs/da/go.mdx b/packages/web/src/content/docs/da/go.mdx index 5254c13431..54f17bd77b 100644 --- a/packages/web/src/content/docs/da/go.mdx +++ b/packages/web/src/content/docs/da/go.mdx @@ -89,11 +89,17 @@ Grænserne er defineret i dollarværdi. Det betyder, at dit faktiske antal anmod Tabellen nedenfor giver et estimeret antal anmodninger baseret på typiske Go-forbrugsmønstre: -| | GLM-5.1 | GLM-5 | Kimi K2.5 | MiMo-V2-Pro | MiMo-V2-Omni | MiniMax M2.7 | MiniMax M2.5 | Qwen3.6 Plus | Qwen3.5 Plus | -| ----------------------- | ------- | ----- | --------- | ----------- | ------------ | ------------ | ------------ | ------------ | ------------ | -| anmodninger pr. 5 timer | 880 | 1.150 | 1.850 | 1.290 | 2.150 | 14.000 | 20.000 | 3,300 | 10,200 | -| anmodninger pr. uge | 2.150 | 2.880 | 4.630 | 3.225 | 5.450 | 35.000 | 50.000 | 8,200 | 25,200 | -| anmodninger pr. måned | 4.300 | 5.750 | 9.250 | 6.450 | 10.900 | 70.000 | 100.000 | 20,500 | 50,500 | +| Model | anmodninger pr. 5 timer | anmodninger pr. uge | anmodninger pr. måned | +| ------------ | ----------------------- | ------------------- | --------------------- | +| GLM-5.1 | 880 | 2,150 | 4,300 | +| GLM-5 | 1,150 | 2,880 | 5,750 | +| Kimi K2.5 | 1,850 | 4,630 | 9,250 | +| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | +| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | +| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | +| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | +| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | Estimaterne er baseret på observerede gennemsnitlige anmodningsmønstre: diff --git a/packages/web/src/content/docs/de/go.mdx b/packages/web/src/content/docs/de/go.mdx index f739ef1933..0c5ec931ae 100644 --- a/packages/web/src/content/docs/de/go.mdx +++ b/packages/web/src/content/docs/de/go.mdx @@ -81,11 +81,17 @@ Limits sind in Dollarwerten definiert. Das bedeutet, dass die tatsächliche Anza Die folgende Tabelle zeigt eine geschätzte Anzahl von Anfragen basierend auf typischen Go-Nutzungsmustern: -| | GLM-5.1 | GLM-5 | Kimi K2.5 | MiMo-V2-Pro | MiMo-V2-Omni | MiniMax M2.7 | MiniMax M2.5 | Qwen3.6 Plus | Qwen3.5 Plus | -| ---------------------- | ------- | ----- | --------- | ----------- | ------------ | ------------ | ------------ | ------------ | ------------ | -| Anfragen pro 5 Stunden | 880 | 1.150 | 1.850 | 1.290 | 2.150 | 14.000 | 20.000 | 3,300 | 10,200 | -| Anfragen pro Woche | 2.150 | 2.880 | 4.630 | 3.225 | 5.450 | 35.000 | 50.000 | 8,200 | 25,200 | -| Anfragen pro Monat | 4.300 | 5.750 | 9.250 | 6.450 | 10.900 | 70.000 | 100.000 | 20,500 | 50,500 | +| Model | Anfragen pro 5 Stunden | Anfragen pro Woche | Anfragen pro Monat | +| ------------ | ---------------------- | ------------------ | ------------------ | +| GLM-5.1 | 880 | 2,150 | 4,300 | +| GLM-5 | 1,150 | 2,880 | 5,750 | +| Kimi K2.5 | 1,850 | 4,630 | 9,250 | +| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | +| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | +| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | +| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | +| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | Die Schätzungen basieren auf beobachteten durchschnittlichen Anfragemustern: diff --git a/packages/web/src/content/docs/es/go.mdx b/packages/web/src/content/docs/es/go.mdx index d8a7a66243..86ddbe81e8 100644 --- a/packages/web/src/content/docs/es/go.mdx +++ b/packages/web/src/content/docs/es/go.mdx @@ -89,11 +89,17 @@ Los límites se definen en valor en dólares. Esto significa que tu cantidad rea La siguiente tabla proporciona una cantidad estimada de peticiones basada en los patrones típicos de uso de Go: -| | GLM-5.1 | GLM-5 | Kimi K2.5 | MiMo-V2-Pro | MiMo-V2-Omni | MiniMax M2.7 | MiniMax M2.5 | Qwen3.6 Plus | Qwen3.5 Plus | -| ---------------------- | ------- | ----- | --------- | ----------- | ------------ | ------------ | ------------ | ------------ | ------------ | -| peticiones por 5 horas | 880 | 1,150 | 1,850 | 1,290 | 2,150 | 14,000 | 20,000 | 3,300 | 10,200 | -| peticiones por semana | 2,150 | 2,880 | 4,630 | 3,225 | 5,450 | 35,000 | 50,000 | 8,200 | 25,200 | -| peticiones por mes | 4,300 | 5,750 | 9,250 | 6,450 | 10,900 | 70,000 | 100,000 | 20,500 | 50,500 | +| Model | peticiones por 5 horas | peticiones por semana | peticiones por mes | +| ------------ | ---------------------- | --------------------- | ------------------ | +| GLM-5.1 | 880 | 2,150 | 4,300 | +| GLM-5 | 1,150 | 2,880 | 5,750 | +| Kimi K2.5 | 1,850 | 4,630 | 9,250 | +| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | +| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | +| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | +| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | +| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | Las estimaciones se basan en los patrones de peticiones promedio observados: diff --git a/packages/web/src/content/docs/fr/go.mdx b/packages/web/src/content/docs/fr/go.mdx index b5e3b76576..b10cf9141e 100644 --- a/packages/web/src/content/docs/fr/go.mdx +++ b/packages/web/src/content/docs/fr/go.mdx @@ -79,11 +79,17 @@ Les limites sont définies en valeur monétaire (dollars). Cela signifie que vot Le tableau ci-dessous fournit une estimation du nombre de requêtes basée sur des modèles d'utilisation typiques de Go : -| | GLM-5.1 | GLM-5 | Kimi K2.5 | MiMo-V2-Pro | MiMo-V2-Omni | MiniMax M2.7 | MiniMax M2.5 | Qwen3.6 Plus | Qwen3.5 Plus | -| --------------------- | ------- | ----- | --------- | ----------- | ------------ | ------------ | ------------ | ------------ | ------------ | -| requêtes par 5 heures | 880 | 1,150 | 1,850 | 1,290 | 2,150 | 14,000 | 20,000 | 3,300 | 10,200 | -| requêtes par semaine | 2,150 | 2,880 | 4,630 | 3,225 | 5,450 | 35,000 | 50,000 | 8,200 | 25,200 | -| requêtes par mois | 4,300 | 5,750 | 9,250 | 6,450 | 10,900 | 70,000 | 100,000 | 20,500 | 50,500 | +| Model | requêtes par 5 heures | requêtes par semaine | requêtes par mois | +| ------------ | --------------------- | -------------------- | ----------------- | +| GLM-5.1 | 880 | 2,150 | 4,300 | +| GLM-5 | 1,150 | 2,880 | 5,750 | +| Kimi K2.5 | 1,850 | 4,630 | 9,250 | +| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | +| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | +| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | +| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | +| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | Les estimations sont basées sur les modèles de requêtes moyens observés : diff --git a/packages/web/src/content/docs/it/go.mdx b/packages/web/src/content/docs/it/go.mdx index 92fc4b1e4d..f90dce5094 100644 --- a/packages/web/src/content/docs/it/go.mdx +++ b/packages/web/src/content/docs/it/go.mdx @@ -87,11 +87,17 @@ I limiti sono definiti in valore in dollari. Questo significa che il conteggio e La tabella seguente fornisce una stima del conteggio delle richieste in base a pattern di utilizzo tipici di Go: -| | GLM-5.1 | GLM-5 | Kimi K2.5 | MiMo-V2-Pro | MiMo-V2-Omni | MiniMax M2.7 | MiniMax M2.5 | Qwen3.6 Plus | Qwen3.5 Plus | -| --------------------- | ------- | ----- | --------- | ----------- | ------------ | ------------ | ------------ | ------------ | ------------ | -| richieste ogni 5 ore | 880 | 1.150 | 1.850 | 1.290 | 2.150 | 14.000 | 20.000 | 3,300 | 10,200 | -| richieste a settimana | 2.150 | 2.880 | 4.630 | 3.225 | 5.450 | 35.000 | 50.000 | 8,200 | 25,200 | -| richieste al mese | 4.300 | 5.750 | 9.250 | 6.450 | 10.900 | 70.000 | 100.000 | 20,500 | 50,500 | +| Model | richieste ogni 5 ore | richieste a settimana | richieste al mese | +| ------------ | -------------------- | --------------------- | ----------------- | +| GLM-5.1 | 880 | 2,150 | 4,300 | +| GLM-5 | 1,150 | 2,880 | 5,750 | +| Kimi K2.5 | 1,850 | 4,630 | 9,250 | +| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | +| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | +| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | +| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | +| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | Le stime si basano sui pattern medi di richieste osservati: diff --git a/packages/web/src/content/docs/ja/go.mdx b/packages/web/src/content/docs/ja/go.mdx index 25f5811ea4..01f1d6390b 100644 --- a/packages/web/src/content/docs/ja/go.mdx +++ b/packages/web/src/content/docs/ja/go.mdx @@ -79,11 +79,17 @@ OpenCode Goには以下の制限が含まれています: 以下の表は、一般的なGoの利用パターンに基づいた推定リクエスト数を示しています: -| | GLM-5.1 | GLM-5 | Kimi K2.5 | MiMo-V2-Pro | MiMo-V2-Omni | MiniMax M2.7 | MiniMax M2.5 | Qwen3.6 Plus | Qwen3.5 Plus | -| ------------------------- | ------- | ----- | --------- | ----------- | ------------ | ------------ | ------------ | ------------ | ------------ | -| 5時間あたりのリクエスト数 | 880 | 1,150 | 1,850 | 1,290 | 2,150 | 14,000 | 20,000 | 3,300 | 10,200 | -| 週間リクエスト数 | 2,150 | 2,880 | 4,630 | 3,225 | 5,450 | 35,000 | 50,000 | 8,200 | 25,200 | -| 月間リクエスト数 | 4,300 | 5,750 | 9,250 | 6,450 | 10,900 | 70,000 | 100,000 | 20,500 | 50,500 | +| Model | 5時間あたりのリクエスト数 | 週間リクエスト数 | 月間リクエスト数 | +| ------------ | ------------------------- | ---------------- | ---------------- | +| GLM-5.1 | 880 | 2,150 | 4,300 | +| GLM-5 | 1,150 | 2,880 | 5,750 | +| Kimi K2.5 | 1,850 | 4,630 | 9,250 | +| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | +| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | +| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | +| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | +| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | 推定値は、観測された平均的なリクエストパターンに基づいています: diff --git a/packages/web/src/content/docs/ko/go.mdx b/packages/web/src/content/docs/ko/go.mdx index 7fee280b00..9af7541e10 100644 --- a/packages/web/src/content/docs/ko/go.mdx +++ b/packages/web/src/content/docs/ko/go.mdx @@ -79,11 +79,17 @@ OpenCode Go에는 다음과 같은 한도가 포함됩니다. 아래 표는 일반적인 Go 사용 패턴을 기준으로 한 예상 요청 횟수를 보여줍니다. -| | GLM-5.1 | GLM-5 | Kimi K2.5 | MiMo-V2-Pro | MiMo-V2-Omni | MiniMax M2.7 | MiniMax M2.5 | Qwen3.6 Plus | Qwen3.5 Plus | -| ----------------- | ------- | ----- | --------- | ----------- | ------------ | ------------ | ------------ | ------------ | ------------ | -| 5시간당 요청 횟수 | 880 | 1,150 | 1,850 | 1,290 | 2,150 | 14,000 | 20,000 | 3,300 | 10,200 | -| 주간 요청 횟수 | 2,150 | 2,880 | 4,630 | 3,225 | 5,450 | 35,000 | 50,000 | 8,200 | 25,200 | -| 월간 요청 횟수 | 4,300 | 5,750 | 9,250 | 6,450 | 10,900 | 70,000 | 100,000 | 20,500 | 50,500 | +| Model | 5시간당 요청 횟수 | 주간 요청 횟수 | 월간 요청 횟수 | +| ------------ | ----------------- | -------------- | -------------- | +| GLM-5.1 | 880 | 2,150 | 4,300 | +| GLM-5 | 1,150 | 2,880 | 5,750 | +| Kimi K2.5 | 1,850 | 4,630 | 9,250 | +| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | +| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | +| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | +| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | +| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | 예상치는 관찰된 평균 요청 패턴을 기준으로 합니다. diff --git a/packages/web/src/content/docs/nb/go.mdx b/packages/web/src/content/docs/nb/go.mdx index 6b71f08ca9..1638e5e489 100644 --- a/packages/web/src/content/docs/nb/go.mdx +++ b/packages/web/src/content/docs/nb/go.mdx @@ -89,11 +89,17 @@ Grensene er definert i dollarverdi. Dette betyr at ditt faktiske antall forespø Tabellen nedenfor gir et estimert antall forespørsler basert på typiske bruksmønstre for Go: -| | GLM-5.1 | GLM-5 | Kimi K2.5 | MiMo-V2-Pro | MiMo-V2-Omni | MiniMax M2.7 | MiniMax M2.5 | Qwen3.6 Plus | Qwen3.5 Plus | -| ------------------------ | ------- | ----- | --------- | ----------- | ------------ | ------------ | ------------ | ------------ | ------------ | -| forespørsler per 5 timer | 880 | 1 150 | 1 850 | 1 290 | 2 150 | 14 000 | 20 000 | 3,300 | 10,200 | -| forespørsler per uke | 2 150 | 2 880 | 4 630 | 3 225 | 5 450 | 35 000 | 50 000 | 8,200 | 25,200 | -| forespørsler per måned | 4 300 | 5 750 | 9 250 | 6 450 | 10 900 | 70 000 | 100 000 | 20,500 | 50,500 | +| Model | forespørsler per 5 timer | forespørsler per uke | forespørsler per måned | +| ------------ | ------------------------ | -------------------- | ---------------------- | +| GLM-5.1 | 880 | 2,150 | 4,300 | +| GLM-5 | 1,150 | 2,880 | 5,750 | +| Kimi K2.5 | 1,850 | 4,630 | 9,250 | +| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | +| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | +| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | +| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | +| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | Estimatene er basert på observerte gjennomsnittlige forespørselsmønstre: diff --git a/packages/web/src/content/docs/pl/go.mdx b/packages/web/src/content/docs/pl/go.mdx index 8b7520aed1..c5f17672ae 100644 --- a/packages/web/src/content/docs/pl/go.mdx +++ b/packages/web/src/content/docs/pl/go.mdx @@ -83,11 +83,17 @@ Limity są zdefiniowane w wartości w dolarach. Oznacza to, że rzeczywista licz Poniższa tabela przedstawia szacunkową liczbę żądań na podstawie typowych wzorców korzystania z Go: -| | GLM-5.1 | GLM-5 | Kimi K2.5 | MiMo-V2-Pro | MiMo-V2-Omni | MiniMax M2.7 | MiniMax M2.5 | Qwen3.6 Plus | Qwen3.5 Plus | -| ------------------- | ------- | ----- | --------- | ----------- | ------------ | ------------ | ------------ | ------------ | ------------ | -| żądania na 5 godzin | 880 | 1,150 | 1,850 | 1,290 | 2,150 | 14,000 | 20,000 | 3,300 | 10,200 | -| żądania na tydzień | 2,150 | 2,880 | 4,630 | 3,225 | 5,450 | 35,000 | 50,000 | 8,200 | 25,200 | -| żądania na miesiąc | 4,300 | 5,750 | 9,250 | 6,450 | 10,900 | 70,000 | 100,000 | 20,500 | 50,500 | +| Model | żądania na 5 godzin | żądania na tydzień | żądania na miesiąc | +| ------------ | ------------------- | ------------------ | ------------------ | +| GLM-5.1 | 880 | 2,150 | 4,300 | +| GLM-5 | 1,150 | 2,880 | 5,750 | +| Kimi K2.5 | 1,850 | 4,630 | 9,250 | +| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | +| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | +| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | +| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | +| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | Szacunki opierają się na zaobserwowanych średnich wzorcach żądań: diff --git a/packages/web/src/content/docs/pt-br/go.mdx b/packages/web/src/content/docs/pt-br/go.mdx index 840254c5cb..48afe36d90 100644 --- a/packages/web/src/content/docs/pt-br/go.mdx +++ b/packages/web/src/content/docs/pt-br/go.mdx @@ -89,11 +89,17 @@ Os limites são definidos em valor em dólares. Isso significa que a sua contage A tabela abaixo fornece uma contagem estimada de requisições com base nos padrões típicos de uso do Go: -| | GLM-5.1 | GLM-5 | Kimi K2.5 | MiMo-V2-Pro | MiMo-V2-Omni | MiniMax M2.7 | MiniMax M2.5 | Qwen3.6 Plus | Qwen3.5 Plus | -| ----------------------- | ------- | ----- | --------- | ----------- | ------------ | ------------ | ------------ | ------------ | ------------ | -| requisições por 5 horas | 880 | 1.150 | 1.850 | 1.290 | 2.150 | 14.000 | 20.000 | 3,300 | 10,200 | -| requisições por semana | 2.150 | 2.880 | 4.630 | 3.225 | 5.450 | 35.000 | 50.000 | 8,200 | 25,200 | -| requisições por mês | 4.300 | 5.750 | 9.250 | 6.450 | 10.900 | 70.000 | 100.000 | 20,500 | 50,500 | +| Model | requisições por 5 horas | requisições por semana | requisições por mês | +| ------------ | ----------------------- | ---------------------- | ------------------- | +| GLM-5.1 | 880 | 2,150 | 4,300 | +| GLM-5 | 1,150 | 2,880 | 5,750 | +| Kimi K2.5 | 1,850 | 4,630 | 9,250 | +| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | +| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | +| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | +| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | +| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | As estimativas baseiam-se nos padrões médios de requisições observados: diff --git a/packages/web/src/content/docs/ru/go.mdx b/packages/web/src/content/docs/ru/go.mdx index fae41dc262..dacaf65273 100644 --- a/packages/web/src/content/docs/ru/go.mdx +++ b/packages/web/src/content/docs/ru/go.mdx @@ -89,11 +89,17 @@ OpenCode Go включает следующие лимиты: В таблице ниже приведено примерное количество запросов на основе типичных сценариев использования Go: -| | GLM-5.1 | GLM-5 | Kimi K2.5 | MiMo-V2-Pro | MiMo-V2-Omni | MiniMax M2.7 | MiniMax M2.5 | Qwen3.6 Plus | Qwen3.5 Plus | -| ------------------- | ------- | ----- | --------- | ----------- | ------------ | ------------ | ------------ | ------------ | ------------ | -| запросов за 5 часов | 880 | 1,150 | 1,850 | 1,290 | 2,150 | 14,000 | 20,000 | 3,300 | 10,200 | -| запросов в неделю | 2,150 | 2,880 | 4,630 | 3,225 | 5,450 | 35,000 | 50,000 | 8,200 | 25,200 | -| запросов в месяц | 4,300 | 5,750 | 9,250 | 6,450 | 10,900 | 70,000 | 100,000 | 20,500 | 50,500 | +| Model | запросов за 5 часов | запросов в неделю | запросов в месяц | +| ------------ | ------------------- | ----------------- | ---------------- | +| GLM-5.1 | 880 | 2,150 | 4,300 | +| GLM-5 | 1,150 | 2,880 | 5,750 | +| Kimi K2.5 | 1,850 | 4,630 | 9,250 | +| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | +| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | +| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | +| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | +| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | Оценки основаны на наблюдаемых средних показателях запросов: diff --git a/packages/web/src/content/docs/th/go.mdx b/packages/web/src/content/docs/th/go.mdx index 94fae4883d..aa26f7dcf1 100644 --- a/packages/web/src/content/docs/th/go.mdx +++ b/packages/web/src/content/docs/th/go.mdx @@ -79,11 +79,17 @@ OpenCode Go มีขีดจำกัดดังต่อไปนี้: ตารางด้านล่างแสดงจำนวน request โดยประมาณตามรูปแบบการใช้งานปกติของ Go: -| | GLM-5.1 | GLM-5 | Kimi K2.5 | MiMo-V2-Pro | MiMo-V2-Omni | MiniMax M2.7 | MiniMax M2.5 | Qwen3.6 Plus | Qwen3.5 Plus | -| ---------------------- | ------- | ----- | --------- | ----------- | ------------ | ------------ | ------------ | ------------ | ------------ | -| requests ต่อ 5 ชั่วโมง | 880 | 1,150 | 1,850 | 1,290 | 2,150 | 14,000 | 20,000 | 3,300 | 10,200 | -| requests ต่อสัปดาห์ | 2,150 | 2,880 | 4,630 | 3,225 | 5,450 | 35,000 | 50,000 | 8,200 | 25,200 | -| requests ต่อเดือน | 4,300 | 5,750 | 9,250 | 6,450 | 10,900 | 70,000 | 100,000 | 20,500 | 50,500 | +| Model | requests ต่อ 5 ชั่วโมง | requests ต่อสัปดาห์ | requests ต่อเดือน | +| ------------ | ---------------------- | ------------------- | ----------------- | +| GLM-5.1 | 880 | 2,150 | 4,300 | +| GLM-5 | 1,150 | 2,880 | 5,750 | +| Kimi K2.5 | 1,850 | 4,630 | 9,250 | +| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | +| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | +| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | +| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | +| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | การประมาณการอ้างอิงจากรูปแบบการใช้งาน request โดยเฉลี่ยที่สังเกตพบ: diff --git a/packages/web/src/content/docs/tr/go.mdx b/packages/web/src/content/docs/tr/go.mdx index 31d72f28d5..b085f6d060 100644 --- a/packages/web/src/content/docs/tr/go.mdx +++ b/packages/web/src/content/docs/tr/go.mdx @@ -79,11 +79,17 @@ Limitler dolar değeri üzerinden belirlenmiştir. Bu, gerçek istek sayınızı Aşağıdaki tablo, tipik Go kullanım modellerine dayalı tahmini bir istek sayısı sunmaktadır: -| | GLM-5.1 | GLM-5 | Kimi K2.5 | MiMo-V2-Pro | MiMo-V2-Omni | MiniMax M2.7 | MiniMax M2.5 | Qwen3.6 Plus | Qwen3.5 Plus | -| ------------------ | ------- | ----- | --------- | ----------- | ------------ | ------------ | ------------ | ------------ | ------------ | -| 5 saatte bir istek | 880 | 1.150 | 1.850 | 1.290 | 2.150 | 14.000 | 20.000 | 3,300 | 10,200 | -| haftalık istek | 2.150 | 2.880 | 4.630 | 3.225 | 5.450 | 35.000 | 50.000 | 8,200 | 25,200 | -| aylık istek | 4.300 | 5.750 | 9.250 | 6.450 | 10.900 | 70.000 | 100.000 | 20,500 | 50,500 | +| Model | 5 saatte bir istek | haftalık istek | aylık istek | +| ------------ | ------------------ | -------------- | ----------- | +| GLM-5.1 | 880 | 2,150 | 4,300 | +| GLM-5 | 1,150 | 2,880 | 5,750 | +| Kimi K2.5 | 1,850 | 4,630 | 9,250 | +| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | +| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | +| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | +| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | +| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | Tahminler, gözlemlenen ortalama istek modellerine dayanmaktadır: diff --git a/packages/web/src/content/docs/zh-cn/go.mdx b/packages/web/src/content/docs/zh-cn/go.mdx index 3bf1bc08db..df74f35ec1 100644 --- a/packages/web/src/content/docs/zh-cn/go.mdx +++ b/packages/web/src/content/docs/zh-cn/go.mdx @@ -79,11 +79,17 @@ OpenCode Go 包含以下限制: 下表提供了基于典型 Go 使用模式的预估请求数: -| | GLM-5.1 | GLM-5 | Kimi K2.5 | MiMo-V2-Pro | MiMo-V2-Omni | MiniMax M2.7 | MiniMax M2.5 | Qwen3.6 Plus | Qwen3.5 Plus | -| --------------- | ------- | ----- | --------- | ----------- | ------------ | ------------ | ------------ | ------------ | ------------ | -| 每 5 小时请求数 | 880 | 1,150 | 1,850 | 1,290 | 2,150 | 14,000 | 20,000 | 3,300 | 10,200 | -| 每周请求数 | 2,150 | 2,880 | 4,630 | 3,225 | 5,450 | 35,000 | 50,000 | 8,200 | 25,200 | -| 每月请求数 | 4,300 | 5,750 | 9,250 | 6,450 | 10,900 | 70,000 | 100,000 | 20,500 | 50,500 | +| Model | 每 5 小时请求数 | 每周请求数 | 每月请求数 | +| ------------ | --------------- | ---------- | ---------- | +| GLM-5.1 | 880 | 2,150 | 4,300 | +| GLM-5 | 1,150 | 2,880 | 5,750 | +| Kimi K2.5 | 1,850 | 4,630 | 9,250 | +| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | +| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | +| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | +| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | +| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | 预估值基于观察到的平均请求模式: diff --git a/packages/web/src/content/docs/zh-tw/go.mdx b/packages/web/src/content/docs/zh-tw/go.mdx index ad34192c60..9c12710a07 100644 --- a/packages/web/src/content/docs/zh-tw/go.mdx +++ b/packages/web/src/content/docs/zh-tw/go.mdx @@ -79,11 +79,17 @@ OpenCode Go 包含以下限制: 下表提供了基於典型 Go 使用模式的預估請求次數: -| | GLM-5.1 | GLM-5 | Kimi K2.5 | MiMo-V2-Pro | MiMo-V2-Omni | MiniMax M2.7 | MiniMax M2.5 | Qwen3.6 Plus | Qwen3.5 Plus | -| --------------- | ------- | ----- | --------- | ----------- | ------------ | ------------ | ------------ | ------------ | ------------ | -| 每 5 小時請求數 | 880 | 1,150 | 1,850 | 1,290 | 2,150 | 14,000 | 20,000 | 3,300 | 10,200 | -| 每週請求數 | 2,150 | 2,880 | 4,630 | 3,225 | 5,450 | 35,000 | 50,000 | 8,200 | 25,200 | -| 每月請求數 | 4,300 | 5,750 | 9,250 | 6,450 | 10,900 | 70,000 | 100,000 | 20,500 | 50,500 | +| Model | 每 5 小時請求數 | 每週請求數 | 每月請求數 | +| ------------ | --------------- | ---------- | ---------- | +| GLM-5.1 | 880 | 2,150 | 4,300 | +| GLM-5 | 1,150 | 2,880 | 5,750 | +| Kimi K2.5 | 1,850 | 4,630 | 9,250 | +| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | +| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | +| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | +| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | +| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | 預估值是基於觀察到的平均請求模式: From 47af00b2452ef7374cdda8769910799938d1303c Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 15 Apr 2026 09:19:26 -0400 Subject: [PATCH 137/300] zen: better error --- .../app/src/routes/zen/util/handler.ts | 25 ++++++++++--------- packages/console/core/src/model.ts | 1 + 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/console/app/src/routes/zen/util/handler.ts b/packages/console/app/src/routes/zen/util/handler.ts index 46d8435225..58df618094 100644 --- a/packages/console/app/src/routes/zen/util/handler.ts +++ b/packages/console/app/src/routes/zen/util/handler.ts @@ -222,22 +222,23 @@ export async function handler( logger.debug("STATUS: " + res.status + " " + res.statusText) // Handle non-streaming response - if (!isStream) { + if (!isStream || res.status === 429) { const json = await res.json() - const usageInfo = providerInfo.normalizeUsage(json.usage) - const costInfo = calculateCost(modelInfo, usageInfo) - await trialLimiter?.track(usageInfo) await rateLimiter?.track() - await trackUsage(sessionId, billingSource, authInfo, modelInfo, providerInfo, usageInfo, costInfo) - await reload(billingSource, authInfo, costInfo) + if (json.usage) { + const usageInfo = providerInfo.normalizeUsage(json.usage) + const costInfo = calculateCost(modelInfo, usageInfo) + await trialLimiter?.track(usageInfo) + await trackUsage(sessionId, billingSource, authInfo, modelInfo, providerInfo, usageInfo, costInfo) + await reload(billingSource, authInfo, costInfo) + json.cost = calculateOccurredCost(billingSource, costInfo) + } + if (json.error?.message) { + json.error.message = `Error from provider${providerInfo.displayName ? ` (${providerInfo.displayName})` : ""}: ${json.error.message}` + } const responseConverter = createResponseConverter(providerInfo.format, opts.format) - const body = JSON.stringify( - responseConverter({ - ...json, - cost: calculateOccurredCost(billingSource, costInfo), - }), - ) + const body = JSON.stringify(responseConverter(json)) logger.metric({ response_length: body.length }) logger.debug("RESPONSE: " + body) dataDumper?.provideResponse(body) diff --git a/packages/console/core/src/model.ts b/packages/console/core/src/model.ts index b4149373fe..3d614d3034 100644 --- a/packages/console/core/src/model.ts +++ b/packages/console/core/src/model.ts @@ -44,6 +44,7 @@ export namespace ZenData { }) const ProviderSchema = z.object({ + displayName: z.string().optional(), api: z.string(), apiKey: z.union([z.string(), z.record(z.string(), z.string())]), format: FormatSchema.optional(), From af20191d1cd60a7f4a421ad81eca5053f7deace1 Mon Sep 17 00:00:00 2001 From: James Long Date: Wed, 15 Apr 2026 10:18:48 -0400 Subject: [PATCH 138/300] feat(core): sync routes, refactor proxy, session restore, and more syncing (#22518) --- .../opencode/src/control-plane/workspace.ts | 260 ++++++++++++-- .../opencode/src/server/instance/index.ts | 2 + .../src/server/instance/middleware.ts | 66 ++-- packages/opencode/src/server/instance/sync.ts | 118 +++++++ .../opencode/src/server/instance/workspace.ts | 71 +++- packages/opencode/src/server/middleware.ts | 2 +- packages/opencode/src/server/proxy.ts | 55 ++- packages/opencode/src/sync/index.ts | 19 ++ packages/sdk/js/src/v2/gen/sdk.gen.ts | 157 +++++++++ packages/sdk/js/src/v2/gen/types.gen.ts | 127 +++++++ packages/sdk/openapi.json | 320 ++++++++++++++++++ 11 files changed, 1133 insertions(+), 64 deletions(-) create mode 100644 packages/opencode/src/server/instance/sync.ts diff --git a/packages/opencode/src/control-plane/workspace.ts b/packages/opencode/src/control-plane/workspace.ts index f330e07b7a..78f3d770eb 100644 --- a/packages/opencode/src/control-plane/workspace.ts +++ b/packages/opencode/src/control-plane/workspace.ts @@ -1,11 +1,13 @@ import z from "zod" import { setTimeout as sleep } from "node:timers/promises" import { fn } from "@/util/fn" -import { Database, eq } from "@/storage/db" +import { Database, asc, eq } from "@/storage/db" import { Project } from "@/project/project" import { BusEvent } from "@/bus/bus-event" import { GlobalBus } from "@/bus/global" import { SyncEvent } from "@/sync" +import { EventTable } from "@/sync/event.sql" +import { Flag } from "@/flag/flag" import { Log } from "@/util/log" import { Filesystem } from "@/util/filesystem" import { ProjectID } from "@/project/schema" @@ -15,6 +17,11 @@ import { getAdaptor } from "./adaptors" import { WorkspaceInfo } from "./types" import { WorkspaceID } from "./schema" import { parseSSE } from "./sse" +import { Session } from "@/session" +import { SessionTable } from "@/session/session.sql" +import { SessionID } from "@/session/schema" +import { errorData } from "@/util/error" +import { AppRuntime } from "@/effect/app-runtime" export namespace Workspace { export const Info = WorkspaceInfo.meta({ @@ -29,6 +36,13 @@ export namespace Workspace { }) export type ConnectionStatus = z.infer + const Restore = z.object({ + workspaceID: WorkspaceID.zod, + sessionID: SessionID.zod, + total: z.number().int().min(0), + step: z.number().int().min(0), + }) + export const Event = { Ready: BusEvent.define( "workspace.ready", @@ -42,6 +56,7 @@ export namespace Workspace { message: z.string(), }), ), + Restore: BusEvent.define("workspace.restore", Restore), Status: BusEvent.define("workspace.status", ConnectionStatus), } @@ -102,11 +117,170 @@ export namespace Workspace { return info }) + const SessionRestoreInput = z.object({ + workspaceID: WorkspaceID.zod, + sessionID: SessionID.zod, + }) + + export const sessionRestore = fn(SessionRestoreInput, async (input) => { + log.info("session restore requested", { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + }) + try { + const space = await get(input.workspaceID) + if (!space) throw new Error(`Workspace not found: ${input.workspaceID}`) + + const adaptor = await getAdaptor(space.projectID, space.type) + const target = await adaptor.target(space) + + // Need to switch the workspace of the session + SyncEvent.run(Session.Event.Updated, { + sessionID: input.sessionID, + info: { + workspaceID: input.workspaceID, + }, + }) + + const rows = Database.use((db) => + db + .select({ + id: EventTable.id, + aggregateID: EventTable.aggregate_id, + seq: EventTable.seq, + type: EventTable.type, + data: EventTable.data, + }) + .from(EventTable) + .where(eq(EventTable.aggregate_id, input.sessionID)) + .orderBy(asc(EventTable.seq)) + .all(), + ) + if (rows.length === 0) throw new Error(`No events found for session: ${input.sessionID}`) + + const all = rows + + const size = 10 + const sets = Array.from({ length: Math.ceil(all.length / size) }, (_, i) => all.slice(i * size, (i + 1) * size)) + const total = sets.length + log.info("session restore prepared", { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + workspaceType: space.type, + directory: space.directory, + target: target.type === "remote" ? String(route(target.url, "/sync/replay")) : target.directory, + events: all.length, + batches: total, + first: all[0]?.seq, + last: all.at(-1)?.seq, + }) + GlobalBus.emit("event", { + directory: "global", + workspace: input.workspaceID, + payload: { + type: Event.Restore.type, + properties: { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + total, + step: 0, + }, + }, + }) + for (const [i, events] of sets.entries()) { + log.info("session restore batch starting", { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + step: i + 1, + total, + events: events.length, + first: events[0]?.seq, + last: events.at(-1)?.seq, + target: target.type === "remote" ? String(route(target.url, "/sync/replay")) : target.directory, + }) + if (target.type === "local") { + SyncEvent.replayAll(events) + log.info("session restore batch replayed locally", { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + step: i + 1, + total, + events: events.length, + }) + } else { + const url = route(target.url, "/sync/replay") + const headers = new Headers(target.headers) + headers.set("content-type", "application/json") + const res = await fetch(url, { + method: "POST", + headers, + body: JSON.stringify({ + directory: space.directory ?? "", + events, + }), + }) + if (!res.ok) { + const body = await res.text() + log.error("session restore batch failed", { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + step: i + 1, + total, + status: res.status, + body, + }) + throw new Error( + `Failed to replay session ${input.sessionID} into workspace ${input.workspaceID}: HTTP ${res.status} ${body}`, + ) + } + log.info("session restore batch posted", { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + step: i + 1, + total, + status: res.status, + }) + } + GlobalBus.emit("event", { + directory: "global", + workspace: input.workspaceID, + payload: { + type: Event.Restore.type, + properties: { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + total, + step: i + 1, + }, + }, + }) + } + + log.info("session restore complete", { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + batches: total, + }) + + return { + total, + } + } catch (err) { + log.error("session restore failed", { + workspaceID: input.workspaceID, + sessionID: input.sessionID, + error: errorData(err), + }) + throw err + } + }) + export function list(project: Project.Info) { const rows = Database.use((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.project_id, project.id)).all(), ) const spaces = rows.map(fromRow).sort((a, b) => a.id.localeCompare(b.id)) + for (const space of spaces) startSync(space) return spaces } @@ -120,13 +294,25 @@ export namespace Workspace { }) export const remove = fn(WorkspaceID.zod, async (id) => { + const sessions = Database.use((db) => + db.select({ id: SessionTable.id }).from(SessionTable).where(eq(SessionTable.workspace_id, id)).all(), + ) + for (const session of sessions) { + await AppRuntime.runPromise(Session.Service.use((svc) => svc.remove(session.id))) + } + const row = Database.use((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, id)).get()) + if (row) { stopSync(id) const info = fromRow(row) - const adaptor = await getAdaptor(info.projectID, row.type) - adaptor.remove(info) + try { + const adaptor = await getAdaptor(info.projectID, row.type) + await adaptor.remove(info) + } catch (err) { + log.error("adaptor not available when removing workspace", { type: row.type }) + } Database.use((db) => db.delete(WorkspaceTable).where(eq(WorkspaceTable.id, id)).run()) return info } @@ -156,51 +342,81 @@ export namespace Workspace { const log = Log.create({ service: "workspace-sync" }) - async function workspaceEventLoop(space: Info, signal: AbortSignal) { - log.info("starting sync: " + space.id) + function route(url: string | URL, path: string) { + const next = new URL(url) + next.pathname = `${next.pathname.replace(/\/$/, "")}${path}` + next.search = "" + next.hash = "" + return next + } + async function syncWorkspace(space: Info, signal: AbortSignal) { while (!signal.aborted) { - log.info("connecting to sync: " + space.id) + log.info("connecting to global sync", { workspace: space.name }) - setStatus(space.id, "connecting") const adaptor = await getAdaptor(space.projectID, space.type) const target = await adaptor.target(space) if (target.type === "local") return - const res = await fetch(target.url + "/sync/event", { method: "GET", signal }).catch((err: unknown) => { - setStatus(space.id, "error", String(err)) + const res = await fetch(route(target.url, "/global/event"), { + method: "GET", + headers: target.headers, + signal, + }).catch((err: unknown) => { + setStatus(space.id, "error") + + log.info("failed to connect to global sync", { + workspace: space.name, + error: err, + }) return undefined }) - if (!res || !res.ok || !res.body) { - log.info("failed to connect to sync: " + res?.status) - setStatus(space.id, "error", res ? `HTTP ${res.status}` : "no response") + if (!res || !res.ok || !res.body) { + log.info("failed to connect to global sync", { workspace: space.name }) + setStatus(space.id, "error") await sleep(1000) continue } - setStatus(space.id, "connected") - await parseSSE(res.body, signal, (evt) => { - const event = evt as SyncEvent.SerializedEvent + log.info("global sync connected", { workspace: space.name }) + setStatus(space.id, "connected") + + await parseSSE(res.body, signal, (evt: any) => { try { - if (!event.type.startsWith("server.")) { - SyncEvent.replay(event) + if (!("payload" in evt)) return + + if (evt.payload.type === "sync") { + // This name -> type is temporary + SyncEvent.replay({ ...evt.payload, type: evt.payload.name } as SyncEvent.SerializedEvent) } + + GlobalBus.emit("event", { + directory: evt.directory, + project: evt.project, + workspace: space.id, + payload: evt.payload, + }) } catch (err) { - log.warn("failed to replay sync event", { + log.info("failed to replay global event", { workspaceID: space.id, error: err, }) } }) + + log.info("disconnected from global sync: " + space.id) setStatus(space.id, "disconnected") - log.info("disconnected to sync: " + space.id) - await sleep(250) + + // TODO: Implement exponential backoff + await sleep(1000) } } function startSync(space: Info) { + if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) return + if (space.type === "worktree") { void Filesystem.exists(space.directory!).then((exists) => { setStatus(space.id, exists ? "connected" : "error", exists ? undefined : "directory does not exist") @@ -213,9 +429,9 @@ export namespace Workspace { aborts.set(space.id, abort) setStatus(space.id, "disconnected") - void workspaceEventLoop(space, abort.signal).catch((error) => { + void syncWorkspace(space, abort.signal).catch((error) => { setStatus(space.id, "error", String(error)) - log.warn("workspace sync listener failed", { + log.warn("workspace listener failed", { workspaceID: space.id, error, }) diff --git a/packages/opencode/src/server/instance/index.ts b/packages/opencode/src/server/instance/index.ts index 86a18dc673..4a03b7b29c 100644 --- a/packages/opencode/src/server/instance/index.ts +++ b/packages/opencode/src/server/instance/index.ts @@ -23,6 +23,7 @@ import { ConfigRoutes } from "./config" import { ExperimentalRoutes } from "./experimental" import { ProviderRoutes } from "./provider" import { EventRoutes } from "./event" +import { SyncRoutes } from "./sync" import { WorkspaceRouterMiddleware } from "./middleware" import { AppRuntime } from "@/effect/app-runtime" @@ -37,6 +38,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => .route("/permission", PermissionRoutes()) .route("/question", QuestionRoutes()) .route("/provider", ProviderRoutes()) + .route("/sync", SyncRoutes()) .route("/", FileRoutes()) .route("/", EventRoutes()) .route("/mcp", McpRoutes()) diff --git a/packages/opencode/src/server/instance/middleware.ts b/packages/opencode/src/server/instance/middleware.ts index 9155ad451b..824c265efe 100644 --- a/packages/opencode/src/server/instance/middleware.ts +++ b/packages/opencode/src/server/instance/middleware.ts @@ -11,9 +11,12 @@ import { Session } from "@/session" import { SessionID } from "@/session/schema" import { WorkspaceContext } from "@/control-plane/workspace-context" import { AppRuntime } from "@/effect/app-runtime" +import { Log } from "@/util/log" type Rule = { method?: string; path: string; exact?: boolean; action: "local" | "forward" } +const OPENCODE_WORKSPACE = process.env.OPENCODE_WORKSPACE + const RULES: Array = [ { path: "/session/status", action: "forward" }, { method: "GET", path: "/session", action: "local" }, @@ -46,6 +49,8 @@ async function getSessionWorkspace(url: URL) { } export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): MiddlewareHandler { + const log = Log.create({ service: "workspace-router" }) + return async (c, next) => { const raw = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd() const directory = Filesystem.resolve( @@ -63,8 +68,22 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware const sessionWorkspaceID = await getSessionWorkspace(url) const workspaceID = sessionWorkspaceID || url.searchParams.get("workspace") - // If no workspace is provided we use the project - if (!workspaceID) { + if (!workspaceID || url.pathname.startsWith("/console") || OPENCODE_WORKSPACE) { + if (OPENCODE_WORKSPACE) { + return WorkspaceContext.provide({ + workspaceID: WorkspaceID.make(OPENCODE_WORKSPACE), + async fn() { + return Instance.provide({ + directory, + init: () => AppRuntime.runPromise(InstanceBootstrap), + async fn() { + return next() + }, + }) + }, + }) + } + return Instance.provide({ directory, init: () => AppRuntime.runPromise(InstanceBootstrap), @@ -77,16 +96,6 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware const workspace = await Workspace.get(WorkspaceID.make(workspaceID)) if (!workspace) { - // Special-case deleting a session in case user's data in a - // weird state. Allow them to forcefully delete a synced session - // even if the remote workspace is not in their data. - // - // The lets the `DELETE /session/:id` endpoint through and we've - // made sure that it will run without an instance - if (url.pathname.match(/\/session\/[^/]+$/) && c.req.method === "DELETE") { - return next() - } - return new Response(`Workspace not found: ${workspaceID}`, { status: 500, headers: { @@ -95,6 +104,12 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware }) } + if (local(c.req.method, url.pathname)) { + // No instance provided because we are serving cached data; there + // is no instance to work with + return next() + } + const adaptor = await getAdaptor(workspace.projectID, workspace.type) const target = await adaptor.target(workspace) @@ -112,24 +127,27 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware }) } - if (local(c.req.method, url.pathname)) { - // No instance provided because we are serving cached data; there - // is no instance to work with - return next() - } + const proxyURL = new URL(target.url) + proxyURL.pathname = `${proxyURL.pathname.replace(/\/$/, "")}${url.pathname}` + proxyURL.search = url.search + proxyURL.hash = url.hash + proxyURL.searchParams.delete("workspace") + + log.info("workspace proxy forwarding", { + workspaceID, + request: url.toString(), + target: String(target.url), + proxy: proxyURL.toString(), + }) if (c.req.header("upgrade")?.toLowerCase() === "websocket") { - return ServerProxy.websocket(upgrade, target, c.req.raw, c.env) + return ServerProxy.websocket(upgrade, proxyURL, target.headers, c.req.raw, c.env) } const headers = new Headers(c.req.raw.headers) headers.delete("x-opencode-workspace") - return ServerProxy.http( - target, - new Request(c.req.raw, { - headers, - }), - ) + const req = new Request(c.req.raw, { headers }) + return ServerProxy.http(proxyURL, target.headers, req) } } diff --git a/packages/opencode/src/server/instance/sync.ts b/packages/opencode/src/server/instance/sync.ts new file mode 100644 index 0000000000..c22969130a --- /dev/null +++ b/packages/opencode/src/server/instance/sync.ts @@ -0,0 +1,118 @@ +import z from "zod" +import { Hono } from "hono" +import { describeRoute, validator, resolver } from "hono-openapi" +import { SyncEvent } from "@/sync" +import { Database, asc, and, not, or, lte, eq } from "@/storage/db" +import { EventTable } from "@/sync/event.sql" +import { lazy } from "@/util/lazy" +import { Log } from "@/util/log" +import { errors } from "../error" + +const ReplayEvent = z.object({ + id: z.string(), + aggregateID: z.string(), + seq: z.number().int().min(0), + type: z.string(), + data: z.record(z.string(), z.unknown()), +}) + +const log = Log.create({ service: "server.sync" }) + +export const SyncRoutes = lazy(() => + new Hono() + .post( + "/replay", + describeRoute({ + summary: "Replay sync events", + description: "Validate and replay a complete sync event history.", + operationId: "sync.replay", + responses: { + 200: { + description: "Replayed sync events", + content: { + "application/json": { + schema: resolver( + z.object({ + sessionID: z.string(), + }), + ), + }, + }, + }, + ...errors(400), + }, + }), + validator( + "json", + z.object({ + directory: z.string(), + events: z.array(ReplayEvent).min(1), + }), + ), + async (c) => { + const body = c.req.valid("json") + const events = body.events + const source = events[0].aggregateID + log.info("sync replay requested", { + sessionID: source, + events: events.length, + first: events[0]?.seq, + last: events.at(-1)?.seq, + directory: body.directory, + }) + SyncEvent.replayAll(events) + + log.info("sync replay complete", { + sessionID: source, + events: events.length, + first: events[0]?.seq, + last: events.at(-1)?.seq, + }) + + return c.json({ + sessionID: source, + }) + }, + ) + .get( + "/history", + describeRoute({ + summary: "List sync events", + description: + "List sync events for all aggregates. Keys are aggregate IDs the client already knows about, values are the last known sequence ID. Events with seq > value are returned for those aggregates. Aggregates not listed in the input get their full history.", + operationId: "sync.history.list", + responses: { + 200: { + description: "Sync events", + content: { + "application/json": { + schema: resolver( + z.array( + z.object({ + id: z.string(), + aggregate_id: z.string(), + seq: z.number(), + type: z.string(), + data: z.record(z.string(), z.unknown()), + }), + ), + ), + }, + }, + }, + ...errors(400), + }, + }), + validator("json", z.record(z.string(), z.number().int().min(0))), + async (c) => { + const body = c.req.valid("json") + const exclude = Object.entries(body) + const where = + exclude.length > 0 + ? not(or(...exclude.map(([id, seq]) => and(eq(EventTable.aggregate_id, id), lte(EventTable.seq, seq))))!) + : undefined + const rows = Database.use((db) => db.select().from(EventTable).where(where).orderBy(asc(EventTable.seq)).all()) + return c.json(rows) + }, + ), +) diff --git a/packages/opencode/src/server/instance/workspace.ts b/packages/opencode/src/server/instance/workspace.ts index 7cee031975..a4ff4eda8d 100644 --- a/packages/opencode/src/server/instance/workspace.ts +++ b/packages/opencode/src/server/instance/workspace.ts @@ -6,12 +6,10 @@ import { Workspace } from "../../control-plane/workspace" import { Instance } from "../../project/instance" import { errors } from "../error" import { lazy } from "../../util/lazy" +import { Log } from "@/util/log" +import { errorData } from "@/util/error" -const WorkspaceAdaptor = z.object({ - type: z.string(), - name: z.string(), - description: z.string(), -}) +const log = Log.create({ service: "server.workspace" }) export const WorkspaceRoutes = lazy(() => new Hono() @@ -26,7 +24,15 @@ export const WorkspaceRoutes = lazy(() => description: "Workspace adaptors", content: { "application/json": { - schema: resolver(z.array(WorkspaceAdaptor)), + schema: resolver( + z.array( + z.object({ + type: z.string(), + name: z.string(), + description: z.string(), + }), + ), + ), }, }, }, @@ -140,5 +146,58 @@ export const WorkspaceRoutes = lazy(() => const { id } = c.req.valid("param") return c.json(await Workspace.remove(id)) }, + ) + .post( + "/:id/session-restore", + describeRoute({ + summary: "Restore session into workspace", + description: "Replay a session's sync events into the target workspace in batches.", + operationId: "experimental.workspace.sessionRestore", + responses: { + 200: { + description: "Session replay started", + content: { + "application/json": { + schema: resolver( + z.object({ + total: z.number().int().min(0), + }), + ), + }, + }, + }, + ...errors(400), + }, + }), + validator("param", z.object({ id: Workspace.Info.shape.id })), + validator("json", Workspace.sessionRestore.schema.omit({ workspaceID: true })), + async (c) => { + const { id } = c.req.valid("param") + const body = c.req.valid("json") + log.info("session restore route requested", { + workspaceID: id, + sessionID: body.sessionID, + directory: Instance.directory, + }) + try { + const result = await Workspace.sessionRestore({ + workspaceID: id, + ...body, + }) + log.info("session restore route complete", { + workspaceID: id, + sessionID: body.sessionID, + total: result.total, + }) + return c.json(result) + } catch (err) { + log.error("session restore route failed", { + workspaceID: id, + sessionID: body.sessionID, + error: errorData(err), + }) + throw err + } + }, ), ) diff --git a/packages/opencode/src/server/middleware.ts b/packages/opencode/src/server/middleware.ts index a51ba602b5..d0539eb247 100644 --- a/packages/opencode/src/server/middleware.ts +++ b/packages/opencode/src/server/middleware.ts @@ -86,7 +86,7 @@ const zipped = compress() export const CompressionMiddleware: MiddlewareHandler = (c, next) => { const path = c.req.path const method = c.req.method - if (path === "/event" || path === "/global/event" || path === "/global/sync-event") return next() + if (path === "/event" || path === "/global/event") return next() if (method === "POST" && /\/session\/[^/]+\/(message|prompt_async)$/.test(path)) return next() return zipped(c, next) } diff --git a/packages/opencode/src/server/proxy.ts b/packages/opencode/src/server/proxy.ts index c90a657dc2..0c0deba20c 100644 --- a/packages/opencode/src/server/proxy.ts +++ b/packages/opencode/src/server/proxy.ts @@ -1,6 +1,6 @@ -import type { Target } from "@/control-plane/types" import { Hono } from "hono" import type { UpgradeWebSocket } from "hono/ws" +import { Log } from "@/util/log" const hop = new Set([ "connection", @@ -20,6 +20,7 @@ type Msg = string | ArrayBuffer | Uint8Array function headers(req: Request, extra?: HeadersInit) { const out = new Headers(req.headers) for (const key of hop) out.delete(key) + out.delete("accept-encoding") out.delete("x-opencode-directory") out.delete("x-opencode-workspace") if (!extra) return out @@ -98,31 +99,63 @@ const app = (upgrade: UpgradeWebSocket) => ) export namespace ServerProxy { - export function http(target: Extract, req: Request) { + const log = Log.Default.clone().tag("service", "server-proxy") + + export function http(url: string | URL, extra: HeadersInit | undefined, req: Request) { + console.log("proxy http request", { + method: req.method, + request: req.url, + url: String(url), + }) return fetch( - new Request(target.url, { + new Request(url, { method: req.method, - headers: headers(req, target.headers), + headers: headers(req, extra), body: req.method === "GET" || req.method === "HEAD" ? undefined : req.body, redirect: "manual", signal: req.signal, }), - ) + ).then((res) => { + const next = new Headers(res.headers) + next.delete("content-encoding") + next.delete("content-length") + + console.log("proxy http response", { + method: req.method, + request: req.url, + url: String(url), + status: res.status, + statusText: res.statusText, + }) + return new Response(res.body, { + status: res.status, + statusText: res.statusText, + headers: next, + }) + }) } export function websocket( upgrade: UpgradeWebSocket, - target: Extract, + target: string | URL, + extra: HeadersInit | undefined, req: Request, env: unknown, ) { - const url = new URL(req.url) - url.pathname = "/__workspace_ws" - url.search = "" + const proxy = new URL(req.url) + proxy.pathname = "/__workspace_ws" + proxy.search = "" const next = new Headers(req.headers) - next.set("x-opencode-proxy-url", socket(target.url)) + next.set("x-opencode-proxy-url", socket(target)) + for (const [key, value] of new Headers(extra).entries()) { + next.set(key, value) + } + log.info("proxy websocket", { + request: req.url, + target: String(target), + }) return app(upgrade).fetch( - new Request(url, { + new Request(proxy, { method: req.method, headers: next, signal: req.signal, diff --git a/packages/opencode/src/sync/index.ts b/packages/opencode/src/sync/index.ts index d7cb7f774f..ce598dae67 100644 --- a/packages/opencode/src/sync/index.ts +++ b/packages/opencode/src/sync/index.ts @@ -199,6 +199,25 @@ export namespace SyncEvent { process(def, event, { publish: !!options?.publish }) } + export function replayAll(events: SerializedEvent[], options?: { publish: boolean }) { + const source = events[0]?.aggregateID + if (!source) return + if (events.some((item) => item.aggregateID !== source)) { + throw new Error("Replay events must belong to the same session") + } + const start = events[0].seq + for (const [i, item] of events.entries()) { + const seq = start + i + if (item.seq !== seq) { + throw new Error(`Replay sequence mismatch at index ${i}: expected ${seq}, got ${item.seq}`) + } + } + for (const item of events) { + replay(item, options) + } + return source + } + export function run(def: Def, data: Event["data"], options?: { publish?: boolean }) { const agg = (data as Record)[def.aggregate] // This should never happen: we've enforced it via typescript in diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index b5fc976bba..d7bf43f506 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -35,6 +35,8 @@ import type { ExperimentalWorkspaceListResponses, ExperimentalWorkspaceRemoveErrors, ExperimentalWorkspaceRemoveResponses, + ExperimentalWorkspaceSessionRestoreErrors, + ExperimentalWorkspaceSessionRestoreResponses, ExperimentalWorkspaceStatusResponses, FileListResponses, FilePartInput, @@ -157,6 +159,10 @@ import type { SessionUpdateErrors, SessionUpdateResponses, SubtaskPartInput, + SyncHistoryListErrors, + SyncHistoryListResponses, + SyncReplayErrors, + SyncReplayResponses, TextPartInput, ToolIdsErrors, ToolIdsResponses, @@ -1243,6 +1249,49 @@ export class Workspace extends HeyApiClient { }) } + /** + * Restore session into workspace + * + * Replay a session's sync events into the target workspace in batches. + */ + public sessionRestore( + parameters: { + id: string + directory?: string + workspace?: string + sessionID?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "id" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "sessionID" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post< + ExperimentalWorkspaceSessionRestoreResponses, + ExperimentalWorkspaceSessionRestoreErrors, + ThrowOnError + >({ + url: "/experimental/workspace/{id}/session-restore", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + private _adaptor?: Adaptor get adaptor(): Adaptor { return (this._adaptor ??= new Adaptor({ client: this.client })) @@ -2961,6 +3010,109 @@ export class Provider extends HeyApiClient { } } +export class History extends HeyApiClient { + /** + * List sync events + * + * List sync events for all aggregates. Keys are aggregate IDs the client already knows about, values are the last known sequence ID. Events with seq > value are returned for those aggregates. Aggregates not listed in the input get their full history. + */ + public list( + parameters?: { + directory?: string + workspace?: string + body?: { + [key: string]: number + } + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { key: "body", map: "body" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/sync/history", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + +export class Sync extends HeyApiClient { + /** + * Replay sync events + * + * Validate and replay a complete sync event history. + */ + public replay( + parameters?: { + query_directory?: string + workspace?: string + body_directory?: string + events?: Array<{ + id: string + aggregateID: string + seq: number + type: string + data: { + [key: string]: unknown + } + }> + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { + in: "query", + key: "query_directory", + map: "directory", + }, + { in: "query", key: "workspace" }, + { + in: "body", + key: "body_directory", + map: "directory", + }, + { in: "body", key: "events" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/sync/replay", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + private _history?: History + get history(): History { + return (this._history ??= new History({ client: this.client })) + } +} + export class Find extends HeyApiClient { /** * Find text @@ -4217,6 +4369,11 @@ export class OpencodeClient extends HeyApiClient { return (this._provider ??= new Provider({ client: this.client })) } + private _sync?: Sync + get sync(): Sync { + return (this._sync ??= new Sync({ client: this.client })) + } + private _find?: Find get find(): Find { return (this._find ??= new Find({ client: this.client })) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 8f4c16c5bd..24c1d53bf7 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -520,6 +520,16 @@ export type EventWorkspaceFailed = { } } +export type EventWorkspaceRestore = { + type: "workspace.restore" + properties: { + workspaceID: string + sessionID: string + total: number + step: number + } +} + export type EventWorkspaceStatus = { type: "workspace.status" properties: { @@ -1137,6 +1147,7 @@ export type GlobalEvent = { | EventPtyDeleted | EventWorkspaceReady | EventWorkspaceFailed + | EventWorkspaceRestore | EventWorkspaceStatus | EventMessageUpdated | EventMessageRemoved @@ -2049,6 +2060,7 @@ export type Event = | EventPtyDeleted | EventWorkspaceReady | EventWorkspaceFailed + | EventWorkspaceRestore | EventWorkspaceStatus | EventMessageUpdated | EventMessageRemoved @@ -3006,6 +3018,42 @@ export type ExperimentalWorkspaceRemoveResponses = { export type ExperimentalWorkspaceRemoveResponse = ExperimentalWorkspaceRemoveResponses[keyof ExperimentalWorkspaceRemoveResponses] +export type ExperimentalWorkspaceSessionRestoreData = { + body?: { + sessionID: string + } + path: { + id: string + } + query?: { + directory?: string + workspace?: string + } + url: "/experimental/workspace/{id}/session-restore" +} + +export type ExperimentalWorkspaceSessionRestoreErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ExperimentalWorkspaceSessionRestoreError = + ExperimentalWorkspaceSessionRestoreErrors[keyof ExperimentalWorkspaceSessionRestoreErrors] + +export type ExperimentalWorkspaceSessionRestoreResponses = { + /** + * Session replay started + */ + 200: { + total: number + } +} + +export type ExperimentalWorkspaceSessionRestoreResponse = + ExperimentalWorkspaceSessionRestoreResponses[keyof ExperimentalWorkspaceSessionRestoreResponses] + export type WorktreeRemoveData = { body?: WorktreeRemoveInput path?: never @@ -4456,6 +4504,85 @@ export type ProviderOauthCallbackResponses = { export type ProviderOauthCallbackResponse = ProviderOauthCallbackResponses[keyof ProviderOauthCallbackResponses] +export type SyncReplayData = { + body?: { + directory: string + events: Array<{ + id: string + aggregateID: string + seq: number + type: string + data: { + [key: string]: unknown + } + }> + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/sync/replay" +} + +export type SyncReplayErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type SyncReplayError = SyncReplayErrors[keyof SyncReplayErrors] + +export type SyncReplayResponses = { + /** + * Replayed sync events + */ + 200: { + sessionID: string + } +} + +export type SyncReplayResponse = SyncReplayResponses[keyof SyncReplayResponses] + +export type SyncHistoryListData = { + body?: { + [key: string]: number + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/sync/history" +} + +export type SyncHistoryListErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type SyncHistoryListError = SyncHistoryListErrors[keyof SyncHistoryListErrors] + +export type SyncHistoryListResponses = { + /** + * Sync events + */ + 200: Array<{ + id: string + aggregate_id: string + seq: number + type: string + data: { + [key: string]: unknown + } + }> +} + +export type SyncHistoryListResponse = SyncHistoryListResponses[keyof SyncHistoryListResponses] + export type FindTextData = { body?: never path?: never diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 6000e66042..ee3538d55f 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -1805,6 +1805,90 @@ ] } }, + "/experimental/workspace/{id}/session-restore": { + "post": { + "operationId": "experimental.workspace.sessionRestore", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "workspace", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "id", + "schema": { + "type": "string", + "pattern": "^wrk.*" + }, + "required": true + } + ], + "summary": "Restore session into workspace", + "description": "Replay a session's sync events into the target workspace in batches.", + "responses": { + "200": { + "description": "Session replay started", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + } + }, + "required": ["total"] + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "pattern": "^ses.*" + } + }, + "required": ["sessionID"] + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.sessionRestore({\n ...\n})" + } + ] + } + }, "/experimental/worktree": { "post": { "operationId": "worktree.create", @@ -5143,6 +5227,202 @@ ] } }, + "/sync/replay": { + "post": { + "operationId": "sync.replay", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "workspace", + "schema": { + "type": "string" + } + } + ], + "summary": "Replay sync events", + "description": "Validate and replay a complete sync event history.", + "responses": { + "200": { + "description": "Replayed sync events", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sessionID": { + "type": "string" + } + }, + "required": ["sessionID"] + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "directory": { + "type": "string" + }, + "events": { + "minItems": 1, + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "aggregateID": { + "type": "string" + }, + "seq": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "type": { + "type": "string" + }, + "data": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["id", "aggregateID", "seq", "type", "data"] + } + } + }, + "required": ["directory", "events"] + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.sync.replay({\n ...\n})" + } + ] + } + }, + "/sync/history": { + "get": { + "operationId": "sync.history.list", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "workspace", + "schema": { + "type": "string" + } + } + ], + "summary": "List sync events", + "description": "List sync events for all aggregates. Keys are aggregate IDs the client already knows about, values are the last known sequence ID. Events with seq > value are returned for those aggregates. Aggregates not listed in the input get their full history.", + "responses": { + "200": { + "description": "Sync events", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "aggregate_id": { + "type": "string" + }, + "seq": { + "type": "number" + }, + "type": { + "type": "string" + }, + "data": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["id", "aggregate_id", "seq", "type", "data"] + } + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.sync.history.list({\n ...\n})" + } + ] + } + }, "/find": { "get": { "operationId": "find.text", @@ -8514,6 +8794,40 @@ }, "required": ["type", "properties"] }, + "Event.workspace.restore": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "workspace.restore" + }, + "properties": { + "type": "object", + "properties": { + "workspaceID": { + "type": "string", + "pattern": "^wrk.*" + }, + "sessionID": { + "type": "string", + "pattern": "^ses.*" + }, + "total": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "step": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + } + }, + "required": ["workspaceID", "sessionID", "total", "step"] + } + }, + "required": ["type", "properties"] + }, "Event.workspace.status": { "type": "object", "properties": { @@ -10523,6 +10837,9 @@ { "$ref": "#/components/schemas/Event.workspace.failed" }, + { + "$ref": "#/components/schemas/Event.workspace.restore" + }, { "$ref": "#/components/schemas/Event.workspace.status" }, @@ -12780,6 +13097,9 @@ { "$ref": "#/components/schemas/Event.workspace.failed" }, + { + "$ref": "#/components/schemas/Event.workspace.restore" + }, { "$ref": "#/components/schemas/Event.workspace.status" }, From be9432a893dd1662c10ff41c7ab552bcba8f3e1b Mon Sep 17 00:00:00 2001 From: Dax Date: Wed, 15 Apr 2026 10:26:20 -0400 Subject: [PATCH 139/300] shared package (#22626) --- bun.lock | 80 +++++++++---------- package.json | 2 + packages/app/package.json | 2 +- .../src/components/dialog-edit-project.tsx | 2 +- packages/app/src/components/dialog-fork.tsx | 2 +- .../components/dialog-select-directory.tsx | 2 +- .../app/src/components/dialog-select-file.tsx | 4 +- .../prompt-input/build-request-parts.ts | 2 +- .../components/prompt-input/context-items.tsx | 2 +- .../components/prompt-input/slash-popover.tsx | 2 +- .../components/prompt-input/submit.test.ts | 2 +- .../app/src/components/prompt-input/submit.ts | 4 +- .../session/session-context-tab.tsx | 4 +- .../src/components/session/session-header.tsx | 2 +- .../components/session/session-new-view.tsx | 2 +- .../session/session-sortable-tab.tsx | 2 +- packages/app/src/context/file.tsx | 2 +- packages/app/src/context/global-sync.tsx | 2 +- .../app/src/context/global-sync/bootstrap.ts | 4 +- .../src/context/global-sync/event-reducer.ts | 2 +- packages/app/src/context/local.tsx | 2 +- packages/app/src/context/notification.tsx | 4 +- .../context/permission-auto-respond.test.ts | 2 +- .../src/context/permission-auto-respond.ts | 2 +- packages/app/src/context/prompt.tsx | 2 +- packages/app/src/context/sync.tsx | 4 +- packages/app/src/pages/directory-layout.tsx | 2 +- packages/app/src/pages/home.tsx | 2 +- packages/app/src/pages/layout.tsx | 8 +- packages/app/src/pages/layout/helpers.ts | 2 +- .../app/src/pages/layout/sidebar-items.tsx | 2 +- .../app/src/pages/layout/sidebar-project.tsx | 2 +- .../src/pages/layout/sidebar-workspace.tsx | 4 +- packages/app/src/pages/session.tsx | 2 +- packages/app/src/pages/session/file-tabs.tsx | 2 +- .../src/pages/session/message-timeline.tsx | 4 +- .../pages/session/use-session-commands.tsx | 2 +- packages/app/src/utils/base64.ts | 2 +- packages/app/src/utils/persist.ts | 2 +- packages/enterprise/package.json | 2 +- packages/enterprise/src/core/share.ts | 4 +- packages/enterprise/src/core/storage.ts | 2 +- .../enterprise/src/routes/share/[shareID].tsx | 6 +- packages/enterprise/test/core/share.test.ts | 2 +- packages/opencode/package.json | 2 +- packages/opencode/src/auth/index.ts | 2 +- .../opencode/src/cli/cmd/tui/context/sync.tsx | 2 +- .../src/cli/cmd/tui/context/theme.tsx | 2 +- packages/opencode/src/cli/ui.ts | 2 +- packages/opencode/src/config/config.ts | 8 +- packages/opencode/src/config/markdown.ts | 2 +- packages/opencode/src/config/paths.ts | 2 +- packages/opencode/src/config/tui.ts | 2 +- .../opencode/src/control-plane/workspace.ts | 2 +- packages/opencode/src/effect/app-runtime.ts | 2 +- packages/opencode/src/file/ignore.ts | 2 +- packages/opencode/src/file/index.ts | 2 +- packages/opencode/src/file/time.ts | 2 +- packages/opencode/src/format/formatter.ts | 2 +- packages/opencode/src/ide/index.ts | 2 +- packages/opencode/src/index.ts | 2 +- packages/opencode/src/lsp/client.ts | 2 +- packages/opencode/src/lsp/server.ts | 4 +- packages/opencode/src/mcp/auth.ts | 2 +- packages/opencode/src/mcp/index.ts | 4 +- packages/opencode/src/npm/index.ts | 2 +- packages/opencode/src/plugin/index.ts | 2 +- packages/opencode/src/plugin/shared.ts | 2 +- packages/opencode/src/project/project.ts | 2 +- packages/opencode/src/project/vcs.ts | 2 +- packages/opencode/src/provider/auth.ts | 2 +- packages/opencode/src/provider/provider.ts | 4 +- packages/opencode/src/pty/index.ts | 2 +- .../opencode/src/server/instance/session.ts | 2 +- packages/opencode/src/server/middleware.ts | 2 +- packages/opencode/src/session/index.ts | 2 +- packages/opencode/src/session/instruction.ts | 2 +- packages/opencode/src/session/message-v2.ts | 2 +- packages/opencode/src/session/message.ts | 2 +- packages/opencode/src/session/prompt.ts | 4 +- packages/opencode/src/session/retry.ts | 2 +- packages/opencode/src/skill/discovery.ts | 2 +- packages/opencode/src/skill/index.ts | 6 +- packages/opencode/src/snapshot/index.ts | 2 +- packages/opencode/src/storage/db.ts | 2 +- .../opencode/src/storage/json-migration.ts | 2 +- packages/opencode/src/storage/storage.ts | 4 +- packages/opencode/src/tool/apply_patch.ts | 2 +- packages/opencode/src/tool/bash.ts | 2 +- packages/opencode/src/tool/edit.ts | 2 +- .../opencode/src/tool/external-directory.ts | 2 +- packages/opencode/src/tool/glob.ts | 2 +- packages/opencode/src/tool/grep.ts | 2 +- packages/opencode/src/tool/lsp.ts | 2 +- packages/opencode/src/tool/read.ts | 2 +- packages/opencode/src/tool/registry.ts | 4 +- packages/opencode/src/tool/truncate.ts | 2 +- packages/opencode/src/tool/write.ts | 2 +- packages/opencode/src/util/filesystem.ts | 2 +- packages/opencode/src/util/log.ts | 2 +- packages/opencode/src/worktree/index.ts | 6 +- packages/opencode/test/config/config.test.ts | 2 +- .../test/filesystem/filesystem.test.ts | 2 +- .../opencode/test/project/project.test.ts | 2 +- .../test/session/prompt-effect.test.ts | 2 +- packages/opencode/test/session/prompt.test.ts | 2 +- packages/opencode/test/session/retry.test.ts | 2 +- .../test/session/snapshot-tool-race.test.ts | 2 +- .../opencode/test/storage/storage.test.ts | 2 +- .../opencode/test/tool/apply_patch.test.ts | 2 +- packages/opencode/test/tool/bash.test.ts | 2 +- packages/opencode/test/tool/edit.test.ts | 2 +- packages/opencode/test/tool/glob.test.ts | 2 +- packages/opencode/test/tool/grep.test.ts | 2 +- packages/opencode/test/tool/read.test.ts | 2 +- packages/opencode/test/tool/write.test.ts | 2 +- packages/opencode/test/util/glob.test.ts | 2 +- packages/opencode/test/util/module.test.ts | 2 +- packages/shared/package.json | 31 +++++++ .../index.ts => shared/src/filesystem.ts} | 2 +- .../{util/src => shared/src/util}/array.ts | 0 .../{util/src => shared/src/util}/binary.ts | 0 .../{util/src => shared/src/util}/encode.ts | 0 .../{util/src => shared/src/util}/error.ts | 0 packages/{util/src => shared/src/util}/fn.ts | 0 .../{opencode => shared}/src/util/glob.ts | 0 .../src => shared/src/util}/identifier.ts | 0 .../{util/src => shared/src/util}/iife.ts | 0 .../{util/src => shared/src/util}/lazy.ts | 0 .../{util/src => shared/src/util}/module.ts | 0 .../{util/src => shared/src/util}/path.ts | 0 .../{util/src => shared/src/util}/retry.ts | 0 .../{util/src => shared/src/util}/slug.ts | 0 packages/shared/tsconfig.json | 23 ++++++ packages/ui/package.json | 2 +- packages/ui/src/components/file.tsx | 2 +- packages/ui/src/components/line-comment.tsx | 2 +- packages/ui/src/components/markdown.tsx | 2 +- packages/ui/src/components/message-part.tsx | 4 +- packages/ui/src/components/session-review.tsx | 4 +- packages/ui/src/components/session-turn.tsx | 4 +- packages/util/package.json | 20 ----- packages/util/sst-env.d.ts | 10 --- packages/util/tsconfig.json | 14 ---- 144 files changed, 246 insertions(+), 242 deletions(-) create mode 100644 packages/shared/package.json rename packages/{opencode/src/filesystem/index.ts => shared/src/filesystem.ts} (99%) rename packages/{util/src => shared/src/util}/array.ts (100%) rename packages/{util/src => shared/src/util}/binary.ts (100%) rename packages/{util/src => shared/src/util}/encode.ts (100%) rename packages/{util/src => shared/src/util}/error.ts (100%) rename packages/{util/src => shared/src/util}/fn.ts (100%) rename packages/{opencode => shared}/src/util/glob.ts (100%) rename packages/{util/src => shared/src/util}/identifier.ts (100%) rename packages/{util/src => shared/src/util}/iife.ts (100%) rename packages/{util/src => shared/src/util}/lazy.ts (100%) rename packages/{util/src => shared/src/util}/module.ts (100%) rename packages/{util/src => shared/src/util}/path.ts (100%) rename packages/{util/src => shared/src/util}/retry.ts (100%) rename packages/{util/src => shared/src/util}/slug.ts (100%) create mode 100644 packages/shared/tsconfig.json delete mode 100644 packages/util/package.json delete mode 100644 packages/util/sst-env.d.ts delete mode 100644 packages/util/tsconfig.json diff --git a/bun.lock b/bun.lock index e7085b31da..01966b826a 100644 --- a/bun.lock +++ b/bun.lock @@ -31,8 +31,8 @@ "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", + "@opencode-ai/shared": "workspace:*", "@opencode-ai/ui": "workspace:*", - "@opencode-ai/util": "workspace:*", "@shikijs/transformers": "3.9.2", "@solid-primitives/active-element": "2.1.3", "@solid-primitives/audio": "1.4.2", @@ -268,8 +268,8 @@ "name": "@opencode-ai/enterprise", "version": "1.4.6", "dependencies": { + "@opencode-ai/shared": "workspace:*", "@opencode-ai/ui": "workspace:*", - "@opencode-ai/util": "workspace:*", "@pierre/diffs": "catalog:", "@solidjs/meta": "catalog:", "@solidjs/router": "catalog:", @@ -358,7 +358,6 @@ "@opencode-ai/script": "workspace:*", "@opencode-ai/sdk": "workspace:*", "@opencode-ai/server": "workspace:*", - "@opencode-ai/util": "workspace:*", "@openrouter/ai-sdk-provider": "2.5.1", "@opentelemetry/exporter-trace-otlp-http": "0.214.0", "@opentelemetry/sdk-trace-base": "2.6.1", @@ -424,6 +423,7 @@ "@effect/language-service": "0.84.2", "@octokit/webhooks-types": "7.6.1", "@opencode-ai/script": "workspace:*", + "@opencode-ai/shared": "workspace:*", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", @@ -513,6 +513,25 @@ "typescript": "catalog:", }, }, + "packages/shared": { + "name": "@opencode-ai/shared", + "version": "1.4.6", + "bin": { + "opencode": "./bin/opencode", + }, + "dependencies": { + "@effect/platform-node": "catalog:", + "@npmcli/arborist": "catalog:", + "effect": "catalog:", + "mime-types": "3.0.2", + "minimatch": "10.2.5", + "semver": "catalog:", + "zod": "catalog:", + }, + "devDependencies": { + "@types/semver": "catalog:", + }, + }, "packages/slack": { "name": "@opencode-ai/slack", "version": "1.4.6", @@ -554,7 +573,7 @@ "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", - "@opencode-ai/util": "workspace:*", + "@opencode-ai/shared": "workspace:*", "@pierre/diffs": "catalog:", "@shikijs/transformers": "3.9.2", "@solid-primitives/bounds": "0.1.3", @@ -597,17 +616,6 @@ "vite-plugin-solid": "catalog:", }, }, - "packages/util": { - "name": "@opencode-ai/util", - "version": "1.4.6", - "dependencies": { - "zod": "catalog:", - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - }, "packages/web": { "name": "@opencode-ai/web", "version": "1.4.6", @@ -666,6 +674,7 @@ "@hono/zod-validator": "0.4.2", "@kobalte/core": "0.13.11", "@lydell/node-pty": "1.2.0-beta.10", + "@npmcli/arborist": "9.4.0", "@octokit/rest": "22.0.0", "@openauthjs/openauth": "0.0.0-20250322224806", "@pierre/diffs": "1.1.0-beta.18", @@ -698,6 +707,7 @@ "marked-shiki": "1.2.1", "remeda": "2.26.0", "remend": "1.3.0", + "semver": "7.7.4", "shiki": "3.20.0", "solid-js": "1.9.10", "solid-list": "0.3.0", @@ -1554,14 +1564,14 @@ "@opencode-ai/server": ["@opencode-ai/server@workspace:packages/server"], + "@opencode-ai/shared": ["@opencode-ai/shared@workspace:packages/shared"], + "@opencode-ai/slack": ["@opencode-ai/slack@workspace:packages/slack"], "@opencode-ai/storybook": ["@opencode-ai/storybook@workspace:packages/storybook"], "@opencode-ai/ui": ["@opencode-ai/ui@workspace:packages/ui"], - "@opencode-ai/util": ["@opencode-ai/util@workspace:packages/util"], - "@opencode-ai/web": ["@opencode-ai/web@workspace:packages/web"], "@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.5.1", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-r1fJL1Cb3gQDa2MpWH/sfx1BsEW0uzlRriJM6eihaKqbtKDmZoBisF32VcVaQYassighX7NGCkF68EsrZA43uQ=="], @@ -2780,7 +2790,7 @@ "commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="], - "common-ancestor-path": ["common-ancestor-path@1.0.1", "", {}, "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w=="], + "common-ancestor-path": ["common-ancestor-path@2.0.0", "", {}, "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng=="], "compare-version": ["compare-version@0.1.2", "", {}, "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A=="], @@ -3880,7 +3890,7 @@ "miniflare": ["miniflare@4.20251118.1", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "sharp": "^0.33.5", "stoppable": "1.1.0", "undici": "7.14.0", "workerd": "1.20251118.0", "ws": "8.18.0", "youch": "4.1.0-beta.10", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-uLSAE/DvOm392fiaig4LOaatxLjM7xzIniFRG5Y3yF9IduOYLLK/pkCPQNCgKQH3ou0YJRHnTN+09LPfqYNTQQ=="], - "minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="], + "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], @@ -3938,7 +3948,7 @@ "native-duplexpair": ["native-duplexpair@1.0.0", "", {}, "sha512-E7QQoM+3jvNtlmyfqRZ0/U75VFgCls+fSkbml2MpgWkWyz3ox8Y58gNhfuziuQYGNNQAbFZJQck55LHCnCK6CA=="], - "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], "neotraverse": ["neotraverse@0.6.18", "", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="], @@ -5440,12 +5450,6 @@ "@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], - "@npmcli/arborist/common-ancestor-path": ["common-ancestor-path@2.0.0", "", {}, "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng=="], - - "@npmcli/arborist/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], - - "@npmcli/map-workspaces/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], - "@npmcli/query/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], "@octokit/auth-app/@octokit/request": ["@octokit/request@10.0.8", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw=="], @@ -5614,8 +5618,6 @@ "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], - "@tufjs/models/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], - "@types/plist/xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="], "@vitest/expect/@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], @@ -5628,6 +5630,8 @@ "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + "ai/@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.95", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZmUNNbZl3V42xwQzPaNUi+s8eqR2lnrxf0bvB6YbLXpLjHYv0k2Y78t12cNOfY0bxGeuVVTLyk856uLuQIuXEQ=="], "ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.69", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-LshR7X3pFugY0o41G2VKTmg1XoGpSl7uoYWfzk6zjVZLhCfeFiwgpOga+eTV4XY1VVpZwKVqRnkDbIL7K2eH5g=="], @@ -5650,8 +5654,6 @@ "app-builder-lib/hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="], - "app-builder-lib/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], - "app-builder-lib/which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="], "archiver-utils/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=="], @@ -5660,6 +5662,8 @@ "astro/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.6.1", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="], + "astro/common-ancestor-path": ["common-ancestor-path@1.0.1", "", {}, "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w=="], + "astro/diff": ["diff@5.2.2", "", {}, "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A=="], "astro/unstorage": ["unstorage@1.17.5", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^5.0.0", "destr": "^2.0.5", "h3": "^1.15.10", "lru-cache": "^11.2.7", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.3" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1 || ^2 || ^3", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg=="], @@ -5784,8 +5788,6 @@ "gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "glob/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], - "globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "gray-matter/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], @@ -5802,8 +5804,6 @@ "iconv-corefoundation/node-addon-api": ["node-addon-api@1.7.2", "", {}, "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg=="], - "ignore-walk/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], - "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=="], @@ -5818,8 +5818,6 @@ "log-symbols/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "make-fetch-happen/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - "matcher/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], "md-to-react-email/marked": ["marked@7.0.4", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ=="], @@ -5858,6 +5856,8 @@ "opencode/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="], + "opencode/minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="], + "opencode-gitlab-auth/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], "opencode-poe-auth/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], @@ -6668,8 +6668,6 @@ "type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "vite-plugin-icons-spritesheet/glob/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], - "vitest/@vitest/expect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "vitest/@vitest/expect/chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], @@ -6806,8 +6804,6 @@ "@electron/rebuild/node-gyp/make-fetch-happen/minipass-fetch": ["minipass-fetch@4.0.1", "", { "dependencies": { "minipass": "^7.0.3", "minipass-sized": "^1.0.3", "minizlib": "^3.0.1" }, "optionalDependencies": { "encoding": "^0.1.13" } }, "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ=="], - "@electron/rebuild/node-gyp/make-fetch-happen/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - "@electron/rebuild/node-gyp/make-fetch-happen/ssri": ["ssri@12.0.0", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ=="], "@electron/rebuild/node-gyp/nopt/abbrev": ["abbrev@3.0.1", "", {}, "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg=="], @@ -6872,8 +6868,6 @@ "@jsx-email/cli/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], - "@modelcontextprotocol/sdk/express/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - "@modelcontextprotocol/sdk/express/type-is/media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], "@octokit/auth-app/@octokit/request-error/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], @@ -7072,8 +7066,6 @@ "js-beautify/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "opencontrol/@modelcontextprotocol/sdk/express/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - "opencontrol/@modelcontextprotocol/sdk/express/type-is/media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], "pkg-up/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], diff --git a/package.json b/package.json index 282506206b..abe1b5d362 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "catalog": { "@effect/opentelemetry": "4.0.0-beta.48", "@effect/platform-node": "4.0.0-beta.48", + "@npmcli/arborist": "9.4.0", "@types/bun": "1.3.11", "@types/cross-spawn": "6.0.6", "@octokit/rest": "22.0.0", @@ -59,6 +60,7 @@ "marked-shiki": "1.2.1", "remend": "1.3.0", "@playwright/test": "1.59.1", + "semver": "7.7.4", "typescript": "5.8.2", "@typescript/native-preview": "7.0.0-dev.20251207.1", "zod": "4.1.8", diff --git a/packages/app/package.json b/packages/app/package.json index 9aadb774b7..483c71dc50 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -42,7 +42,7 @@ "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", "@opencode-ai/ui": "workspace:*", - "@opencode-ai/util": "workspace:*", + "@opencode-ai/shared": "workspace:*", "@shikijs/transformers": "3.9.2", "@solid-primitives/active-element": "2.1.3", "@solid-primitives/audio": "1.4.2", diff --git a/packages/app/src/components/dialog-edit-project.tsx b/packages/app/src/components/dialog-edit-project.tsx index eb962f47eb..ea5d70065a 100644 --- a/packages/app/src/components/dialog-edit-project.tsx +++ b/packages/app/src/components/dialog-edit-project.tsx @@ -9,7 +9,7 @@ import { createStore } from "solid-js/store" import { useGlobalSDK } from "@/context/global-sdk" import { useGlobalSync } from "@/context/global-sync" import { type LocalProject, getAvatarColors } from "@/context/layout" -import { getFilename } from "@opencode-ai/util/path" +import { getFilename } from "@opencode-ai/shared/util/path" import { Avatar } from "@opencode-ai/ui/avatar" import { useLanguage } from "@/context/language" diff --git a/packages/app/src/components/dialog-fork.tsx b/packages/app/src/components/dialog-fork.tsx index 9e1b896fa8..710618c301 100644 --- a/packages/app/src/components/dialog-fork.tsx +++ b/packages/app/src/components/dialog-fork.tsx @@ -9,7 +9,7 @@ import { List } from "@opencode-ai/ui/list" import { showToast } from "@opencode-ai/ui/toast" import { extractPromptFromParts } from "@/utils/prompt" import type { TextPart as SDKTextPart } from "@opencode-ai/sdk/v2/client" -import { base64Encode } from "@opencode-ai/util/encode" +import { base64Encode } from "@opencode-ai/shared/util/encode" import { useLanguage } from "@/context/language" interface ForkableMessage { diff --git a/packages/app/src/components/dialog-select-directory.tsx b/packages/app/src/components/dialog-select-directory.tsx index 91e23f8ffa..903cb1915d 100644 --- a/packages/app/src/components/dialog-select-directory.tsx +++ b/packages/app/src/components/dialog-select-directory.tsx @@ -3,7 +3,7 @@ import { Dialog } from "@opencode-ai/ui/dialog" import { FileIcon } from "@opencode-ai/ui/file-icon" import { List } from "@opencode-ai/ui/list" import type { ListRef } from "@opencode-ai/ui/list" -import { getDirectory, getFilename } from "@opencode-ai/util/path" +import { getDirectory, getFilename } from "@opencode-ai/shared/util/path" import fuzzysort from "fuzzysort" import { createMemo, createResource, createSignal } from "solid-js" import { useGlobalSDK } from "@/context/global-sdk" diff --git a/packages/app/src/components/dialog-select-file.tsx b/packages/app/src/components/dialog-select-file.tsx index e21be77fb9..a0347a0399 100644 --- a/packages/app/src/components/dialog-select-file.tsx +++ b/packages/app/src/components/dialog-select-file.tsx @@ -4,8 +4,8 @@ import { FileIcon } from "@opencode-ai/ui/file-icon" import { Icon } from "@opencode-ai/ui/icon" import { Keybind } from "@opencode-ai/ui/keybind" import { List } from "@opencode-ai/ui/list" -import { base64Encode } from "@opencode-ai/util/encode" -import { getDirectory, getFilename } from "@opencode-ai/util/path" +import { base64Encode } from "@opencode-ai/shared/util/encode" +import { getDirectory, getFilename } from "@opencode-ai/shared/util/path" import { useNavigate } from "@solidjs/router" import { createMemo, createSignal, Match, onCleanup, Show, Switch } from "solid-js" import { formatKeybind, useCommand, type CommandOption } from "@/context/command" diff --git a/packages/app/src/components/prompt-input/build-request-parts.ts b/packages/app/src/components/prompt-input/build-request-parts.ts index a1076e60ca..c268af35ee 100644 --- a/packages/app/src/components/prompt-input/build-request-parts.ts +++ b/packages/app/src/components/prompt-input/build-request-parts.ts @@ -1,4 +1,4 @@ -import { getFilename } from "@opencode-ai/util/path" +import { getFilename } from "@opencode-ai/shared/util/path" import { type AgentPartInput, type FilePartInput, type Part, type TextPartInput } from "@opencode-ai/sdk/v2/client" import type { FileSelection } from "@/context/file" import { encodeFilePath } from "@/context/file/path" diff --git a/packages/app/src/components/prompt-input/context-items.tsx b/packages/app/src/components/prompt-input/context-items.tsx index b138fe3ef6..9f20f1c04b 100644 --- a/packages/app/src/components/prompt-input/context-items.tsx +++ b/packages/app/src/components/prompt-input/context-items.tsx @@ -2,7 +2,7 @@ import { Component, For, Show } from "solid-js" import { FileIcon } from "@opencode-ai/ui/file-icon" import { IconButton } from "@opencode-ai/ui/icon-button" import { Tooltip } from "@opencode-ai/ui/tooltip" -import { getDirectory, getFilename, getFilenameTruncated } from "@opencode-ai/util/path" +import { getDirectory, getFilename, getFilenameTruncated } from "@opencode-ai/shared/util/path" import type { ContextItem } from "@/context/prompt" type PromptContextItem = ContextItem & { key: string } diff --git a/packages/app/src/components/prompt-input/slash-popover.tsx b/packages/app/src/components/prompt-input/slash-popover.tsx index 65eb01c797..0c8c959234 100644 --- a/packages/app/src/components/prompt-input/slash-popover.tsx +++ b/packages/app/src/components/prompt-input/slash-popover.tsx @@ -1,7 +1,7 @@ import { Component, For, Match, Show, Switch } from "solid-js" import { FileIcon } from "@opencode-ai/ui/file-icon" import { Icon } from "@opencode-ai/ui/icon" -import { getDirectory, getFilename } from "@opencode-ai/util/path" +import { getDirectory, getFilename } from "@opencode-ai/shared/util/path" export type AtOption = | { type: "agent"; name: string; display: string } diff --git a/packages/app/src/components/prompt-input/submit.test.ts b/packages/app/src/components/prompt-input/submit.test.ts index 03bece2e31..cf99497232 100644 --- a/packages/app/src/components/prompt-input/submit.test.ts +++ b/packages/app/src/components/prompt-input/submit.test.ts @@ -74,7 +74,7 @@ beforeAll(async () => { showToast: () => 0, })) - mock.module("@opencode-ai/util/encode", () => ({ + mock.module("@opencode-ai/shared/util/encode", () => ({ base64Encode: (value: string) => value, })) diff --git a/packages/app/src/components/prompt-input/submit.ts b/packages/app/src/components/prompt-input/submit.ts index d147d7b502..27e8980431 100644 --- a/packages/app/src/components/prompt-input/submit.ts +++ b/packages/app/src/components/prompt-input/submit.ts @@ -1,7 +1,7 @@ import type { Message, Session } from "@opencode-ai/sdk/v2/client" import { showToast } from "@opencode-ai/ui/toast" -import { base64Encode } from "@opencode-ai/util/encode" -import { Binary } from "@opencode-ai/util/binary" +import { base64Encode } from "@opencode-ai/shared/util/encode" +import { Binary } from "@opencode-ai/shared/util/binary" import { useNavigate, useParams } from "@solidjs/router" import { batch, type Accessor } from "solid-js" import type { FileSelection } from "@/context/file" diff --git a/packages/app/src/components/session/session-context-tab.tsx b/packages/app/src/components/session/session-context-tab.tsx index 4e7dc8e783..abf4c93346 100644 --- a/packages/app/src/components/session/session-context-tab.tsx +++ b/packages/app/src/components/session/session-context-tab.tsx @@ -1,8 +1,8 @@ import { createMemo, createEffect, on, onCleanup, For, Show } from "solid-js" import type { JSX } from "solid-js" import { useSync } from "@/context/sync" -import { checksum } from "@opencode-ai/util/encode" -import { findLast } from "@opencode-ai/util/array" +import { checksum } from "@opencode-ai/shared/util/encode" +import { findLast } from "@opencode-ai/shared/util/array" import { same } from "@/utils/same" import { Icon } from "@opencode-ai/ui/icon" import { Accordion } from "@opencode-ai/ui/accordion" diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index 495b323405..e65b575ac5 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -7,7 +7,7 @@ import { Keybind } from "@opencode-ai/ui/keybind" import { Spinner } from "@opencode-ai/ui/spinner" import { showToast } from "@opencode-ai/ui/toast" import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" -import { getFilename } from "@opencode-ai/util/path" +import { getFilename } from "@opencode-ai/shared/util/path" import { createEffect, createMemo, For, onCleanup, Show } from "solid-js" import { createStore } from "solid-js/store" import { Portal } from "solid-js/web" diff --git a/packages/app/src/components/session/session-new-view.tsx b/packages/app/src/components/session/session-new-view.tsx index e4ef363936..d2cac28fc4 100644 --- a/packages/app/src/components/session/session-new-view.tsx +++ b/packages/app/src/components/session/session-new-view.tsx @@ -5,7 +5,7 @@ import { useSDK } from "@/context/sdk" import { useLanguage } from "@/context/language" import { Icon } from "@opencode-ai/ui/icon" import { Mark } from "@opencode-ai/ui/logo" -import { getDirectory, getFilename } from "@opencode-ai/util/path" +import { getDirectory, getFilename } from "@opencode-ai/shared/util/path" const MAIN_WORKTREE = "main" const CREATE_WORKTREE = "create" diff --git a/packages/app/src/components/session/session-sortable-tab.tsx b/packages/app/src/components/session/session-sortable-tab.tsx index dfda91c160..fb2275c445 100644 --- a/packages/app/src/components/session/session-sortable-tab.tsx +++ b/packages/app/src/components/session/session-sortable-tab.tsx @@ -5,7 +5,7 @@ import { FileIcon } from "@opencode-ai/ui/file-icon" import { IconButton } from "@opencode-ai/ui/icon-button" import { TooltipKeybind } from "@opencode-ai/ui/tooltip" import { Tabs } from "@opencode-ai/ui/tabs" -import { getFilename } from "@opencode-ai/util/path" +import { getFilename } from "@opencode-ai/shared/util/path" import { useFile } from "@/context/file" import { useLanguage } from "@/context/language" import { useCommand } from "@/context/command" diff --git a/packages/app/src/context/file.tsx b/packages/app/src/context/file.tsx index f8fec7142d..8998731a6c 100644 --- a/packages/app/src/context/file.tsx +++ b/packages/app/src/context/file.tsx @@ -3,7 +3,7 @@ import { createStore, produce, reconcile } from "solid-js/store" import { createSimpleContext } from "@opencode-ai/ui/context" import { showToast } from "@opencode-ai/ui/toast" import { useParams } from "@solidjs/router" -import { getFilename } from "@opencode-ai/util/path" +import { getFilename } from "@opencode-ai/shared/util/path" import { useSDK } from "./sdk" import { useSync } from "./sync" import { useLanguage } from "@/context/language" diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index 0cf3570a8b..fe5f2f1301 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -8,7 +8,7 @@ import type { Todo, } from "@opencode-ai/sdk/v2/client" import { showToast } from "@opencode-ai/ui/toast" -import { getFilename } from "@opencode-ai/util/path" +import { getFilename } from "@opencode-ai/shared/util/path" import { createContext, getOwner, onCleanup, onMount, type ParentProps, untrack, useContext } from "solid-js" import { createStore, produce, reconcile } from "solid-js/store" import { useLanguage } from "@/context/language" diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts index c4deb431a7..ad987efa6e 100644 --- a/packages/app/src/context/global-sync/bootstrap.ts +++ b/packages/app/src/context/global-sync/bootstrap.ts @@ -11,8 +11,8 @@ import type { Todo, } from "@opencode-ai/sdk/v2/client" import { showToast } from "@opencode-ai/ui/toast" -import { getFilename } from "@opencode-ai/util/path" -import { retry } from "@opencode-ai/util/retry" +import { getFilename } from "@opencode-ai/shared/util/path" +import { retry } from "@opencode-ai/shared/util/retry" import { batch } from "solid-js" import { reconcile, type SetStoreFunction, type Store } from "solid-js/store" import type { State, VcsCache } from "./types" diff --git a/packages/app/src/context/global-sync/event-reducer.ts b/packages/app/src/context/global-sync/event-reducer.ts index 500013c1da..11a0cf83fd 100644 --- a/packages/app/src/context/global-sync/event-reducer.ts +++ b/packages/app/src/context/global-sync/event-reducer.ts @@ -1,4 +1,4 @@ -import { Binary } from "@opencode-ai/util/binary" +import { Binary } from "@opencode-ai/shared/util/binary" import { produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store" import type { Message, diff --git a/packages/app/src/context/local.tsx b/packages/app/src/context/local.tsx index 28ce2770de..0b0972ee67 100644 --- a/packages/app/src/context/local.tsx +++ b/packages/app/src/context/local.tsx @@ -1,5 +1,5 @@ import { createSimpleContext } from "@opencode-ai/ui/context" -import { base64Encode } from "@opencode-ai/util/encode" +import { base64Encode } from "@opencode-ai/shared/util/encode" import { useParams } from "@solidjs/router" import { batch, createEffect, createMemo } from "solid-js" import { createStore } from "solid-js/store" diff --git a/packages/app/src/context/notification.tsx b/packages/app/src/context/notification.tsx index 281a1ef33d..251b67b06c 100644 --- a/packages/app/src/context/notification.tsx +++ b/packages/app/src/context/notification.tsx @@ -7,8 +7,8 @@ import { useGlobalSync } from "./global-sync" import { usePlatform } from "@/context/platform" import { useLanguage } from "@/context/language" import { useSettings } from "@/context/settings" -import { Binary } from "@opencode-ai/util/binary" -import { base64Encode } from "@opencode-ai/util/encode" +import { Binary } from "@opencode-ai/shared/util/binary" +import { base64Encode } from "@opencode-ai/shared/util/encode" import { decode64 } from "@/utils/base64" import { EventSessionError } from "@opencode-ai/sdk/v2" import { Persist, persisted } from "@/utils/persist" diff --git a/packages/app/src/context/permission-auto-respond.test.ts b/packages/app/src/context/permission-auto-respond.test.ts index 7556113005..2f8ca6265e 100644 --- a/packages/app/src/context/permission-auto-respond.test.ts +++ b/packages/app/src/context/permission-auto-respond.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" import type { PermissionRequest, Session } from "@opencode-ai/sdk/v2/client" -import { base64Encode } from "@opencode-ai/util/encode" +import { base64Encode } from "@opencode-ai/shared/util/encode" import { autoRespondsPermission, isDirectoryAutoAccepting } from "./permission-auto-respond" const session = (input: { id: string; parentID?: string }) => diff --git a/packages/app/src/context/permission-auto-respond.ts b/packages/app/src/context/permission-auto-respond.ts index b206deedff..2ebca34347 100644 --- a/packages/app/src/context/permission-auto-respond.ts +++ b/packages/app/src/context/permission-auto-respond.ts @@ -1,4 +1,4 @@ -import { base64Encode } from "@opencode-ai/util/encode" +import { base64Encode } from "@opencode-ai/shared/util/encode" export function acceptKey(sessionID: string, directory?: string) { if (!directory) return sessionID diff --git a/packages/app/src/context/prompt.tsx b/packages/app/src/context/prompt.tsx index 831fdbca83..9b666e5e75 100644 --- a/packages/app/src/context/prompt.tsx +++ b/packages/app/src/context/prompt.tsx @@ -1,5 +1,5 @@ import { createSimpleContext } from "@opencode-ai/ui/context" -import { checksum } from "@opencode-ai/util/encode" +import { checksum } from "@opencode-ai/shared/util/encode" import { useParams } from "@solidjs/router" import { batch, createMemo, createRoot, getOwner, onCleanup } from "solid-js" import { createStore, type SetStoreFunction } from "solid-js/store" diff --git a/packages/app/src/context/sync.tsx b/packages/app/src/context/sync.tsx index fb02a2d2d0..29b7fe68c5 100644 --- a/packages/app/src/context/sync.tsx +++ b/packages/app/src/context/sync.tsx @@ -1,7 +1,7 @@ import { batch, createMemo } from "solid-js" import { createStore, produce, reconcile } from "solid-js/store" -import { Binary } from "@opencode-ai/util/binary" -import { retry } from "@opencode-ai/util/retry" +import { Binary } from "@opencode-ai/shared/util/binary" +import { retry } from "@opencode-ai/shared/util/retry" import { createSimpleContext } from "@opencode-ai/ui/context" import { clearSessionPrefetch, diff --git a/packages/app/src/pages/directory-layout.tsx b/packages/app/src/pages/directory-layout.tsx index 427b4823b5..f604dd6c5c 100644 --- a/packages/app/src/pages/directory-layout.tsx +++ b/packages/app/src/pages/directory-layout.tsx @@ -1,6 +1,6 @@ import { DataProvider } from "@opencode-ai/ui/context" import { showToast } from "@opencode-ai/ui/toast" -import { base64Encode } from "@opencode-ai/util/encode" +import { base64Encode } from "@opencode-ai/shared/util/encode" import { useLocation, useNavigate, useParams } from "@solidjs/router" import { createEffect, createMemo, type ParentProps, Show } from "solid-js" import { useLanguage } from "@/context/language" diff --git a/packages/app/src/pages/home.tsx b/packages/app/src/pages/home.tsx index 4c795b9683..46cacdf627 100644 --- a/packages/app/src/pages/home.tsx +++ b/packages/app/src/pages/home.tsx @@ -3,7 +3,7 @@ import { Button } from "@opencode-ai/ui/button" import { Logo } from "@opencode-ai/ui/logo" import { useLayout } from "@/context/layout" import { useNavigate } from "@solidjs/router" -import { base64Encode } from "@opencode-ai/util/encode" +import { base64Encode } from "@opencode-ai/shared/util/encode" import { Icon } from "@opencode-ai/ui/icon" import { usePlatform } from "@/context/platform" import { DateTime } from "luxon" diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index f402f4bc04..62d5cba615 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -17,7 +17,7 @@ import { useNavigate, useParams } from "@solidjs/router" import { useLayout, LocalProject } from "@/context/layout" import { useGlobalSync } from "@/context/global-sync" import { Persist, persisted } from "@/utils/persist" -import { base64Encode } from "@opencode-ai/util/encode" +import { base64Encode } from "@opencode-ai/shared/util/encode" import { decode64 } from "@/utils/base64" import { ResizeHandle } from "@opencode-ai/ui/resize-handle" import { Button } from "@opencode-ai/ui/button" @@ -25,7 +25,7 @@ import { IconButton } from "@opencode-ai/ui/icon-button" import { Tooltip } from "@opencode-ai/ui/tooltip" import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" import { Dialog } from "@opencode-ai/ui/dialog" -import { getFilename } from "@opencode-ai/util/path" +import { getFilename } from "@opencode-ai/shared/util/path" import { Session, type Message } from "@opencode-ai/sdk/v2/client" import { usePlatform } from "@/context/platform" import { useSettings } from "@/context/settings" @@ -48,8 +48,8 @@ import { } from "@/context/global-sync/session-prefetch" import { useNotification } from "@/context/notification" import { usePermission } from "@/context/permission" -import { Binary } from "@opencode-ai/util/binary" -import { retry } from "@opencode-ai/util/retry" +import { Binary } from "@opencode-ai/shared/util/binary" +import { retry } from "@opencode-ai/shared/util/retry" import { playSoundById } from "@/utils/sound" import { createAim } from "@/utils/aim" import { setNavigate } from "@/utils/notification-click" diff --git a/packages/app/src/pages/layout/helpers.ts b/packages/app/src/pages/layout/helpers.ts index 48158debba..26b66d1668 100644 --- a/packages/app/src/pages/layout/helpers.ts +++ b/packages/app/src/pages/layout/helpers.ts @@ -1,4 +1,4 @@ -import { getFilename } from "@opencode-ai/util/path" +import { getFilename } from "@opencode-ai/shared/util/path" import { type Session } from "@opencode-ai/sdk/v2/client" type SessionStore = { diff --git a/packages/app/src/pages/layout/sidebar-items.tsx b/packages/app/src/pages/layout/sidebar-items.tsx index e56accfc83..b0f45859a4 100644 --- a/packages/app/src/pages/layout/sidebar-items.tsx +++ b/packages/app/src/pages/layout/sidebar-items.tsx @@ -4,7 +4,7 @@ import { Icon } from "@opencode-ai/ui/icon" import { IconButton } from "@opencode-ai/ui/icon-button" import { Spinner } from "@opencode-ai/ui/spinner" import { Tooltip } from "@opencode-ai/ui/tooltip" -import { getFilename } from "@opencode-ai/util/path" +import { getFilename } from "@opencode-ai/shared/util/path" import { A, useParams } from "@solidjs/router" import { type Accessor, createMemo, For, type JSX, Match, Show, Switch } from "solid-js" import { useGlobalSync } from "@/context/global-sync" diff --git a/packages/app/src/pages/layout/sidebar-project.tsx b/packages/app/src/pages/layout/sidebar-project.tsx index 7c9ae1aafb..076e1ef88b 100644 --- a/packages/app/src/pages/layout/sidebar-project.tsx +++ b/packages/app/src/pages/layout/sidebar-project.tsx @@ -1,6 +1,6 @@ import { createMemo, For, Show, type Accessor, type JSX } from "solid-js" import { createStore } from "solid-js/store" -import { base64Encode } from "@opencode-ai/util/encode" +import { base64Encode } from "@opencode-ai/shared/util/encode" import { Button } from "@opencode-ai/ui/button" import { ContextMenu } from "@opencode-ai/ui/context-menu" import { HoverCard } from "@opencode-ai/ui/hover-card" diff --git a/packages/app/src/pages/layout/sidebar-workspace.tsx b/packages/app/src/pages/layout/sidebar-workspace.tsx index 878b6e5fa2..9e00691471 100644 --- a/packages/app/src/pages/layout/sidebar-workspace.tsx +++ b/packages/app/src/pages/layout/sidebar-workspace.tsx @@ -3,8 +3,8 @@ import { createEffect, createMemo, For, Show, type Accessor, type JSX } from "so import { createStore } from "solid-js/store" import { createSortable } from "@thisbeyond/solid-dnd" import { createMediaQuery } from "@solid-primitives/media" -import { base64Encode } from "@opencode-ai/util/encode" -import { getFilename } from "@opencode-ai/util/path" +import { base64Encode } from "@opencode-ai/shared/util/encode" +import { getFilename } from "@opencode-ai/shared/util/path" import { Button } from "@opencode-ai/ui/button" import { Collapsible } from "@opencode-ai/ui/collapsible" import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index eb6a494119..e328e3f0cc 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -27,7 +27,7 @@ import { createAutoScroll } from "@opencode-ai/ui/hooks" import { previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge" import { Button } from "@opencode-ai/ui/button" import { showToast } from "@opencode-ai/ui/toast" -import { checksum } from "@opencode-ai/util/encode" +import { checksum } from "@opencode-ai/shared/util/encode" import { useSearchParams } from "@solidjs/router" import { NewSessionView, SessionHeader } from "@/components/session" import { useComments } from "@/context/comments" diff --git a/packages/app/src/pages/session/file-tabs.tsx b/packages/app/src/pages/session/file-tabs.tsx index cb76175236..a64dff64e2 100644 --- a/packages/app/src/pages/session/file-tabs.tsx +++ b/packages/app/src/pages/session/file-tabs.tsx @@ -6,7 +6,7 @@ import type { FileSearchHandle } from "@opencode-ai/ui/file" import { useFileComponent } from "@opencode-ai/ui/context/file" import { cloneSelectedLineRange, previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge" import { createLineCommentController } from "@opencode-ai/ui/line-comment-annotations" -import { sampledChecksum } from "@opencode-ai/util/encode" +import { sampledChecksum } from "@opencode-ai/shared/util/encode" import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" import { IconButton } from "@opencode-ai/ui/icon-button" import { Tabs } from "@opencode-ai/ui/tabs" diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx index 8380163957..978f188b6b 100644 --- a/packages/app/src/pages/session/message-timeline.tsx +++ b/packages/app/src/pages/session/message-timeline.tsx @@ -15,8 +15,8 @@ import { ScrollView } from "@opencode-ai/ui/scroll-view" import { TextField } from "@opencode-ai/ui/text-field" import type { AssistantMessage, Message as MessageType, Part, TextPart, UserMessage } from "@opencode-ai/sdk/v2" import { showToast } from "@opencode-ai/ui/toast" -import { Binary } from "@opencode-ai/util/binary" -import { getFilename } from "@opencode-ai/util/path" +import { Binary } from "@opencode-ai/shared/util/binary" +import { getFilename } from "@opencode-ai/shared/util/path" import { Popover as KobaltePopover } from "@kobalte/core/popover" import { shouldMarkBoundaryGesture, normalizeWheelDelta } from "@/pages/session/message-gesture" import { SessionContextUsage } from "@/components/session-context-usage" diff --git a/packages/app/src/pages/session/use-session-commands.tsx b/packages/app/src/pages/session/use-session-commands.tsx index 2397953737..b5d2544636 100644 --- a/packages/app/src/pages/session/use-session-commands.tsx +++ b/packages/app/src/pages/session/use-session-commands.tsx @@ -12,7 +12,7 @@ import { useSDK } from "@/context/sdk" import { useSync } from "@/context/sync" import { useTerminal } from "@/context/terminal" import { showToast } from "@opencode-ai/ui/toast" -import { findLast } from "@opencode-ai/util/array" +import { findLast } from "@opencode-ai/shared/util/array" import { createSessionTabs } from "@/pages/session/helpers" import { extractPromptFromParts } from "@/utils/prompt" import { UserMessage } from "@opencode-ai/sdk/v2" diff --git a/packages/app/src/utils/base64.ts b/packages/app/src/utils/base64.ts index c1f9d88c6e..f60dff2b6d 100644 --- a/packages/app/src/utils/base64.ts +++ b/packages/app/src/utils/base64.ts @@ -1,4 +1,4 @@ -import { base64Decode } from "@opencode-ai/util/encode" +import { base64Decode } from "@opencode-ai/shared/util/encode" export function decode64(value: string | undefined) { if (value === undefined) return diff --git a/packages/app/src/utils/persist.ts b/packages/app/src/utils/persist.ts index 3dcbeb7d36..dce0e94c3b 100644 --- a/packages/app/src/utils/persist.ts +++ b/packages/app/src/utils/persist.ts @@ -1,6 +1,6 @@ import { Platform, usePlatform } from "@/context/platform" import { makePersisted, type AsyncStorage, type SyncStorage } from "@solid-primitives/storage" -import { checksum } from "@opencode-ai/util/encode" +import { checksum } from "@opencode-ai/shared/util/encode" import { createResource, type Accessor } from "solid-js" import type { SetStoreFunction, Store } from "solid-js/store" diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 0d165e95ff..3c4a835f35 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -13,7 +13,7 @@ "shell-prod": "sst shell --target Teams --stage production" }, "dependencies": { - "@opencode-ai/util": "workspace:*", + "@opencode-ai/shared": "workspace:*", "@opencode-ai/ui": "workspace:*", "aws4fetch": "^1.0.20", "@pierre/diffs": "catalog:", diff --git a/packages/enterprise/src/core/share.ts b/packages/enterprise/src/core/share.ts index 18fcd7a071..1a343272f7 100644 --- a/packages/enterprise/src/core/share.ts +++ b/packages/enterprise/src/core/share.ts @@ -1,6 +1,6 @@ import { Message, Model, Part, Session, SnapshotFileDiff } from "@opencode-ai/sdk/v2" -import { fn } from "@opencode-ai/util/fn" -import { iife } from "@opencode-ai/util/iife" +import { fn } from "@opencode-ai/shared/util/fn" +import { iife } from "@opencode-ai/shared/util/iife" import z from "zod" import { Storage } from "./storage" diff --git a/packages/enterprise/src/core/storage.ts b/packages/enterprise/src/core/storage.ts index b8030b4f90..a6222e4154 100644 --- a/packages/enterprise/src/core/storage.ts +++ b/packages/enterprise/src/core/storage.ts @@ -1,5 +1,5 @@ import { AwsClient } from "aws4fetch" -import { lazy } from "@opencode-ai/util/lazy" +import { lazy } from "@opencode-ai/shared/util/lazy" export namespace Storage { export interface Adapter { diff --git a/packages/enterprise/src/routes/share/[shareID].tsx b/packages/enterprise/src/routes/share/[shareID].tsx index edeeaf1ad5..f3be14e393 100644 --- a/packages/enterprise/src/routes/share/[shareID].tsx +++ b/packages/enterprise/src/routes/share/[shareID].tsx @@ -10,9 +10,9 @@ import { Share } from "~/core/share" import { Logo, Mark } from "@opencode-ai/ui/logo" import { IconButton } from "@opencode-ai/ui/icon-button" import { ProviderIcon } from "@opencode-ai/ui/provider-icon" -import { iife } from "@opencode-ai/util/iife" -import { Binary } from "@opencode-ai/util/binary" -import { NamedError } from "@opencode-ai/util/error" +import { iife } from "@opencode-ai/shared/util/iife" +import { Binary } from "@opencode-ai/shared/util/binary" +import { NamedError } from "@opencode-ai/shared/util/error" import { DateTime } from "luxon" import { createStore } from "solid-js/store" import z from "zod" diff --git a/packages/enterprise/test/core/share.test.ts b/packages/enterprise/test/core/share.test.ts index d49d4b7639..34f3b17a3f 100644 --- a/packages/enterprise/test/core/share.test.ts +++ b/packages/enterprise/test/core/share.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test, afterAll } from "bun:test" import { Share } from "../../src/core/share" import { Storage } from "../../src/core/storage" -import { Identifier } from "@opencode-ai/util/identifier" +import { Identifier } from "@opencode-ai/shared/util/identifier" describe.concurrent("core.share", () => { test("should create a share", async () => { diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 0f0c26ed96..9ddf1fa9f6 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -44,6 +44,7 @@ "@effect/language-service": "0.84.2", "@octokit/webhooks-types": "7.6.1", "@opencode-ai/script": "workspace:*", + "@opencode-ai/shared": "workspace:*", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", @@ -115,7 +116,6 @@ "@opencode-ai/server": "workspace:*", "@opencode-ai/script": "workspace:*", "@opencode-ai/sdk": "workspace:*", - "@opencode-ai/util": "workspace:*", "@opentelemetry/exporter-trace-otlp-http": "0.214.0", "@opentelemetry/sdk-trace-base": "2.6.1", "@opentelemetry/sdk-trace-node": "2.6.1", diff --git a/packages/opencode/src/auth/index.ts b/packages/opencode/src/auth/index.ts index b1502da78c..b287ce551e 100644 --- a/packages/opencode/src/auth/index.ts +++ b/packages/opencode/src/auth/index.ts @@ -2,7 +2,7 @@ import path from "path" import { Effect, Layer, Record, Result, Schema, Context } from "effect" import { zod } from "@/util/effect-zod" import { Global } from "../global" -import { AppFileSystem } from "../filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" export const OAUTH_DUMMY_KEY = "opencode-oauth-dummy-key" diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index 772b5d9a0c..cab162f8f0 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -22,7 +22,7 @@ import { createStore, produce, reconcile } from "solid-js/store" import { useProject } from "@tui/context/project" import { useEvent } from "@tui/context/event" import { useSDK } from "@tui/context/sdk" -import { Binary } from "@opencode-ai/util/binary" +import { Binary } from "@opencode-ai/shared/util/binary" import { createSimpleContext } from "./helper" import type { Snapshot } from "@/snapshot" import { useExit } from "./exit" diff --git a/packages/opencode/src/cli/cmd/tui/context/theme.tsx b/packages/opencode/src/cli/cmd/tui/context/theme.tsx index de81529961..0c0658e743 100644 --- a/packages/opencode/src/cli/cmd/tui/context/theme.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/theme.tsx @@ -2,7 +2,7 @@ import { CliRenderEvents, SyntaxStyle, RGBA, type TerminalColors } from "@opentu import path from "path" import { createEffect, createMemo, onCleanup, onMount } from "solid-js" import { createSimpleContext } from "./helper" -import { Glob } from "../../../../util/glob" +import { Glob } from "@opencode-ai/shared/util/glob" import aura from "./theme/aura.json" with { type: "json" } import ayu from "./theme/ayu.json" with { type: "json" } import catppuccin from "./theme/catppuccin.json" with { type: "json" } diff --git a/packages/opencode/src/cli/ui.ts b/packages/opencode/src/cli/ui.ts index b24a2a2f44..d735a55417 100644 --- a/packages/opencode/src/cli/ui.ts +++ b/packages/opencode/src/cli/ui.ts @@ -1,6 +1,6 @@ import z from "zod" import { EOL } from "os" -import { NamedError } from "@opencode-ai/util/error" +import { NamedError } from "@opencode-ai/shared/util/error" import { logo as glyphs } from "./logo" export namespace UI { diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index f9ca883414..f8205bac26 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -7,7 +7,7 @@ import z from "zod" import { mergeDeep, pipe, unique } from "remeda" import { Global } from "../global" import fsNode from "fs/promises" -import { NamedError } from "@opencode-ai/util/error" +import { NamedError } from "@opencode-ai/shared/util/error" import { Flag } from "../flag/flag" import { Auth } from "../auth" import { Env } from "../env" @@ -26,17 +26,17 @@ import { existsSync } from "fs" import { Bus } from "@/bus" import { GlobalBus } from "@/bus/global" import { Event } from "../server/event" -import { Glob } from "../util/glob" +import { Glob } from "@opencode-ai/shared/util/glob" import { Account } from "@/account" import { isRecord } from "@/util/record" import { ConfigPaths } from "./paths" import type { ConsoleState } from "./console-state" -import { AppFileSystem } from "@/filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { InstanceState } from "@/effect/instance-state" import { Context, Duration, Effect, Exit, Fiber, Layer, Option } from "effect" import { Flock } from "@/util/flock" import { isPathPluginSpec, parsePluginSpecifier, resolvePathPluginTarget } from "@/plugin/shared" -import { Npm } from "@/npm" +import { Npm } from "../npm" import { InstanceRef } from "@/effect/instance-ref" export namespace Config { diff --git a/packages/opencode/src/config/markdown.ts b/packages/opencode/src/config/markdown.ts index 3c9709b5b3..2f1483dca3 100644 --- a/packages/opencode/src/config/markdown.ts +++ b/packages/opencode/src/config/markdown.ts @@ -1,4 +1,4 @@ -import { NamedError } from "@opencode-ai/util/error" +import { NamedError } from "@opencode-ai/shared/util/error" import matter from "gray-matter" import { z } from "zod" import { Filesystem } from "../util/filesystem" diff --git a/packages/opencode/src/config/paths.ts b/packages/opencode/src/config/paths.ts index 82ccf3945f..884a774499 100644 --- a/packages/opencode/src/config/paths.ts +++ b/packages/opencode/src/config/paths.ts @@ -2,7 +2,7 @@ import path from "path" import os from "os" import z from "zod" import { type ParseError as JsoncParseError, parse as parseJsonc, printParseErrorCode } from "jsonc-parser" -import { NamedError } from "@opencode-ai/util/error" +import { NamedError } from "@opencode-ai/shared/util/error" import { Filesystem } from "@/util/filesystem" import { Flag } from "@/flag/flag" import { Global } from "@/global" diff --git a/packages/opencode/src/config/tui.ts b/packages/opencode/src/config/tui.ts index 9347a8cc4c..87c39e700a 100644 --- a/packages/opencode/src/config/tui.ts +++ b/packages/opencode/src/config/tui.ts @@ -13,7 +13,7 @@ import { Global } from "@/global" import { Filesystem } from "@/util/filesystem" import { InstanceState } from "@/effect/instance-state" import { makeRuntime } from "@/effect/run-service" -import { AppFileSystem } from "@/filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" export namespace TuiConfig { const log = Log.create({ service: "tui.config" }) diff --git a/packages/opencode/src/control-plane/workspace.ts b/packages/opencode/src/control-plane/workspace.ts index 78f3d770eb..b9ac0a6b43 100644 --- a/packages/opencode/src/control-plane/workspace.ts +++ b/packages/opencode/src/control-plane/workspace.ts @@ -11,7 +11,7 @@ import { Flag } from "@/flag/flag" import { Log } from "@/util/log" import { Filesystem } from "@/util/filesystem" import { ProjectID } from "@/project/schema" -import { Slug } from "@opencode-ai/util/slug" +import { Slug } from "@opencode-ai/shared/util/slug" import { WorkspaceTable } from "./workspace.sql" import { getAdaptor } from "./adaptors" import { WorkspaceInfo } from "./types" diff --git a/packages/opencode/src/effect/app-runtime.ts b/packages/opencode/src/effect/app-runtime.ts index b13396615f..5948bd25e6 100644 --- a/packages/opencode/src/effect/app-runtime.ts +++ b/packages/opencode/src/effect/app-runtime.ts @@ -2,7 +2,7 @@ import { Layer, ManagedRuntime } from "effect" import { attach, memoMap } from "./run-service" import { Observability } from "./observability" -import { AppFileSystem } from "@/filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { Bus } from "@/bus" import { Auth } from "@/auth" import { Account } from "@/account" diff --git a/packages/opencode/src/file/ignore.ts b/packages/opencode/src/file/ignore.ts index b9731040c7..a102e7d170 100644 --- a/packages/opencode/src/file/ignore.ts +++ b/packages/opencode/src/file/ignore.ts @@ -1,5 +1,5 @@ import { sep } from "node:path" -import { Glob } from "../util/glob" +import { Glob } from "@opencode-ai/shared/util/glob" export namespace FileIgnore { const FOLDERS = new Set([ diff --git a/packages/opencode/src/file/index.ts b/packages/opencode/src/file/index.ts index 6730957f23..113dc59096 100644 --- a/packages/opencode/src/file/index.ts +++ b/packages/opencode/src/file/index.ts @@ -1,7 +1,7 @@ import { BusEvent } from "@/bus/bus-event" import { InstanceState } from "@/effect/instance-state" import { makeRuntime } from "@/effect/run-service" -import { AppFileSystem } from "@/filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { Git } from "@/git" import { Effect, Layer, Context } from "effect" import * as Stream from "effect/Stream" diff --git a/packages/opencode/src/file/time.ts b/packages/opencode/src/file/time.ts index e5055b6718..5537526730 100644 --- a/packages/opencode/src/file/time.ts +++ b/packages/opencode/src/file/time.ts @@ -1,6 +1,6 @@ import { DateTime, Effect, Layer, Option, Semaphore, Context } from "effect" import { InstanceState } from "@/effect/instance-state" -import { AppFileSystem } from "@/filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { Flag } from "@/flag/flag" import type { SessionID } from "@/session/schema" import { Filesystem } from "@/util/filesystem" diff --git a/packages/opencode/src/format/formatter.ts b/packages/opencode/src/format/formatter.ts index ada661ebab..6c17310ff8 100644 --- a/packages/opencode/src/format/formatter.ts +++ b/packages/opencode/src/format/formatter.ts @@ -1,4 +1,4 @@ -import { Npm } from "@/npm" +import { Npm } from "../npm" import { Instance } from "../project/instance" import { Filesystem } from "../util/filesystem" import { Process } from "../util/process" diff --git a/packages/opencode/src/ide/index.ts b/packages/opencode/src/ide/index.ts index ce4128b906..46efea2cce 100644 --- a/packages/opencode/src/ide/index.ts +++ b/packages/opencode/src/ide/index.ts @@ -1,7 +1,7 @@ import { BusEvent } from "@/bus/bus-event" import { Bus } from "@/bus" import z from "zod" -import { NamedError } from "@opencode-ai/util/error" +import { NamedError } from "@opencode-ai/shared/util/error" import { Log } from "../util/log" import { Process } from "@/util/process" diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 753becc267..641411461d 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -11,7 +11,7 @@ import { UninstallCommand } from "./cli/cmd/uninstall" import { ModelsCommand } from "./cli/cmd/models" import { UI } from "./cli/ui" import { Installation } from "./installation" -import { NamedError } from "@opencode-ai/util/error" +import { NamedError } from "@opencode-ai/shared/util/error" import { FormatError } from "./cli/error" import { ServeCommand } from "./cli/cmd/serve" import { Filesystem } from "./util/filesystem" diff --git a/packages/opencode/src/lsp/client.ts b/packages/opencode/src/lsp/client.ts index de0c438626..fe5a9ab182 100644 --- a/packages/opencode/src/lsp/client.ts +++ b/packages/opencode/src/lsp/client.ts @@ -9,7 +9,7 @@ import { Process } from "../util/process" import { LANGUAGE_EXTENSIONS } from "./language" import z from "zod" import type { LSPServer } from "./server" -import { NamedError } from "@opencode-ai/util/error" +import { NamedError } from "@opencode-ai/shared/util/error" import { withTimeout } from "../util/timeout" import { Instance } from "../project/instance" import { Filesystem } from "../util/filesystem" diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index abfb31ead0..9ffef7a425 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -11,9 +11,9 @@ import { Flag } from "../flag/flag" import { Archive } from "../util/archive" import { Process } from "../util/process" import { which } from "../util/which" -import { Module } from "@opencode-ai/util/module" +import { Module } from "@opencode-ai/shared/util/module" import { spawn } from "./launch" -import { Npm } from "@/npm" +import { Npm } from "../npm" export namespace LSPServer { const log = Log.create({ service: "lsp.server" }) diff --git a/packages/opencode/src/mcp/auth.ts b/packages/opencode/src/mcp/auth.ts index 6eefc107d9..85f9e1d8c9 100644 --- a/packages/opencode/src/mcp/auth.ts +++ b/packages/opencode/src/mcp/auth.ts @@ -2,7 +2,7 @@ import path from "path" import z from "zod" import { Global } from "../global" import { Effect, Layer, Context } from "effect" -import { AppFileSystem } from "@/filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" export namespace McpAuth { export const Tokens = z.object({ diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 3de427d7e4..3b66909340 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -11,12 +11,12 @@ import { } from "@modelcontextprotocol/sdk/types.js" import { Config } from "../config/config" import { Log } from "../util/log" -import { NamedError } from "@opencode-ai/util/error" +import { NamedError } from "@opencode-ai/shared/util/error" import z from "zod/v4" import { Instance } from "../project/instance" import { Installation } from "../installation" import { withTimeout } from "@/util/timeout" -import { AppFileSystem } from "@/filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { McpOAuthProvider } from "./oauth-provider" import { McpOAuthCallback } from "./oauth-callback" import { McpAuth } from "./auth" diff --git a/packages/opencode/src/npm/index.ts b/packages/opencode/src/npm/index.ts index 3568ff20e2..5b708431c6 100644 --- a/packages/opencode/src/npm/index.ts +++ b/packages/opencode/src/npm/index.ts @@ -1,6 +1,6 @@ import semver from "semver" import z from "zod" -import { NamedError } from "@opencode-ai/util/error" +import { NamedError } from "@opencode-ai/shared/util/error" import { Global } from "../global" import { Log } from "../util/log" import path from "path" diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index 0dc53d997d..c716ffdf8d 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -12,7 +12,7 @@ import { createOpencodeClient } from "@opencode-ai/sdk" import { Flag } from "../flag/flag" import { CodexAuthPlugin } from "./codex" import { Session } from "../session" -import { NamedError } from "@opencode-ai/util/error" +import { NamedError } from "@opencode-ai/shared/util/error" import { CopilotAuthPlugin } from "./github-copilot/copilot" import { gitlabAuthPlugin as GitlabAuthPlugin } from "opencode-gitlab-auth" import { PoeAuthPlugin } from "opencode-poe-auth" diff --git a/packages/opencode/src/plugin/shared.ts b/packages/opencode/src/plugin/shared.ts index 6cda49786b..54cc32af5b 100644 --- a/packages/opencode/src/plugin/shared.ts +++ b/packages/opencode/src/plugin/shared.ts @@ -2,7 +2,7 @@ import path from "path" import { fileURLToPath, pathToFileURL } from "url" import npa from "npm-package-arg" import semver from "semver" -import { Npm } from "@/npm" +import { Npm } from "../npm" import { Filesystem } from "@/util/filesystem" import { isRecord } from "@/util/record" diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index f9d634a1cd..d20bf42494 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -11,7 +11,7 @@ import { ProjectID } from "./schema" import { Effect, Layer, Path, Scope, Context, Stream } from "effect" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { NodePath } from "@effect/platform-node" -import { AppFileSystem } from "@/filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" export namespace Project { diff --git a/packages/opencode/src/project/vcs.ts b/packages/opencode/src/project/vcs.ts index aede980d6d..e4093fd456 100644 --- a/packages/opencode/src/project/vcs.ts +++ b/packages/opencode/src/project/vcs.ts @@ -4,7 +4,7 @@ import path from "path" import { Bus } from "@/bus" import { BusEvent } from "@/bus/bus-event" import { InstanceState } from "@/effect/instance-state" -import { AppFileSystem } from "@/filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { FileWatcher } from "@/file/watcher" import { Git } from "@/git" import { Log } from "@/util/log" diff --git a/packages/opencode/src/provider/auth.ts b/packages/opencode/src/provider/auth.ts index e410b86365..c66ccffc12 100644 --- a/packages/opencode/src/provider/auth.ts +++ b/packages/opencode/src/provider/auth.ts @@ -1,5 +1,5 @@ import type { AuthOAuthResult, Hooks } from "@opencode-ai/plugin" -import { NamedError } from "@opencode-ai/util/error" +import { NamedError } from "@opencode-ai/shared/util/error" import { Auth } from "@/auth" import { InstanceState } from "@/effect/instance-state" import { Plugin } from "../plugin" diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 84f074d42d..d34721f1d8 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -8,7 +8,7 @@ import { Log } from "../util/log" import { Npm } from "../npm" import { Hash } from "../util/hash" import { Plugin } from "../plugin" -import { NamedError } from "@opencode-ai/util/error" +import { NamedError } from "@opencode-ai/shared/util/error" import { type LanguageModelV3 } from "@ai-sdk/provider" import { ModelsDev } from "./models" import { Auth } from "../auth" @@ -21,7 +21,7 @@ import path from "path" import { Effect, Layer, Context } from "effect" import { EffectLogger } from "@/effect/logger" import { InstanceState } from "@/effect/instance-state" -import { AppFileSystem } from "@/filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { isRecord } from "@/util/record" // Direct imports for bundled providers diff --git a/packages/opencode/src/pty/index.ts b/packages/opencode/src/pty/index.ts index 1891721851..9c79eb2d4c 100644 --- a/packages/opencode/src/pty/index.ts +++ b/packages/opencode/src/pty/index.ts @@ -5,7 +5,7 @@ import { Instance } from "@/project/instance" import type { Proc } from "#pty" import z from "zod" import { Log } from "../util/log" -import { lazy } from "@opencode-ai/util/lazy" +import { lazy } from "@opencode-ai/shared/util/lazy" import { Shell } from "@/shell/shell" import { Plugin } from "@/plugin" import { PtyID } from "./schema" diff --git a/packages/opencode/src/server/instance/session.ts b/packages/opencode/src/server/instance/session.ts index e443a6ddd2..a011c32f9b 100644 --- a/packages/opencode/src/server/instance/session.ts +++ b/packages/opencode/src/server/instance/session.ts @@ -25,7 +25,7 @@ import { ModelID, ProviderID } from "@/provider/schema" import { errors } from "../error" import { lazy } from "../../util/lazy" import { Bus } from "../../bus" -import { NamedError } from "@opencode-ai/util/error" +import { NamedError } from "@opencode-ai/shared/util/error" const log = Log.create({ service: "server" }) diff --git a/packages/opencode/src/server/middleware.ts b/packages/opencode/src/server/middleware.ts index d0539eb247..6e91651866 100644 --- a/packages/opencode/src/server/middleware.ts +++ b/packages/opencode/src/server/middleware.ts @@ -1,5 +1,5 @@ import { Provider } from "../provider/provider" -import { NamedError } from "@opencode-ai/util/error" +import { NamedError } from "@opencode-ai/shared/util/error" import { NotFoundError } from "../storage/db" import { Session } from "../session" import type { ContentfulStatusCode } from "hono/utils/http-status" diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index aa3404ad75..d8ab812349 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -1,4 +1,4 @@ -import { Slug } from "@opencode-ai/util/slug" +import { Slug } from "@opencode-ai/shared/util/slug" import path from "path" import { BusEvent } from "@/bus/bus-event" import { Bus } from "@/bus" diff --git a/packages/opencode/src/session/instruction.ts b/packages/opencode/src/session/instruction.ts index 04f2610dfe..b4794ba5b1 100644 --- a/packages/opencode/src/session/instruction.ts +++ b/packages/opencode/src/session/instruction.ts @@ -5,7 +5,7 @@ import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/ import { Config } from "@/config/config" import { InstanceState } from "@/effect/instance-state" import { Flag } from "@/flag/flag" -import { AppFileSystem } from "@/filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { withTransientReadRetry } from "@/util/effect-http-client" import { Global } from "../global" import { Instance } from "../project/instance" diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 4c18d1f7e0..8c82d4d73f 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -1,7 +1,7 @@ import { BusEvent } from "@/bus/bus-event" import { SessionID, MessageID, PartID } from "./schema" import z from "zod" -import { NamedError } from "@opencode-ai/util/error" +import { NamedError } from "@opencode-ai/shared/util/error" import { APICallError, convertToModelMessages, LoadAPIKeyError, type ModelMessage, type UIMessage } from "ai" import { LSP } from "../lsp" import { Snapshot } from "@/snapshot" diff --git a/packages/opencode/src/session/message.ts b/packages/opencode/src/session/message.ts index ee5eac08b6..396034825a 100644 --- a/packages/opencode/src/session/message.ts +++ b/packages/opencode/src/session/message.ts @@ -1,7 +1,7 @@ import z from "zod" import { SessionID } from "./schema" import { ModelID, ProviderID } from "../provider/schema" -import { NamedError } from "@opencode-ai/util/error" +import { NamedError } from "@opencode-ai/shared/util/error" export namespace Message { export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({})) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 3efcc03657..f8c794505e 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -32,14 +32,14 @@ import { Command } from "../command" import { pathToFileURL, fileURLToPath } from "url" import { ConfigMarkdown } from "../config/markdown" import { SessionSummary } from "./summary" -import { NamedError } from "@opencode-ai/util/error" +import { NamedError } from "@opencode-ai/shared/util/error" import { SessionProcessor } from "./processor" import { Tool } from "@/tool/tool" import { Permission } from "@/permission" import { SessionStatus } from "./status" import { LLM } from "./llm" import { Shell } from "@/shell/shell" -import { AppFileSystem } from "@/filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { Truncate } from "@/tool/truncate" import { decodeDataUrl } from "@/util/data-url" import { Process } from "@/util/process" diff --git a/packages/opencode/src/session/retry.ts b/packages/opencode/src/session/retry.ts index 5ec9a585b0..39eb8cfb74 100644 --- a/packages/opencode/src/session/retry.ts +++ b/packages/opencode/src/session/retry.ts @@ -1,4 +1,4 @@ -import type { NamedError } from "@opencode-ai/util/error" +import type { NamedError } from "@opencode-ai/shared/util/error" import { Cause, Clock, Duration, Effect, Schedule } from "effect" import { MessageV2 } from "./message-v2" import { iife } from "@/util/iife" diff --git a/packages/opencode/src/skill/discovery.ts b/packages/opencode/src/skill/discovery.ts index 0bc3ee6290..0323f250f6 100644 --- a/packages/opencode/src/skill/discovery.ts +++ b/packages/opencode/src/skill/discovery.ts @@ -2,7 +2,7 @@ import { NodePath } from "@effect/platform-node" import { Effect, Layer, Path, Schema, Context } from "effect" import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" import { withTransientReadRetry } from "@/util/effect-http-client" -import { AppFileSystem } from "@/filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { Global } from "../global" import { Log } from "../util/log" diff --git a/packages/opencode/src/skill/index.ts b/packages/opencode/src/skill/index.ts index 6c4f290a08..79b426c69c 100644 --- a/packages/opencode/src/skill/index.ts +++ b/packages/opencode/src/skill/index.ts @@ -3,17 +3,17 @@ import path from "path" import { pathToFileURL } from "url" import z from "zod" import { Effect, Layer, Context } from "effect" -import { NamedError } from "@opencode-ai/util/error" +import { NamedError } from "@opencode-ai/shared/util/error" import type { Agent } from "@/agent/agent" import { Bus } from "@/bus" import { InstanceState } from "@/effect/instance-state" import { Flag } from "@/flag/flag" import { Global } from "@/global" import { Permission } from "@/permission" -import { AppFileSystem } from "@/filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { Config } from "../config/config" import { ConfigMarkdown } from "../config/markdown" -import { Glob } from "../util/glob" +import { Glob } from "@opencode-ai/shared/util/glob" import { Log } from "../util/log" import { Discovery } from "./discovery" diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 06c91442ac..2b21f7e895 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -5,7 +5,7 @@ import path from "path" import z from "zod" import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" import { InstanceState } from "@/effect/instance-state" -import { AppFileSystem } from "@/filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { Hash } from "@/util/hash" import { Config } from "../config/config" import { Global } from "../global" diff --git a/packages/opencode/src/storage/db.ts b/packages/opencode/src/storage/db.ts index a7dbf93800..68a41e471f 100644 --- a/packages/opencode/src/storage/db.ts +++ b/packages/opencode/src/storage/db.ts @@ -6,7 +6,7 @@ import { LocalContext } from "../util/local-context" import { lazy } from "../util/lazy" import { Global } from "../global" import { Log } from "../util/log" -import { NamedError } from "@opencode-ai/util/error" +import { NamedError } from "@opencode-ai/shared/util/error" import z from "zod" import path from "path" import { readFileSync, readdirSync, existsSync } from "fs" diff --git a/packages/opencode/src/storage/json-migration.ts b/packages/opencode/src/storage/json-migration.ts index 400e3dc9ef..89d27b9a7b 100644 --- a/packages/opencode/src/storage/json-migration.ts +++ b/packages/opencode/src/storage/json-migration.ts @@ -8,7 +8,7 @@ import { SessionShareTable } from "../share/share.sql" import path from "path" import { existsSync } from "fs" import { Filesystem } from "../util/filesystem" -import { Glob } from "../util/glob" +import { Glob } from "@opencode-ai/shared/util/glob" export namespace JsonMigration { const log = Log.create({ service: "json-migration" }) diff --git a/packages/opencode/src/storage/storage.ts b/packages/opencode/src/storage/storage.ts index a123cd664f..359c750ced 100644 --- a/packages/opencode/src/storage/storage.ts +++ b/packages/opencode/src/storage/storage.ts @@ -1,9 +1,9 @@ import { Log } from "../util/log" import path from "path" import { Global } from "../global" -import { NamedError } from "@opencode-ai/util/error" +import { NamedError } from "@opencode-ai/shared/util/error" import z from "zod" -import { AppFileSystem } from "@/filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { Effect, Exit, Layer, Option, RcMap, Schema, Context, TxReentrantLock } from "effect" import { Git } from "@/git" diff --git a/packages/opencode/src/tool/apply_patch.ts b/packages/opencode/src/tool/apply_patch.ts index fd38a9b224..b9877d8fec 100644 --- a/packages/opencode/src/tool/apply_patch.ts +++ b/packages/opencode/src/tool/apply_patch.ts @@ -10,7 +10,7 @@ import { createTwoFilesPatch, diffLines } from "diff" import { assertExternalDirectoryEffect } from "./external-directory" import { trimDiff } from "./edit" import { LSP } from "../lsp" -import { AppFileSystem } from "../filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" import DESCRIPTION from "./apply_patch.txt" import { File } from "../file" import { Format } from "../format" diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index ad6739b9a9..3bb936944c 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -8,7 +8,7 @@ import { Instance } from "../project/instance" import { lazy } from "@/util/lazy" import { Language, type Node } from "web-tree-sitter" -import { AppFileSystem } from "@/filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { fileURLToPath } from "url" import { Flag } from "@/flag/flag" import { Shell } from "@/shell/shell" diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index a835714c69..bc8478e39f 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -19,7 +19,7 @@ import { Filesystem } from "../util/filesystem" import { Instance } from "../project/instance" import { Snapshot } from "@/snapshot" import { assertExternalDirectoryEffect } from "./external-directory" -import { AppFileSystem } from "../filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" function normalizeLineEndings(text: string): string { return text.replaceAll("\r\n", "\n") diff --git a/packages/opencode/src/tool/external-directory.ts b/packages/opencode/src/tool/external-directory.ts index 9df3e0aaf5..352cc07390 100644 --- a/packages/opencode/src/tool/external-directory.ts +++ b/packages/opencode/src/tool/external-directory.ts @@ -4,7 +4,7 @@ import { EffectLogger } from "@/effect/logger" import { InstanceState } from "@/effect/instance-state" import type { Tool } from "./tool" import { Instance } from "../project/instance" -import { AppFileSystem } from "../filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" type Kind = "file" | "directory" diff --git a/packages/opencode/src/tool/glob.ts b/packages/opencode/src/tool/glob.ts index c1577bc7d6..778a74ddcf 100644 --- a/packages/opencode/src/tool/glob.ts +++ b/packages/opencode/src/tool/glob.ts @@ -3,7 +3,7 @@ import z from "zod" import { Effect, Option } from "effect" import * as Stream from "effect/Stream" import { InstanceState } from "@/effect/instance-state" -import { AppFileSystem } from "../filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { Ripgrep } from "../file/ripgrep" import { assertExternalDirectoryEffect } from "./external-directory" import DESCRIPTION from "./glob.txt" diff --git a/packages/opencode/src/tool/grep.ts b/packages/opencode/src/tool/grep.ts index 0d717ba372..9a2bab5b2d 100644 --- a/packages/opencode/src/tool/grep.ts +++ b/packages/opencode/src/tool/grep.ts @@ -2,7 +2,7 @@ import path from "path" import z from "zod" import { Effect, Option } from "effect" import { InstanceState } from "@/effect/instance-state" -import { AppFileSystem } from "../filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { Ripgrep } from "../file/ripgrep" import { assertExternalDirectoryEffect } from "./external-directory" import DESCRIPTION from "./grep.txt" diff --git a/packages/opencode/src/tool/lsp.ts b/packages/opencode/src/tool/lsp.ts index c5a5d6f819..36cab3c1c3 100644 --- a/packages/opencode/src/tool/lsp.ts +++ b/packages/opencode/src/tool/lsp.ts @@ -7,7 +7,7 @@ import DESCRIPTION from "./lsp.txt" import { Instance } from "../project/instance" import { pathToFileURL } from "url" import { assertExternalDirectoryEffect } from "./external-directory" -import { AppFileSystem } from "../filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" const operations = [ "goToDefinition", diff --git a/packages/opencode/src/tool/read.ts b/packages/opencode/src/tool/read.ts index 501a8c97ed..701bfc4b9d 100644 --- a/packages/opencode/src/tool/read.ts +++ b/packages/opencode/src/tool/read.ts @@ -5,7 +5,7 @@ import { open } from "fs/promises" import * as path from "path" import { createInterface } from "readline" import { Tool } from "./tool" -import { AppFileSystem } from "../filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { LSP } from "../lsp" import { FileTime } from "../file/time" import DESCRIPTION from "./read.txt" diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 2e7da5b506..6900feecc3 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -26,7 +26,7 @@ import { Log } from "@/util/log" import { LspTool } from "./lsp" import { Truncate } from "./truncate" import { ApplyPatchTool } from "./apply_patch" -import { Glob } from "../util/glob" +import { Glob } from "@opencode-ai/shared/util/glob" import path from "path" import { pathToFileURL } from "url" import { Effect, Layer, Context } from "effect" @@ -41,7 +41,7 @@ import { Todo } from "../session/todo" import { LSP } from "../lsp" import { FileTime } from "../file/time" import { Instruction } from "../session/instruction" -import { AppFileSystem } from "../filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { Bus } from "../bus" import { Agent } from "../agent/agent" import { Skill } from "../skill" diff --git a/packages/opencode/src/tool/truncate.ts b/packages/opencode/src/tool/truncate.ts index e6bab1a16b..a7bd8a4b16 100644 --- a/packages/opencode/src/tool/truncate.ts +++ b/packages/opencode/src/tool/truncate.ts @@ -2,7 +2,7 @@ import { NodePath } from "@effect/platform-node" import { Cause, Duration, Effect, Layer, Schedule, Context } from "effect" import path from "path" import type { Agent } from "../agent/agent" -import { AppFileSystem } from "@/filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { evaluate } from "@/permission/evaluate" import { Identifier } from "../id/id" import { Log } from "../util/log" diff --git a/packages/opencode/src/tool/write.ts b/packages/opencode/src/tool/write.ts index 7a9d82cf8b..337c2708c9 100644 --- a/packages/opencode/src/tool/write.ts +++ b/packages/opencode/src/tool/write.ts @@ -10,7 +10,7 @@ import { File } from "../file" import { FileWatcher } from "../file/watcher" import { Format } from "../format" import { FileTime } from "../file/time" -import { AppFileSystem } from "../filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { Instance } from "../project/instance" import { trimDiff } from "./edit" import { assertExternalDirectoryEffect } from "./external-directory" diff --git a/packages/opencode/src/util/filesystem.ts b/packages/opencode/src/util/filesystem.ts index 5f50231b03..b4aef05456 100644 --- a/packages/opencode/src/util/filesystem.ts +++ b/packages/opencode/src/util/filesystem.ts @@ -5,7 +5,7 @@ import { realpathSync } from "fs" import { dirname, join, relative, resolve as pathResolve, win32 } from "path" import { Readable } from "stream" import { pipeline } from "stream/promises" -import { Glob } from "./glob" +import { Glob } from "@opencode-ai/shared/util/glob" export namespace Filesystem { // Fast sync version for metadata checks diff --git a/packages/opencode/src/util/log.ts b/packages/opencode/src/util/log.ts index f94e9866f7..7812632768 100644 --- a/packages/opencode/src/util/log.ts +++ b/packages/opencode/src/util/log.ts @@ -3,7 +3,7 @@ import fs from "fs/promises" import { createWriteStream } from "fs" import { Global } from "../global" import z from "zod" -import { Glob } from "./glob" +import { Glob } from "@opencode-ai/shared/util/glob" export namespace Log { export const Level = z.enum(["DEBUG", "INFO", "WARN", "ERROR"]).meta({ ref: "LogLevel", description: "Log level" }) diff --git a/packages/opencode/src/worktree/index.ts b/packages/opencode/src/worktree/index.ts index 18240524a1..14a3a0dc9b 100644 --- a/packages/opencode/src/worktree/index.ts +++ b/packages/opencode/src/worktree/index.ts @@ -1,5 +1,5 @@ import z from "zod" -import { NamedError } from "@opencode-ai/util/error" +import { NamedError } from "@opencode-ai/shared/util/error" import { Global } from "../global" import { Instance } from "../project/instance" import { InstanceBootstrap } from "../project/bootstrap" @@ -8,7 +8,7 @@ import { Database, eq } from "../storage/db" import { ProjectTable } from "../project/project.sql" import type { ProjectID } from "../project/schema" import { Log } from "../util/log" -import { Slug } from "@opencode-ai/util/slug" +import { Slug } from "@opencode-ai/shared/util/slug" import { errorMessage } from "../util/error" import { BusEvent } from "@/bus/bus-event" import { GlobalBus } from "@/bus/global" @@ -16,7 +16,7 @@ import { Git } from "@/git" import { Effect, Layer, Path, Scope, Context, Stream } from "effect" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { NodePath } from "@effect/platform-node" -import { AppFileSystem } from "@/filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { BootstrapRuntime } from "@/effect/bootstrap-runtime" import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" import { InstanceState } from "@/effect/instance-state" diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index e759985feb..ed7e689da4 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -5,7 +5,7 @@ import { Config } from "../../src/config/config" import { Instance } from "../../src/project/instance" import { Auth } from "../../src/auth" import { AccessToken, Account, AccountID, OrgID } from "../../src/account" -import { AppFileSystem } from "../../src/filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { Env } from "../../src/env" import { provideTmpdirInstance } from "../fixture/fixture" import { tmpdir, tmpdirScoped } from "../fixture/fixture" diff --git a/packages/opencode/test/filesystem/filesystem.test.ts b/packages/opencode/test/filesystem/filesystem.test.ts index ca73b3336b..0bb4ba5839 100644 --- a/packages/opencode/test/filesystem/filesystem.test.ts +++ b/packages/opencode/test/filesystem/filesystem.test.ts @@ -1,7 +1,7 @@ import { describe, test, expect } from "bun:test" import { Effect, Layer } from "effect" import { NodeFileSystem } from "@effect/platform-node" -import { AppFileSystem } from "../../src/filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { testEffect } from "../lib/effect" import path from "path" diff --git a/packages/opencode/test/project/project.test.ts b/packages/opencode/test/project/project.test.ts index eddd79bc6f..ba253a9205 100644 --- a/packages/opencode/test/project/project.test.ts +++ b/packages/opencode/test/project/project.test.ts @@ -9,7 +9,7 @@ import { ProjectID } from "../../src/project/schema" import { Effect, Layer, Stream } from "effect" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { NodePath } from "@effect/platform-node" -import { AppFileSystem } from "../../src/filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" Log.init({ print: false }) diff --git a/packages/opencode/test/session/prompt-effect.test.ts b/packages/opencode/test/session/prompt-effect.test.ts index 244f778ca8..94561206e2 100644 --- a/packages/opencode/test/session/prompt-effect.test.ts +++ b/packages/opencode/test/session/prompt-effect.test.ts @@ -22,7 +22,7 @@ import { Todo } from "../../src/session/todo" import { Session } from "../../src/session" import { LLM } from "../../src/session/llm" import { MessageV2 } from "../../src/session/message-v2" -import { AppFileSystem } from "../../src/filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { SessionCompaction } from "../../src/session/compaction" import { SessionSummary } from "../../src/session/summary" import { Instruction } from "../../src/session/instruction" diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index c1d6f1da97..1290570b81 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -1,6 +1,6 @@ import path from "path" import { describe, expect, test } from "bun:test" -import { NamedError } from "@opencode-ai/util/error" +import { NamedError } from "@opencode-ai/shared/util/error" import { fileURLToPath } from "url" import { Effect, Layer } from "effect" import { Instance } from "../../src/project/instance" diff --git a/packages/opencode/test/session/retry.test.ts b/packages/opencode/test/session/retry.test.ts index a598c37fcd..314306ba62 100644 --- a/packages/opencode/test/session/retry.test.ts +++ b/packages/opencode/test/session/retry.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import type { NamedError } from "@opencode-ai/util/error" +import type { NamedError } from "@opencode-ai/shared/util/error" import { APICallError } from "ai" import { setTimeout as sleep } from "node:timers/promises" import { Effect, Schedule } from "effect" diff --git a/packages/opencode/test/session/snapshot-tool-race.test.ts b/packages/opencode/test/session/snapshot-tool-race.test.ts index 464182395a..80d74c7565 100644 --- a/packages/opencode/test/session/snapshot-tool-race.test.ts +++ b/packages/opencode/test/session/snapshot-tool-race.test.ts @@ -53,7 +53,7 @@ import { Shell } from "../../src/shell/shell" import { Snapshot } from "../../src/snapshot" import { ToolRegistry } from "../../src/tool/registry" import { Truncate } from "../../src/tool/truncate" -import { AppFileSystem } from "../../src/filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" import { Ripgrep } from "../../src/file/ripgrep" import { Format } from "../../src/format" diff --git a/packages/opencode/test/storage/storage.test.ts b/packages/opencode/test/storage/storage.test.ts index ea8f1feb4f..60b458bb30 100644 --- a/packages/opencode/test/storage/storage.test.ts +++ b/packages/opencode/test/storage/storage.test.ts @@ -1,7 +1,7 @@ import { describe, expect } from "bun:test" import path from "path" import { Effect, Exit, Layer } from "effect" -import { AppFileSystem } from "../../src/filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" import { Git } from "../../src/git" import { Global } from "../../src/global" diff --git a/packages/opencode/test/tool/apply_patch.test.ts b/packages/opencode/test/tool/apply_patch.test.ts index 03220ea3b1..c0448c78cb 100644 --- a/packages/opencode/test/tool/apply_patch.test.ts +++ b/packages/opencode/test/tool/apply_patch.test.ts @@ -5,7 +5,7 @@ import { Effect, ManagedRuntime, Layer } from "effect" import { ApplyPatchTool } from "../../src/tool/apply_patch" import { Instance } from "../../src/project/instance" import { LSP } from "../../src/lsp" -import { AppFileSystem } from "../../src/filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { Format } from "../../src/format" import { Agent } from "../../src/agent/agent" import { Bus } from "../../src/bus" diff --git a/packages/opencode/test/tool/bash.test.ts b/packages/opencode/test/tool/bash.test.ts index fc7a9e73e4..3b03da57ee 100644 --- a/packages/opencode/test/tool/bash.test.ts +++ b/packages/opencode/test/tool/bash.test.ts @@ -12,7 +12,7 @@ import { Agent } from "../../src/agent/agent" import { Truncate } from "../../src/tool/truncate" import { SessionID, MessageID } from "../../src/session/schema" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" -import { AppFileSystem } from "../../src/filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { Plugin } from "../../src/plugin" const runtime = ManagedRuntime.make( diff --git a/packages/opencode/test/tool/edit.test.ts b/packages/opencode/test/tool/edit.test.ts index 9fe24b49b2..37a19a5fda 100644 --- a/packages/opencode/test/tool/edit.test.ts +++ b/packages/opencode/test/tool/edit.test.ts @@ -7,7 +7,7 @@ import { Instance } from "../../src/project/instance" import { tmpdir } from "../fixture/fixture" import { FileTime } from "../../src/file/time" import { LSP } from "../../src/lsp" -import { AppFileSystem } from "../../src/filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { Format } from "../../src/format" import { Agent } from "../../src/agent/agent" import { Bus } from "../../src/bus" diff --git a/packages/opencode/test/tool/glob.test.ts b/packages/opencode/test/tool/glob.test.ts index 092885ed18..20e761fc10 100644 --- a/packages/opencode/test/tool/glob.test.ts +++ b/packages/opencode/test/tool/glob.test.ts @@ -5,7 +5,7 @@ import { GlobTool } from "../../src/tool/glob" import { SessionID, MessageID } from "../../src/session/schema" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" import { Ripgrep } from "../../src/file/ripgrep" -import { AppFileSystem } from "../../src/filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { Truncate } from "../../src/tool/truncate" import { Agent } from "../../src/agent/agent" import { provideTmpdirInstance } from "../fixture/fixture" diff --git a/packages/opencode/test/tool/grep.test.ts b/packages/opencode/test/tool/grep.test.ts index 7cdf6a0aa1..35467aeab4 100644 --- a/packages/opencode/test/tool/grep.test.ts +++ b/packages/opencode/test/tool/grep.test.ts @@ -8,7 +8,7 @@ import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" import { Truncate } from "../../src/tool/truncate" import { Agent } from "../../src/agent/agent" import { Ripgrep } from "../../src/file/ripgrep" -import { AppFileSystem } from "../../src/filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { testEffect } from "../lib/effect" const it = testEffect( diff --git a/packages/opencode/test/tool/read.test.ts b/packages/opencode/test/tool/read.test.ts index 2064193d5b..f14ec33105 100644 --- a/packages/opencode/test/tool/read.test.ts +++ b/packages/opencode/test/tool/read.test.ts @@ -3,7 +3,7 @@ import { Cause, Effect, Exit, Layer } from "effect" import path from "path" import { Agent } from "../../src/agent/agent" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" -import { AppFileSystem } from "../../src/filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { FileTime } from "../../src/file/time" import { LSP } from "../../src/lsp" import { Permission } from "../../src/permission" diff --git a/packages/opencode/test/tool/write.test.ts b/packages/opencode/test/tool/write.test.ts index f7daa1e971..e83ec2efdb 100644 --- a/packages/opencode/test/tool/write.test.ts +++ b/packages/opencode/test/tool/write.test.ts @@ -5,7 +5,7 @@ import fs from "fs/promises" import { WriteTool } from "../../src/tool/write" import { Instance } from "../../src/project/instance" import { LSP } from "../../src/lsp" -import { AppFileSystem } from "../../src/filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { FileTime } from "../../src/file/time" import { Bus } from "../../src/bus" import { Format } from "../../src/format" diff --git a/packages/opencode/test/util/glob.test.ts b/packages/opencode/test/util/glob.test.ts index e58d92c85c..e982d5194c 100644 --- a/packages/opencode/test/util/glob.test.ts +++ b/packages/opencode/test/util/glob.test.ts @@ -1,7 +1,7 @@ import { describe, test, expect } from "bun:test" import path from "path" import fs from "fs/promises" -import { Glob } from "../../src/util/glob" +import { Glob } from "@opencode-ai/shared/util/glob" import { tmpdir } from "../fixture/fixture" describe("Glob", () => { diff --git a/packages/opencode/test/util/module.test.ts b/packages/opencode/test/util/module.test.ts index 738b4a785b..6f8539bfb7 100644 --- a/packages/opencode/test/util/module.test.ts +++ b/packages/opencode/test/util/module.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" import path from "path" -import { Module } from "@opencode-ai/util/module" +import { Module } from "@opencode-ai/shared/util/module" import { Filesystem } from "../../src/util/filesystem" import { tmpdir } from "../fixture/fixture" diff --git a/packages/shared/package.json b/packages/shared/package.json new file mode 100644 index 0000000000..1bb1ca47ef --- /dev/null +++ b/packages/shared/package.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "version": "1.4.6", + "name": "@opencode-ai/shared", + "type": "module", + "license": "MIT", + "private": true, + "scripts": {}, + "bin": { + "opencode": "./bin/opencode" + }, + "exports": { + "./*": "./src/*.ts" + }, + "imports": {}, + "devDependencies": { + "@types/semver": "catalog:" + }, + "dependencies": { + "@effect/platform-node": "catalog:", + "@npmcli/arborist": "catalog:", + "effect": "catalog:", + "mime-types": "3.0.2", + "minimatch": "10.2.5", + "semver": "catalog:", + "zod": "catalog:" + }, + "overrides": { + "drizzle-orm": "catalog:" + } +} diff --git a/packages/opencode/src/filesystem/index.ts b/packages/shared/src/filesystem.ts similarity index 99% rename from packages/opencode/src/filesystem/index.ts rename to packages/shared/src/filesystem.ts index 2c3964ec25..44346be8f9 100644 --- a/packages/opencode/src/filesystem/index.ts +++ b/packages/shared/src/filesystem.ts @@ -5,7 +5,7 @@ import * as NFS from "fs/promises" import { lookup } from "mime-types" import { Effect, FileSystem, Layer, Schema, Context } from "effect" import type { PlatformError } from "effect/PlatformError" -import { Glob } from "../util/glob" +import { Glob } from "./util/glob" export namespace AppFileSystem { export class FileSystemError extends Schema.TaggedErrorClass()("FileSystemError", { diff --git a/packages/util/src/array.ts b/packages/shared/src/util/array.ts similarity index 100% rename from packages/util/src/array.ts rename to packages/shared/src/util/array.ts diff --git a/packages/util/src/binary.ts b/packages/shared/src/util/binary.ts similarity index 100% rename from packages/util/src/binary.ts rename to packages/shared/src/util/binary.ts diff --git a/packages/util/src/encode.ts b/packages/shared/src/util/encode.ts similarity index 100% rename from packages/util/src/encode.ts rename to packages/shared/src/util/encode.ts diff --git a/packages/util/src/error.ts b/packages/shared/src/util/error.ts similarity index 100% rename from packages/util/src/error.ts rename to packages/shared/src/util/error.ts diff --git a/packages/util/src/fn.ts b/packages/shared/src/util/fn.ts similarity index 100% rename from packages/util/src/fn.ts rename to packages/shared/src/util/fn.ts diff --git a/packages/opencode/src/util/glob.ts b/packages/shared/src/util/glob.ts similarity index 100% rename from packages/opencode/src/util/glob.ts rename to packages/shared/src/util/glob.ts diff --git a/packages/util/src/identifier.ts b/packages/shared/src/util/identifier.ts similarity index 100% rename from packages/util/src/identifier.ts rename to packages/shared/src/util/identifier.ts diff --git a/packages/util/src/iife.ts b/packages/shared/src/util/iife.ts similarity index 100% rename from packages/util/src/iife.ts rename to packages/shared/src/util/iife.ts diff --git a/packages/util/src/lazy.ts b/packages/shared/src/util/lazy.ts similarity index 100% rename from packages/util/src/lazy.ts rename to packages/shared/src/util/lazy.ts diff --git a/packages/util/src/module.ts b/packages/shared/src/util/module.ts similarity index 100% rename from packages/util/src/module.ts rename to packages/shared/src/util/module.ts diff --git a/packages/util/src/path.ts b/packages/shared/src/util/path.ts similarity index 100% rename from packages/util/src/path.ts rename to packages/shared/src/util/path.ts diff --git a/packages/util/src/retry.ts b/packages/shared/src/util/retry.ts similarity index 100% rename from packages/util/src/retry.ts rename to packages/shared/src/util/retry.ts diff --git a/packages/util/src/slug.ts b/packages/shared/src/util/slug.ts similarity index 100% rename from packages/util/src/slug.ts rename to packages/shared/src/util/slug.ts diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json new file mode 100644 index 0000000000..ff9886313a --- /dev/null +++ b/packages/shared/tsconfig.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@tsconfig/bun/tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "@opentui/solid", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "types": [], + "noUncheckedIndexedAccess": false, + "customConditions": ["browser"], + "paths": { + "@/*": ["./src/*"], + "@tui/*": ["./src/cli/cmd/tui/*"] + }, + "plugins": [ + { + "name": "@effect/language-service", + "transform": "@effect/language-service/transform", + "namespaceImportPackages": ["effect", "@effect/*"] + } + ] + } +} diff --git a/packages/ui/package.json b/packages/ui/package.json index e406ecf165..21974e3ec7 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -44,7 +44,7 @@ "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", - "@opencode-ai/util": "workspace:*", + "@opencode-ai/shared": "workspace:*", "@pierre/diffs": "catalog:", "@shikijs/transformers": "3.9.2", "@solid-primitives/bounds": "0.1.3", diff --git a/packages/ui/src/components/file.tsx b/packages/ui/src/components/file.tsx index b78f0bae44..51c2892737 100644 --- a/packages/ui/src/components/file.tsx +++ b/packages/ui/src/components/file.tsx @@ -1,4 +1,4 @@ -import { sampledChecksum } from "@opencode-ai/util/encode" +import { sampledChecksum } from "@opencode-ai/shared/util/encode" import { DEFAULT_VIRTUAL_FILE_METRICS, type DiffLineAnnotation, diff --git a/packages/ui/src/components/line-comment.tsx b/packages/ui/src/components/line-comment.tsx index 26e763bb3e..e20da5a8d3 100644 --- a/packages/ui/src/components/line-comment.tsx +++ b/packages/ui/src/components/line-comment.tsx @@ -1,5 +1,5 @@ import { useFilteredList } from "@opencode-ai/ui/hooks" -import { getDirectory, getFilename } from "@opencode-ai/util/path" +import { getDirectory, getFilename } from "@opencode-ai/shared/util/path" import { createSignal, For, onMount, Show, splitProps, type JSX } from "solid-js" import { Button } from "./button" import { FileIcon } from "./file-icon" diff --git a/packages/ui/src/components/markdown.tsx b/packages/ui/src/components/markdown.tsx index ceab10df98..f3037da8bc 100644 --- a/packages/ui/src/components/markdown.tsx +++ b/packages/ui/src/components/markdown.tsx @@ -2,7 +2,7 @@ import { useMarked } from "../context/marked" import { useI18n } from "../context/i18n" import DOMPurify from "dompurify" import morphdom from "morphdom" -import { checksum } from "@opencode-ai/util/encode" +import { checksum } from "@opencode-ai/shared/util/encode" import { ComponentProps, createEffect, createResource, createSignal, onCleanup, splitProps } from "solid-js" import { isServer } from "solid-js/web" import { stream } from "./markdown-stream" diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 02bd80ac9c..48444cd017 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -46,8 +46,8 @@ import { Checkbox } from "./checkbox" import { DiffChanges } from "./diff-changes" import { Markdown } from "./markdown" import { ImagePreview } from "./image-preview" -import { getDirectory as _getDirectory, getFilename } from "@opencode-ai/util/path" -import { checksum } from "@opencode-ai/util/encode" +import { getDirectory as _getDirectory, getFilename } from "@opencode-ai/shared/util/path" +import { checksum } from "@opencode-ai/shared/util/encode" import { Tooltip } from "./tooltip" import { IconButton } from "./icon-button" import { Spinner } from "./spinner" diff --git a/packages/ui/src/components/session-review.tsx b/packages/ui/src/components/session-review.tsx index 3223f5d08d..bb19d099e0 100644 --- a/packages/ui/src/components/session-review.tsx +++ b/packages/ui/src/components/session-review.tsx @@ -11,8 +11,8 @@ import { Tooltip } from "./tooltip" import { ScrollView } from "./scroll-view" import { useFileComponent } from "../context/file" import { useI18n } from "../context/i18n" -import { getDirectory, getFilename } from "@opencode-ai/util/path" -import { checksum } from "@opencode-ai/util/encode" +import { getDirectory, getFilename } from "@opencode-ai/shared/util/path" +import { checksum } from "@opencode-ai/shared/util/encode" import { createEffect, createMemo, For, Match, onCleanup, Show, Switch, untrack, type JSX } from "solid-js" import { createStore } from "solid-js/store" import { type FileContent, type SnapshotFileDiff, type VcsFileDiff } from "@opencode-ai/sdk/v2" diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index e891a6febe..6d43a575a7 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -8,8 +8,8 @@ import type { SessionStatus } from "@opencode-ai/sdk/v2" import { useData } from "../context" import { useFileComponent } from "../context/file" -import { Binary } from "@opencode-ai/util/binary" -import { getDirectory, getFilename } from "@opencode-ai/util/path" +import { Binary } from "@opencode-ai/shared/util/binary" +import { getDirectory, getFilename } from "@opencode-ai/shared/util/path" import { createEffect, createMemo, createSignal, For, on, ParentProps, Show } from "solid-js" import { createStore } from "solid-js/store" import { Dynamic } from "solid-js/web" diff --git a/packages/util/package.json b/packages/util/package.json deleted file mode 100644 index 35aaa9b7c5..0000000000 --- a/packages/util/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "@opencode-ai/util", - "version": "1.4.6", - "private": true, - "type": "module", - "license": "MIT", - "exports": { - "./*": "./src/*.ts" - }, - "scripts": { - "typecheck": "tsc --noEmit" - }, - "dependencies": { - "zod": "catalog:" - }, - "devDependencies": { - "typescript": "catalog:", - "@types/bun": "catalog:" - } -} diff --git a/packages/util/sst-env.d.ts b/packages/util/sst-env.d.ts deleted file mode 100644 index 64441936d7..0000000000 --- a/packages/util/sst-env.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -/* biome-ignore-all lint: auto-generated */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/packages/util/tsconfig.json b/packages/util/tsconfig.json deleted file mode 100644 index 528dcd91d9..0000000000 --- a/packages/util/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "bundler", - "skipLibCheck": true, - "allowSyntheticDefaultImports": true, - "esModuleInterop": true, - "allowJs": true, - "noEmit": true, - "strict": true, - "isolatedModules": true - } -} From 685d79e953a02a3a78b91235c811932105574a66 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 10:49:47 -0400 Subject: [PATCH 140/300] feat(opencode): trace tool execution spans (#22531) --- packages/opencode/src/tool/tool.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/tool/tool.ts b/packages/opencode/src/tool/tool.ts index 49dd2b0605..30be63a320 100644 --- a/packages/opencode/src/tool/tool.ts +++ b/packages/opencode/src/tool/tool.ts @@ -80,8 +80,14 @@ export namespace Tool { Effect.gen(function* () { const toolInfo = init instanceof Function ? { ...(yield* init()) } : { ...init } const execute = toolInfo.execute - toolInfo.execute = (args, ctx) => - Effect.gen(function* () { + toolInfo.execute = (args, ctx) => { + const attrs = { + "tool.name": id, + "session.id": ctx.sessionID, + "message.id": ctx.messageID, + ...(ctx.callID ? { "tool.call_id": ctx.callID } : {}), + } + return Effect.gen(function* () { yield* Effect.try({ try: () => toolInfo.parameters.parse(args), catch: (error) => { @@ -109,7 +115,8 @@ export namespace Tool { ...(truncated.truncated && { outputPath: truncated.outputPath }), }, } - }).pipe(Effect.orDie) + }).pipe(Effect.orDie, Effect.withSpan("Tool.execute", { attributes: attrs })) + } return toolInfo }) } From fe01fa7249f84100e97d97f346dcda4647e5bc5b Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 10:55:50 -0400 Subject: [PATCH 141/300] remove makeRuntime facade from Env (#22523) --- packages/opencode/src/env/index.ts | 19 --- .../test/provider/amazon-bedrock.test.ts | 40 +++--- .../opencode/test/provider/provider.test.ts | 122 +++++++++--------- 3 files changed, 85 insertions(+), 96 deletions(-) diff --git a/packages/opencode/src/env/index.ts b/packages/opencode/src/env/index.ts index 930287899c..b9efb68520 100644 --- a/packages/opencode/src/env/index.ts +++ b/packages/opencode/src/env/index.ts @@ -1,6 +1,5 @@ import { Context, Effect, Layer } from "effect" import { InstanceState } from "@/effect/instance-state" -import { makeRuntime } from "@/effect/run-service" export namespace Env { type State = Record @@ -35,22 +34,4 @@ export namespace Env { ) export const defaultLayer = layer - - const rt = makeRuntime(Service, defaultLayer) - - export function get(key: string) { - return rt.runSync((svc) => svc.get(key)) - } - - export function all() { - return rt.runSync((svc) => svc.all()) - } - - export function set(key: string, value: string) { - return rt.runSync((svc) => svc.set(key, value)) - } - - export function remove(key: string) { - return rt.runSync((svc) => svc.remove(key)) - } } diff --git a/packages/opencode/test/provider/amazon-bedrock.test.ts b/packages/opencode/test/provider/amazon-bedrock.test.ts index 712f36086f..6783ff5889 100644 --- a/packages/opencode/test/provider/amazon-bedrock.test.ts +++ b/packages/opencode/test/provider/amazon-bedrock.test.ts @@ -11,6 +11,10 @@ import { Global } from "../../src/global" import { Filesystem } from "../../src/util/filesystem" import { Effect } from "effect" import { AppRuntime } from "../../src/effect/app-runtime" +import { makeRuntime } from "../../src/effect/run-service" + +const env = makeRuntime(Env.Service, Env.defaultLayer) +const set = (k: string, v: string) => env.runSync((svc) => svc.set(k, v)) async function list() { return AppRuntime.runPromise( @@ -42,8 +46,8 @@ test("Bedrock: config region takes precedence over AWS_REGION env var", async () await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("AWS_REGION", "us-east-1") - Env.set("AWS_PROFILE", "default") + set("AWS_REGION", "us-east-1") + set("AWS_PROFILE", "default") }, fn: async () => { const providers = await list() @@ -67,8 +71,8 @@ test("Bedrock: falls back to AWS_REGION env var when no config region", async () await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("AWS_REGION", "eu-west-1") - Env.set("AWS_PROFILE", "default") + set("AWS_REGION", "eu-west-1") + set("AWS_PROFILE", "default") }, fn: async () => { const providers = await list() @@ -122,9 +126,9 @@ test("Bedrock: loads when bearer token from auth.json is present", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("AWS_PROFILE", "") - Env.set("AWS_ACCESS_KEY_ID", "") - Env.set("AWS_BEARER_TOKEN_BEDROCK", "") + set("AWS_PROFILE", "") + set("AWS_ACCESS_KEY_ID", "") + set("AWS_BEARER_TOKEN_BEDROCK", "") }, fn: async () => { const providers = await list() @@ -168,8 +172,8 @@ test("Bedrock: config profile takes precedence over AWS_PROFILE env var", async await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("AWS_PROFILE", "default") - Env.set("AWS_ACCESS_KEY_ID", "test-key-id") + set("AWS_PROFILE", "default") + set("AWS_ACCESS_KEY_ID", "test-key-id") }, fn: async () => { const providers = await list() @@ -200,7 +204,7 @@ test("Bedrock: includes custom endpoint in options when specified", async () => await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("AWS_PROFILE", "default") + set("AWS_PROFILE", "default") }, fn: async () => { const providers = await list() @@ -233,10 +237,10 @@ test("Bedrock: autoloads when AWS_WEB_IDENTITY_TOKEN_FILE is present", async () await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("AWS_WEB_IDENTITY_TOKEN_FILE", "/var/run/secrets/eks.amazonaws.com/serviceaccount/token") - Env.set("AWS_ROLE_ARN", "arn:aws:iam::123456789012:role/my-eks-role") - Env.set("AWS_PROFILE", "") - Env.set("AWS_ACCESS_KEY_ID", "") + set("AWS_WEB_IDENTITY_TOKEN_FILE", "/var/run/secrets/eks.amazonaws.com/serviceaccount/token") + set("AWS_ROLE_ARN", "arn:aws:iam::123456789012:role/my-eks-role") + set("AWS_PROFILE", "") + set("AWS_ACCESS_KEY_ID", "") }, fn: async () => { const providers = await list() @@ -276,7 +280,7 @@ test("Bedrock: model with us. prefix should not be double-prefixed", async () => await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("AWS_PROFILE", "default") + set("AWS_PROFILE", "default") }, fn: async () => { const providers = await list() @@ -313,7 +317,7 @@ test("Bedrock: model with global. prefix should not be prefixed", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("AWS_PROFILE", "default") + set("AWS_PROFILE", "default") }, fn: async () => { const providers = await list() @@ -349,7 +353,7 @@ test("Bedrock: model with eu. prefix should not be double-prefixed", async () => await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("AWS_PROFILE", "default") + set("AWS_PROFILE", "default") }, fn: async () => { const providers = await list() @@ -385,7 +389,7 @@ test("Bedrock: model without prefix in US region should get us. prefix added", a await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("AWS_PROFILE", "default") + set("AWS_PROFILE", "default") }, fn: async () => { const providers = await list() diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index ac990bc0fb..dafa9dd822 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -13,6 +13,10 @@ import { Filesystem } from "../../src/util/filesystem" import { Env } from "../../src/env" import { Effect } from "effect" import { AppRuntime } from "../../src/effect/app-runtime" +import { makeRuntime } from "../../src/effect/run-service" + +const env = makeRuntime(Env.Service, Env.defaultLayer) +const set = (k: string, v: string) => env.runSync((svc) => svc.set(k, v)) async function run(fn: (provider: Provider.Interface) => Effect.Effect) { return AppRuntime.runPromise( @@ -71,7 +75,7 @@ test("provider loaded from env variable", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("ANTHROPIC_API_KEY", "test-api-key") + set("ANTHROPIC_API_KEY", "test-api-key") }, fn: async () => { const providers = await list() @@ -126,7 +130,7 @@ test("disabled_providers excludes provider", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("ANTHROPIC_API_KEY", "test-api-key") + set("ANTHROPIC_API_KEY", "test-api-key") }, fn: async () => { const providers = await list() @@ -150,8 +154,8 @@ test("enabled_providers restricts to only listed providers", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("ANTHROPIC_API_KEY", "test-api-key") - Env.set("OPENAI_API_KEY", "test-openai-key") + set("ANTHROPIC_API_KEY", "test-api-key") + set("OPENAI_API_KEY", "test-openai-key") }, fn: async () => { const providers = await list() @@ -180,7 +184,7 @@ test("model whitelist filters models for provider", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("ANTHROPIC_API_KEY", "test-api-key") + set("ANTHROPIC_API_KEY", "test-api-key") }, fn: async () => { const providers = await list() @@ -211,7 +215,7 @@ test("model blacklist excludes specific models", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("ANTHROPIC_API_KEY", "test-api-key") + set("ANTHROPIC_API_KEY", "test-api-key") }, fn: async () => { const providers = await list() @@ -246,7 +250,7 @@ test("custom model alias via config", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("ANTHROPIC_API_KEY", "test-api-key") + set("ANTHROPIC_API_KEY", "test-api-key") }, fn: async () => { const providers = await list() @@ -322,7 +326,7 @@ test("env variable takes precedence, config merges options", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("ANTHROPIC_API_KEY", "env-api-key") + set("ANTHROPIC_API_KEY", "env-api-key") }, fn: async () => { const providers = await list() @@ -348,7 +352,7 @@ test("getModel returns model for valid provider/model", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("ANTHROPIC_API_KEY", "test-api-key") + set("ANTHROPIC_API_KEY", "test-api-key") }, fn: async () => { const model = await getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514")) @@ -375,7 +379,7 @@ test("getModel throws ModelNotFoundError for invalid model", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("ANTHROPIC_API_KEY", "test-api-key") + set("ANTHROPIC_API_KEY", "test-api-key") }, fn: async () => { expect(getModel(ProviderID.anthropic, ModelID.make("nonexistent-model"))).rejects.toThrow() @@ -428,7 +432,7 @@ test("defaultModel returns first available model when no config set", async () = await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("ANTHROPIC_API_KEY", "test-api-key") + set("ANTHROPIC_API_KEY", "test-api-key") }, fn: async () => { const model = await defaultModel() @@ -453,7 +457,7 @@ test("defaultModel respects config model setting", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("ANTHROPIC_API_KEY", "test-api-key") + set("ANTHROPIC_API_KEY", "test-api-key") }, fn: async () => { const model = await defaultModel() @@ -568,7 +572,7 @@ test("model options are merged from existing model", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("ANTHROPIC_API_KEY", "test-api-key") + set("ANTHROPIC_API_KEY", "test-api-key") }, fn: async () => { const providers = await list() @@ -597,7 +601,7 @@ test("provider removed when all models filtered out", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("ANTHROPIC_API_KEY", "test-api-key") + set("ANTHROPIC_API_KEY", "test-api-key") }, fn: async () => { const providers = await list() @@ -620,7 +624,7 @@ test("closest finds model by partial match", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("ANTHROPIC_API_KEY", "test-api-key") + set("ANTHROPIC_API_KEY", "test-api-key") }, fn: async () => { const result = await closest(ProviderID.anthropic, ["sonnet-4"]) @@ -675,7 +679,7 @@ test("getModel uses realIdByKey for aliased models", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("ANTHROPIC_API_KEY", "test-api-key") + set("ANTHROPIC_API_KEY", "test-api-key") }, fn: async () => { const providers = await list() @@ -790,7 +794,7 @@ test("model inherits properties from existing database model", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("ANTHROPIC_API_KEY", "test-api-key") + set("ANTHROPIC_API_KEY", "test-api-key") }, fn: async () => { const providers = await list() @@ -818,7 +822,7 @@ test("disabled_providers prevents loading even with env var", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("OPENAI_API_KEY", "test-openai-key") + set("OPENAI_API_KEY", "test-openai-key") }, fn: async () => { const providers = await list() @@ -842,8 +846,8 @@ test("enabled_providers with empty array allows no providers", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("ANTHROPIC_API_KEY", "test-api-key") - Env.set("OPENAI_API_KEY", "test-openai-key") + set("ANTHROPIC_API_KEY", "test-api-key") + set("OPENAI_API_KEY", "test-openai-key") }, fn: async () => { const providers = await list() @@ -872,7 +876,7 @@ test("whitelist and blacklist can be combined", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("ANTHROPIC_API_KEY", "test-api-key") + set("ANTHROPIC_API_KEY", "test-api-key") }, fn: async () => { const providers = await list() @@ -981,7 +985,7 @@ test("getSmallModel returns appropriate small model", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("ANTHROPIC_API_KEY", "test-api-key") + set("ANTHROPIC_API_KEY", "test-api-key") }, fn: async () => { const model = await getSmallModel(ProviderID.anthropic) @@ -1006,7 +1010,7 @@ test("getSmallModel respects config small_model override", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("ANTHROPIC_API_KEY", "test-api-key") + set("ANTHROPIC_API_KEY", "test-api-key") }, fn: async () => { const model = await getSmallModel(ProviderID.anthropic) @@ -1054,8 +1058,8 @@ test("multiple providers can be configured simultaneously", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("ANTHROPIC_API_KEY", "test-anthropic-key") - Env.set("OPENAI_API_KEY", "test-openai-key") + set("ANTHROPIC_API_KEY", "test-anthropic-key") + set("OPENAI_API_KEY", "test-openai-key") }, fn: async () => { const providers = await list() @@ -1133,7 +1137,7 @@ test("model alias name defaults to alias key when id differs", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("ANTHROPIC_API_KEY", "test-api-key") + set("ANTHROPIC_API_KEY", "test-api-key") }, fn: async () => { const providers = await list() @@ -1173,7 +1177,7 @@ test("provider with multiple env var options only includes apiKey when single en await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("MULTI_ENV_KEY_1", "test-key") + set("MULTI_ENV_KEY_1", "test-key") }, fn: async () => { const providers = await list() @@ -1215,7 +1219,7 @@ test("provider with single env var includes apiKey automatically", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("SINGLE_ENV_KEY", "my-api-key") + set("SINGLE_ENV_KEY", "my-api-key") }, fn: async () => { const providers = await list() @@ -1252,7 +1256,7 @@ test("model cost overrides existing cost values", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("ANTHROPIC_API_KEY", "test-api-key") + set("ANTHROPIC_API_KEY", "test-api-key") }, fn: async () => { const providers = await list() @@ -1331,9 +1335,9 @@ test("disabled_providers and enabled_providers interaction", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("ANTHROPIC_API_KEY", "test-anthropic") - Env.set("OPENAI_API_KEY", "test-openai") - Env.set("GOOGLE_GENERATIVE_AI_API_KEY", "test-google") + set("ANTHROPIC_API_KEY", "test-anthropic") + set("OPENAI_API_KEY", "test-openai") + set("GOOGLE_GENERATIVE_AI_API_KEY", "test-google") }, fn: async () => { const providers = await list() @@ -1490,7 +1494,7 @@ test("provider env fallback - second env var used if first missing", async () => directory: tmp.path, init: async () => { // Only set fallback, not primary - Env.set("FALLBACK_KEY", "fallback-api-key") + set("FALLBACK_KEY", "fallback-api-key") }, fn: async () => { const providers = await list() @@ -1514,7 +1518,7 @@ test("getModel returns consistent results", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("ANTHROPIC_API_KEY", "test-api-key") + set("ANTHROPIC_API_KEY", "test-api-key") }, fn: async () => { const model1 = await getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514")) @@ -1575,7 +1579,7 @@ test("ModelNotFoundError includes suggestions for typos", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("ANTHROPIC_API_KEY", "test-api-key") + set("ANTHROPIC_API_KEY", "test-api-key") }, fn: async () => { try { @@ -1603,7 +1607,7 @@ test("ModelNotFoundError for provider includes suggestions", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("ANTHROPIC_API_KEY", "test-api-key") + set("ANTHROPIC_API_KEY", "test-api-key") }, fn: async () => { try { @@ -1651,7 +1655,7 @@ test("getProvider returns provider info", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("ANTHROPIC_API_KEY", "test-api-key") + set("ANTHROPIC_API_KEY", "test-api-key") }, fn: async () => { const provider = await getProvider(ProviderID.anthropic) @@ -1675,7 +1679,7 @@ test("closest returns undefined when no partial match found", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("ANTHROPIC_API_KEY", "test-api-key") + set("ANTHROPIC_API_KEY", "test-api-key") }, fn: async () => { const result = await closest(ProviderID.anthropic, ["nonexistent-xyz-model"]) @@ -1698,7 +1702,7 @@ test("closest checks multiple query terms in order", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("ANTHROPIC_API_KEY", "test-api-key") + set("ANTHROPIC_API_KEY", "test-api-key") }, fn: async () => { // First term won't match, second will @@ -1770,7 +1774,7 @@ test("provider options are deeply merged", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("ANTHROPIC_API_KEY", "test-api-key") + set("ANTHROPIC_API_KEY", "test-api-key") }, fn: async () => { const providers = await list() @@ -1808,7 +1812,7 @@ test("custom model inherits npm package from models.dev provider config", async await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("OPENAI_API_KEY", "test-api-key") + set("OPENAI_API_KEY", "test-api-key") }, fn: async () => { const providers = await list() @@ -1843,7 +1847,7 @@ test("custom model inherits api.url from models.dev provider", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("OPENROUTER_API_KEY", "test-api-key") + set("OPENROUTER_API_KEY", "test-api-key") }, fn: async () => { const providers = await list() @@ -1944,7 +1948,7 @@ test("model variants are generated for reasoning models", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("ANTHROPIC_API_KEY", "test-api-key") + set("ANTHROPIC_API_KEY", "test-api-key") }, fn: async () => { const providers = await list() @@ -1982,7 +1986,7 @@ test("model variants can be disabled via config", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("ANTHROPIC_API_KEY", "test-api-key") + set("ANTHROPIC_API_KEY", "test-api-key") }, fn: async () => { const providers = await list() @@ -2025,7 +2029,7 @@ test("model variants can be customized via config", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("ANTHROPIC_API_KEY", "test-api-key") + set("ANTHROPIC_API_KEY", "test-api-key") }, fn: async () => { const providers = await list() @@ -2064,7 +2068,7 @@ test("disabled key is stripped from variant config", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("ANTHROPIC_API_KEY", "test-api-key") + set("ANTHROPIC_API_KEY", "test-api-key") }, fn: async () => { const providers = await list() @@ -2102,7 +2106,7 @@ test("all variants can be disabled via config", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("ANTHROPIC_API_KEY", "test-api-key") + set("ANTHROPIC_API_KEY", "test-api-key") }, fn: async () => { const providers = await list() @@ -2140,7 +2144,7 @@ test("variant config merges with generated variants", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("ANTHROPIC_API_KEY", "test-api-key") + set("ANTHROPIC_API_KEY", "test-api-key") }, fn: async () => { const providers = await list() @@ -2178,7 +2182,7 @@ test("variants filtered in second pass for database models", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("OPENAI_API_KEY", "test-api-key") + set("OPENAI_API_KEY", "test-api-key") }, fn: async () => { const providers = await list() @@ -2282,7 +2286,7 @@ test("Google Vertex: retains baseURL for custom proxy", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("GOOGLE_APPLICATION_CREDENTIALS", "test-creds") + set("GOOGLE_APPLICATION_CREDENTIALS", "test-creds") }, fn: async () => { const providers = await list() @@ -2327,7 +2331,7 @@ test("Google Vertex: supports OpenAI compatible models", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("GOOGLE_APPLICATION_CREDENTIALS", "test-creds") + set("GOOGLE_APPLICATION_CREDENTIALS", "test-creds") }, fn: async () => { const providers = await list() @@ -2353,9 +2357,9 @@ test("cloudflare-ai-gateway loads with env variables", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("CLOUDFLARE_ACCOUNT_ID", "test-account") - Env.set("CLOUDFLARE_GATEWAY_ID", "test-gateway") - Env.set("CLOUDFLARE_API_TOKEN", "test-token") + set("CLOUDFLARE_ACCOUNT_ID", "test-account") + set("CLOUDFLARE_GATEWAY_ID", "test-gateway") + set("CLOUDFLARE_API_TOKEN", "test-token") }, fn: async () => { const providers = await list() @@ -2385,9 +2389,9 @@ test("cloudflare-ai-gateway forwards config metadata options", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("CLOUDFLARE_ACCOUNT_ID", "test-account") - Env.set("CLOUDFLARE_GATEWAY_ID", "test-gateway") - Env.set("CLOUDFLARE_API_TOKEN", "test-token") + set("CLOUDFLARE_ACCOUNT_ID", "test-account") + set("CLOUDFLARE_GATEWAY_ID", "test-gateway") + set("CLOUDFLARE_API_TOKEN", "test-token") }, fn: async () => { const providers = await list() @@ -2485,8 +2489,8 @@ test("plugin config enabled and disabled providers are honored", async () => { await Instance.provide({ directory: tmp.path, init: async () => { - Env.set("ANTHROPIC_API_KEY", "test-anthropic-key") - Env.set("OPENAI_API_KEY", "test-openai-key") + set("ANTHROPIC_API_KEY", "test-anthropic-key") + set("OPENAI_API_KEY", "test-openai-key") }, fn: async () => { const providers = await list() From 5fc656e2a0dff7e1cfb8baa40a63418673a1be48 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 10:57:58 -0400 Subject: [PATCH 142/300] docs(opencode): add instance context migration plan (#22529) --- .../opencode/specs/effect/instance-context.md | 310 ++++++++++++++++++ packages/opencode/specs/effect/migration.md | 4 + 2 files changed, 314 insertions(+) create mode 100644 packages/opencode/specs/effect/instance-context.md diff --git a/packages/opencode/specs/effect/instance-context.md b/packages/opencode/specs/effect/instance-context.md new file mode 100644 index 0000000000..6c160a9477 --- /dev/null +++ b/packages/opencode/specs/effect/instance-context.md @@ -0,0 +1,310 @@ +# Instance context migration + +Practical plan for retiring the promise-backed / ALS-backed `Instance` helper in `src/project/instance.ts` and moving instance selection fully into Effect-provided scope. + +## Goal + +End state: + +- request, CLI, TUI, and tool entrypoints shift into an instance through Effect, not `Instance.provide(...)` +- Effect code reads the current instance from `InstanceRef` or its eventual replacement, not from ALS-backed sync getters +- per-directory boot, caching, and disposal are scoped Effect resources, not a module-level `Map>` +- ALS remains only as a temporary bridge for native callback APIs that fire outside the Effect fiber tree + +## Current split + +Today `src/project/instance.ts` still owns two separate concerns: + +- ambient current-instance context through `LocalContext` / `AsyncLocalStorage` +- per-directory boot and deduplication through `cache: Map>` + +At the same time, the Effect side already exists: + +- `src/effect/instance-ref.ts` provides `InstanceRef` and `WorkspaceRef` +- `src/effect/run-service.ts` already attaches those refs when a runtime starts inside an active instance ALS context +- `src/effect/instance-state.ts` already prefers `InstanceRef` and only falls back to ALS when needed + +That means the migration is not "invent instance context in Effect". The migration is "stop relying on the legacy helper as the primary source of truth". + +## End state shape + +Near-term target shape: + +```ts +InstanceScope.with({ directory, workspaceID }, effect) +``` + +Responsibilities of `InstanceScope.with(...)`: + +- resolve `directory`, `project`, and `worktree` +- acquire or reuse the scoped per-directory instance environment +- provide `InstanceRef` and `WorkspaceRef` +- run the caller's Effect inside that environment + +Code inside the boundary should then do one of these: + +```ts +const ctx = yield * InstanceState.context +const dir = yield * InstanceState.directory +``` + +Long-term, once `InstanceState` itself is replaced by keyed layers / `LayerMap`, those reads can move to an `InstanceContext` service without changing the outer migration order. + +## Migration phases + +### Phase 1: stop expanding the legacy surface + +Rules for all new code: + +- do not add new `Instance.directory`, `Instance.worktree`, `Instance.project`, or `Instance.current` reads inside Effect code +- do not add new `Instance.provide(...)` boundaries unless there is no Effect-native seam yet +- use `InstanceState.context`, `InstanceState.directory`, or an explicit `ctx` parameter inside Effect code + +Success condition: + +- the file inventory below only shrinks from here + +### Phase 2: remove direct sync getter reads from Effect services + +Convert Effect services first, before replacing the top-level boundary. These modules already run inside Effect and mostly need `yield* InstanceState.context` or a yielded `ctx` instead of ambient sync access. + +Primary batch, highest payoff: + +- `src/file/index.ts` +- `src/lsp/server.ts` +- `src/worktree/index.ts` +- `src/file/watcher.ts` +- `src/format/formatter.ts` +- `src/session/index.ts` +- `src/project/vcs.ts` + +Mechanical replacement rule: + +- `Instance.directory` -> `ctx.directory` or `yield* InstanceState.directory` +- `Instance.worktree` -> `ctx.worktree` +- `Instance.project` -> `ctx.project` + +Do not thread strings manually through every public method if the service already has access to Effect context. + +### Phase 3: convert entry boundaries to provide instance refs directly + +After the service bodies stop assuming ALS, move the top-level boundaries to shift into Effect explicitly. + +Main boundaries: + +- HTTP server middleware and experimental `HttpApi` entrypoints +- CLI commands +- TUI worker / attach / thread entrypoints +- tool execution entrypoints + +These boundaries should become Effect-native wrappers that: + +- decode directory / workspace inputs +- resolve the instance context once +- provide `InstanceRef` and `WorkspaceRef` +- run the requested Effect + +At that point `Instance.provide(...)` becomes a legacy adapter instead of the normal code path. + +### Phase 4: replace promise boot cache with scoped instance runtime + +Once boundaries and services both rely on Effect context, replace the module-level promise cache in `src/project/instance.ts`. + +Target replacement: + +- keyed scoped runtime or keyed layer acquisition for each directory +- reuse via `ScopedCache`, `LayerMap`, or another keyed Effect resource manager +- cleanup performed by scope finalizers instead of `disposeAll()` iterating a Promise map + +This phase should absorb the current responsibilities of: + +- `cache` in `src/project/instance.ts` +- `boot(...)` +- most of `disposeInstance(...)` +- manual `reload(...)` / `disposeAll()` fan-out logic + +### Phase 5: shrink ALS to callback bridges only + +Keep ALS only where a library invokes callbacks outside the Effect fiber tree and we still need to call code that reads instance context synchronously. + +Known bridge cases today: + +- `src/file/watcher.ts` +- `src/session/llm.ts` +- some LSP and plugin callback paths + +If those libraries become fully wrapped in Effect services, the remaining `Instance.bind(...)` uses can disappear too. + +### Phase 6: delete the legacy sync API + +Only after earlier phases land: + +- remove broad use of `Instance.current`, `Instance.directory`, `Instance.worktree`, `Instance.project` +- reduce `src/project/instance.ts` to a thin compatibility shim or delete it entirely +- remove the ALS fallback from `InstanceState.context` + +## Inventory of direct legacy usage + +Direct legacy usage means any source file that still calls one of: + +- `Instance.current` +- `Instance.directory` +- `Instance.worktree` +- `Instance.project` +- `Instance.provide(...)` +- `Instance.bind(...)` +- `Instance.restore(...)` +- `Instance.reload(...)` +- `Instance.dispose()` / `Instance.disposeAll()` + +Current total: `54` files in `packages/opencode/src`. + +### Core bridge and plumbing + +These files define or adapt the current bridge. They should change last, after callers have moved. + +- `src/project/instance.ts` +- `src/effect/run-service.ts` +- `src/effect/instance-state.ts` +- `src/project/bootstrap.ts` +- `src/config/config.ts` + +Migration rule: + +- keep these as compatibility glue until the outer boundaries and inner services stop depending on ALS + +### HTTP and server boundaries + +These are the current request-entry seams that still create or consume instance context through the legacy helper. + +- `src/server/instance/middleware.ts` +- `src/server/instance/index.ts` +- `src/server/instance/project.ts` +- `src/server/instance/workspace.ts` +- `src/server/instance/file.ts` +- `src/server/instance/experimental.ts` +- `src/server/instance/global.ts` + +Migration rule: + +- move these to explicit Effect entrypoints that provide `InstanceRef` / `WorkspaceRef` +- do not move these first; first reduce the number of downstream handlers and services that still expect ambient ALS + +### CLI and TUI boundaries + +These commands still enter an instance through `Instance.provide(...)` or read sync getters directly. + +- `src/cli/bootstrap.ts` +- `src/cli/cmd/agent.ts` +- `src/cli/cmd/debug/agent.ts` +- `src/cli/cmd/debug/ripgrep.ts` +- `src/cli/cmd/github.ts` +- `src/cli/cmd/import.ts` +- `src/cli/cmd/mcp.ts` +- `src/cli/cmd/models.ts` +- `src/cli/cmd/plug.ts` +- `src/cli/cmd/pr.ts` +- `src/cli/cmd/providers.ts` +- `src/cli/cmd/stats.ts` +- `src/cli/cmd/tui/attach.ts` +- `src/cli/cmd/tui/plugin/runtime.ts` +- `src/cli/cmd/tui/thread.ts` +- `src/cli/cmd/tui/worker.ts` + +Migration rule: + +- converge these on one shared `withInstance(...)` Effect entry helper instead of open-coded `Instance.provide(...)` +- after that helper is proven, inline the legacy implementation behind an Effect-native scope provider + +### Tool boundary code + +These tools mostly use direct getters for path resolution and repo-relative display logic. + +- `src/tool/apply_patch.ts` +- `src/tool/bash.ts` +- `src/tool/edit.ts` +- `src/tool/lsp.ts` +- `src/tool/multiedit.ts` +- `src/tool/plan.ts` +- `src/tool/read.ts` +- `src/tool/write.ts` + +Migration rule: + +- expose the current instance as an explicit Effect dependency for tool execution +- keep path logic local; avoid introducing another global singleton for tool state + +### Effect services still reading ambient instance state + +These modules are already the best near-term migration targets because they are in Effect code but still read sync getters from the legacy helper. + +- `src/agent/agent.ts` +- `src/config/tui-migrate.ts` +- `src/file/index.ts` +- `src/file/watcher.ts` +- `src/format/formatter.ts` +- `src/lsp/client.ts` +- `src/lsp/index.ts` +- `src/lsp/server.ts` +- `src/mcp/index.ts` +- `src/project/vcs.ts` +- `src/provider/provider.ts` +- `src/pty/index.ts` +- `src/session/index.ts` +- `src/session/instruction.ts` +- `src/session/llm.ts` +- `src/session/system.ts` +- `src/sync/index.ts` +- `src/worktree/index.ts` + +Migration rule: + +- replace direct getter reads with `yield* InstanceState.context` or a yielded `ctx` +- isolate `Instance.bind(...)` callers and convert only the truly callback-driven edges to bridge mode + +### Highest-churn hotspots + +Current highest direct-usage counts by file: + +- `src/file/index.ts` - `18` +- `src/lsp/server.ts` - `14` +- `src/worktree/index.ts` - `12` +- `src/file/watcher.ts` - `9` +- `src/cli/cmd/mcp.ts` - `8` +- `src/format/formatter.ts` - `8` +- `src/tool/apply_patch.ts` - `8` +- `src/cli/cmd/github.ts` - `7` + +These files should drive the first measurable burn-down. + +## Recommended implementation order + +1. Migrate direct getter reads inside Effect services, starting with `file`, `lsp`, `worktree`, `format`, and `session`. +2. Add one shared Effect-native boundary helper for CLI / tool / HTTP entrypoints so we stop open-coding `Instance.provide(...)`. +3. Move experimental `HttpApi` entrypoints to that helper so the new server stack proves the pattern. +4. Convert remaining CLI and tool boundaries. +5. Replace the promise cache with a keyed scoped runtime or keyed layer map. +6. Delete ALS fallback paths once only callback bridges still depend on them. + +## Definition of done + +This migration is done when all of the following are true: + +- new requests and commands enter an instance by providing Effect context, not ALS +- Effect services no longer read `Instance.directory`, `Instance.worktree`, `Instance.project`, or `Instance.current` +- `Instance.provide(...)` is gone from normal request / CLI / tool execution +- per-directory boot and disposal are handled by scoped Effect resources +- `Instance.bind(...)` is either gone or confined to a tiny set of native callback adapters + +## Tracker and worktree + +Active tracker items: + +- `lh7l73` - overall `HttpApi` migration +- `yobwlk` - remove direct `Instance.*` reads inside Effect services +- `7irl1e` - replace `InstanceState` / legacy instance caching with keyed Effect layers + +Dedicated worktree for this transition: + +- path: `/Users/kit/code/open-source/opencode-worktrees/instance-effect-shift` +- branch: `kit/instance-effect-shift` diff --git a/packages/opencode/specs/effect/migration.md b/packages/opencode/specs/effect/migration.md index b8d4d12597..105a82290b 100644 --- a/packages/opencode/specs/effect/migration.md +++ b/packages/opencode/specs/effect/migration.md @@ -13,6 +13,10 @@ Use `makeRuntime` (from `src/effect/run-service.ts`) to create a per-service `Ma Rule of thumb: if two open directories should not share one copy of the service, it needs `InstanceState`. +## Instance context transition + +See `instance-context.md` for the phased plan to remove the legacy ALS / promise-backed `Instance` helper and move request / CLI / tool boundaries onto Effect-provided instance scope. + ## Service shape Every service follows the same pattern — a single namespace with the service definition, layer, `runPromise`, and async facade functions: From f06d82b6e8b98b77b0f8f63d824c800c6b99fa6e Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Wed, 15 Apr 2026 15:08:13 +0000 Subject: [PATCH 143/300] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index 0558d6b3d8..f860e3774e 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-gdS7MkWGeVO0qLs0HKD156YE0uCk5vWeYjKu4JR1Apw=", - "aarch64-linux": "sha256-tF4pyVqzbrvdkRG23Fot37FCg8guRZkcU738fHPr/OQ=", - "aarch64-darwin": "sha256-FugTWzGMb2ktAbNwQvWRM3GWOb5RTR++8EocDDrQMLc=", - "x86_64-darwin": "sha256-jpe6EiwKr+CS00cn0eHwcDluO4LvO3t/5l/LcFBBKP0=" + "x86_64-linux": "sha256-3kpnjBg7AQanyDGTOFdYBFvo9O9Rfnu0Wmi8bY5LpEI=", + "aarch64-linux": "sha256-8rQ+SNUiSpA2Ea3NrYNGopHQsnY7Y8qBsXCqL6GMt24=", + "aarch64-darwin": "sha256-OASMkW5hnXucV6lSmxrQo73lGSEKN4MQPNGNV0i7jdo=", + "x86_64-darwin": "sha256-CmHqXlm8wnLcwSSK0ghxAf+DVurEltMaxrUbWh9/ZGE=" } } From f1751401aa2c53a4a0215c6deddf93df306aac8b Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 11:22:34 -0400 Subject: [PATCH 144/300] fix(effect): add effect bridge for callback contexts (#22504) --- bun.lock | 1 + packages/opencode/src/bus/index.ts | 5 +- packages/opencode/src/command/index.ts | 6 +- .../src/control-plane/workspace-context.ts | 4 + packages/opencode/src/effect/bridge.ts | 49 ++ packages/opencode/src/effect/run-service.ts | 23 +- packages/opencode/src/mcp/index.ts | 16 +- packages/opencode/src/plugin/index.ts | 23 +- packages/opencode/src/provider/provider.ts | 6 +- packages/opencode/src/pty/index.ts | 7 +- packages/opencode/src/session/llm.ts | 717 +++++++++--------- packages/opencode/src/session/prompt.ts | 7 +- .../test/effect/app-runtime-logger.test.ts | 31 + packages/server/package.json | 3 +- 14 files changed, 499 insertions(+), 399 deletions(-) create mode 100644 packages/opencode/src/effect/bridge.ts diff --git a/bun.lock b/bun.lock index 01966b826a..fe5d42d7cc 100644 --- a/bun.lock +++ b/bun.lock @@ -510,6 +510,7 @@ "effect": "catalog:", }, "devDependencies": { + "@typescript/native-preview": "catalog:", "typescript": "catalog:", }, }, diff --git a/packages/opencode/src/bus/index.ts b/packages/opencode/src/bus/index.ts index 0638777bd4..3a1eea5c73 100644 --- a/packages/opencode/src/bus/index.ts +++ b/packages/opencode/src/bus/index.ts @@ -1,6 +1,6 @@ import z from "zod" import { Effect, Exit, Layer, PubSub, Scope, Context, Stream } from "effect" -import { EffectLogger } from "@/effect/logger" +import { EffectBridge } from "@/effect/bridge" import { Log } from "../util/log" import { BusEvent } from "./bus-event" import { GlobalBus } from "./global" @@ -128,6 +128,7 @@ export namespace Bus { function on(pubsub: PubSub.PubSub, type: string, callback: (event: T) => unknown) { return Effect.gen(function* () { log.info("subscribing", { type }) + const bridge = yield* EffectBridge.make() const scope = yield* Scope.make() const subscription = yield* Scope.provide(scope)(PubSub.subscribe(pubsub)) @@ -147,7 +148,7 @@ export namespace Bus { return () => { log.info("unsubscribing", { type }) - Effect.runFork(Scope.close(scope, Exit.void).pipe(Effect.provide(EffectLogger.layer))) + bridge.fork(Scope.close(scope, Exit.void)) } }) } diff --git a/packages/opencode/src/command/index.ts b/packages/opencode/src/command/index.ts index 42f53301b2..91a9e1b405 100644 --- a/packages/opencode/src/command/index.ts +++ b/packages/opencode/src/command/index.ts @@ -1,9 +1,9 @@ import { BusEvent } from "@/bus/bus-event" import { InstanceState } from "@/effect/instance-state" +import { EffectBridge } from "@/effect/bridge" import type { InstanceContext } from "@/project/instance" import { SessionID, MessageID } from "@/session/schema" import { Effect, Layer, Context } from "effect" -import { EffectLogger } from "@/effect/logger" import z from "zod" import { Config } from "../config/config" import { MCP } from "../mcp" @@ -82,6 +82,7 @@ export namespace Command { const init = Effect.fn("Command.state")(function* (ctx: InstanceContext) { const cfg = yield* config.get() + const bridge = yield* EffectBridge.make() const commands: Record = {} commands[Default.INIT] = { @@ -125,7 +126,7 @@ export namespace Command { source: "mcp", description: prompt.description, get template() { - return Effect.runPromise( + return bridge.promise( mcp .getPrompt( prompt.client, @@ -141,7 +142,6 @@ export namespace Command { .map((message) => (message.content.type === "text" ? message.content.text : "")) .join("\n") || "", ), - Effect.provide(EffectLogger.layer), ), ) }, diff --git a/packages/opencode/src/control-plane/workspace-context.ts b/packages/opencode/src/control-plane/workspace-context.ts index 173ec6178a..541657b88c 100644 --- a/packages/opencode/src/control-plane/workspace-context.ts +++ b/packages/opencode/src/control-plane/workspace-context.ts @@ -12,6 +12,10 @@ export const WorkspaceContext = { return context.provide({ workspaceID: input.workspaceID as string }, () => input.fn()) }, + restore(workspaceID: string, fn: () => R): R { + return context.provide({ workspaceID }, fn) + }, + get workspaceID() { try { return context.use().workspaceID diff --git a/packages/opencode/src/effect/bridge.ts b/packages/opencode/src/effect/bridge.ts new file mode 100644 index 0000000000..bafa5a0ea6 --- /dev/null +++ b/packages/opencode/src/effect/bridge.ts @@ -0,0 +1,49 @@ +import { Effect, Fiber } from "effect" +import { WorkspaceContext } from "@/control-plane/workspace-context" +import { Instance, type InstanceContext } from "@/project/instance" +import { LocalContext } from "@/util/local-context" +import { InstanceRef, WorkspaceRef } from "./instance-ref" +import { attachWith } from "./run-service" + +export namespace EffectBridge { + export interface Shape { + readonly promise: (effect: Effect.Effect) => Promise + readonly fork: (effect: Effect.Effect) => Fiber.Fiber + } + + function restore(instance: InstanceContext | undefined, workspace: string | undefined, fn: () => R): R { + if (instance && workspace !== undefined) { + return WorkspaceContext.restore(workspace, () => Instance.restore(instance, fn)) + } + if (instance) return Instance.restore(instance, fn) + if (workspace !== undefined) return WorkspaceContext.restore(workspace, fn) + return fn() + } + + export function make(): Effect.Effect { + return Effect.gen(function* () { + const ctx = yield* Effect.context() + const value = yield* InstanceRef + const instance = + value ?? + (() => { + try { + return Instance.current + } catch (err) { + if (!(err instanceof LocalContext.NotFound)) throw err + } + })() + const workspace = (yield* WorkspaceRef) ?? WorkspaceContext.workspaceID + const attach = (effect: Effect.Effect) => attachWith(effect, { instance, workspace }) + const wrap = (effect: Effect.Effect) => + attach(effect).pipe(Effect.provide(ctx)) as Effect.Effect + + return { + promise: (effect: Effect.Effect) => + restore(instance, workspace, () => Effect.runPromise(wrap(effect))), + fork: (effect: Effect.Effect) => + restore(instance, workspace, () => Effect.runFork(wrap(effect))), + } satisfies Shape + }) + } +} diff --git a/packages/opencode/src/effect/run-service.ts b/packages/opencode/src/effect/run-service.ts index bb4307b57c..13104c88b3 100644 --- a/packages/opencode/src/effect/run-service.ts +++ b/packages/opencode/src/effect/run-service.ts @@ -5,14 +5,31 @@ import { LocalContext } from "@/util/local-context" import { InstanceRef, WorkspaceRef } from "./instance-ref" import { Observability } from "./observability" import { WorkspaceContext } from "@/control-plane/workspace-context" +import type { InstanceContext } from "@/project/instance" export const memoMap = Layer.makeMemoMapUnsafe() +type Refs = { + instance?: InstanceContext + workspace?: string +} + +export function attachWith(effect: Effect.Effect, refs: Refs): Effect.Effect { + if (!refs.instance && !refs.workspace) return effect + if (!refs.instance) return effect.pipe(Effect.provideService(WorkspaceRef, refs.workspace)) + if (!refs.workspace) return effect.pipe(Effect.provideService(InstanceRef, refs.instance)) + return effect.pipe( + Effect.provideService(InstanceRef, refs.instance), + Effect.provideService(WorkspaceRef, refs.workspace), + ) +} + export function attach(effect: Effect.Effect): Effect.Effect { try { - const ctx = Instance.current - const workspaceID = WorkspaceContext.workspaceID - return effect.pipe(Effect.provideService(InstanceRef, ctx), Effect.provideService(WorkspaceRef, workspaceID)) + return attachWith(effect, { + instance: Instance.current, + workspace: WorkspaceContext.workspaceID, + }) } catch (err) { if (!(err instanceof LocalContext.NotFound)) throw err } diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 3b66909340..a68c6c1d8d 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -25,7 +25,7 @@ import { Bus } from "@/bus" import { TuiEvent } from "@/cli/cmd/tui/event" import open from "open" import { Effect, Exit, Layer, Option, Context, Stream } from "effect" -import { EffectLogger } from "@/effect/logger" +import { EffectBridge } from "@/effect/bridge" import { InstanceState } from "@/effect/instance-state" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" @@ -471,25 +471,24 @@ export namespace MCP { Effect.catch(() => Effect.succeed([] as number[])), ) - function watch(s: State, name: string, client: MCPClient, timeout?: number) { + function watch(s: State, name: string, client: MCPClient, bridge: EffectBridge.Shape, timeout?: number) { client.setNotificationHandler(ToolListChangedNotificationSchema, async () => { log.info("tools list changed notification received", { server: name }) if (s.clients[name] !== client || s.status[name]?.status !== "connected") return - const listed = await Effect.runPromise(defs(name, client, timeout).pipe(Effect.provide(EffectLogger.layer))) + const listed = await bridge.promise(defs(name, client, timeout)) if (!listed) return if (s.clients[name] !== client || s.status[name]?.status !== "connected") return s.defs[name] = listed - await Effect.runPromise( - bus.publish(ToolsChanged, { server: name }).pipe(Effect.ignore, Effect.provide(EffectLogger.layer)), - ) + await bridge.promise(bus.publish(ToolsChanged, { server: name }).pipe(Effect.ignore)) }) } const state = yield* InstanceState.make( Effect.fn("MCP.state")(function* () { const cfg = yield* cfgSvc.get() + const bridge = yield* EffectBridge.make() const config = cfg.mcp ?? {} const s: State = { status: {}, @@ -518,7 +517,7 @@ export namespace MCP { if (result.mcpClient) { s.clients[key] = result.mcpClient s.defs[key] = result.defs! - watch(s, key, result.mcpClient, mcp.timeout) + watch(s, key, result.mcpClient, bridge, mcp.timeout) } }), { concurrency: "unbounded" }, @@ -565,11 +564,12 @@ export namespace MCP { listed: MCPToolDef[], timeout?: number, ) { + const bridge = yield* EffectBridge.make() yield* closeClient(s, name) s.status[name] = { status: "connected" } s.clients[name] = client s.defs[name] = listed - watch(s, name, client, timeout) + watch(s, name, client, bridge, timeout) return s.status[name] }) diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index c716ffdf8d..9f618eff8c 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -18,7 +18,7 @@ import { gitlabAuthPlugin as GitlabAuthPlugin } from "opencode-gitlab-auth" import { PoeAuthPlugin } from "opencode-poe-auth" import { CloudflareAIGatewayAuthPlugin, CloudflareWorkersAuthPlugin } from "./cloudflare" import { Effect, Layer, Context, Stream } from "effect" -import { EffectLogger } from "@/effect/logger" +import { EffectBridge } from "@/effect/bridge" import { InstanceState } from "@/effect/instance-state" import { errorMessage } from "@/util/error" import { PluginLoader } from "./loader" @@ -90,14 +90,6 @@ export namespace Plugin { return result } - function publishPluginError(bus: Bus.Interface, message: string) { - Effect.runFork( - bus - .publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() }) - .pipe(Effect.provide(EffectLogger.layer)), - ) - } - async function applyPlugin(load: PluginLoader.Loaded, input: PluginInput, hooks: Hooks[]) { const plugin = readV1Plugin(load.mod, load.spec, "server", "detect") if (plugin) { @@ -120,6 +112,11 @@ export namespace Plugin { const state = yield* InstanceState.make( Effect.fn("Plugin.state")(function* (ctx) { const hooks: Hooks[] = [] + const bridge = yield* EffectBridge.make() + + function publishPluginError(message: string) { + bridge.fork(bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() })) + } const { Server } = yield* Effect.promise(() => import("../server/server")) @@ -187,24 +184,24 @@ export namespace Plugin { if (stage === "install") { const parsed = parsePluginSpecifier(spec) log.error("failed to install plugin", { pkg: parsed.pkg, version: parsed.version, error: message }) - publishPluginError(bus, `Failed to install plugin ${parsed.pkg}@${parsed.version}: ${message}`) + publishPluginError(`Failed to install plugin ${parsed.pkg}@${parsed.version}: ${message}`) return } if (stage === "compatibility") { log.warn("plugin incompatible", { path: spec, error: message }) - publishPluginError(bus, `Plugin ${spec} skipped: ${message}`) + publishPluginError(`Plugin ${spec} skipped: ${message}`) return } if (stage === "entry") { log.error("failed to resolve plugin server entry", { path: spec, error: message }) - publishPluginError(bus, `Failed to load plugin ${spec}: ${message}`) + publishPluginError(`Failed to load plugin ${spec}: ${message}`) return } log.error("failed to load plugin", { path: spec, target: resolved?.entry, error: message }) - publishPluginError(bus, `Failed to load plugin ${spec}: ${message}`) + publishPluginError(`Failed to load plugin ${spec}: ${message}`) }, }, }), diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index d34721f1d8..8833cfd05f 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -19,7 +19,7 @@ import { iife } from "@/util/iife" import { Global } from "../global" import path from "path" import { Effect, Layer, Context } from "effect" -import { EffectLogger } from "@/effect/logger" +import { EffectBridge } from "@/effect/bridge" import { InstanceState } from "@/effect/instance-state" import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { isRecord } from "@/util/record" @@ -1043,6 +1043,7 @@ export namespace Provider { const state = yield* InstanceState.make(() => Effect.gen(function* () { using _ = log.time("state") + const bridge = yield* EffectBridge.make() const cfg = yield* config.get() const modelsDev = yield* Effect.promise(() => ModelsDev.get()) const database = mapValues(modelsDev, fromModelsDevProvider) @@ -1223,8 +1224,7 @@ export namespace Provider { const options = yield* Effect.promise(() => plugin.auth!.loader!( - () => - Effect.runPromise(auth.get(providerID).pipe(Effect.orDie, Effect.provide(EffectLogger.layer))) as any, + () => bridge.promise(auth.get(providerID).pipe(Effect.orDie)) as any, database[plugin.auth!.provider], ), ) diff --git a/packages/opencode/src/pty/index.ts b/packages/opencode/src/pty/index.ts index 9c79eb2d4c..1c969b4b93 100644 --- a/packages/opencode/src/pty/index.ts +++ b/packages/opencode/src/pty/index.ts @@ -10,7 +10,7 @@ import { Shell } from "@/shell/shell" import { Plugin } from "@/plugin" import { PtyID } from "./schema" import { Effect, Layer, Context } from "effect" -import { EffectLogger } from "@/effect/logger" +import { EffectBridge } from "@/effect/bridge" export namespace Pty { const log = Log.create({ service: "pty" }) @@ -173,6 +173,7 @@ export namespace Pty { const create = Effect.fn("Pty.create")(function* (input: CreateInput) { const s = yield* InstanceState.get(state) + const bridge = yield* EffectBridge.make() const id = PtyID.ascending() const command = input.command || Shell.preferred() const args = input.args || [] @@ -256,8 +257,8 @@ export namespace Pty { if (session.info.status === "exited") return log.info("session exited", { id, exitCode }) session.info.status = "exited" - Effect.runFork(bus.publish(Event.Exited, { id, exitCode }).pipe(Effect.provide(EffectLogger.layer))) - Effect.runFork(remove(id).pipe(Effect.provide(EffectLogger.layer))) + bridge.fork(bus.publish(Event.Exited, { id, exitCode })) + bridge.fork(remove(id)) }), ) yield* bus.publish(Event.Created, { info }) diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index 5a4c041196..05d7882757 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -20,13 +20,12 @@ import { Wildcard } from "@/util/wildcard" import { SessionID } from "@/session/schema" import { Auth } from "@/auth" import { Installation } from "@/installation" -import { makeRuntime } from "@/effect/run-service" +import { EffectBridge } from "@/effect/bridge" import * as Option from "effect/Option" import * as OtelTracer from "@effect/opentelemetry/Tracer" export namespace LLM { const log = Log.create({ service: "llm" }) - const perms = makeRuntime(Permission.Service, Permission.defaultLayer) export const OUTPUT_TOKEN_MAX = ProviderTransform.OUTPUT_TOKEN_MAX type Result = Awaited> @@ -57,369 +56,371 @@ export namespace LLM { export class Service extends Context.Service()("@opencode/LLM") {} - export const layer: Layer.Layer = - Layer.effect( - Service, - Effect.gen(function* () { - const auth = yield* Auth.Service - const config = yield* Config.Service - const provider = yield* Provider.Service - const plugin = yield* Plugin.Service + const live: Layer.Layer< + Service, + never, + Auth.Service | Config.Service | Provider.Service | Plugin.Service | Permission.Service + > = Layer.effect( + Service, + Effect.gen(function* () { + const auth = yield* Auth.Service + const config = yield* Config.Service + const provider = yield* Provider.Service + const plugin = yield* Plugin.Service + const perm = yield* Permission.Service - const run = Effect.fn("LLM.run")(function* (input: StreamRequest) { - const l = log - .clone() - .tag("providerID", input.model.providerID) - .tag("modelID", input.model.id) - .tag("sessionID", input.sessionID) - .tag("small", (input.small ?? false).toString()) - .tag("agent", input.agent.name) - .tag("mode", input.agent.mode) - l.info("stream", { - modelID: input.model.id, - providerID: input.model.providerID, - }) - - const [language, cfg, item, info] = yield* Effect.all( - [ - provider.getLanguage(input.model), - config.get(), - provider.getProvider(input.model.providerID), - auth.get(input.model.providerID), - ], - { concurrency: "unbounded" }, - ) - - // TODO: move this to a proper hook - const isOpenaiOauth = item.id === "openai" && info?.type === "oauth" - - const system: string[] = [] - system.push( - [ - // use agent prompt otherwise provider prompt - ...(input.agent.prompt ? [input.agent.prompt] : SystemPrompt.provider(input.model)), - // any custom prompt passed into this call - ...input.system, - // any custom prompt from last user message - ...(input.user.system ? [input.user.system] : []), - ] - .filter((x) => x) - .join("\n"), - ) - - const header = system[0] - yield* plugin.trigger( - "experimental.chat.system.transform", - { sessionID: input.sessionID, model: input.model }, - { system }, - ) - // rejoin to maintain 2-part structure for caching if header unchanged - if (system.length > 2 && system[0] === header) { - const rest = system.slice(1) - system.length = 0 - system.push(header, rest.join("\n")) - } - - const variant = - !input.small && input.model.variants && input.user.model.variant - ? input.model.variants[input.user.model.variant] - : {} - const base = input.small - ? ProviderTransform.smallOptions(input.model) - : ProviderTransform.options({ - model: input.model, - sessionID: input.sessionID, - providerOptions: item.options, - }) - const options: Record = pipe( - base, - mergeDeep(input.model.options), - mergeDeep(input.agent.options), - mergeDeep(variant), - ) - if (isOpenaiOauth) { - options.instructions = system.join("\n") - } - - const isWorkflow = language instanceof GitLabWorkflowLanguageModel - const messages = isOpenaiOauth - ? input.messages - : isWorkflow - ? input.messages - : [ - ...system.map( - (x): ModelMessage => ({ - role: "system", - content: x, - }), - ), - ...input.messages, - ] - - const params = yield* plugin.trigger( - "chat.params", - { - sessionID: input.sessionID, - agent: input.agent.name, - model: input.model, - provider: item, - message: input.user, - }, - { - temperature: input.model.capabilities.temperature - ? (input.agent.temperature ?? ProviderTransform.temperature(input.model)) - : undefined, - topP: input.agent.topP ?? ProviderTransform.topP(input.model), - topK: ProviderTransform.topK(input.model), - maxOutputTokens: ProviderTransform.maxOutputTokens(input.model), - options, - }, - ) - - const { headers } = yield* plugin.trigger( - "chat.headers", - { - sessionID: input.sessionID, - agent: input.agent.name, - model: input.model, - provider: item, - message: input.user, - }, - { - headers: {}, - }, - ) - - const tools = resolveTools(input) - - // LiteLLM and some Anthropic proxies require the tools parameter to be present - // when message history contains tool calls, even if no tools are being used. - // Add a dummy tool that is never called to satisfy this validation. - // This is enabled for: - // 1. Providers with "litellm" in their ID or API ID (auto-detected) - // 2. Providers with explicit "litellmProxy: true" option (opt-in for custom gateways) - const isLiteLLMProxy = - item.options?.["litellmProxy"] === true || - input.model.providerID.toLowerCase().includes("litellm") || - input.model.api.id.toLowerCase().includes("litellm") - - // LiteLLM/Bedrock rejects requests where the message history contains tool - // calls but no tools param is present. When there are no active tools (e.g. - // during compaction), inject a stub tool to satisfy the validation requirement. - // The stub description explicitly tells the model not to call it. - if ( - (isLiteLLMProxy || input.model.providerID.includes("github-copilot")) && - Object.keys(tools).length === 0 && - hasToolCalls(input.messages) - ) { - tools["_noop"] = tool({ - description: "Do not call this tool. It exists only for API compatibility and must never be invoked.", - inputSchema: jsonSchema({ - type: "object", - properties: { - reason: { type: "string", description: "Unused" }, - }, - }), - execute: async () => ({ output: "", title: "", metadata: {} }), - }) - } - - // Wire up toolExecutor for DWS workflow models so that tool calls - // from the workflow service are executed via opencode's tool system - // and results sent back over the WebSocket. - if (language instanceof GitLabWorkflowLanguageModel) { - const workflowModel = language as GitLabWorkflowLanguageModel & { - sessionID?: string - sessionPreapprovedTools?: string[] - approvalHandler?: (approvalTools: { name: string; args: string }[]) => Promise<{ approved: boolean }> - } - workflowModel.sessionID = input.sessionID - workflowModel.systemPrompt = system.join("\n") - workflowModel.toolExecutor = async (toolName, argsJson, _requestID) => { - const t = tools[toolName] - if (!t || !t.execute) { - return { result: "", error: `Unknown tool: ${toolName}` } - } - try { - const result = await t.execute!(JSON.parse(argsJson), { - toolCallId: _requestID, - messages: input.messages, - abortSignal: input.abort, - }) - const output = typeof result === "string" ? result : (result?.output ?? JSON.stringify(result)) - return { - result: output, - metadata: typeof result === "object" ? result?.metadata : undefined, - title: typeof result === "object" ? result?.title : undefined, - } - } catch (e: any) { - return { result: "", error: e.message ?? String(e) } - } - } - - const ruleset = Permission.merge(input.agent.permission ?? [], input.permission ?? []) - workflowModel.sessionPreapprovedTools = Object.keys(tools).filter((name) => { - const match = ruleset.findLast((rule) => Wildcard.match(name, rule.permission)) - return !match || match.action !== "ask" - }) - - const approvedToolsForSession = new Set() - workflowModel.approvalHandler = Instance.bind(async (approvalTools) => { - const uniqueNames = [...new Set(approvalTools.map((t: { name: string }) => t.name))] as string[] - // Auto-approve tools that were already approved in this session - // (prevents infinite approval loops for server-side MCP tools) - if (uniqueNames.every((name) => approvedToolsForSession.has(name))) { - return { approved: true } - } - - const id = PermissionID.ascending() - let reply: Permission.Reply | undefined - let unsub: (() => void) | undefined - try { - unsub = Bus.subscribe(Permission.Event.Replied, (evt) => { - if (evt.properties.requestID === id) reply = evt.properties.reply - }) - const toolPatterns = approvalTools.map((t: { name: string; args: string }) => { - try { - const parsed = JSON.parse(t.args) as Record - const title = (parsed?.title ?? parsed?.name ?? "") as string - return title ? `${t.name}: ${title}` : t.name - } catch { - return t.name - } - }) - const uniquePatterns = [...new Set(toolPatterns)] as string[] - await perms.runPromise((svc) => - svc.ask({ - id, - sessionID: SessionID.make(input.sessionID), - permission: "workflow_tool_approval", - patterns: uniquePatterns, - metadata: { tools: approvalTools }, - always: uniquePatterns, - ruleset: [], - }), - ) - for (const name of uniqueNames) approvedToolsForSession.add(name) - workflowModel.sessionPreapprovedTools = [ - ...(workflowModel.sessionPreapprovedTools ?? []), - ...uniqueNames, - ] - return { approved: true } - } catch { - return { approved: false } - } finally { - unsub?.() - } - }) - } - - const tracer = cfg.experimental?.openTelemetry - ? Option.getOrUndefined(yield* Effect.serviceOption(OtelTracer.OtelTracer)) - : undefined - - return streamText({ - onError(error) { - l.error("stream error", { - error, - }) - }, - async experimental_repairToolCall(failed) { - const lower = failed.toolCall.toolName.toLowerCase() - if (lower !== failed.toolCall.toolName && tools[lower]) { - l.info("repairing tool call", { - tool: failed.toolCall.toolName, - repaired: lower, - }) - return { - ...failed.toolCall, - toolName: lower, - } - } - return { - ...failed.toolCall, - input: JSON.stringify({ - tool: failed.toolCall.toolName, - error: failed.error.message, - }), - toolName: "invalid", - } - }, - temperature: params.temperature, - topP: params.topP, - topK: params.topK, - providerOptions: ProviderTransform.providerOptions(input.model, params.options), - activeTools: Object.keys(tools).filter((x) => x !== "invalid"), - tools, - toolChoice: input.toolChoice, - maxOutputTokens: params.maxOutputTokens, - abortSignal: input.abort, - headers: { - ...(input.model.providerID.startsWith("opencode") - ? { - "x-opencode-project": Instance.project.id, - "x-opencode-session": input.sessionID, - "x-opencode-request": input.user.id, - "x-opencode-client": Flag.OPENCODE_CLIENT, - } - : { - "x-session-affinity": input.sessionID, - ...(input.parentSessionID ? { "x-parent-session-id": input.parentSessionID } : {}), - "User-Agent": `opencode/${Installation.VERSION}`, - }), - ...input.model.headers, - ...headers, - }, - maxRetries: input.retries ?? 0, - messages, - model: wrapLanguageModel({ - model: language, - middleware: [ - { - specificationVersion: "v3" as const, - async transformParams(args) { - if (args.type === "stream") { - // @ts-expect-error - args.params.prompt = ProviderTransform.message(args.params.prompt, input.model, options) - } - return args.params - }, - }, - ], - }), - experimental_telemetry: { - isEnabled: cfg.experimental?.openTelemetry, - functionId: "session.llm", - tracer, - metadata: { - userId: cfg.username ?? "unknown", - sessionId: input.sessionID, - }, - }, - }) + const run = Effect.fn("LLM.run")(function* (input: StreamRequest) { + const l = log + .clone() + .tag("providerID", input.model.providerID) + .tag("modelID", input.model.id) + .tag("sessionID", input.sessionID) + .tag("small", (input.small ?? false).toString()) + .tag("agent", input.agent.name) + .tag("mode", input.agent.mode) + l.info("stream", { + modelID: input.model.id, + providerID: input.model.providerID, }) - const stream: Interface["stream"] = (input) => - Stream.scoped( - Stream.unwrap( - Effect.gen(function* () { - const ctrl = yield* Effect.acquireRelease( - Effect.sync(() => new AbortController()), - (ctrl) => Effect.sync(() => ctrl.abort()), - ) + const [language, cfg, item, info] = yield* Effect.all( + [ + provider.getLanguage(input.model), + config.get(), + provider.getProvider(input.model.providerID), + auth.get(input.model.providerID), + ], + { concurrency: "unbounded" }, + ) - const result = yield* run({ ...input, abort: ctrl.signal }) + // TODO: move this to a proper hook + const isOpenaiOauth = item.id === "openai" && info?.type === "oauth" - return Stream.fromAsyncIterable(result.fullStream, (e) => - e instanceof Error ? e : new Error(String(e)), - ) + const system: string[] = [] + system.push( + [ + // use agent prompt otherwise provider prompt + ...(input.agent.prompt ? [input.agent.prompt] : SystemPrompt.provider(input.model)), + // any custom prompt passed into this call + ...input.system, + // any custom prompt from last user message + ...(input.user.system ? [input.user.system] : []), + ] + .filter((x) => x) + .join("\n"), + ) + + const header = system[0] + yield* plugin.trigger( + "experimental.chat.system.transform", + { sessionID: input.sessionID, model: input.model }, + { system }, + ) + // rejoin to maintain 2-part structure for caching if header unchanged + if (system.length > 2 && system[0] === header) { + const rest = system.slice(1) + system.length = 0 + system.push(header, rest.join("\n")) + } + + const variant = + !input.small && input.model.variants && input.user.model.variant + ? input.model.variants[input.user.model.variant] + : {} + const base = input.small + ? ProviderTransform.smallOptions(input.model) + : ProviderTransform.options({ + model: input.model, + sessionID: input.sessionID, + providerOptions: item.options, + }) + const options: Record = pipe( + base, + mergeDeep(input.model.options), + mergeDeep(input.agent.options), + mergeDeep(variant), + ) + if (isOpenaiOauth) { + options.instructions = system.join("\n") + } + + const isWorkflow = language instanceof GitLabWorkflowLanguageModel + const messages = isOpenaiOauth + ? input.messages + : isWorkflow + ? input.messages + : [ + ...system.map( + (x): ModelMessage => ({ + role: "system", + content: x, + }), + ), + ...input.messages, + ] + + const params = yield* plugin.trigger( + "chat.params", + { + sessionID: input.sessionID, + agent: input.agent.name, + model: input.model, + provider: item, + message: input.user, + }, + { + temperature: input.model.capabilities.temperature + ? (input.agent.temperature ?? ProviderTransform.temperature(input.model)) + : undefined, + topP: input.agent.topP ?? ProviderTransform.topP(input.model), + topK: ProviderTransform.topK(input.model), + maxOutputTokens: ProviderTransform.maxOutputTokens(input.model), + options, + }, + ) + + const { headers } = yield* plugin.trigger( + "chat.headers", + { + sessionID: input.sessionID, + agent: input.agent.name, + model: input.model, + provider: item, + message: input.user, + }, + { + headers: {}, + }, + ) + + const tools = resolveTools(input) + + // LiteLLM and some Anthropic proxies require the tools parameter to be present + // when message history contains tool calls, even if no tools are being used. + // Add a dummy tool that is never called to satisfy this validation. + // This is enabled for: + // 1. Providers with "litellm" in their ID or API ID (auto-detected) + // 2. Providers with explicit "litellmProxy: true" option (opt-in for custom gateways) + const isLiteLLMProxy = + item.options?.["litellmProxy"] === true || + input.model.providerID.toLowerCase().includes("litellm") || + input.model.api.id.toLowerCase().includes("litellm") + + // LiteLLM/Bedrock rejects requests where the message history contains tool + // calls but no tools param is present. When there are no active tools (e.g. + // during compaction), inject a stub tool to satisfy the validation requirement. + // The stub description explicitly tells the model not to call it. + if ( + (isLiteLLMProxy || input.model.providerID.includes("github-copilot")) && + Object.keys(tools).length === 0 && + hasToolCalls(input.messages) + ) { + tools["_noop"] = tool({ + description: "Do not call this tool. It exists only for API compatibility and must never be invoked.", + inputSchema: jsonSchema({ + type: "object", + properties: { + reason: { type: "string", description: "Unused" }, + }, + }), + execute: async () => ({ output: "", title: "", metadata: {} }), + }) + } + + // Wire up toolExecutor for DWS workflow models so that tool calls + // from the workflow service are executed via opencode's tool system + // and results sent back over the WebSocket. + if (language instanceof GitLabWorkflowLanguageModel) { + const workflowModel = language as GitLabWorkflowLanguageModel & { + sessionID?: string + sessionPreapprovedTools?: string[] + approvalHandler?: (approvalTools: { name: string; args: string }[]) => Promise<{ approved: boolean }> + } + workflowModel.sessionID = input.sessionID + workflowModel.systemPrompt = system.join("\n") + workflowModel.toolExecutor = async (toolName, argsJson, _requestID) => { + const t = tools[toolName] + if (!t || !t.execute) { + return { result: "", error: `Unknown tool: ${toolName}` } + } + try { + const result = await t.execute!(JSON.parse(argsJson), { + toolCallId: _requestID, + messages: input.messages, + abortSignal: input.abort, + }) + const output = typeof result === "string" ? result : (result?.output ?? JSON.stringify(result)) + return { + result: output, + metadata: typeof result === "object" ? result?.metadata : undefined, + title: typeof result === "object" ? result?.title : undefined, + } + } catch (e: any) { + return { result: "", error: e.message ?? String(e) } + } + } + + const ruleset = Permission.merge(input.agent.permission ?? [], input.permission ?? []) + workflowModel.sessionPreapprovedTools = Object.keys(tools).filter((name) => { + const match = ruleset.findLast((rule) => Wildcard.match(name, rule.permission)) + return !match || match.action !== "ask" + }) + + const bridge = yield* EffectBridge.make() + const approvedToolsForSession = new Set() + workflowModel.approvalHandler = Instance.bind(async (approvalTools) => { + const uniqueNames = [...new Set(approvalTools.map((t: { name: string }) => t.name))] as string[] + // Auto-approve tools that were already approved in this session + // (prevents infinite approval loops for server-side MCP tools) + if (uniqueNames.every((name) => approvedToolsForSession.has(name))) { + return { approved: true } + } + + const id = PermissionID.ascending() + let reply: Permission.Reply | undefined + let unsub: (() => void) | undefined + try { + unsub = Bus.subscribe(Permission.Event.Replied, (evt) => { + if (evt.properties.requestID === id) reply = evt.properties.reply + }) + const toolPatterns = approvalTools.map((t: { name: string; args: string }) => { + try { + const parsed = JSON.parse(t.args) as Record + const title = (parsed?.title ?? parsed?.name ?? "") as string + return title ? `${t.name}: ${title}` : t.name + } catch { + return t.name + } + }) + const uniquePatterns = [...new Set(toolPatterns)] as string[] + await bridge.promise( + perm.ask({ + id, + sessionID: SessionID.make(input.sessionID), + permission: "workflow_tool_approval", + patterns: uniquePatterns, + metadata: { tools: approvalTools }, + always: uniquePatterns, + ruleset: [], + }), + ) + for (const name of uniqueNames) approvedToolsForSession.add(name) + workflowModel.sessionPreapprovedTools = [...(workflowModel.sessionPreapprovedTools ?? []), ...uniqueNames] + return { approved: true } + } catch { + return { approved: false } + } finally { + unsub?.() + } + }) + } + + const tracer = cfg.experimental?.openTelemetry + ? Option.getOrUndefined(yield* Effect.serviceOption(OtelTracer.OtelTracer)) + : undefined + + return streamText({ + onError(error) { + l.error("stream error", { + error, + }) + }, + async experimental_repairToolCall(failed) { + const lower = failed.toolCall.toolName.toLowerCase() + if (lower !== failed.toolCall.toolName && tools[lower]) { + l.info("repairing tool call", { + tool: failed.toolCall.toolName, + repaired: lower, + }) + return { + ...failed.toolCall, + toolName: lower, + } + } + return { + ...failed.toolCall, + input: JSON.stringify({ + tool: failed.toolCall.toolName, + error: failed.error.message, }), - ), - ) + toolName: "invalid", + } + }, + temperature: params.temperature, + topP: params.topP, + topK: params.topK, + providerOptions: ProviderTransform.providerOptions(input.model, params.options), + activeTools: Object.keys(tools).filter((x) => x !== "invalid"), + tools, + toolChoice: input.toolChoice, + maxOutputTokens: params.maxOutputTokens, + abortSignal: input.abort, + headers: { + ...(input.model.providerID.startsWith("opencode") + ? { + "x-opencode-project": Instance.project.id, + "x-opencode-session": input.sessionID, + "x-opencode-request": input.user.id, + "x-opencode-client": Flag.OPENCODE_CLIENT, + } + : { + "x-session-affinity": input.sessionID, + ...(input.parentSessionID ? { "x-parent-session-id": input.parentSessionID } : {}), + "User-Agent": `opencode/${Installation.VERSION}`, + }), + ...input.model.headers, + ...headers, + }, + maxRetries: input.retries ?? 0, + messages, + model: wrapLanguageModel({ + model: language, + middleware: [ + { + specificationVersion: "v3" as const, + async transformParams(args) { + if (args.type === "stream") { + // @ts-expect-error + args.params.prompt = ProviderTransform.message(args.params.prompt, input.model, options) + } + return args.params + }, + }, + ], + }), + experimental_telemetry: { + isEnabled: cfg.experimental?.openTelemetry, + functionId: "session.llm", + tracer, + metadata: { + userId: cfg.username ?? "unknown", + sessionId: input.sessionID, + }, + }, + }) + }) - return Service.of({ stream }) - }), - ) + const stream: Interface["stream"] = (input) => + Stream.scoped( + Stream.unwrap( + Effect.gen(function* () { + const ctrl = yield* Effect.acquireRelease( + Effect.sync(() => new AbortController()), + (ctrl) => Effect.sync(() => ctrl.abort()), + ) + + const result = yield* run({ ...input, abort: ctrl.signal }) + + return Stream.fromAsyncIterable(result.fullStream, (e) => (e instanceof Error ? e : new Error(String(e)))) + }), + ), + ) + + return Service.of({ stream }) + }), + ) + + export const layer = live.pipe(Layer.provide(Permission.defaultLayer)) export const defaultLayer = Layer.suspend(() => layer.pipe( diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index f8c794505e..ffd074d3f8 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -48,6 +48,7 @@ import { EffectLogger } from "@/effect/logger" import { InstanceState } from "@/effect/instance-state" import { TaskTool, type TaskPromptOps } from "@/tool/task" import { SessionRunState } from "./run-state" +import { EffectBridge } from "@/effect/bridge" // @ts-ignore globalThis.AI_SDK_LOG_WARNINGS = false @@ -105,11 +106,7 @@ export namespace SessionPrompt { const sys = yield* SystemPrompt.Service const llm = yield* LLM.Service const runner = Effect.fn("SessionPrompt.runner")(function* () { - const ctx = yield* Effect.context() - return { - promise: (effect: Effect.Effect) => Effect.runPromiseWith(ctx)(effect), - fork: (effect: Effect.Effect) => Effect.runForkWith(ctx)(effect), - } + return yield* EffectBridge.make() }) const ops = Effect.fn("SessionPrompt.ops")(function* () { const run = yield* runner() diff --git a/packages/opencode/test/effect/app-runtime-logger.test.ts b/packages/opencode/test/effect/app-runtime-logger.test.ts index 8a7aab6cf8..7388748f92 100644 --- a/packages/opencode/test/effect/app-runtime-logger.test.ts +++ b/packages/opencode/test/effect/app-runtime-logger.test.ts @@ -1,6 +1,7 @@ import { expect, test } from "bun:test" import { Context, Effect, Layer, Logger } from "effect" import { AppRuntime } from "../../src/effect/app-runtime" +import { EffectBridge } from "../../src/effect/bridge" import { InstanceRef } from "../../src/effect/instance-ref" import { EffectLogger } from "../../src/effect/logger" import { makeRuntime } from "../../src/effect/run-service" @@ -59,3 +60,33 @@ test("AppRuntime attaches InstanceRef from ALS", async () => { expect(dir).toBe(tmp.path) }) + +test("EffectBridge preserves logger and instance context across async boundaries", async () => { + await using tmp = await tmpdir({ git: true }) + + const result = await Instance.provide({ + directory: tmp.path, + fn: () => + AppRuntime.runPromise( + Effect.gen(function* () { + const bridge = yield* EffectBridge.make() + return yield* Effect.promise(() => + Promise.resolve().then(() => + bridge.promise( + Effect.gen(function* () { + return { + directory: (yield* InstanceRef)?.directory, + ...check(yield* Effect.service(Logger.CurrentLoggers)), + } + }), + ), + ), + ) + }), + ), + }) + + expect(result.directory).toBe(tmp.path) + expect(result.effectLogger).toBe(true) + expect(result.defaultLogger).toBe(false) +}) diff --git a/packages/server/package.json b/packages/server/package.json index c397c40d90..9b8b31299d 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -17,10 +17,11 @@ "dist" ], "scripts": { - "typecheck": "tsc --noEmit", + "typecheck": "tsgo --noEmit", "build": "tsc" }, "devDependencies": { + "@typescript/native-preview": "catalog:", "typescript": "catalog:" }, "dependencies": { From 4ae7c77f8abda8d51ddf52ee6e07890fa19b6629 Mon Sep 17 00:00:00 2001 From: Dax Date: Wed, 15 Apr 2026 11:50:24 -0400 Subject: [PATCH 145/300] migrate: move flock and hash utilities to shared package (#22640) --- bun.lock | 2 + packages/opencode/src/acp/agent.ts | 2 +- .../src/cli/cmd/tui/plugin/runtime.ts | 2 +- packages/opencode/src/config/config.ts | 2 +- packages/opencode/src/global/index.ts | 4 + packages/opencode/src/npm/index.ts | 2 +- packages/opencode/src/plugin/install.ts | 2 +- packages/opencode/src/plugin/meta.ts | 2 +- packages/opencode/src/provider/models.ts | 4 +- packages/opencode/src/provider/provider.ts | 2 +- packages/opencode/src/snapshot/index.ts | 2 +- .../opencode/test/fixture/flock-worker.ts | 2 +- packages/shared/package.json | 8 +- packages/shared/src/global.ts | 42 +++ packages/shared/src/npm.ts | 247 +++++++++++++ packages/shared/src/types.d.ts | 44 +++ .../{opencode => shared}/src/util/flock.ts | 29 +- .../{opencode => shared}/src/util/hash.ts | 0 .../shared/test/filesystem/filesystem.test.ts | 338 ++++++++++++++++++ packages/shared/test/fixture/flock-worker.ts | 72 ++++ packages/shared/test/lib/effect.ts | 53 +++ packages/shared/test/npm.test.ts | 18 + .../test/util/flock.test.ts | 91 +++-- 23 files changed, 929 insertions(+), 41 deletions(-) create mode 100644 packages/shared/src/global.ts create mode 100644 packages/shared/src/npm.ts create mode 100644 packages/shared/src/types.d.ts rename packages/{opencode => shared}/src/util/flock.ts (93%) rename packages/{opencode => shared}/src/util/hash.ts (100%) create mode 100644 packages/shared/test/filesystem/filesystem.test.ts create mode 100644 packages/shared/test/fixture/flock-worker.ts create mode 100644 packages/shared/test/lib/effect.ts create mode 100644 packages/shared/test/npm.test.ts rename packages/{opencode => shared}/test/util/flock.test.ts (80%) diff --git a/bun.lock b/bun.lock index fe5d42d7cc..a6f9891dd1 100644 --- a/bun.lock +++ b/bun.lock @@ -527,9 +527,11 @@ "mime-types": "3.0.2", "minimatch": "10.2.5", "semver": "catalog:", + "xdg-basedir": "5.1.0", "zod": "catalog:", }, "devDependencies": { + "@types/bun": "catalog:", "@types/semver": "catalog:", }, }, diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index 09f8663ed0..8ac09e4bb3 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -34,7 +34,7 @@ import { import { Log } from "../util/log" import { pathToFileURL } from "url" import { Filesystem } from "../util/filesystem" -import { Hash } from "../util/hash" +import { Hash } from "@opencode-ai/shared/util/hash" import { ACPSessionManager } from "./session" import type { ACPConfig } from "./types" import { Provider } from "../provider/provider" diff --git a/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts b/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts index 2f7fd51643..7f12106b2c 100644 --- a/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts +++ b/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts @@ -34,7 +34,7 @@ import { hasTheme, upsertTheme } from "../context/theme" import { Global } from "@/global" import { Filesystem } from "@/util/filesystem" import { Process } from "@/util/process" -import { Flock } from "@/util/flock" +import { Flock } from "@opencode-ai/shared/util/flock" import { Flag } from "@/flag/flag" import { INTERNAL_TUI_PLUGINS, type InternalTuiPlugin } from "./internal" import { setupSlots, Slot as View } from "./slots" diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index f8205bac26..915e604e90 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -34,7 +34,7 @@ import type { ConsoleState } from "./console-state" import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { InstanceState } from "@/effect/instance-state" import { Context, Duration, Effect, Exit, Fiber, Layer, Option } from "effect" -import { Flock } from "@/util/flock" +import { Flock } from "@opencode-ai/shared/util/flock" import { isPathPluginSpec, parsePluginSpecifier, resolvePathPluginTarget } from "@/plugin/shared" import { Npm } from "../npm" import { InstanceRef } from "@/effect/instance-ref" diff --git a/packages/opencode/src/global/index.ts b/packages/opencode/src/global/index.ts index 869019e2ce..32d5153213 100644 --- a/packages/opencode/src/global/index.ts +++ b/packages/opencode/src/global/index.ts @@ -3,6 +3,7 @@ import { xdgData, xdgCache, xdgConfig, xdgState } from "xdg-basedir" import path from "path" import os from "os" import { Filesystem } from "../util/filesystem" +import { Flock } from "@opencode-ai/shared/util/flock" const app = "opencode" @@ -26,6 +27,9 @@ export namespace Global { } } +// Initialize Flock with global state path +Flock.setGlobal({ state }) + await Promise.all([ fs.mkdir(Global.Path.data, { recursive: true }), fs.mkdir(Global.Path.config, { recursive: true }), diff --git a/packages/opencode/src/npm/index.ts b/packages/opencode/src/npm/index.ts index 5b708431c6..e648fd899c 100644 --- a/packages/opencode/src/npm/index.ts +++ b/packages/opencode/src/npm/index.ts @@ -6,7 +6,7 @@ import { Log } from "../util/log" import path from "path" import { readdir, rm } from "fs/promises" import { Filesystem } from "@/util/filesystem" -import { Flock } from "@/util/flock" +import { Flock } from "@opencode-ai/shared/util/flock" import { Arborist } from "@npmcli/arborist" export namespace Npm { diff --git a/packages/opencode/src/plugin/install.ts b/packages/opencode/src/plugin/install.ts index b6bac42a7f..8dd8212965 100644 --- a/packages/opencode/src/plugin/install.ts +++ b/packages/opencode/src/plugin/install.ts @@ -10,7 +10,7 @@ import { import { ConfigPaths } from "@/config/paths" import { Global } from "@/global" import { Filesystem } from "@/util/filesystem" -import { Flock } from "@/util/flock" +import { Flock } from "@opencode-ai/shared/util/flock" import { isRecord } from "@/util/record" import { parsePluginSpecifier, readPackageThemes, readPluginPackage, resolvePluginTarget } from "./shared" diff --git a/packages/opencode/src/plugin/meta.ts b/packages/opencode/src/plugin/meta.ts index cbfaf6ae15..f408954690 100644 --- a/packages/opencode/src/plugin/meta.ts +++ b/packages/opencode/src/plugin/meta.ts @@ -4,7 +4,7 @@ import { fileURLToPath } from "url" import { Flag } from "@/flag/flag" import { Global } from "@/global" import { Filesystem } from "@/util/filesystem" -import { Flock } from "@/util/flock" +import { Flock } from "@opencode-ai/shared/util/flock" import { parsePluginSpecifier, pluginSource } from "./shared" diff --git a/packages/opencode/src/provider/models.ts b/packages/opencode/src/provider/models.ts index 2d787588b0..55f137aa0b 100644 --- a/packages/opencode/src/provider/models.ts +++ b/packages/opencode/src/provider/models.ts @@ -6,8 +6,8 @@ import { Installation } from "../installation" import { Flag } from "../flag/flag" import { lazy } from "@/util/lazy" import { Filesystem } from "../util/filesystem" -import { Flock } from "@/util/flock" -import { Hash } from "@/util/hash" +import { Flock } from "@opencode-ai/shared/util/flock" +import { Hash } from "@opencode-ai/shared/util/hash" // Try to import bundled snapshot (generated at build time) // Falls back to undefined in dev mode when snapshot doesn't exist diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 8833cfd05f..9ec5dfc6b5 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -6,7 +6,7 @@ import { mapValues, mergeDeep, omit, pickBy, sortBy } from "remeda" import { NoSuchModelError, type Provider as SDK } from "ai" import { Log } from "../util/log" import { Npm } from "../npm" -import { Hash } from "../util/hash" +import { Hash } from "@opencode-ai/shared/util/hash" import { Plugin } from "../plugin" import { NamedError } from "@opencode-ai/shared/util/error" import { type LanguageModelV3 } from "@ai-sdk/provider" diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 2b21f7e895..9378e309aa 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -6,7 +6,7 @@ import z from "zod" import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" import { InstanceState } from "@/effect/instance-state" import { AppFileSystem } from "@opencode-ai/shared/filesystem" -import { Hash } from "@/util/hash" +import { Hash } from "@opencode-ai/shared/util/hash" import { Config } from "../config/config" import { Global } from "../global" import { Log } from "../util/log" diff --git a/packages/opencode/test/fixture/flock-worker.ts b/packages/opencode/test/fixture/flock-worker.ts index ac05fe810c..9954d290cc 100644 --- a/packages/opencode/test/fixture/flock-worker.ts +++ b/packages/opencode/test/fixture/flock-worker.ts @@ -1,5 +1,5 @@ import fs from "fs/promises" -import { Flock } from "../../src/util/flock" +import { Flock } from "@opencode-ai/shared/util/flock" type Msg = { key: string diff --git a/packages/shared/package.json b/packages/shared/package.json index 1bb1ca47ef..252b381d48 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -5,7 +5,9 @@ "type": "module", "license": "MIT", "private": true, - "scripts": {}, + "scripts": { + "test": "bun test" + }, "bin": { "opencode": "./bin/opencode" }, @@ -14,7 +16,8 @@ }, "imports": {}, "devDependencies": { - "@types/semver": "catalog:" + "@types/semver": "catalog:", + "@types/bun": "catalog:" }, "dependencies": { "@effect/platform-node": "catalog:", @@ -23,6 +26,7 @@ "mime-types": "3.0.2", "minimatch": "10.2.5", "semver": "catalog:", + "xdg-basedir": "5.1.0", "zod": "catalog:" }, "overrides": { diff --git a/packages/shared/src/global.ts b/packages/shared/src/global.ts new file mode 100644 index 0000000000..538cc091b5 --- /dev/null +++ b/packages/shared/src/global.ts @@ -0,0 +1,42 @@ +import path from "path" +import { xdgData, xdgCache, xdgConfig, xdgState } from "xdg-basedir" +import os from "os" +import { Context, Effect, Layer } from "effect" + +export namespace Global { + export class Service extends Context.Service()("@opencode/Global") {} + + export interface Interface { + readonly home: string + readonly data: string + readonly cache: string + readonly config: string + readonly state: string + readonly bin: string + readonly log: string + } + + export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const app = "opencode" + const home = process.env.OPENCODE_TEST_HOME ?? os.homedir() + const data = path.join(xdgData!, app) + const cache = path.join(xdgCache!, app) + const cfg = path.join(xdgConfig!, app) + const state = path.join(xdgState!, app) + const bin = path.join(cache, "bin") + const log = path.join(data, "log") + + return Service.of({ + home, + data, + cache, + config: cfg, + state, + bin, + log, + }) + }), + ) +} diff --git a/packages/shared/src/npm.ts b/packages/shared/src/npm.ts new file mode 100644 index 0000000000..994ec04dae --- /dev/null +++ b/packages/shared/src/npm.ts @@ -0,0 +1,247 @@ +import path from "path" +import semver from "semver" +import { Arborist } from "@npmcli/arborist" +import { Effect, Schema, Context, Layer, Option, FileSystem } from "effect" +import { NodeFileSystem } from "@effect/platform-node" +import { AppFileSystem } from "./filesystem" +import { Global } from "./global" +import { Flock } from "./util/flock" + +export namespace Npm { + export class InstallFailedError extends Schema.TaggedErrorClass()("NpmInstallFailedError", { + pkg: Schema.String, + cause: Schema.optional(Schema.Defect), + }) {} + + export interface EntryPoint { + readonly directory: string + readonly entrypoint: Option.Option + } + + export interface Interface { + readonly add: (pkg: string) => Effect.Effect + readonly install: (dir: string) => Effect.Effect + readonly outdated: (pkg: string, cachedVersion: string) => Effect.Effect + readonly which: (pkg: string) => Effect.Effect> + } + + export class Service extends Context.Service()("@opencode/Npm") {} + + const illegal = process.platform === "win32" ? new Set(["<", ">", ":", '"', "|", "?", "*"]) : undefined + + export function sanitize(pkg: string) { + if (!illegal) return pkg + return Array.from(pkg, (char) => (illegal.has(char) || char.charCodeAt(0) < 32 ? "_" : char)).join("") + } + + const resolveEntryPoint = (name: string, dir: string): EntryPoint => { + let entrypoint: Option.Option + try { + const resolved = typeof Bun !== "undefined" ? import.meta.resolve(name, dir) : import.meta.resolve(dir) + entrypoint = Option.some(resolved) + } catch { + entrypoint = Option.none() + } + return { + directory: dir, + entrypoint, + } + } + + interface ArboristNode { + name: string + path: string + } + + interface ArboristTree { + edgesOut: Map + } + export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const afs = yield* AppFileSystem.Service + const global = yield* Global.Service + const fs = yield* FileSystem.FileSystem + const directory = (pkg: string) => path.join(global.cache, "packages", sanitize(pkg)) + + const outdated = Effect.fn("Npm.outdated")(function* (pkg: string, cachedVersion: string) { + const response = yield* Effect.tryPromise({ + try: () => fetch(`https://registry.npmjs.org/${pkg}`), + catch: () => undefined, + }).pipe(Effect.orElseSucceed(() => undefined)) + + if (!response || !response.ok) { + return false + } + + const data = yield* Effect.tryPromise({ + try: () => response.json() as Promise<{ "dist-tags"?: { latest?: string } }>, + catch: () => undefined, + }).pipe(Effect.orElseSucceed(() => undefined)) + + const latestVersion = data?.["dist-tags"]?.latest + if (!latestVersion) { + return false + } + + const range = /[\s^~*xX<>|=]/.test(cachedVersion) + if (range) return !semver.satisfies(latestVersion, cachedVersion) + + return semver.lt(cachedVersion, latestVersion) + }) + + const add = Effect.fn("Npm.add")(function* (pkg: string) { + const dir = directory(pkg) + yield* Flock.effect(`npm-install:${dir}`) + + const arborist = new Arborist({ + path: dir, + binLinks: true, + progress: false, + savePrefix: "", + ignoreScripts: true, + }) + + const tree = yield* Effect.tryPromise({ + try: () => arborist.loadVirtual().catch(() => undefined), + catch: () => undefined, + }).pipe(Effect.orElseSucceed(() => undefined)) as Effect.Effect + + if (tree) { + const first = tree.edgesOut.values().next().value?.to + if (first) { + return resolveEntryPoint(first.name, first.path) + } + } + + const result = yield* Effect.tryPromise({ + try: () => + arborist.reify({ + add: [pkg], + save: true, + saveType: "prod", + }), + catch: (cause) => new InstallFailedError({ pkg, cause }), + }) as Effect.Effect + + const first = result.edgesOut.values().next().value?.to + if (!first) { + return yield* new InstallFailedError({ pkg }) + } + + return resolveEntryPoint(first.name, first.path) + }, Effect.scoped) + + const install = Effect.fn("Npm.install")(function* (dir: string) { + yield* Flock.effect(`npm-install:${dir}`) + + const reify = Effect.fnUntraced(function* () { + const arb = new Arborist({ + path: dir, + binLinks: true, + progress: false, + savePrefix: "", + ignoreScripts: true, + }) + yield* Effect.tryPromise({ + try: () => arb.reify().catch(() => {}), + catch: () => {}, + }).pipe(Effect.orElseSucceed(() => {})) + }) + + const nodeModulesExists = yield* afs.existsSafe(path.join(dir, "node_modules")) + if (!nodeModulesExists) { + yield* reify() + return + } + + const pkg = yield* afs.readJson(path.join(dir, "package.json")).pipe(Effect.orElseSucceed(() => ({}))) + const lock = yield* afs.readJson(path.join(dir, "package-lock.json")).pipe(Effect.orElseSucceed(() => ({}))) + + const pkgAny = pkg as any + const lockAny = lock as any + + const declared = new Set([ + ...Object.keys(pkgAny?.dependencies || {}), + ...Object.keys(pkgAny?.devDependencies || {}), + ...Object.keys(pkgAny?.peerDependencies || {}), + ...Object.keys(pkgAny?.optionalDependencies || {}), + ]) + + const root = lockAny?.packages?.[""] || {} + const locked = new Set([ + ...Object.keys(root?.dependencies || {}), + ...Object.keys(root?.devDependencies || {}), + ...Object.keys(root?.peerDependencies || {}), + ...Object.keys(root?.optionalDependencies || {}), + ]) + + for (const name of declared) { + if (!locked.has(name)) { + yield* reify() + return + } + } + }, Effect.scoped) + + const which = Effect.fn("Npm.which")(function* (pkg: string) { + const dir = directory(pkg) + const binDir = path.join(dir, "node_modules", ".bin") + + const pick = Effect.fnUntraced(function* () { + const files = yield* fs.readDirectory(binDir).pipe(Effect.catch(() => Effect.succeed([] as string[]))) + + if (files.length === 0) return Option.none() + if (files.length === 1) return Option.some(files[0]) + + const pkgJson = yield* afs.readJson(path.join(dir, "node_modules", pkg, "package.json")).pipe(Effect.option) + + if (Option.isSome(pkgJson)) { + const parsed = pkgJson.value as { bin?: string | Record } + if (parsed?.bin) { + const unscoped = pkg.startsWith("@") ? pkg.split("/")[1] : pkg + const bin = parsed.bin + if (typeof bin === "string") return Option.some(unscoped) + const keys = Object.keys(bin) + if (keys.length === 1) return Option.some(keys[0]) + return bin[unscoped] ? Option.some(unscoped) : Option.some(keys[0]) + } + } + + return Option.some(files[0]) + }) + + return yield* Effect.gen(function* () { + const bin = yield* pick() + if (Option.isSome(bin)) { + return Option.some(path.join(binDir, bin.value)) + } + + yield* fs.remove(path.join(dir, "package-lock.json")).pipe(Effect.orElseSucceed(() => {})) + + yield* add(pkg) + + const resolved = yield* pick() + if (Option.isNone(resolved)) return Option.none() + return Option.some(path.join(binDir, resolved.value)) + }).pipe( + Effect.scoped, + Effect.orElseSucceed(() => Option.none()), + ) + }) + + return Service.of({ + add, + install, + outdated, + which, + }) + }), + ) + + export const defaultLayer = layer.pipe( + Layer.provide(AppFileSystem.layer), + Layer.provide(Global.layer), + Layer.provide(NodeFileSystem.layer), + ) +} diff --git a/packages/shared/src/types.d.ts b/packages/shared/src/types.d.ts new file mode 100644 index 0000000000..b5d667f1d9 --- /dev/null +++ b/packages/shared/src/types.d.ts @@ -0,0 +1,44 @@ +declare module "@npmcli/arborist" { + export interface ArboristOptions { + path: string + binLinks?: boolean + progress?: boolean + savePrefix?: string + ignoreScripts?: boolean + } + + export interface ArboristNode { + name: string + path: string + } + + export interface ArboristEdge { + to?: ArboristNode + } + + export interface ArboristTree { + edgesOut: Map + } + + export interface ReifyOptions { + add?: string[] + save?: boolean + saveType?: "prod" | "dev" | "optional" | "peer" + } + + export class Arborist { + constructor(options: ArboristOptions) + loadVirtual(): Promise + reify(options?: ReifyOptions): Promise + } +} + +declare var Bun: + | { + file(path: string): { + text(): Promise + json(): Promise + } + write(path: string, content: string | Uint8Array): Promise + } + | undefined diff --git a/packages/opencode/src/util/flock.ts b/packages/shared/src/util/flock.ts similarity index 93% rename from packages/opencode/src/util/flock.ts rename to packages/shared/src/util/flock.ts index 74c7905ebb..4a1df1dee7 100644 --- a/packages/opencode/src/util/flock.ts +++ b/packages/shared/src/util/flock.ts @@ -2,11 +2,25 @@ import path from "path" import os from "os" import { randomBytes, randomUUID } from "crypto" import { mkdir, readFile, rm, stat, utimes, writeFile } from "fs/promises" -import { Global } from "@/global" -import { Hash } from "@/util/hash" +import { Hash } from "./hash" +import { Effect } from "effect" + +export type FlockGlobal = { + state: string +} export namespace Flock { - const root = path.join(Global.Path.state, "locks") + let global: FlockGlobal | undefined + + export function setGlobal(g: FlockGlobal) { + global = g + } + + const root = () => { + if (!global) throw new Error("Flock global not set") + return path.join(global.state, "locks") + } + // Defaults for callers that do not provide timing options. const defaultOpts = { staleMs: 60_000, @@ -301,7 +315,7 @@ export namespace Flock { baseDelayMs: input.baseDelayMs ?? defaultOpts.baseDelayMs, maxDelayMs: input.maxDelayMs ?? defaultOpts.maxDelayMs, } - const dir = input.dir ?? root + const dir = input.dir ?? root() await mkdir(dir, { recursive: true }) const lockfile = path.join(dir, Hash.fast(key) + ".lock") @@ -330,4 +344,11 @@ export namespace Flock { input.signal?.throwIfAborted() return await fn() } + + export const effect = Effect.fn("Flock.effect")(function* (key: string) { + return yield* Effect.acquireRelease( + Effect.promise((signal) => Flock.acquire(key, { signal })), + (foo) => Effect.promise(() => foo.release()), + ).pipe(Effect.asVoid) + }) } diff --git a/packages/opencode/src/util/hash.ts b/packages/shared/src/util/hash.ts similarity index 100% rename from packages/opencode/src/util/hash.ts rename to packages/shared/src/util/hash.ts diff --git a/packages/shared/test/filesystem/filesystem.test.ts b/packages/shared/test/filesystem/filesystem.test.ts new file mode 100644 index 0000000000..ce990d3795 --- /dev/null +++ b/packages/shared/test/filesystem/filesystem.test.ts @@ -0,0 +1,338 @@ +import { describe, test, expect } from "bun:test" +import { Effect, Layer, FileSystem } from "effect" +import { NodeFileSystem } from "@effect/platform-node" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { testEffect } from "../lib/effect" +import path from "path" + +const live = AppFileSystem.layer.pipe(Layer.provideMerge(NodeFileSystem.layer)) +const { effect: it } = testEffect(live) + +describe("AppFileSystem", () => { + describe("isDir", () => { + it( + "returns true for directories", + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + const filesys = yield* FileSystem.FileSystem + const tmp = yield* filesys.makeTempDirectoryScoped() + expect(yield* fs.isDir(tmp)).toBe(true) + }), + ) + + it( + "returns false for files", + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + const filesys = yield* FileSystem.FileSystem + const tmp = yield* filesys.makeTempDirectoryScoped() + const file = path.join(tmp, "test.txt") + yield* filesys.writeFileString(file, "hello") + expect(yield* fs.isDir(file)).toBe(false) + }), + ) + + it( + "returns false for non-existent paths", + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + expect(yield* fs.isDir("/tmp/nonexistent-" + Math.random())).toBe(false) + }), + ) + }) + + describe("isFile", () => { + it( + "returns true for files", + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + const filesys = yield* FileSystem.FileSystem + const tmp = yield* filesys.makeTempDirectoryScoped() + const file = path.join(tmp, "test.txt") + yield* filesys.writeFileString(file, "hello") + expect(yield* fs.isFile(file)).toBe(true) + }), + ) + + it( + "returns false for directories", + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + const filesys = yield* FileSystem.FileSystem + const tmp = yield* filesys.makeTempDirectoryScoped() + expect(yield* fs.isFile(tmp)).toBe(false) + }), + ) + }) + + describe("readJson / writeJson", () => { + it( + "round-trips JSON data", + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + const filesys = yield* FileSystem.FileSystem + const tmp = yield* filesys.makeTempDirectoryScoped() + const file = path.join(tmp, "data.json") + const data = { name: "test", count: 42, nested: { ok: true } } + + yield* fs.writeJson(file, data) + const result = yield* fs.readJson(file) + + expect(result).toEqual(data) + }), + ) + }) + + describe("ensureDir", () => { + it( + "creates nested directories", + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + const filesys = yield* FileSystem.FileSystem + const tmp = yield* filesys.makeTempDirectoryScoped() + const nested = path.join(tmp, "a", "b", "c") + + yield* fs.ensureDir(nested) + + const info = yield* filesys.stat(nested) + expect(info.type).toBe("Directory") + }), + ) + + it( + "is idempotent", + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + const filesys = yield* FileSystem.FileSystem + const tmp = yield* filesys.makeTempDirectoryScoped() + const dir = path.join(tmp, "existing") + yield* filesys.makeDirectory(dir) + + yield* fs.ensureDir(dir) + + const info = yield* filesys.stat(dir) + expect(info.type).toBe("Directory") + }), + ) + }) + + describe("writeWithDirs", () => { + it( + "creates parent directories if missing", + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + const filesys = yield* FileSystem.FileSystem + const tmp = yield* filesys.makeTempDirectoryScoped() + const file = path.join(tmp, "deep", "nested", "file.txt") + + yield* fs.writeWithDirs(file, "hello") + + expect(yield* filesys.readFileString(file)).toBe("hello") + }), + ) + + it( + "writes directly when parent exists", + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + const filesys = yield* FileSystem.FileSystem + const tmp = yield* filesys.makeTempDirectoryScoped() + const file = path.join(tmp, "direct.txt") + + yield* fs.writeWithDirs(file, "world") + + expect(yield* filesys.readFileString(file)).toBe("world") + }), + ) + + it( + "writes Uint8Array content", + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + const filesys = yield* FileSystem.FileSystem + const tmp = yield* filesys.makeTempDirectoryScoped() + const file = path.join(tmp, "binary.bin") + const content = new Uint8Array([0x00, 0x01, 0x02, 0x03]) + + yield* fs.writeWithDirs(file, content) + + const result = yield* filesys.readFile(file) + expect(new Uint8Array(result)).toEqual(content) + }), + ) + }) + + describe("findUp", () => { + it( + "finds target in start directory", + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + const filesys = yield* FileSystem.FileSystem + const tmp = yield* filesys.makeTempDirectoryScoped() + yield* filesys.writeFileString(path.join(tmp, "target.txt"), "found") + + const result = yield* fs.findUp("target.txt", tmp) + expect(result).toEqual([path.join(tmp, "target.txt")]) + }), + ) + + it( + "finds target in parent directories", + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + const filesys = yield* FileSystem.FileSystem + const tmp = yield* filesys.makeTempDirectoryScoped() + yield* filesys.writeFileString(path.join(tmp, "marker"), "root") + const child = path.join(tmp, "a", "b") + yield* filesys.makeDirectory(child, { recursive: true }) + + const result = yield* fs.findUp("marker", child, tmp) + expect(result).toEqual([path.join(tmp, "marker")]) + }), + ) + + it( + "returns empty array when not found", + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + const filesys = yield* FileSystem.FileSystem + const tmp = yield* filesys.makeTempDirectoryScoped() + const result = yield* fs.findUp("nonexistent", tmp, tmp) + expect(result).toEqual([]) + }), + ) + }) + + describe("up", () => { + it( + "finds multiple targets walking up", + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + const filesys = yield* FileSystem.FileSystem + const tmp = yield* filesys.makeTempDirectoryScoped() + yield* filesys.writeFileString(path.join(tmp, "a.txt"), "a") + yield* filesys.writeFileString(path.join(tmp, "b.txt"), "b") + const child = path.join(tmp, "sub") + yield* filesys.makeDirectory(child) + yield* filesys.writeFileString(path.join(child, "a.txt"), "a-child") + + const result = yield* fs.up({ targets: ["a.txt", "b.txt"], start: child, stop: tmp }) + + expect(result).toContain(path.join(child, "a.txt")) + expect(result).toContain(path.join(tmp, "a.txt")) + expect(result).toContain(path.join(tmp, "b.txt")) + }), + ) + }) + + describe("glob", () => { + it( + "finds files matching pattern", + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + const filesys = yield* FileSystem.FileSystem + const tmp = yield* filesys.makeTempDirectoryScoped() + yield* filesys.writeFileString(path.join(tmp, "a.ts"), "a") + yield* filesys.writeFileString(path.join(tmp, "b.ts"), "b") + yield* filesys.writeFileString(path.join(tmp, "c.json"), "c") + + const result = yield* fs.glob("*.ts", { cwd: tmp }) + expect(result.sort()).toEqual(["a.ts", "b.ts"]) + }), + ) + + it( + "supports absolute paths", + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + const filesys = yield* FileSystem.FileSystem + const tmp = yield* filesys.makeTempDirectoryScoped() + yield* filesys.writeFileString(path.join(tmp, "file.txt"), "hello") + + const result = yield* fs.glob("*.txt", { cwd: tmp, absolute: true }) + expect(result).toEqual([path.join(tmp, "file.txt")]) + }), + ) + }) + + describe("globMatch", () => { + it( + "matches patterns", + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + expect(fs.globMatch("*.ts", "foo.ts")).toBe(true) + expect(fs.globMatch("*.ts", "foo.json")).toBe(false) + expect(fs.globMatch("src/**", "src/a/b.ts")).toBe(true) + }), + ) + }) + + describe("globUp", () => { + it( + "finds files walking up directories", + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + const filesys = yield* FileSystem.FileSystem + const tmp = yield* filesys.makeTempDirectoryScoped() + yield* filesys.writeFileString(path.join(tmp, "root.md"), "root") + const child = path.join(tmp, "a", "b") + yield* filesys.makeDirectory(child, { recursive: true }) + yield* filesys.writeFileString(path.join(child, "leaf.md"), "leaf") + + const result = yield* fs.globUp("*.md", child, tmp) + expect(result).toContain(path.join(child, "leaf.md")) + expect(result).toContain(path.join(tmp, "root.md")) + }), + ) + }) + + describe("built-in passthrough", () => { + it( + "exists works", + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + const filesys = yield* FileSystem.FileSystem + const tmp = yield* filesys.makeTempDirectoryScoped() + const file = path.join(tmp, "exists.txt") + yield* filesys.writeFileString(file, "yes") + + expect(yield* filesys.exists(file)).toBe(true) + expect(yield* filesys.exists(file + ".nope")).toBe(false) + }), + ) + + it( + "remove works", + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + const filesys = yield* FileSystem.FileSystem + const tmp = yield* filesys.makeTempDirectoryScoped() + const file = path.join(tmp, "delete-me.txt") + yield* filesys.writeFileString(file, "bye") + + yield* filesys.remove(file) + + expect(yield* filesys.exists(file)).toBe(false) + }), + ) + }) + + describe("pure helpers", () => { + test("mimeType returns correct types", () => { + expect(AppFileSystem.mimeType("file.json")).toBe("application/json") + expect(AppFileSystem.mimeType("image.png")).toBe("image/png") + expect(AppFileSystem.mimeType("unknown.qzx")).toBe("application/octet-stream") + }) + + test("contains checks path containment", () => { + expect(AppFileSystem.contains("/a/b", "/a/b/c")).toBe(true) + expect(AppFileSystem.contains("/a/b", "/a/c")).toBe(false) + }) + + test("overlaps detects overlapping paths", () => { + expect(AppFileSystem.overlaps("/a/b", "/a/b/c")).toBe(true) + expect(AppFileSystem.overlaps("/a/b/c", "/a/b")).toBe(true) + expect(AppFileSystem.overlaps("/a", "/b")).toBe(false) + }) + }) +}) diff --git a/packages/shared/test/fixture/flock-worker.ts b/packages/shared/test/fixture/flock-worker.ts new file mode 100644 index 0000000000..9954d290cc --- /dev/null +++ b/packages/shared/test/fixture/flock-worker.ts @@ -0,0 +1,72 @@ +import fs from "fs/promises" +import { Flock } from "@opencode-ai/shared/util/flock" + +type Msg = { + key: string + dir: string + staleMs?: number + timeoutMs?: number + baseDelayMs?: number + maxDelayMs?: number + holdMs?: number + ready?: string + active?: string + done?: string +} + +function sleep(ms: number) { + return new Promise((resolve) => { + setTimeout(resolve, ms) + }) +} + +function input() { + const raw = process.argv[2] + if (!raw) { + throw new Error("Missing flock worker input") + } + + return JSON.parse(raw) as Msg +} + +async function job(input: Msg) { + if (input.ready) { + await fs.writeFile(input.ready, String(process.pid)) + } + + if (input.active) { + await fs.writeFile(input.active, String(process.pid), { flag: "wx" }) + } + + try { + if (input.holdMs && input.holdMs > 0) { + await sleep(input.holdMs) + } + + if (input.done) { + await fs.appendFile(input.done, "1\n") + } + } finally { + if (input.active) { + await fs.rm(input.active, { force: true }) + } + } +} + +async function main() { + const msg = input() + + await Flock.withLock(msg.key, () => job(msg), { + dir: msg.dir, + staleMs: msg.staleMs, + timeoutMs: msg.timeoutMs, + baseDelayMs: msg.baseDelayMs, + maxDelayMs: msg.maxDelayMs, + }) +} + +await main().catch((err) => { + const text = err instanceof Error ? (err.stack ?? err.message) : String(err) + process.stderr.write(text) + process.exit(1) +}) diff --git a/packages/shared/test/lib/effect.ts b/packages/shared/test/lib/effect.ts new file mode 100644 index 0000000000..131ec5cc6b --- /dev/null +++ b/packages/shared/test/lib/effect.ts @@ -0,0 +1,53 @@ +import { test, type TestOptions } from "bun:test" +import { Cause, Effect, Exit, Layer } from "effect" +import type * as Scope from "effect/Scope" +import * as TestClock from "effect/testing/TestClock" +import * as TestConsole from "effect/testing/TestConsole" + +type Body = Effect.Effect | (() => Effect.Effect) + +const body = (value: Body) => Effect.suspend(() => (typeof value === "function" ? value() : value)) + +const run = (value: Body, layer: Layer.Layer) => + Effect.gen(function* () { + const exit = yield* body(value).pipe(Effect.scoped, Effect.provide(layer), Effect.exit) + if (Exit.isFailure(exit)) { + for (const err of Cause.prettyErrors(exit.cause)) { + yield* Effect.logError(err) + } + } + return yield* exit + }).pipe(Effect.runPromise) + +const make = (testLayer: Layer.Layer, liveLayer: Layer.Layer) => { + const effect = (name: string, value: Body, opts?: number | TestOptions) => + test(name, () => run(value, testLayer), opts) + + effect.only = (name: string, value: Body, opts?: number | TestOptions) => + test.only(name, () => run(value, testLayer), opts) + + effect.skip = (name: string, value: Body, opts?: number | TestOptions) => + test.skip(name, () => run(value, testLayer), opts) + + const live = (name: string, value: Body, opts?: number | TestOptions) => + test(name, () => run(value, liveLayer), opts) + + live.only = (name: string, value: Body, opts?: number | TestOptions) => + test.only(name, () => run(value, liveLayer), opts) + + live.skip = (name: string, value: Body, opts?: number | TestOptions) => + test.skip(name, () => run(value, liveLayer), opts) + + return { effect, live } +} + +// Test environment with TestClock and TestConsole +const testEnv = Layer.mergeAll(TestConsole.layer, TestClock.layer()) + +// Live environment - uses real clock, but keeps TestConsole for output capture +const liveEnv = TestConsole.layer + +export const it = make(testEnv, liveEnv) + +export const testEffect = (layer: Layer.Layer) => + make(Layer.provideMerge(layer, testEnv), Layer.provideMerge(layer, liveEnv)) diff --git a/packages/shared/test/npm.test.ts b/packages/shared/test/npm.test.ts new file mode 100644 index 0000000000..4443d2985c --- /dev/null +++ b/packages/shared/test/npm.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, test } from "bun:test" +import { Npm } from "@opencode-ai/shared/npm" + +const win = process.platform === "win32" + +describe("Npm.sanitize", () => { + test("keeps normal scoped package specs unchanged", () => { + expect(Npm.sanitize("@opencode/acme")).toBe("@opencode/acme") + expect(Npm.sanitize("@opencode/acme@1.0.0")).toBe("@opencode/acme@1.0.0") + expect(Npm.sanitize("prettier")).toBe("prettier") + }) + + test("handles git https specs", () => { + const spec = "acme@git+https://github.com/opencode/acme.git" + const expected = win ? "acme@git+https_//github.com/opencode/acme.git" : spec + expect(Npm.sanitize(spec)).toBe(expected) + }) +}) diff --git a/packages/opencode/test/util/flock.test.ts b/packages/shared/test/util/flock.test.ts similarity index 80% rename from packages/opencode/test/util/flock.test.ts rename to packages/shared/test/util/flock.test.ts index fedbfb0697..f1053dfd2b 100644 --- a/packages/opencode/test/util/flock.test.ts +++ b/packages/shared/test/util/flock.test.ts @@ -1,14 +1,10 @@ import { describe, expect, test } from "bun:test" import fs from "fs/promises" +import { spawn } from "child_process" import path from "path" -import { Flock } from "../../src/util/flock" -import { Hash } from "../../src/util/hash" -import { Process } from "../../src/util/process" -import { Filesystem } from "../../src/util/filesystem" -import { tmpdir } from "../fixture/fixture" - -const root = path.join(import.meta.dir, "../..") -const worker = path.join(import.meta.dir, "../fixture/flock-worker.ts") +import os from "os" +import { Flock } from "@opencode-ai/shared/util/flock" +import { Hash } from "@opencode-ai/shared/util/hash" type Msg = { key: string @@ -23,6 +19,19 @@ type Msg = { done?: string } +const root = path.join(import.meta.dir, "../..") +const worker = path.join(import.meta.dir, "../fixture/flock-worker.ts") + +async function tmpdir() { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "flock-test-")) + return { + path: dir, + async [Symbol.asyncDispose]() { + await fs.rm(dir, { recursive: true, force: true }) + }, + } +} + function lock(dir: string, key: string) { return path.join(dir, Hash.fast(key) + ".lock") } @@ -51,21 +60,55 @@ async function wait(file: string, timeout = 3_000) { } function run(msg: Msg) { - return Process.run([process.execPath, worker, JSON.stringify(msg)], { - cwd: root, - nothrow: true, + return new Promise<{ code: number; stdout: Buffer; stderr: Buffer }>((resolve) => { + const proc = spawn(process.execPath, [worker, JSON.stringify(msg)], { + cwd: root, + }) + + const stdout: Buffer[] = [] + const stderr: Buffer[] = [] + + proc.stdout?.on("data", (data) => stdout.push(Buffer.from(data))) + proc.stderr?.on("data", (data) => stderr.push(Buffer.from(data))) + + proc.on("close", (code) => { + resolve({ + code: code ?? 1, + stdout: Buffer.concat(stdout), + stderr: Buffer.concat(stderr), + }) + }) }) } -function spawn(msg: Msg) { - return Process.spawn([process.execPath, worker, JSON.stringify(msg)], { +function spawnWorker(msg: Msg) { + return spawn(process.execPath, [worker, JSON.stringify(msg)], { cwd: root, - stdin: "ignore", - stdout: "pipe", - stderr: "pipe", + stdio: ["ignore", "pipe", "pipe"], }) } +function stopWorker(proc: ReturnType) { + if (proc.exitCode !== null || proc.signalCode !== null) return Promise.resolve() + + if (process.platform !== "win32" || !proc.pid) { + proc.kill() + return Promise.resolve() + } + + return new Promise((resolve) => { + const killProc = spawn("taskkill", ["/pid", String(proc.pid), "/T", "/F"]) + killProc.on("close", () => { + proc.kill() + resolve() + }) + }) +} + +async function readJson(p: string): Promise { + return JSON.parse(await fs.readFile(p, "utf8")) +} + describe("util.flock", () => { test("enforces mutual exclusion under process contention", async () => { await using tmp = await tmpdir() @@ -104,7 +147,7 @@ describe("util.flock", () => { const dir = path.join(tmp.path, "locks") const key = "flock:timeout" const ready = path.join(tmp.path, "ready") - const proc = spawn({ + const proc = spawnWorker({ key, dir, ready, @@ -131,8 +174,8 @@ describe("util.flock", () => { expect(seen.length).toBeGreaterThan(0) expect(seen.every((x) => x === key)).toBe(true) } finally { - await Process.stop(proc).catch(() => undefined) - await proc.exited.catch(() => undefined) + await stopWorker(proc).catch(() => undefined) + await new Promise((resolve) => proc.on("close", resolve)) } }, 15_000) @@ -141,7 +184,7 @@ describe("util.flock", () => { const dir = path.join(tmp.path, "locks") const key = "flock:crash" const ready = path.join(tmp.path, "ready") - const proc = spawn({ + const proc = spawnWorker({ key, dir, ready, @@ -151,8 +194,8 @@ describe("util.flock", () => { }) await wait(ready, 5_000) - await Process.stop(proc) - await proc.exited.catch(() => undefined) + await stopWorker(proc) + await new Promise((resolve) => proc.on("close", resolve)) let hit = false await Flock.withLock( @@ -276,7 +319,7 @@ describe("util.flock", () => { await Flock.withLock( key, async () => { - const json = await Filesystem.readJson<{ + const json = await readJson<{ token?: unknown pid?: unknown hostname?: unknown @@ -324,7 +367,7 @@ describe("util.flock", () => { const err = await Flock.withLock( key, async () => { - const json = await Filesystem.readJson<{ token?: string }>(meta) + const json = await readJson<{ token?: string }>(meta) json.token = "tampered" await fs.writeFile(meta, JSON.stringify(json, null, 2)) }, From 9640d889baa58fa01ed612a6372ba77462f79d9f Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 12:35:14 -0400 Subject: [PATCH 146/300] fix: register OTel context manager so AI SDK spans thread into Effect traces (#22645) --- bun.lock | 2 ++ packages/opencode/package.json | 6 ++++-- packages/opencode/src/effect/observability.ts | 12 ++++++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/bun.lock b/bun.lock index a6f9891dd1..aeab042cf3 100644 --- a/bun.lock +++ b/bun.lock @@ -359,6 +359,8 @@ "@opencode-ai/sdk": "workspace:*", "@opencode-ai/server": "workspace:*", "@openrouter/ai-sdk-provider": "2.5.1", + "@opentelemetry/api": "1.9.0", + "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/exporter-trace-otlp-http": "0.214.0", "@opentelemetry/sdk-trace-base": "2.6.1", "@opentelemetry/sdk-trace-node": "2.6.1", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 9ddf1fa9f6..59be93d620 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -113,13 +113,15 @@ "@octokit/rest": "catalog:", "@openauthjs/openauth": "catalog:", "@opencode-ai/plugin": "workspace:*", - "@opencode-ai/server": "workspace:*", "@opencode-ai/script": "workspace:*", "@opencode-ai/sdk": "workspace:*", + "@opencode-ai/server": "workspace:*", + "@openrouter/ai-sdk-provider": "2.5.1", + "@opentelemetry/api": "1.9.0", + "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/exporter-trace-otlp-http": "0.214.0", "@opentelemetry/sdk-trace-base": "2.6.1", "@opentelemetry/sdk-trace-node": "2.6.1", - "@openrouter/ai-sdk-provider": "2.5.1", "@opentui/core": "0.1.99", "@opentui/solid": "0.1.99", "@parcel/watcher": "2.5.1", diff --git a/packages/opencode/src/effect/observability.ts b/packages/opencode/src/effect/observability.ts index 1e4863f924..f79306bf1e 100644 --- a/packages/opencode/src/effect/observability.ts +++ b/packages/opencode/src/effect/observability.ts @@ -46,6 +46,18 @@ export namespace Observability { const OTLP = await import("@opentelemetry/exporter-trace-otlp-http") const SdkBase = await import("@opentelemetry/sdk-trace-base") + // @effect/opentelemetry creates a NodeTracerProvider but never calls + // register(), so the global @opentelemetry/api context manager stays + // as the no-op default. Non-Effect code (like the AI SDK) that calls + // tracer.startActiveSpan() relies on context.active() to find the + // parent span — without a real context manager every span starts a + // new trace. Registering AsyncLocalStorageContextManager fixes this. + const { AsyncLocalStorageContextManager } = await import("@opentelemetry/context-async-hooks") + const { context } = await import("@opentelemetry/api") + const mgr = new AsyncLocalStorageContextManager() + mgr.enable() + context.setGlobalContextManager(mgr) + return NodeSdk.layer(() => ({ resource, spanProcessor: new SdkBase.BatchSpanProcessor( From 8ba4799b3ee67e681ada264702d235522520c570 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Wed, 15 Apr 2026 17:38:21 +0000 Subject: [PATCH 147/300] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index f860e3774e..a12e6f7e5e 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-3kpnjBg7AQanyDGTOFdYBFvo9O9Rfnu0Wmi8bY5LpEI=", - "aarch64-linux": "sha256-8rQ+SNUiSpA2Ea3NrYNGopHQsnY7Y8qBsXCqL6GMt24=", - "aarch64-darwin": "sha256-OASMkW5hnXucV6lSmxrQo73lGSEKN4MQPNGNV0i7jdo=", - "x86_64-darwin": "sha256-CmHqXlm8wnLcwSSK0ghxAf+DVurEltMaxrUbWh9/ZGE=" + "x86_64-linux": "sha256-PvIx2g1J5QIUIzkz2ABaAM4K/k/+xlBPDUExoOJNNuo=", + "aarch64-linux": "sha256-YTAL+P13L5hgNJdDSiBED/UNa5zdTntnUUYDYL+Jdzo=", + "aarch64-darwin": "sha256-y2VCJifYAp+H0lpDcJ0QfKNMG00Q/usFElaUIpdc8Vs=", + "x86_64-darwin": "sha256-yz8edIlqLp06Y95ad8YjKz5azP7YATPle4TcDx6lM+U=" } } From 348a84969de64cc1623d8ddcf73336b449c5b1f5 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Wed, 15 Apr 2026 14:56:45 -0500 Subject: [PATCH 148/300] fix: ensure tool_use is always followed by tool_result (#22646) Co-authored-by: opencode-agent[bot] --- packages/opencode/src/provider/transform.ts | 27 +- .../opencode/test/provider/transform.test.ts | 104 +++++++ packages/opencode/test/session/llm.test.ts | 265 +++++++++++++++++- 3 files changed, 394 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index bab056dae7..61561ec969 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -75,7 +75,7 @@ export namespace ProviderTransform { if (model.api.id.includes("claude")) { const scrub = (id: string) => id.replace(/[^a-zA-Z0-9_-]/g, "_") - return msgs.map((msg) => { + msgs = msgs.map((msg) => { if (msg.role === "assistant" && Array.isArray(msg.content)) { return { ...msg, @@ -101,6 +101,31 @@ export namespace ProviderTransform { return msg }) } + if (["@ai-sdk/anthropic", "@ai-sdk/google-vertex/anthropic"].includes(model.api.npm)) { + // Anthropic rejects assistant turns where tool_use blocks are followed by non-tool + // content, e.g. [tool_use, tool_use, text], with: + // `tool_use` ids were found without `tool_result` blocks immediately after... + // + // Reorder that invalid shape into [text] + [tool_use, tool_use]. Consecutive + // assistant messages are later merged by the provider/SDK, so preserving the + // original [tool_use...] then [text] order still produces the invalid payload. + // + // The root cause appears to be somewhere upstream where the stream is originally + // processed. We were unable to locate an exact narrower reproduction elsewhere, + // so we keep this transform in place for the time being. + msgs = msgs.flatMap((msg) => { + if (msg.role !== "assistant" || !Array.isArray(msg.content)) return [msg] + + const parts = msg.content + const first = parts.findIndex((part) => part.type === "tool-call") + if (first === -1) return [msg] + if (!parts.slice(first).some((part) => part.type !== "tool-call")) return [msg] + return [ + { ...msg, content: parts.filter((part) => part.type !== "tool-call") }, + { ...msg, content: parts.filter((part) => part.type === "tool-call") }, + ] + }) + } if ( model.providerID === "mistral" || model.api.id.toLowerCase().includes("mistral") || diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 1b750d1b93..4952a126b3 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -1271,6 +1271,110 @@ describe("ProviderTransform.message - anthropic empty content filtering", () => expect(result[0].content).toBe("") expect(result[1].content).toHaveLength(1) }) + + test("splits anthropic assistant messages when text trails tool calls", () => { + const msgs = [ + { + role: "user", + content: [{ type: "text", text: "Check my home directory for PDFs" }], + }, + { + role: "assistant", + content: [ + { type: "tool-call", toolCallId: "toolu_1", toolName: "read", input: { filePath: "/root" } }, + { type: "tool-call", toolCallId: "toolu_2", toolName: "glob", input: { pattern: "**/*.pdf" } }, + { type: "text", text: "I checked your home directory and looked for PDF files." }, + ], + }, + { + role: "tool", + content: [ + { type: "tool-result", toolCallId: "toolu_1", toolName: "read", output: { type: "text", value: "ok" } }, + { + type: "tool-result", + toolCallId: "toolu_2", + toolName: "glob", + output: { type: "text", value: "No files found" }, + }, + ], + }, + ] as any[] + + const result = ProviderTransform.message(msgs, anthropicModel, {}) as any[] + + expect(result).toHaveLength(4) + expect(result[1]).toMatchObject({ + role: "assistant", + content: [{ type: "text", text: "I checked your home directory and looked for PDF files." }], + }) + expect(result[2]).toMatchObject({ + role: "assistant", + content: [ + { type: "tool-call", toolCallId: "toolu_1", toolName: "read", input: { filePath: "/root" } }, + { type: "tool-call", toolCallId: "toolu_2", toolName: "glob", input: { pattern: "**/*.pdf" } }, + ], + }) + }) + + test("leaves valid anthropic assistant tool ordering unchanged", () => { + const msgs = [ + { + role: "assistant", + content: [ + { type: "text", text: "I checked your home directory and looked for PDF files." }, + { type: "tool-call", toolCallId: "toolu_1", toolName: "read", input: { filePath: "/root" } }, + { type: "tool-call", toolCallId: "toolu_2", toolName: "glob", input: { pattern: "**/*.pdf" } }, + ], + }, + ] as any[] + + const result = ProviderTransform.message(msgs, anthropicModel, {}) as any[] + + expect(result).toHaveLength(1) + expect(result[0].content).toMatchObject([ + { type: "text", text: "I checked your home directory and looked for PDF files." }, + { type: "tool-call", toolCallId: "toolu_1", toolName: "read", input: { filePath: "/root" } }, + { type: "tool-call", toolCallId: "toolu_2", toolName: "glob", input: { pattern: "**/*.pdf" } }, + ]) + }) + + test("splits vertex anthropic assistant messages when text trails tool calls", () => { + const model = { + ...anthropicModel, + providerID: "google-vertex-anthropic", + api: { + id: "claude-sonnet-4@20250514", + url: "https://us-central1-aiplatform.googleapis.com", + npm: "@ai-sdk/google-vertex/anthropic", + }, + } + + const msgs = [ + { + role: "assistant", + content: [ + { type: "tool-call", toolCallId: "toolu_1", toolName: "read", input: { filePath: "/root" } }, + { type: "tool-call", toolCallId: "toolu_2", toolName: "glob", input: { pattern: "**/*.pdf" } }, + { type: "text", text: "I checked your home directory and looked for PDF files." }, + ], + }, + ] as any[] + + const result = ProviderTransform.message(msgs, model, {}) as any[] + + expect(result).toHaveLength(2) + expect(result[0]).toMatchObject({ + role: "assistant", + content: [{ type: "text", text: "I checked your home directory and looked for PDF files." }], + }) + expect(result[1]).toMatchObject({ + role: "assistant", + content: [ + { type: "tool-call", toolCallId: "toolu_1", toolName: "read", input: { filePath: "/root" } }, + { type: "tool-call", toolCallId: "toolu_2", toolName: "glob", input: { pattern: "**/*.pdf" } }, + ], + }) + }) }) describe("ProviderTransform.message - strip openai metadata when store=false", () => { diff --git a/packages/opencode/test/session/llm.test.ts b/packages/opencode/test/session/llm.test.ts index cbf767b4bd..a7fde90f01 100644 --- a/packages/opencode/test/session/llm.test.ts +++ b/packages/opencode/test/session/llm.test.ts @@ -13,7 +13,7 @@ import { ProviderID, ModelID } from "../../src/provider/schema" import { Filesystem } from "../../src/util/filesystem" import { tmpdir } from "../fixture/fixture" import type { Agent } from "../../src/agent/agent" -import type { MessageV2 } from "../../src/session/message-v2" +import { MessageV2 } from "../../src/session/message-v2" import { SessionID, MessageID } from "../../src/session/schema" import { AppRuntime } from "../../src/effect/app-runtime" @@ -909,6 +909,269 @@ describe("session.llm.stream", () => { }) }) + test("sends anthropic tool_use blocks with tool_result immediately after them", async () => { + const server = state.server + if (!server) { + throw new Error("Server not initialized") + } + + const source = await loadFixture("anthropic", "claude-opus-4-6") + const model = source.model + const chunks = [ + { + type: "message_start", + message: { + id: "msg-tool-order", + model: model.id, + usage: { + input_tokens: 3, + cache_creation_input_tokens: null, + cache_read_input_tokens: null, + }, + }, + }, + { + type: "content_block_start", + index: 0, + content_block: { type: "text", text: "" }, + }, + { + type: "content_block_delta", + index: 0, + delta: { type: "text_delta", text: "ok" }, + }, + { type: "content_block_stop", index: 0 }, + { + type: "message_delta", + delta: { stop_reason: "end_turn", stop_sequence: null, container: null }, + usage: { + input_tokens: 3, + output_tokens: 2, + cache_creation_input_tokens: null, + cache_read_input_tokens: null, + }, + }, + { type: "message_stop" }, + ] + const request = waitRequest("/messages", createEventResponse(chunks)) + + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + enabled_providers: ["anthropic"], + provider: { + anthropic: { + name: "Anthropic", + env: ["ANTHROPIC_API_KEY"], + npm: "@ai-sdk/anthropic", + api: "https://api.anthropic.com/v1", + models: { + [model.id]: model, + }, + options: { + apiKey: "test-anthropic-key", + baseURL: `${server.url.origin}/v1`, + }, + }, + }, + }), + ) + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const resolved = await getModel(ProviderID.make("anthropic"), ModelID.make(model.id)) + const sessionID = SessionID.make("session-test-anthropic-tools") + const agent = { + name: "test", + mode: "primary", + options: {}, + permission: [{ permission: "*", pattern: "*", action: "allow" }], + } satisfies Agent.Info + const user = { + id: MessageID.make("user-anthropic-tools"), + sessionID, + role: "user", + time: { created: Date.now() }, + agent: agent.name, + model: { providerID: ProviderID.make("anthropic"), modelID: resolved.id, variant: "max" }, + } satisfies MessageV2.User + + const input = [ + { + info: { + id: "msg_user", + sessionID, + role: "user", + time: { created: 1 }, + agent: "gentleman", + model: { providerID: "anthropic", modelID: "claude-opus-4-6", variant: "max" }, + }, + parts: [ + { + id: "p_user", + sessionID, + messageID: "msg_user", + type: "text", + text: "Can you check whether there are any PDF files in my home directory?", + }, + ], + }, + { + info: { + id: "msg_call", + sessionID, + parentID: "msg_user", + role: "assistant", + mode: "gentleman", + agent: "gentleman", + variant: "max", + path: { cwd: "/root", root: "/" }, + cost: 0, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + modelID: "claude-opus-4-6", + providerID: "anthropic", + time: { created: 2, completed: 3 }, + finish: "tool-calls", + }, + parts: [ + { + id: "p_step", + sessionID, + messageID: "msg_call", + type: "step-start", + }, + { + id: "p_read", + sessionID, + messageID: "msg_call", + type: "tool", + tool: "read", + callID: "toolu_01N8mDEzG8DSTs7UPHFtmgCT", + state: { + status: "completed", + input: { filePath: "/root" }, + output: "/root", + metadata: {}, + title: "root", + time: { start: 10, end: 11 }, + }, + }, + { + id: "p_glob", + sessionID, + messageID: "msg_call", + type: "tool", + tool: "glob", + callID: "toolu_01APxrADs7VozN8uWzw9WwHr", + state: { + status: "completed", + input: { pattern: "**/*.pdf", path: "/root" }, + output: "No files found", + metadata: {}, + title: "root", + time: { start: 12, end: 13 }, + }, + }, + { + id: "p_text", + sessionID, + messageID: "msg_call", + type: "text", + text: "I checked your home directory and looked for PDF files.", + time: { start: 14, end: 15 }, + }, + ], + }, + ] as any[] + + await drain({ + user, + sessionID, + model: resolved, + agent, + system: [], + messages: await MessageV2.toModelMessages(input as any, resolved), + tools: { + read: tool({ + description: "Stub read tool", + inputSchema: z.object({ + filePath: z.string(), + }), + execute: async () => ({ output: "stub" }), + }), + glob: tool({ + description: "Stub glob tool", + inputSchema: z.object({ + pattern: z.string(), + path: z.string().optional(), + }), + execute: async () => ({ output: "stub" }), + }), + }, + }) + + const capture = await request + const body = capture.body + + expect(capture.url.pathname.endsWith("/messages")).toBe(true) + expect(body.messages).toStrictEqual([ + { + role: "user", + content: [{ type: "text", text: "Can you check whether there are any PDF files in my home directory?" }], + }, + { + role: "assistant", + content: [ + { + type: "text", + text: "I checked your home directory and looked for PDF files.", + }, + { + type: "tool_use", + id: "toolu_01N8mDEzG8DSTs7UPHFtmgCT", + name: "read", + input: { filePath: "/root" }, + }, + { + type: "tool_use", + id: "toolu_01APxrADs7VozN8uWzw9WwHr", + name: "glob", + input: { pattern: "**/*.pdf", path: "/root" }, + cache_control: { + type: "ephemeral", + }, + }, + ], + }, + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "toolu_01N8mDEzG8DSTs7UPHFtmgCT", + content: "/root", + }, + { + type: "tool_result", + tool_use_id: "toolu_01APxrADs7VozN8uWzw9WwHr", + content: "No files found", + cache_control: { + type: "ephemeral", + }, + }, + ], + }, + ]) + }, + }) + }) + test("sends Google API payload for Gemini models", async () => { const server = state.server if (!server) { From e83b22159d0c6b393acf9b04fdd6798397bb782d Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Wed, 15 Apr 2026 15:50:33 -0500 Subject: [PATCH 149/300] tweak: ensure auto continuing compaction is tracked as agent initiated for github copilot (#22567) Co-authored-by: opencode-agent[bot] --- packages/opencode/src/plugin/github-copilot/copilot.ts | 10 +++++++++- packages/opencode/src/session/compaction.ts | 4 ++++ packages/opencode/test/session/compaction.test.ts | 1 + 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/plugin/github-copilot/copilot.ts b/packages/opencode/src/plugin/github-copilot/copilot.ts index ac685f74da..e12d182e4f 100644 --- a/packages/opencode/src/plugin/github-copilot/copilot.ts +++ b/packages/opencode/src/plugin/github-copilot/copilot.ts @@ -355,7 +355,15 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise { }) .catch(() => undefined) - if (parts?.data.parts?.some((part) => part.type === "compaction")) { + if ( + parts?.data.parts?.some( + (part) => + part.type === "compaction" || + // Auto-compaction resumes via a synthetic user text part. Treat only + // that marked followup as agent-initiated so manual prompts stay user-initiated. + (part.type === "text" && part.synthetic && part.metadata?.compaction_continue === true), + ) + ) { output.headers["x-initiator"] = "agent" return } diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index de0f8d0788..4978ef5478 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -345,6 +345,10 @@ When constructing the summary, try to stick to this template: messageID: continueMsg.id, sessionID: input.sessionID, type: "text", + // Internal marker for auto-compaction followups so provider plugins + // can distinguish them from manual post-compaction user prompts. + // This is not a stable plugin contract and may change or disappear. + metadata: { compaction_continue: true }, synthetic: true, text, time: { diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index ddfe859113..251447762d 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/packages/opencode/test/session/compaction.test.ts @@ -843,6 +843,7 @@ describe("session.compaction.process", () => { expect(last?.parts[0]).toMatchObject({ type: "text", synthetic: true, + metadata: { compaction_continue: true }, }) if (last?.parts[0]?.type === "text") { expect(last.parts[0].text).toContain("Continue if you have next steps") From 250e30bc7da7afa1c83578108e9af23210dda87a Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 17:28:01 -0400 Subject: [PATCH 150/300] add experimental permission HttpApi slice (#22385) --- packages/opencode/specs/effect/http-api.md | 77 ++++------ packages/opencode/src/agent/agent.ts | 2 +- packages/opencode/src/control-plane/schema.ts | 7 +- packages/opencode/src/permission/index.ts | 140 ++++++++++-------- packages/opencode/src/permission/schema.ts | 6 +- packages/opencode/src/pty/schema.ts | 3 +- packages/opencode/src/question/schema.ts | 6 +- .../src/server/instance/experimental.ts | 2 - .../src/server/instance/httpapi/index.ts | 7 - .../src/server/instance/httpapi/permission.ts | 72 +++++++++ .../src/server/instance/httpapi/question.ts | 101 ++++++++----- .../src/server/instance/httpapi/server.ts | 135 +++++++++++++++++ .../src/server/instance/permission.ts | 4 +- .../opencode/src/server/instance/session.ts | 4 +- packages/opencode/src/session/index.ts | 4 +- packages/opencode/src/session/schema.ts | 7 +- packages/opencode/src/sync/schema.ts | 3 +- packages/opencode/src/tool/schema.ts | 3 +- packages/opencode/src/util/effect-zod.ts | 16 ++ .../test/server/question-httpapi.test.ts | 78 ---------- .../opencode/test/util/effect-zod.test.ts | 130 +++++++++++++++- 21 files changed, 553 insertions(+), 254 deletions(-) delete mode 100644 packages/opencode/src/server/instance/httpapi/index.ts create mode 100644 packages/opencode/src/server/instance/httpapi/permission.ts create mode 100644 packages/opencode/src/server/instance/httpapi/server.ts delete mode 100644 packages/opencode/test/server/question-httpapi.test.ts diff --git a/packages/opencode/specs/effect/http-api.md b/packages/opencode/specs/effect/http-api.md index cce3f4081f..1794927cce 100644 --- a/packages/opencode/specs/effect/http-api.md +++ b/packages/opencode/specs/effect/http-api.md @@ -121,14 +121,13 @@ Why `question` first: Do not re-architect business logic during the HTTP migration. `HttpApi` handlers should call the same Effect services already used by the Hono handlers. -### 4. Run in parallel before replacing +### 4. Build in parallel, do not bridge into Hono -Prefer mounting an experimental `HttpApi` surface alongside the existing Hono routes first. That lowers migration risk and lets us compare: +The `HttpApi` implementation lives under `src/server/instance/httpapi/` as a standalone Effect HTTP server. It is **not mounted into the Hono app**. There is no `toWebHandler` bridge, no Hono `Handler` export, and no `.route()` call wiring it into `experimental.ts`. -- handler ergonomics -- OpenAPI output -- auth and middleware integration -- test ergonomics +The standalone server (`httpapi/server.ts`) can be started independently and proves the routes work. Tests exercise it via `HttpRouter.serve` with `NodeHttpServer.layerTest`. + +The goal is to build enough route coverage in the Effect server that the Hono server can eventually be replaced entirely. Until then, the two implementations exist side by side but are completely separate processes. ### 5. Migrate JSON route groups gradually @@ -218,17 +217,15 @@ Placement rule: Suggested file layout for a repeatable spike: -- `src/server/instance/httpapi/question.ts` -- `src/server/instance/httpapi/index.ts` -- `test/server/question-httpapi.test.ts` -- `test/server/question-httpapi-openapi.test.ts` +- `src/server/instance/httpapi/question.ts` — contract and handler layer for one route group +- `src/server/instance/httpapi/server.ts` — standalone Effect HTTP server that composes all groups +- `test/server/question-httpapi.test.ts` — end-to-end test against the real service Suggested responsibilities: -- `question.ts` defines the `HttpApi` contract and `HttpApiBuilder.group(...)` handlers for the experimental slice -- `index.ts` combines experimental `HttpApi` groups and exposes the mounted handler or layer -- `question-httpapi.test.ts` proves the route works end-to-end against the real service -- `question-httpapi-openapi.test.ts` proves the generated OpenAPI is acceptable for the migrated endpoints +- `question.ts` defines the `HttpApi` contract and `HttpApiBuilder.group(...)` handlers +- `server.ts` composes all route groups into one `HttpRouter.serve` layer with shared middleware (auth, instance lookup) +- tests use `ExperimentalHttpApiServer.layerTest` to run against a real in-process HTTP server ## Example migration shape @@ -248,11 +245,12 @@ Each route-group spike should follow the same shape. - keep handler bodies thin - keep transport mapping at the HTTP boundary only -### 3. Mounting +### 3. Standalone server -- mount under an experimental prefix such as `/experimental/httpapi` -- keep existing Hono routes unchanged -- expose separate OpenAPI output for the experimental slice first +- the Effect HTTP server is self-contained in `httpapi/server.ts` +- it is **not** mounted into the Hono app — no bridge, no `toWebHandler` +- route paths use the `/experimental/httpapi` prefix so they match the eventual cutover +- each route group exposes its own OpenAPI doc endpoint ### 4. Verification @@ -263,53 +261,32 @@ Each route-group spike should follow the same shape. ## Boundary composition -The first slices should keep the existing outer server composition and only replace the route contract and handler layer. +The standalone Effect server owns its own middleware stack. It does not share middleware with the Hono server. ### Auth -- keep `AuthMiddleware` at the outer Hono app level -- do not duplicate auth checks inside each `HttpApi` group for the first parallel slices -- treat auth as an already-satisfied transport concern before the request reaches the `HttpApi` handler - -Practical rule: - -- if a route is currently protected by the shared server middleware stack, the experimental `HttpApi` route should stay mounted behind that same stack +- the standalone server implements auth as an `HttpApiMiddleware.Service` using `HttpApiSecurity.basic` +- each route group's `HttpApi` is wrapped with `.middleware(Authorization)` before being served +- this is independent of the Hono `AuthMiddleware` — when the Effect server eventually replaces Hono, this becomes the only auth layer ### Instance and workspace lookup -- keep `WorkspaceRouterMiddleware` as the source of truth for resolving `directory`, `workspace`, and session-derived workspace context -- let that middleware provide `Instance.current` and `WorkspaceContext` before the request reaches the `HttpApi` handler -- keep the `HttpApi` handlers unaware of path-to-instance lookup details when the existing Hono middleware already handles them - -Practical rule: - -- `HttpApi` handlers should yield services from context and assume the correct instance has already been provided -- only move instance lookup into the `HttpApi` layer if we later decide to migrate the outer middleware boundary itself +- the standalone server resolves instance context via an `HttpRouter.middleware` that reads `x-opencode-directory` headers and `directory` query params +- this is the Effect equivalent of the Hono `WorkspaceRouterMiddleware` +- `HttpApi` handlers yield services from context and assume the correct instance has already been provided ### Error mapping - keep domain and service errors typed in the service layer - declare typed transport errors on the endpoint only when the route can actually return them intentionally -- prefer explicit endpoint-level error schemas over relying on the outer Hono `ErrorMiddleware` for expected route behavior - -Practical rule: - -- request decoding failures should remain transport-level `400`s +- request decoding failures are transport-level `400`s handled by Effect `HttpApi` automatically - storage or lookup failures that are part of the route contract should be declared as typed endpoint errors -- unexpected defects can still fall through to the outer error middleware while the slice is experimental - -For the current parallel slices, this means: - -- auth still composes outside `HttpApi` -- instance selection still composes outside `HttpApi` -- success payloads should be schema-defined from canonical Effect schemas -- known route errors should be modeled at the endpoint boundary incrementally instead of all at once ## Exit criteria for the spike The first slice is successful if: -- the endpoints run in parallel with the current Hono routes +- the standalone Effect server starts and serves the endpoints independently of the Hono server - the handlers reuse the existing Effect service - request decoding and response shapes are schema-defined from canonical Effect schemas - any remaining Zod boundary usage is derived from `.zod` or clearly temporary @@ -324,8 +301,8 @@ The first parallel `question` spike gave us a concrete pattern to reuse. - scalar or collection schemas such as `Question.Answer` should stay as schemas and use helpers like `withStatics(...)` instead of being forced into classes. - if an `HttpApi` success schema uses `Schema.Class`, the handler or underlying service needs to return real schema instances rather than plain objects. - internal event payloads can stay anonymous when we want to avoid adding extra named OpenAPI component churn for non-route shapes. -- the experimental slice should stay mounted in parallel and keep calling the existing service layer unchanged. -- compare generated OpenAPI semantically at the route and schema level; in the current setup the exported OpenAPI paths do not include the outer Hono mount prefix. +- the experimental slice should stay as a standalone Effect server and keep calling the existing service layer unchanged. +- compare generated OpenAPI semantically at the route and schema level. ## Route inventory diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index ce49218b71..8857696b05 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -35,7 +35,7 @@ export namespace Agent { topP: z.number().optional(), temperature: z.number().optional(), color: z.string().optional(), - permission: Permission.Ruleset, + permission: Permission.Ruleset.zod, model: z .object({ modelID: ModelID.zod, diff --git a/packages/opencode/src/control-plane/schema.ts b/packages/opencode/src/control-plane/schema.ts index 7262a380b0..4c7ced010d 100644 --- a/packages/opencode/src/control-plane/schema.ts +++ b/packages/opencode/src/control-plane/schema.ts @@ -1,10 +1,13 @@ import { Schema } from "effect" import z from "zod" -import { withStatics } from "@/util/schema" import { Identifier } from "@/id/id" +import { ZodOverride } from "@/util/effect-zod" +import { withStatics } from "@/util/schema" -const workspaceIdSchema = Schema.String.pipe(Schema.brand("WorkspaceID")) +const workspaceIdSchema = Schema.String.annotate({ [ZodOverride]: Identifier.schema("workspace") }).pipe( + Schema.brand("WorkspaceID"), +) export type WorkspaceID = typeof workspaceIdSchema.Type diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts index dc22d32b4b..b6a44e2582 100644 --- a/packages/opencode/src/permission/index.ts +++ b/packages/opencode/src/permission/index.ts @@ -7,75 +7,84 @@ import { Instance } from "@/project/instance" import { MessageID, SessionID } from "@/session/schema" import { PermissionTable } from "@/session/session.sql" import { Database, eq } from "@/storage/db" +import { zod } from "@/util/effect-zod" import { Log } from "@/util/log" +import { withStatics } from "@/util/schema" import { Wildcard } from "@/util/wildcard" import { Deferred, Effect, Layer, Schema, Context } from "effect" import os from "os" -import z from "zod" import { evaluate as evalRule } from "./evaluate" import { PermissionID } from "./schema" export namespace Permission { const log = Log.create({ service: "permission" }) - export const Action = z.enum(["allow", "deny", "ask"]).meta({ - ref: "PermissionAction", - }) - export type Action = z.infer + export const Action = Schema.Literals(["allow", "deny", "ask"]) + .annotate({ identifier: "PermissionAction" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) + export type Action = Schema.Schema.Type - export const Rule = z - .object({ - permission: z.string(), - pattern: z.string(), - action: Action, - }) - .meta({ - ref: "PermissionRule", - }) - export type Rule = z.infer + export class Rule extends Schema.Class("PermissionRule")({ + permission: Schema.String, + pattern: Schema.String, + action: Action, + }) { + static readonly zod = zod(this) + } - export const Ruleset = Rule.array().meta({ - ref: "PermissionRuleset", - }) - export type Ruleset = z.infer + export const Ruleset = Schema.mutable(Schema.Array(Rule)) + .annotate({ identifier: "PermissionRuleset" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) + export type Ruleset = Schema.Schema.Type - export const Request = z - .object({ - id: PermissionID.zod, - sessionID: SessionID.zod, - permission: z.string(), - patterns: z.string().array(), - metadata: z.record(z.string(), z.any()), - always: z.string().array(), - tool: z - .object({ - messageID: MessageID.zod, - callID: z.string(), - }) - .optional(), - }) - .meta({ - ref: "PermissionRequest", - }) - export type Request = z.infer + export class Request extends Schema.Class("PermissionRequest")({ + id: PermissionID, + sessionID: SessionID, + permission: Schema.String, + patterns: Schema.Array(Schema.String), + metadata: Schema.Record(Schema.String, Schema.Unknown), + always: Schema.Array(Schema.String), + tool: Schema.optional( + Schema.Struct({ + messageID: MessageID, + callID: Schema.String, + }), + ), + }) { + static readonly zod = zod(this) + } - export const Reply = z.enum(["once", "always", "reject"]) - export type Reply = z.infer + export const Reply = Schema.Literals(["once", "always", "reject"]).pipe(withStatics((s) => ({ zod: zod(s) }))) + export type Reply = Schema.Schema.Type - export const Approval = z.object({ - projectID: ProjectID.zod, - patterns: z.string().array(), - }) + const reply = { + reply: Reply, + message: Schema.optional(Schema.String), + } + + export const ReplyBody = Schema.Struct(reply) + .annotate({ identifier: "PermissionReplyBody" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) + export type ReplyBody = Schema.Schema.Type + + export class Approval extends Schema.Class("PermissionApproval")({ + projectID: ProjectID, + patterns: Schema.Array(Schema.String), + }) { + static readonly zod = zod(this) + } export const Event = { - Asked: BusEvent.define("permission.asked", Request), + Asked: BusEvent.define("permission.asked", Request.zod), Replied: BusEvent.define( "permission.replied", - z.object({ - sessionID: SessionID.zod, - requestID: PermissionID.zod, - reply: Reply, - }), + zod( + Schema.Struct({ + sessionID: SessionID, + requestID: PermissionID, + reply: Reply, + }), + ), ), } @@ -103,20 +112,27 @@ export namespace Permission { export type Error = DeniedError | RejectedError | CorrectedError - export const AskInput = Request.partial({ id: true }).extend({ + export const AskInput = Schema.Struct({ + ...Request.fields, + id: Schema.optional(PermissionID), ruleset: Ruleset, }) + .annotate({ identifier: "PermissionAskInput" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) + export type AskInput = Schema.Schema.Type - export const ReplyInput = z.object({ - requestID: PermissionID.zod, - reply: Reply, - message: z.string().optional(), + export const ReplyInput = Schema.Struct({ + requestID: PermissionID, + ...reply, }) + .annotate({ identifier: "PermissionReplyInput" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) + export type ReplyInput = Schema.Schema.Type export interface Interface { - readonly ask: (input: z.infer) => Effect.Effect - readonly reply: (input: z.infer) => Effect.Effect - readonly list: () => Effect.Effect + readonly ask: (input: AskInput) => Effect.Effect + readonly reply: (input: ReplyInput) => Effect.Effect + readonly list: () => Effect.Effect> } interface PendingEntry { @@ -163,7 +179,7 @@ export namespace Permission { }), ) - const ask = Effect.fn("Permission.ask")(function* (input: z.infer) { + const ask = Effect.fn("Permission.ask")(function* (input: AskInput) { const { approved, pending } = yield* InstanceState.get(state) const { ruleset, ...request } = input let needsAsk = false @@ -183,10 +199,10 @@ export namespace Permission { if (!needsAsk) return const id = request.id ?? PermissionID.ascending() - const info: Request = { + const info = Schema.decodeUnknownSync(Request)({ id, ...request, - } + }) log.info("asking", { id, permission: info.permission, patterns: info.patterns }) const deferred = yield* Deferred.make() @@ -200,7 +216,7 @@ export namespace Permission { ) }) - const reply = Effect.fn("Permission.reply")(function* (input: z.infer) { + const reply = Effect.fn("Permission.reply")(function* (input: ReplyInput) { const { approved, pending } = yield* InstanceState.get(state) const existing = pending.get(input.requestID) if (!existing) return diff --git a/packages/opencode/src/permission/schema.ts b/packages/opencode/src/permission/schema.ts index 2f1190a238..6ac9389a58 100644 --- a/packages/opencode/src/permission/schema.ts +++ b/packages/opencode/src/permission/schema.ts @@ -2,9 +2,13 @@ import { Schema } from "effect" import z from "zod" import { Identifier } from "@/id/id" +import { ZodOverride } from "@/util/effect-zod" import { Newtype } from "@/util/schema" -export class PermissionID extends Newtype()("PermissionID", Schema.String) { +export class PermissionID extends Newtype()( + "PermissionID", + Schema.String.annotate({ [ZodOverride]: Identifier.schema("permission") }), +) { static ascending(id?: string): PermissionID { return this.make(Identifier.ascending("permission", id)) } diff --git a/packages/opencode/src/pty/schema.ts b/packages/opencode/src/pty/schema.ts index deb498891a..0758fe8206 100644 --- a/packages/opencode/src/pty/schema.ts +++ b/packages/opencode/src/pty/schema.ts @@ -2,9 +2,10 @@ import { Schema } from "effect" import z from "zod" import { Identifier } from "@/id/id" +import { ZodOverride } from "@/util/effect-zod" import { withStatics } from "@/util/schema" -const ptyIdSchema = Schema.String.pipe(Schema.brand("PtyID")) +const ptyIdSchema = Schema.String.annotate({ [ZodOverride]: Identifier.schema("pty") }).pipe(Schema.brand("PtyID")) export type PtyID = typeof ptyIdSchema.Type diff --git a/packages/opencode/src/question/schema.ts b/packages/opencode/src/question/schema.ts index e5a0496c96..41186161d0 100644 --- a/packages/opencode/src/question/schema.ts +++ b/packages/opencode/src/question/schema.ts @@ -2,9 +2,13 @@ import { Schema } from "effect" import z from "zod" import { Identifier } from "@/id/id" +import { ZodOverride } from "@/util/effect-zod" import { Newtype } from "@/util/schema" -export class QuestionID extends Newtype()("QuestionID", Schema.String) { +export class QuestionID extends Newtype()( + "QuestionID", + Schema.String.annotate({ [ZodOverride]: Identifier.schema("question") }), +) { static ascending(id?: string): QuestionID { return this.make(Identifier.ascending("question", id)) } diff --git a/packages/opencode/src/server/instance/experimental.ts b/packages/opencode/src/server/instance/experimental.ts index 6309a21bb9..e8e46b2e3b 100644 --- a/packages/opencode/src/server/instance/experimental.ts +++ b/packages/opencode/src/server/instance/experimental.ts @@ -18,7 +18,6 @@ import { lazy } from "../../util/lazy" import { Effect, Option } from "effect" import { WorkspaceRoutes } from "./workspace" import { Agent } from "@/agent/agent" -import { HttpApiRoutes } from "./httpapi" const ConsoleOrgOption = z.object({ accountID: z.string(), @@ -40,7 +39,6 @@ const ConsoleSwitchBody = z.object({ export const ExperimentalRoutes = lazy(() => new Hono() - .route("/httpapi", HttpApiRoutes()) .get( "/console", describeRoute({ diff --git a/packages/opencode/src/server/instance/httpapi/index.ts b/packages/opencode/src/server/instance/httpapi/index.ts deleted file mode 100644 index 523041de84..0000000000 --- a/packages/opencode/src/server/instance/httpapi/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { lazy } from "@/util/lazy" -import { Hono } from "hono" -import { QuestionHttpApiHandler } from "./question" - -export const HttpApiRoutes = lazy(() => - new Hono().all("/question", QuestionHttpApiHandler).all("/question/*", QuestionHttpApiHandler), -) diff --git a/packages/opencode/src/server/instance/httpapi/permission.ts b/packages/opencode/src/server/instance/httpapi/permission.ts new file mode 100644 index 0000000000..e3d152c5a4 --- /dev/null +++ b/packages/opencode/src/server/instance/httpapi/permission.ts @@ -0,0 +1,72 @@ +import { Permission } from "@/permission" +import { PermissionID } from "@/permission/schema" +import { Effect, Layer, Schema } from "effect" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" + +const root = "/experimental/httpapi/permission" + +export const PermissionApi = HttpApi.make("permission") + .add( + HttpApiGroup.make("permission") + .add( + HttpApiEndpoint.get("list", root, { + success: Schema.Array(Permission.Request), + }).annotateMerge( + OpenApi.annotations({ + identifier: "permission.list", + summary: "List pending permissions", + description: "Get all pending permission requests across all sessions.", + }), + ), + HttpApiEndpoint.post("reply", `${root}/:requestID/reply`, { + params: { requestID: PermissionID }, + payload: Permission.ReplyBody, + success: Schema.Boolean, + }).annotateMerge( + OpenApi.annotations({ + identifier: "permission.reply", + summary: "Respond to permission request", + description: "Approve or deny a permission request from the AI assistant.", + }), + ), + ) + .annotateMerge( + OpenApi.annotations({ + title: "permission", + description: "Experimental HttpApi permission routes.", + }), + ), + ) + .annotateMerge( + OpenApi.annotations({ + title: "opencode experimental HttpApi", + version: "0.0.1", + description: "Experimental HttpApi surface for selected instance routes.", + }), + ) + +export const PermissionLive = Layer.unwrap( + Effect.gen(function* () { + const svc = yield* Permission.Service + + const list = Effect.fn("PermissionHttpApi.list")(function* () { + return yield* svc.list() + }) + + const reply = Effect.fn("PermissionHttpApi.reply")(function* (ctx: { + params: { requestID: PermissionID } + payload: Permission.ReplyBody + }) { + yield* svc.reply({ + requestID: ctx.params.requestID, + reply: ctx.payload.reply, + message: ctx.payload.message, + }) + return true + }) + + return HttpApiBuilder.group(PermissionApi, "permission", (handlers) => + handlers.handle("list", list).handle("reply", reply), + ) + }), +).pipe(Layer.provide(Permission.defaultLayer)) diff --git a/packages/opencode/src/server/instance/httpapi/question.ts b/packages/opencode/src/server/instance/httpapi/question.ts index ef0f41734b..686c6abb17 100644 --- a/packages/opencode/src/server/instance/httpapi/question.ts +++ b/packages/opencode/src/server/instance/httpapi/question.ts @@ -1,44 +1,71 @@ -import { AppLayer } from "@/effect/app-runtime" -import { memoMap } from "@/effect/run-service" import { Question } from "@/question" import { QuestionID } from "@/question/schema" -import { lazy } from "@/util/lazy" -import { makeQuestionHandler, questionApi } from "@opencode-ai/server" -import { Effect, Layer } from "effect" -import { HttpRouter, HttpServer } from "effect/unstable/http" -import { HttpApiBuilder } from "effect/unstable/httpapi" -import type { Handler } from "hono" +import { Effect, Layer, Schema } from "effect" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" const root = "/experimental/httpapi/question" -const QuestionLive = makeQuestionHandler({ - list: Effect.fn("QuestionHttpApi.host.list")(function* () { - const svc = yield* Question.Service - return yield* svc.list() - }), - reply: Effect.fn("QuestionHttpApi.host.reply")(function* (input) { - const svc = yield* Question.Service - yield* svc.reply({ - requestID: QuestionID.make(input.requestID), - answers: input.answers, - }) - }), -}).pipe(Layer.provide(Question.defaultLayer)) - -const web = lazy(() => - HttpRouter.toWebHandler( - Layer.mergeAll( - AppLayer, - HttpApiBuilder.layer(questionApi, { openapiPath: `${root}/doc` }).pipe( - Layer.provide(QuestionLive), - Layer.provide(HttpServer.layerServices), +export const QuestionApi = HttpApi.make("question") + .add( + HttpApiGroup.make("question") + .add( + HttpApiEndpoint.get("list", root, { + success: Schema.Array(Question.Request), + }).annotateMerge( + OpenApi.annotations({ + identifier: "question.list", + summary: "List pending questions", + description: "Get all pending question requests across all sessions.", + }), + ), + HttpApiEndpoint.post("reply", `${root}/:requestID/reply`, { + params: { requestID: QuestionID }, + payload: Question.Reply, + success: Schema.Boolean, + }).annotateMerge( + OpenApi.annotations({ + identifier: "question.reply", + summary: "Reply to question request", + description: "Provide answers to a question request from the AI assistant.", + }), + ), + ) + .annotateMerge( + OpenApi.annotations({ + title: "question", + description: "Experimental HttpApi question routes.", + }), ), - ), - { - disableLogger: true, - memoMap, - }, - ), -) + ) + .annotateMerge( + OpenApi.annotations({ + title: "opencode experimental HttpApi", + version: "0.0.1", + description: "Experimental HttpApi surface for selected instance routes.", + }), + ) -export const QuestionHttpApiHandler: Handler = (c, _next) => web().handler(c.req.raw) +export const QuestionLive = Layer.unwrap( + Effect.gen(function* () { + const svc = yield* Question.Service + + const list = Effect.fn("QuestionHttpApi.list")(function* () { + return yield* svc.list() + }) + + const reply = Effect.fn("QuestionHttpApi.reply")(function* (ctx: { + params: { requestID: QuestionID } + payload: Question.Reply + }) { + yield* svc.reply({ + requestID: ctx.params.requestID, + answers: ctx.payload.answers, + }) + return true + }) + + return HttpApiBuilder.group(QuestionApi, "question", (handlers) => + handlers.handle("list", list).handle("reply", reply), + ) + }), +).pipe(Layer.provide(Question.defaultLayer)) diff --git a/packages/opencode/src/server/instance/httpapi/server.ts b/packages/opencode/src/server/instance/httpapi/server.ts new file mode 100644 index 0000000000..363e93a240 --- /dev/null +++ b/packages/opencode/src/server/instance/httpapi/server.ts @@ -0,0 +1,135 @@ +import { NodeHttpServer } from "@effect/platform-node" +import { Effect, Layer, Redacted, Schema } from "effect" +import { HttpApiBuilder, HttpApiMiddleware, HttpApiSecurity } from "effect/unstable/httpapi" +import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http" +import { createServer } from "node:http" +import { AppRuntime } from "@/effect/app-runtime" +import { InstanceRef, WorkspaceRef } from "@/effect/instance-ref" +import { Flag } from "@/flag/flag" +import { InstanceBootstrap } from "@/project/bootstrap" +import { Instance } from "@/project/instance" +import { Filesystem } from "@/util/filesystem" +import { Permission } from "@/permission" +import { Question } from "@/question" +import { PermissionApi, PermissionLive } from "./permission" +import { QuestionApi, QuestionLive } from "./question" + +const Query = Schema.Struct({ + directory: Schema.optional(Schema.String), + workspace: Schema.optional(Schema.String), + auth_token: Schema.optional(Schema.String), +}) + +const Headers = Schema.Struct({ + authorization: Schema.optional(Schema.String), + "x-opencode-directory": Schema.optional(Schema.String), +}) + +export namespace ExperimentalHttpApiServer { + function text(input: string, status: number, headers?: Record) { + return HttpServerResponse.text(input, { status, headers }) + } + + function decode(input: string) { + try { + return decodeURIComponent(input) + } catch { + return input + } + } + + class Unauthorized extends Schema.TaggedErrorClass()( + "Unauthorized", + { message: Schema.String }, + { httpApiStatus: 401 }, + ) {} + + class Authorization extends HttpApiMiddleware.Service()("@opencode/ExperimentalHttpApiAuthorization", { + error: Unauthorized, + security: { + basic: HttpApiSecurity.basic, + }, + }) {} + + const normalize = HttpRouter.middleware()( + Effect.gen(function* () { + return (effect) => + Effect.gen(function* () { + const query = yield* HttpServerRequest.schemaSearchParams(Query) + if (!query.auth_token) return yield* effect + const req = yield* HttpServerRequest.HttpServerRequest + const next = req.modify({ + headers: { + ...req.headers, + authorization: `Basic ${query.auth_token}`, + }, + }) + return yield* effect.pipe(Effect.provideService(HttpServerRequest.HttpServerRequest, next)) + }) + }), + ).layer + + const auth = Layer.succeed( + Authorization, + Authorization.of({ + basic: (effect, { credential }) => + Effect.gen(function* () { + if (!Flag.OPENCODE_SERVER_PASSWORD) return yield* effect + + const user = Flag.OPENCODE_SERVER_USERNAME ?? "opencode" + if (credential.username !== user) { + return yield* new Unauthorized({ message: "Unauthorized" }) + } + if (Redacted.value(credential.password) !== Flag.OPENCODE_SERVER_PASSWORD) { + return yield* new Unauthorized({ message: "Unauthorized" }) + } + return yield* effect + }), + }), + ) + + const instance = HttpRouter.middleware()( + Effect.gen(function* () { + return (effect) => + Effect.gen(function* () { + const query = yield* HttpServerRequest.schemaSearchParams(Query) + const headers = yield* HttpServerRequest.schemaHeaders(Headers) + const raw = query.directory || headers["x-opencode-directory"] || process.cwd() + const workspace = query.workspace || undefined + const ctx = yield* Effect.promise(() => + Instance.provide({ + directory: Filesystem.resolve(decode(raw)), + init: () => AppRuntime.runPromise(InstanceBootstrap), + fn: () => Instance.current, + }), + ) + + const next = workspace ? effect.pipe(Effect.provideService(WorkspaceRef, workspace)) : effect + return yield* next.pipe(Effect.provideService(InstanceRef, ctx)) + }) + }), + ).layer + + const QuestionSecured = QuestionApi.middleware(Authorization) + const PermissionSecured = PermissionApi.middleware(Authorization) + + export const routes = Layer.mergeAll( + HttpApiBuilder.layer(QuestionSecured, { openapiPath: "/experimental/httpapi/question/doc" }).pipe( + Layer.provide(QuestionLive), + ), + HttpApiBuilder.layer(PermissionSecured, { openapiPath: "/experimental/httpapi/permission/doc" }).pipe( + Layer.provide(PermissionLive), + ), + ).pipe(Layer.provide(auth), Layer.provide(normalize), Layer.provide(instance)) + + export const layer = (opts: { hostname: string; port: number }) => + HttpRouter.serve(routes, { disableListenLog: true, disableLogger: true }).pipe( + Layer.provideMerge(NodeHttpServer.layer(createServer, { port: opts.port, host: opts.hostname })), + ) + + export const layerTest = HttpRouter.serve(routes, { disableListenLog: true, disableLogger: true }).pipe( + Layer.provideMerge(NodeHttpServer.layerTest), + Layer.provideMerge(Question.defaultLayer), + Layer.provideMerge(Permission.defaultLayer), + ) +} diff --git a/packages/opencode/src/server/instance/permission.ts b/packages/opencode/src/server/instance/permission.ts index 3f93709354..b8c2244140 100644 --- a/packages/opencode/src/server/instance/permission.ts +++ b/packages/opencode/src/server/instance/permission.ts @@ -33,7 +33,7 @@ export const PermissionRoutes = lazy(() => requestID: PermissionID.zod, }), ), - validator("json", z.object({ reply: Permission.Reply, message: z.string().optional() })), + validator("json", z.object({ reply: Permission.Reply.zod, message: z.string().optional() })), async (c) => { const params = c.req.valid("param") const json = c.req.valid("json") @@ -60,7 +60,7 @@ export const PermissionRoutes = lazy(() => description: "List of pending permissions", content: { "application/json": { - schema: resolver(Permission.Request.array()), + schema: resolver(Permission.Request.zod.array()), }, }, }, diff --git a/packages/opencode/src/server/instance/session.ts b/packages/opencode/src/server/instance/session.ts index a011c32f9b..4f02e35fac 100644 --- a/packages/opencode/src/server/instance/session.ts +++ b/packages/opencode/src/server/instance/session.ts @@ -274,7 +274,7 @@ export const SessionRoutes = lazy(() => "json", z.object({ title: z.string().optional(), - permission: Permission.Ruleset.optional(), + permission: Permission.Ruleset.zod.optional(), time: z .object({ archived: z.number().optional(), @@ -1093,7 +1093,7 @@ export const SessionRoutes = lazy(() => permissionID: PermissionID.zod, }), ), - validator("json", z.object({ response: Permission.Reply })), + validator("json", z.object({ response: Permission.Reply.zod })), async (c) => { const params = c.req.valid("param") await AppRuntime.runPromise( diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index d8ab812349..49d8359497 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -144,7 +144,7 @@ export namespace Session { compacting: z.number().optional(), archived: z.number().optional(), }), - permission: Permission.Ruleset.optional(), + permission: Permission.Ruleset.zod.optional(), revert: z .object({ messageID: MessageID.zod, @@ -193,7 +193,7 @@ export namespace Session { export const RemoveInput = SessionID.zod export const SetTitleInput = z.object({ sessionID: SessionID.zod, title: z.string() }) export const SetArchivedInput = z.object({ sessionID: SessionID.zod, time: z.number().optional() }) - export const SetPermissionInput = z.object({ sessionID: SessionID.zod, permission: Permission.Ruleset }) + export const SetPermissionInput = z.object({ sessionID: SessionID.zod, permission: Permission.Ruleset.zod }) export const SetRevertInput = z.object({ sessionID: SessionID.zod, revert: Info.shape.revert, diff --git a/packages/opencode/src/session/schema.ts b/packages/opencode/src/session/schema.ts index 856ab31142..efed280c98 100644 --- a/packages/opencode/src/session/schema.ts +++ b/packages/opencode/src/session/schema.ts @@ -2,9 +2,10 @@ import { Schema } from "effect" import z from "zod" import { Identifier } from "@/id/id" +import { ZodOverride } from "@/util/effect-zod" import { withStatics } from "@/util/schema" -export const SessionID = Schema.String.pipe( +export const SessionID = Schema.String.annotate({ [ZodOverride]: Identifier.schema("session") }).pipe( Schema.brand("SessionID"), withStatics((s) => ({ descending: (id?: string) => s.make(Identifier.descending("session", id)), @@ -14,7 +15,7 @@ export const SessionID = Schema.String.pipe( export type SessionID = Schema.Schema.Type -export const MessageID = Schema.String.pipe( +export const MessageID = Schema.String.annotate({ [ZodOverride]: Identifier.schema("message") }).pipe( Schema.brand("MessageID"), withStatics((s) => ({ ascending: (id?: string) => s.make(Identifier.ascending("message", id)), @@ -24,7 +25,7 @@ export const MessageID = Schema.String.pipe( export type MessageID = Schema.Schema.Type -export const PartID = Schema.String.pipe( +export const PartID = Schema.String.annotate({ [ZodOverride]: Identifier.schema("part") }).pipe( Schema.brand("PartID"), withStatics((s) => ({ ascending: (id?: string) => s.make(Identifier.ascending("part", id)), diff --git a/packages/opencode/src/sync/schema.ts b/packages/opencode/src/sync/schema.ts index 5cec8b1f7a..37cdbd718f 100644 --- a/packages/opencode/src/sync/schema.ts +++ b/packages/opencode/src/sync/schema.ts @@ -2,9 +2,10 @@ import { Schema } from "effect" import z from "zod" import { Identifier } from "@/id/id" +import { ZodOverride } from "@/util/effect-zod" import { withStatics } from "@/util/schema" -export const EventID = Schema.String.pipe( +export const EventID = Schema.String.annotate({ [ZodOverride]: Identifier.schema("event") }).pipe( Schema.brand("EventID"), withStatics((s) => ({ ascending: (id?: string) => s.make(Identifier.ascending("event", id)), diff --git a/packages/opencode/src/tool/schema.ts b/packages/opencode/src/tool/schema.ts index 823bb0aede..ac41fd1606 100644 --- a/packages/opencode/src/tool/schema.ts +++ b/packages/opencode/src/tool/schema.ts @@ -2,9 +2,10 @@ import { Schema } from "effect" import z from "zod" import { Identifier } from "@/id/id" +import { ZodOverride } from "@/util/effect-zod" import { withStatics } from "@/util/schema" -const toolIdSchema = Schema.String.pipe(Schema.brand("ToolID")) +const toolIdSchema = Schema.String.annotate({ [ZodOverride]: Identifier.schema("tool") }).pipe(Schema.brand("ToolID")) export type ToolID = typeof toolIdSchema.Type diff --git a/packages/opencode/src/util/effect-zod.ts b/packages/opencode/src/util/effect-zod.ts index 97cbbd2fc9..553d7a0650 100644 --- a/packages/opencode/src/util/effect-zod.ts +++ b/packages/opencode/src/util/effect-zod.ts @@ -1,11 +1,21 @@ import { Schema, SchemaAST } from "effect" import z from "zod" +/** + * Annotation key for providing a hand-crafted Zod schema that the walker + * should use instead of re-deriving from the AST. Attach it via + * `Schema.String.annotate({ [ZodOverride]: z.string().startsWith("per") })`. + */ +export const ZodOverride: unique symbol = Symbol.for("effect-zod/override") + export function zod(schema: S): z.ZodType> { return walk(schema.ast) as z.ZodType> } function walk(ast: SchemaAST.AST): z.ZodTypeAny { + const override = (ast.annotations as any)?.[ZodOverride] as z.ZodTypeAny | undefined + if (override) return override + const out = body(ast) const desc = SchemaAST.resolveDescription(ast) const ref = SchemaAST.resolveIdentifier(ast) @@ -57,6 +67,12 @@ function opt(ast: SchemaAST.AST): z.ZodTypeAny { } function union(ast: SchemaAST.Union): z.ZodTypeAny { + // When every member is a string literal, emit z.enum() so that + // JSON Schema produces { "enum": [...] } instead of { "anyOf": [{ "const": ... }] }. + if (ast.types.length >= 2 && ast.types.every((t) => t._tag === "Literal" && typeof t.literal === "string")) { + return z.enum(ast.types.map((t) => (t as SchemaAST.Literal).literal as string) as [string, ...string[]]) + } + const items = ast.types.map(walk) if (items.length === 1) return items[0] if (items.length < 2) return fail(ast) diff --git a/packages/opencode/test/server/question-httpapi.test.ts b/packages/opencode/test/server/question-httpapi.test.ts deleted file mode 100644 index 00cc32f59e..0000000000 --- a/packages/opencode/test/server/question-httpapi.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { afterEach, describe, expect, test } from "bun:test" -import { AppRuntime } from "../../src/effect/app-runtime" -import { Instance } from "../../src/project/instance" -import { Question } from "../../src/question" -import { Server } from "../../src/server/server" -import { SessionID } from "../../src/session/schema" -import { Log } from "../../src/util/log" -import { tmpdir } from "../fixture/fixture" - -Log.init({ print: false }) - -const ask = (input: { sessionID: SessionID; questions: ReadonlyArray }) => - AppRuntime.runPromise(Question.Service.use((svc) => svc.ask(input))) - -afterEach(async () => { - await Instance.disposeAll() -}) - -describe("experimental question httpapi", () => { - test("lists pending questions, replies, and serves docs", async () => { - await using tmp = await tmpdir({ git: true }) - const app = Server.Default().app - const headers = { - "content-type": "application/json", - "x-opencode-directory": tmp.path, - } - const questions: ReadonlyArray = [ - { - question: "What would you like to do?", - header: "Action", - options: [ - { label: "Option 1", description: "First option" }, - { label: "Option 2", description: "Second option" }, - ], - }, - ] - - let pending!: ReturnType - - await Instance.provide({ - directory: tmp.path, - fn: async () => { - pending = ask({ - sessionID: SessionID.make("ses_test"), - questions, - }) - }, - }) - - const list = await app.request("/experimental/httpapi/question", { - headers, - }) - - expect(list.status).toBe(200) - const items = await list.json() - expect(items).toHaveLength(1) - expect(items[0]).toMatchObject({ questions }) - - const doc = await app.request("/experimental/httpapi/question/doc", { - headers, - }) - - expect(doc.status).toBe(200) - const spec = await doc.json() - expect(spec.paths["/experimental/httpapi/question"]?.get?.operationId).toBe("question.list") - expect(spec.paths["/experimental/httpapi/question/{requestID}/reply"]?.post?.operationId).toBe("question.reply") - - const reply = await app.request(`/experimental/httpapi/question/${items[0].id}/reply`, { - method: "POST", - headers, - body: JSON.stringify({ answers: [["Option 1"]] }), - }) - - expect(reply.status).toBe(200) - expect(await reply.json()).toBe(true) - expect(await pending).toEqual([["Option 1"]]) - }) -}) diff --git a/packages/opencode/test/util/effect-zod.test.ts b/packages/opencode/test/util/effect-zod.test.ts index 4004ca2d23..7f7249514d 100644 --- a/packages/opencode/test/util/effect-zod.test.ts +++ b/packages/opencode/test/util/effect-zod.test.ts @@ -1,7 +1,13 @@ import { describe, expect, test } from "bun:test" import { Schema } from "effect" +import z from "zod" -import { zod } from "../../src/util/effect-zod" +import { zod, ZodOverride } from "../../src/util/effect-zod" + +function json(schema: z.ZodTypeAny) { + const { $schema: _, ...rest } = z.toJSONSchema(schema) + return rest +} describe("util.effect-zod", () => { test("converts class schemas for route dto shapes", () => { @@ -58,4 +64,126 @@ describe("util.effect-zod", () => { test("throws for unsupported tuple schemas", () => { expect(() => zod(Schema.Tuple([Schema.String, Schema.Number]))).toThrow("unsupported effect schema") }) + + test("string literal unions produce z.enum with enum in JSON Schema", () => { + const Action = Schema.Literals(["allow", "deny", "ask"]) + const out = zod(Action) + + expect(out.parse("allow")).toBe("allow") + expect(out.parse("deny")).toBe("deny") + expect(() => out.parse("nope")).toThrow() + + // Matches native z.enum JSON Schema output + const bridged = json(out) + const native = json(z.enum(["allow", "deny", "ask"])) + expect(bridged).toEqual(native) + expect(bridged.enum).toEqual(["allow", "deny", "ask"]) + }) + + test("ZodOverride annotation provides the Zod schema for branded IDs", () => { + const override = z.string().startsWith("per") + const ID = Schema.String.annotate({ [ZodOverride]: override }).pipe(Schema.brand("TestID")) + + const Parent = Schema.Struct({ id: ID, name: Schema.String }) + const out = zod(Parent) + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((out as any).parse({ id: "per_abc", name: "test" })).toEqual({ id: "per_abc", name: "test" }) + + const schema = json(out) as any + expect(schema.properties.id).toEqual({ type: "string", pattern: "^per.*" }) + }) + + test("Schema.Class nested in a parent preserves ref via identifier", () => { + class Inner extends Schema.Class("MyInner")({ + value: Schema.String, + }) {} + + class Outer extends Schema.Class("MyOuter")({ + inner: Inner, + }) {} + + const out = zod(Outer) + expect(out.meta()?.ref).toBe("MyOuter") + + const shape = (out as any).shape ?? (out as any)._def?.shape?.() + expect(shape.inner.meta()?.ref).toBe("MyInner") + }) + + test("Schema.Class preserves identifier and uses enum format", () => { + class Rule extends Schema.Class("PermissionRule")({ + permission: Schema.String, + pattern: Schema.String, + action: Schema.Literals(["allow", "deny", "ask"]), + }) {} + + const out = zod(Rule) + expect(out.meta()?.ref).toBe("PermissionRule") + + const schema = json(out) as any + expect(schema.properties.action).toEqual({ + type: "string", + enum: ["allow", "deny", "ask"], + }) + }) + + test("ZodOverride on ID carries pattern through Schema.Class", () => { + const ID = Schema.String.annotate({ + [ZodOverride]: z.string().startsWith("per"), + }) + + class Request extends Schema.Class("TestRequest")({ + id: ID, + name: Schema.String, + }) {} + + const schema = json(zod(Request)) as any + expect(schema.properties.id).toEqual({ type: "string", pattern: "^per.*" }) + expect(schema.properties.name).toEqual({ type: "string" }) + }) + + test("Permission schemas match original Zod equivalents", () => { + const MsgID = Schema.String.annotate({ [ZodOverride]: z.string().startsWith("msg") }) + const PerID = Schema.String.annotate({ [ZodOverride]: z.string().startsWith("per") }) + const SesID = Schema.String.annotate({ [ZodOverride]: z.string().startsWith("ses") }) + + class Tool extends Schema.Class("PermissionTool")({ + messageID: MsgID, + callID: Schema.String, + }) {} + + class Request extends Schema.Class("PermissionRequest")({ + id: PerID, + sessionID: SesID, + permission: Schema.String, + patterns: Schema.Array(Schema.String), + metadata: Schema.Record(Schema.String, Schema.Unknown), + always: Schema.Array(Schema.String), + tool: Schema.optional(Tool), + }) {} + + const bridged = json(zod(Request)) as any + expect(bridged.properties.id).toEqual({ type: "string", pattern: "^per.*" }) + expect(bridged.properties.sessionID).toEqual({ type: "string", pattern: "^ses.*" }) + expect(bridged.properties.permission).toEqual({ type: "string" }) + expect(bridged.required?.sort()).toEqual(["id", "sessionID", "permission", "patterns", "metadata", "always"].sort()) + + // Tool field is present with the ref from Schema.Class identifier + const toolSchema = json(zod(Tool)) as any + expect(toolSchema.properties.messageID).toEqual({ type: "string", pattern: "^msg.*" }) + expect(toolSchema.properties.callID).toEqual({ type: "string" }) + }) + + test("ZodOverride survives Schema.brand", () => { + const override = z.string().startsWith("ses") + const ID = Schema.String.annotate({ [ZodOverride]: override }).pipe(Schema.brand("SessionID")) + + // The branded schema's AST still has the override + class Parent extends Schema.Class("Parent")({ + sessionID: ID, + }) {} + + const schema = json(zod(Parent)) as any + expect(schema.properties.sessionID).toEqual({ type: "string", pattern: "^ses.*" }) + }) }) From 3b75f16119b914fa6eb7dd451a0f3fb20d22d69f Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Wed, 15 Apr 2026 21:29:10 +0000 Subject: [PATCH 151/300] chore: generate --- packages/sdk/openapi.json | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index ee3538d55f..c59e1ab910 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -8143,7 +8143,8 @@ "type": "object", "properties": { "messageID": { - "type": "string" + "type": "string", + "pattern": "^msg.*" }, "callID": { "type": "string" @@ -8155,10 +8156,12 @@ "type": "object", "properties": { "id": { - "type": "string" + "type": "string", + "pattern": "^que.*" }, "sessionID": { - "type": "string" + "type": "string", + "pattern": "^ses.*" }, "questions": { "description": "Questions to ask", @@ -8196,10 +8199,12 @@ "type": "object", "properties": { "sessionID": { - "type": "string" + "type": "string", + "pattern": "^ses.*" }, "requestID": { - "type": "string" + "type": "string", + "pattern": "^que.*" }, "answers": { "type": "array", @@ -8227,10 +8232,12 @@ "type": "object", "properties": { "sessionID": { - "type": "string" + "type": "string", + "pattern": "^ses.*" }, "requestID": { - "type": "string" + "type": "string", + "pattern": "^que.*" } }, "required": ["sessionID", "requestID"] From 6bed7d469d8f6a18d5543cc668d951d0d1e09776 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 17:32:56 -0400 Subject: [PATCH 152/300] feat(opencode): improve telemetry tracing and request spans (#22653) --- packages/opencode/src/effect/app-runtime.ts | 3 +- packages/opencode/src/effect/run-service.ts | 2 +- .../opencode/src/server/instance/config.ts | 34 ++++++++----------- .../opencode/src/server/instance/session.ts | 28 +++++++++------ .../opencode/src/server/instance/trace.ts | 33 ++++++++++++++++++ 5 files changed, 68 insertions(+), 32 deletions(-) create mode 100644 packages/opencode/src/server/instance/trace.ts diff --git a/packages/opencode/src/effect/app-runtime.ts b/packages/opencode/src/effect/app-runtime.ts index 5948bd25e6..257922dafe 100644 --- a/packages/opencode/src/effect/app-runtime.ts +++ b/packages/opencode/src/effect/app-runtime.ts @@ -49,7 +49,6 @@ import { ShareNext } from "@/share/share-next" import { SessionShare } from "@/share/session" export const AppLayer = Layer.mergeAll( - Observability.layer, AppFileSystem.defaultLayer, Bus.defaultLayer, Auth.defaultLayer, @@ -95,7 +94,7 @@ export const AppLayer = Layer.mergeAll( Installation.defaultLayer, ShareNext.defaultLayer, SessionShare.defaultLayer, -) +).pipe(Layer.provideMerge(Observability.layer)) const rt = ManagedRuntime.make(AppLayer, { memoMap }) type Runtime = Pick diff --git a/packages/opencode/src/effect/run-service.ts b/packages/opencode/src/effect/run-service.ts index 13104c88b3..3de82e0d11 100644 --- a/packages/opencode/src/effect/run-service.ts +++ b/packages/opencode/src/effect/run-service.ts @@ -38,7 +38,7 @@ export function attach(effect: Effect.Effect): Effect.Effect(service: Context.Service, layer: Layer.Layer) { let rt: ManagedRuntime.ManagedRuntime | undefined - const getRuntime = () => (rt ??= ManagedRuntime.make(Layer.merge(layer, Observability.layer), { memoMap })) + const getRuntime = () => (rt ??= ManagedRuntime.make(Layer.provideMerge(layer, Observability.layer), { memoMap })) return { runSync: (fn: (svc: S) => Effect.Effect) => getRuntime().runSync(attach(service.use(fn))), diff --git a/packages/opencode/src/server/instance/config.ts b/packages/opencode/src/server/instance/config.ts index 41d5872c98..aa770726df 100644 --- a/packages/opencode/src/server/instance/config.ts +++ b/packages/opencode/src/server/instance/config.ts @@ -5,12 +5,10 @@ import { Config } from "../../config/config" import { Provider } from "../../provider/provider" import { mapValues } from "remeda" import { errors } from "../error" -import { Log } from "../../util/log" import { lazy } from "../../util/lazy" import { AppRuntime } from "../../effect/app-runtime" import { Effect } from "effect" - -const log = Log.create({ service: "server" }) +import { jsonRequest } from "./trace" export const ConfigRoutes = lazy(() => new Hono() @@ -31,9 +29,11 @@ export const ConfigRoutes = lazy(() => }, }, }), - async (c) => { - return c.json(await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.get()))) - }, + async (c) => + jsonRequest("ConfigRoutes.get", c, function* () { + const cfg = yield* Config.Service + return yield* cfg.get() + }), ) .patch( "/", @@ -82,18 +82,14 @@ export const ConfigRoutes = lazy(() => }, }, }), - async (c) => { - using _ = log.time("providers") - const providers = await AppRuntime.runPromise( - Effect.gen(function* () { - const svc = yield* Provider.Service - return mapValues(yield* svc.list(), (item) => item) - }), - ) - return c.json({ - providers: Object.values(providers), - default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id), - }) - }, + async (c) => + jsonRequest("ConfigRoutes.providers", c, function* () { + const svc = yield* Provider.Service + const providers = mapValues(yield* svc.list(), (item) => item) + return { + providers: Object.values(providers), + default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id), + } + }), ), ) diff --git a/packages/opencode/src/server/instance/session.ts b/packages/opencode/src/server/instance/session.ts index 4f02e35fac..0bce3085e0 100644 --- a/packages/opencode/src/server/instance/session.ts +++ b/packages/opencode/src/server/instance/session.ts @@ -26,6 +26,7 @@ import { errors } from "../error" import { lazy } from "../../util/lazy" import { Bus } from "../../bus" import { NamedError } from "@opencode-ai/shared/util/error" +import { jsonRequest } from "./trace" const log = Log.create({ service: "server" }) @@ -94,10 +95,11 @@ export const SessionRoutes = lazy(() => ...errors(400), }, }), - async (c) => { - const result = await AppRuntime.runPromise(SessionStatus.Service.use((svc) => svc.list())) - return c.json(Object.fromEntries(result)) - }, + async (c) => + jsonRequest("SessionRoutes.status", c, function* () { + const svc = yield* SessionStatus.Service + return Object.fromEntries(yield* svc.list()) + }), ) .get( "/:sessionID", @@ -126,8 +128,10 @@ export const SessionRoutes = lazy(() => ), async (c) => { const sessionID = c.req.valid("param").sessionID - const session = await AppRuntime.runPromise(Session.Service.use((svc) => svc.get(sessionID))) - return c.json(session) + return jsonRequest("SessionRoutes.get", c, function* () { + const session = yield* Session.Service + return yield* session.get(sessionID) + }) }, ) .get( @@ -157,8 +161,10 @@ export const SessionRoutes = lazy(() => ), async (c) => { const sessionID = c.req.valid("param").sessionID - const session = await AppRuntime.runPromise(Session.Service.use((svc) => svc.children(sessionID))) - return c.json(session) + return jsonRequest("SessionRoutes.children", c, function* () { + const session = yield* Session.Service + return yield* session.children(sessionID) + }) }, ) .get( @@ -187,8 +193,10 @@ export const SessionRoutes = lazy(() => ), async (c) => { const sessionID = c.req.valid("param").sessionID - const todos = await AppRuntime.runPromise(Todo.Service.use((svc) => svc.get(sessionID))) - return c.json(todos) + return jsonRequest("SessionRoutes.todo", c, function* () { + const todo = yield* Todo.Service + return yield* todo.get(sessionID) + }) }, ) .post( diff --git a/packages/opencode/src/server/instance/trace.ts b/packages/opencode/src/server/instance/trace.ts new file mode 100644 index 0000000000..b3adbb4c80 --- /dev/null +++ b/packages/opencode/src/server/instance/trace.ts @@ -0,0 +1,33 @@ +import type { Context } from "hono" +import { Effect } from "effect" +import { AppRuntime } from "../../effect/app-runtime" + +type AppEnv = Parameters[0] extends Effect.Effect ? R : never + +export function runRequest(name: string, c: Context, effect: Effect.Effect) { + const url = new URL(c.req.url) + return AppRuntime.runPromise( + effect.pipe( + Effect.withSpan(name, { + attributes: { + "http.method": c.req.method, + "http.path": url.pathname, + }, + }), + ), + ) +} + +export async function jsonRequest( + name: string, + c: C, + effect: (c: C) => Effect.gen.Return, +) { + return c.json( + await runRequest( + name, + c, + Effect.gen(() => effect(c)), + ), + ) +} From d2ea6700aa2e6bdf5d04fe70ba893afbb320adbd Mon Sep 17 00:00:00 2001 From: Ariane Emory <97994360+ariane-emory@users.noreply.github.com> Date: Wed, 15 Apr 2026 18:44:53 -0400 Subject: [PATCH 153/300] fix(core): Remove dead code and documentation related to the obsolete list tool. (#22672) --- packages/opencode/src/acp/agent.ts | 3 - packages/opencode/src/cli/cmd/agent.ts | 2 +- packages/opencode/src/cli/cmd/run.ts | 10 -- .../src/cli/cmd/tui/routes/session/index.tsx | 18 --- packages/opencode/src/tool/ls.ts | 122 ------------------ packages/opencode/src/tool/ls.txt | 1 - packages/web/src/content/docs/ar/modes.mdx | 1 - .../web/src/content/docs/ar/permissions.mdx | 3 +- packages/web/src/content/docs/ar/tools.mdx | 18 +-- packages/web/src/content/docs/bs/modes.mdx | 1 - .../web/src/content/docs/bs/permissions.mdx | 3 +- packages/web/src/content/docs/bs/tools.mdx | 18 +-- packages/web/src/content/docs/da/modes.mdx | 1 - .../web/src/content/docs/da/permissions.mdx | 3 +- packages/web/src/content/docs/da/tools.mdx | 18 +-- packages/web/src/content/docs/de/modes.mdx | 1 - .../web/src/content/docs/de/permissions.mdx | 3 +- packages/web/src/content/docs/de/tools.mdx | 18 +-- packages/web/src/content/docs/es/modes.mdx | 1 - .../web/src/content/docs/es/permissions.mdx | 3 +- packages/web/src/content/docs/es/tools.mdx | 18 +-- packages/web/src/content/docs/fr/modes.mdx | 1 - .../web/src/content/docs/fr/permissions.mdx | 3 +- packages/web/src/content/docs/fr/tools.mdx | 18 +-- packages/web/src/content/docs/it/modes.mdx | 1 - .../web/src/content/docs/it/permissions.mdx | 3 +- packages/web/src/content/docs/it/tools.mdx | 18 +-- packages/web/src/content/docs/ja/modes.mdx | 1 - .../web/src/content/docs/ja/permissions.mdx | 3 +- packages/web/src/content/docs/ja/tools.mdx | 18 +-- packages/web/src/content/docs/ko/modes.mdx | 1 - .../web/src/content/docs/ko/permissions.mdx | 3 +- packages/web/src/content/docs/ko/tools.mdx | 18 +-- packages/web/src/content/docs/modes.mdx | 1 - packages/web/src/content/docs/nb/modes.mdx | 1 - .../web/src/content/docs/nb/permissions.mdx | 3 +- packages/web/src/content/docs/nb/tools.mdx | 18 +-- packages/web/src/content/docs/permissions.mdx | 3 +- packages/web/src/content/docs/pl/modes.mdx | 1 - .../web/src/content/docs/pl/permissions.mdx | 3 +- packages/web/src/content/docs/pl/tools.mdx | 18 +-- packages/web/src/content/docs/pt-br/modes.mdx | 1 - .../src/content/docs/pt-br/permissions.mdx | 3 +- packages/web/src/content/docs/pt-br/tools.mdx | 18 +-- packages/web/src/content/docs/ru/modes.mdx | 1 - .../web/src/content/docs/ru/permissions.mdx | 3 +- packages/web/src/content/docs/ru/tools.mdx | 18 +-- packages/web/src/content/docs/th/modes.mdx | 1 - .../web/src/content/docs/th/permissions.mdx | 3 +- packages/web/src/content/docs/th/tools.mdx | 18 +-- packages/web/src/content/docs/tools.mdx | 19 +-- packages/web/src/content/docs/tr/modes.mdx | 1 - .../web/src/content/docs/tr/permissions.mdx | 3 +- packages/web/src/content/docs/tr/tools.mdx | 18 +-- packages/web/src/content/docs/zh-cn/modes.mdx | 1 - .../src/content/docs/zh-cn/permissions.mdx | 3 +- packages/web/src/content/docs/zh-cn/tools.mdx | 18 +-- packages/web/src/content/docs/zh-tw/modes.mdx | 1 - .../src/content/docs/zh-tw/permissions.mdx | 3 +- packages/web/src/content/docs/zh-tw/tools.mdx | 18 +-- 60 files changed, 37 insertions(+), 516 deletions(-) delete mode 100644 packages/opencode/src/tool/ls.ts delete mode 100644 packages/opencode/src/tool/ls.txt diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index 8ac09e4bb3..5cbf4ed1f9 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -1566,7 +1566,6 @@ export namespace ACP { case "context7_get_library_docs": return "search" - case "list": case "read": return "read" @@ -1587,8 +1586,6 @@ export namespace ACP { return input["path"] ? [{ path: input["path"] }] : [] case "bash": return [] - case "list": - return input["path"] ? [{ path: input["path"] }] : [] default: return [] } diff --git a/packages/opencode/src/cli/cmd/agent.ts b/packages/opencode/src/cli/cmd/agent.ts index 60f52e403b..b001389461 100644 --- a/packages/opencode/src/cli/cmd/agent.ts +++ b/packages/opencode/src/cli/cmd/agent.ts @@ -15,7 +15,7 @@ import type { Argv } from "yargs" type AgentMode = "all" | "primary" | "subagent" -const AVAILABLE_TOOLS = ["bash", "read", "write", "edit", "list", "glob", "grep", "webfetch", "task", "todowrite"] +const AVAILABLE_TOOLS = ["bash", "read", "write", "edit", "glob", "grep", "webfetch", "task", "todowrite"] const AgentCreateCommand = cmd({ command: "create", diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 17fc4bc087..2d3574c683 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -15,7 +15,6 @@ import { Permission } from "../../permission" import { Tool } from "../../tool/tool" import { GlobTool } from "../../tool/glob" import { GrepTool } from "../../tool/grep" -import { ListTool } from "../../tool/ls" import { ReadTool } from "../../tool/read" import { WebFetchTool } from "../../tool/webfetch" import { EditTool } from "../../tool/edit" @@ -103,14 +102,6 @@ function grep(info: ToolProps) { }) } -function list(info: ToolProps) { - const dir = info.input.path ? normalizePath(info.input.path) : "" - inline({ - icon: "→", - title: dir ? `List ${dir}` : "List", - }) -} - function read(info: ToolProps) { const file = normalizePath(info.input.filePath) const pairs = Object.entries(info.input).filter(([key, value]) => { @@ -420,7 +411,6 @@ export const RunCommand = cmd({ if (part.tool === "bash") return bash(props(part)) if (part.tool === "glob") return glob(props(part)) if (part.tool === "grep") return grep(props(part)) - if (part.tool === "list") return list(props(part)) if (part.tool === "read") return read(props(part)) if (part.tool === "write") return write(props(part)) if (part.tool === "webfetch") return webfetch(props(part)) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index c7790006f4..2b95cd5ae4 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -41,7 +41,6 @@ import { BashTool } from "@/tool/bash" import type { GlobTool } from "@/tool/glob" import { TodoWriteTool } from "@/tool/todo" import type { GrepTool } from "@/tool/grep" -import type { ListTool } from "@/tool/ls" import type { EditTool } from "@/tool/edit" import type { ApplyPatchTool } from "@/tool/apply_patch" import type { WebFetchTool } from "@/tool/webfetch" @@ -1555,9 +1554,6 @@ function ToolPart(props: { last: boolean; part: ToolPart; message: AssistantMess - - - @@ -1936,20 +1932,6 @@ function Grep(props: ToolProps) { ) } -function List(props: ToolProps) { - const dir = createMemo(() => { - if (props.input.path) { - return normalizePath(props.input.path) - } - return "" - }) - return ( - - List {dir()} - - ) -} - function WebFetch(props: ToolProps) { return ( diff --git a/packages/opencode/src/tool/ls.ts b/packages/opencode/src/tool/ls.ts deleted file mode 100644 index f3b044cbc1..0000000000 --- a/packages/opencode/src/tool/ls.ts +++ /dev/null @@ -1,122 +0,0 @@ -import * as path from "path" -import z from "zod" -import { Effect } from "effect" -import * as Stream from "effect/Stream" -import { InstanceState } from "@/effect/instance-state" -import { Ripgrep } from "../file/ripgrep" -import { assertExternalDirectoryEffect } from "./external-directory" -import DESCRIPTION from "./ls.txt" -import { Tool } from "./tool" - -export const IGNORE_PATTERNS = [ - "node_modules/", - "__pycache__/", - ".git/", - "dist/", - "build/", - "target/", - "vendor/", - "bin/", - "obj/", - ".idea/", - ".vscode/", - ".zig-cache/", - "zig-out", - ".coverage", - "coverage/", - "vendor/", - "tmp/", - "temp/", - ".cache/", - "cache/", - "logs/", - ".venv/", - "venv/", - "env/", -] - -const LIMIT = 100 - -export const ListTool = Tool.define( - "list", - Effect.gen(function* () { - const rg = yield* Ripgrep.Service - - return { - description: DESCRIPTION, - parameters: z.object({ - path: z - .string() - .describe("The absolute path to the directory to list (must be absolute, not relative)") - .optional(), - ignore: z.array(z.string()).describe("List of glob patterns to ignore").optional(), - }), - execute: (params: { path?: string; ignore?: string[] }, ctx: Tool.Context) => - Effect.gen(function* () { - const ins = yield* InstanceState.context - const search = path.resolve(ins.directory, params.path || ".") - yield* assertExternalDirectoryEffect(ctx, search, { kind: "directory" }) - - yield* ctx.ask({ - permission: "list", - patterns: [search], - always: ["*"], - metadata: { - path: search, - }, - }) - - const glob = IGNORE_PATTERNS.map((item) => `!${item}*`).concat(params.ignore?.map((item) => `!${item}`) || []) - const files = yield* rg.files({ cwd: search, glob, signal: ctx.abort }).pipe( - Stream.take(LIMIT + 1), - Stream.runCollect, - Effect.map((chunk) => [...chunk]), - ) - - const truncated = files.length > LIMIT - if (truncated) files.length = LIMIT - - const dirs = new Set() - const map = new Map() - for (const file of files) { - const dir = path.dirname(file) - const parts = dir === "." ? [] : dir.split("/") - for (let i = 0; i <= parts.length; i++) { - dirs.add(i === 0 ? "." : parts.slice(0, i).join("/")) - } - if (!map.has(dir)) map.set(dir, []) - map.get(dir)!.push(path.basename(file)) - } - - function render(dir: string, depth: number): string { - const indent = " ".repeat(depth) - let output = "" - if (depth > 0) output += `${indent}${path.basename(dir)}/\n` - - const child = " ".repeat(depth + 1) - const dirs2 = Array.from(dirs) - .filter((item) => path.dirname(item) === dir && item !== dir) - .sort() - for (const item of dirs2) { - output += render(item, depth + 1) - } - - const files = map.get(dir) || [] - for (const file of files.sort()) { - output += `${child}${file}\n` - } - return output - } - - return { - title: path.relative(ins.worktree, search), - metadata: { - count: files.length, - truncated, - }, - output: `${search}/\n` + render(".", 0), - } - }).pipe(Effect.orDie), - } - }), -) diff --git a/packages/opencode/src/tool/ls.txt b/packages/opencode/src/tool/ls.txt deleted file mode 100644 index 543720d46b..0000000000 --- a/packages/opencode/src/tool/ls.txt +++ /dev/null @@ -1 +0,0 @@ -Lists files and directories in a given path. The path parameter must be absolute; omit it to use the current workspace directory. You can optionally provide an array of glob patterns to ignore with the ignore parameter. You should generally prefer the Glob and Grep tools, if you know which directories to search. diff --git a/packages/web/src/content/docs/ar/modes.mdx b/packages/web/src/content/docs/ar/modes.mdx index ac57b98e96..ed17670a55 100644 --- a/packages/web/src/content/docs/ar/modes.mdx +++ b/packages/web/src/content/docs/ar/modes.mdx @@ -233,7 +233,6 @@ Provide constructive feedback without making direct changes. | `read` | قراءة محتويات الملفات | | `grep` | البحث في محتويات الملفات | | `glob` | العثور على الملفات حسب نمط | -| `list` | سرد محتويات الدليل | | `patch` | تطبيق تصحيحات على الملفات | | `todowrite` | إدارة قوائم المهام | | `webfetch` | جلب محتوى الويب | diff --git a/packages/web/src/content/docs/ar/permissions.mdx b/packages/web/src/content/docs/ar/permissions.mdx index 4391514b43..bb21d00b24 100644 --- a/packages/web/src/content/docs/ar/permissions.mdx +++ b/packages/web/src/content/docs/ar/permissions.mdx @@ -88,7 +88,7 @@ description: تحكّم في الإجراءات التي تتطلب موافقة ### الأدلة الخارجية -استخدم `external_directory` للسماح باستدعاءات الأدوات التي تلمس مسارات خارج دليل العمل الذي بدأ منه OpenCode. ينطبق ذلك على أي أداة تأخذ مسارًا كمدخل (مثل `read` و`edit` و`list` و`glob` و`grep` والعديد من أوامر `bash`). +استخدم `external_directory` للسماح باستدعاءات الأدوات التي تلمس مسارات خارج دليل العمل الذي بدأ منه OpenCode. ينطبق ذلك على أي أداة تأخذ مسارًا كمدخل (مثل `read` و`edit` و`glob` و`grep` والعديد من أوامر `bash`). توسيع المنزل (مثل `~/...`) يؤثر فقط على طريقة كتابة النمط. لا يجعل ذلك المسار الخارجي جزءًا من مساحة العمل الحالية، لذا يجب السماح بالمسارات خارج دليل العمل عبر `external_directory` أيضًا. @@ -133,7 +133,6 @@ description: تحكّم في الإجراءات التي تتطلب موافقة - `edit` — جميع تعديلات الملفات (يشمل `edit` و`write` و`patch` و`multiedit`) - `glob` — مطابقة أسماء الملفات (يطابق نمط الـ glob) - `grep` — البحث في المحتوى (يطابق نمط regex) -- `list` — سرد الملفات في دليل (يطابق مسار الدليل) - `bash` — تشغيل أوامر shell (يطابق الأوامر المُحلَّلة مثل `git status --porcelain`) - `task` — تشغيل وكلاء فرعيين (يطابق نوع الوكيل الفرعي) - `skill` — تحميل مهارة (يطابق اسم المهارة) diff --git a/packages/web/src/content/docs/ar/tools.mdx b/packages/web/src/content/docs/ar/tools.mdx index d820778b40..f1477a08c2 100644 --- a/packages/web/src/content/docs/ar/tools.mdx +++ b/packages/web/src/content/docs/ar/tools.mdx @@ -149,22 +149,6 @@ description: إدارة الأدوات التي يمكن لـ LLM استخدام ابحث عن الملفات باستخدام أنماط glob مثل `**/*.js` أو `src/**/*.ts`. يعيد مسارات الملفات المطابقة مرتبة حسب وقت التعديل. ---- - -### list - -اعرض قائمة بالملفات والمجلدات في مسار محدد. - -```json title="opencode.json" {4} -{ - "$schema": "https://opencode.ai/config.json", - "permission": { - "list": "allow" - } -} -``` - -تعرض هذه الأداة محتويات المجلد. وتقبل أنماط glob لتصفية النتائج. --- @@ -341,7 +325,7 @@ OPENCODE_ENABLE_EXA=1 opencode ## التفاصيل الداخلية -داخليا، تستخدم أدوات مثل `grep` و`glob` و`list` أداة [ripgrep](https://github.com/BurntSushi/ripgrep) في الخلفية. افتراضيا، يحترم ripgrep أنماط `.gitignore`، ما يعني أن الملفات والمجلدات المدرجة في `.gitignore` ستُستبعد من عمليات البحث وعرض القوائم. +داخليا، تستخدم أدوات مثل `grep` و`glob` [ripgrep](https://github.com/BurntSushi/ripgrep) في الخلفية. افتراضيا، يحترم ripgrep أنماط `.gitignore`، ما يعني أن الملفات والمجلدات المدرجة في `.gitignore` ستُستبعد من عمليات البحث وعرض القوائم. --- diff --git a/packages/web/src/content/docs/bs/modes.mdx b/packages/web/src/content/docs/bs/modes.mdx index 6bf4bd27ca..d5f92a9f67 100644 --- a/packages/web/src/content/docs/bs/modes.mdx +++ b/packages/web/src/content/docs/bs/modes.mdx @@ -219,7 +219,6 @@ Ovdje su svi alati koji se mogu kontrolirati kroz konfiguraciju načina rada. | `read` | Pročitajte sadržaj datoteke | | `grep` | Pretraži sadržaj datoteke | | `glob` | Pronađite datoteke po uzorku | -| `list` | Lista sadržaja direktorija | | `patch` | Primijenite zakrpe na datoteke | | `todowrite` | Upravljanje listama zadataka | | `webfetch` | Dohvati web sadržaj | diff --git a/packages/web/src/content/docs/bs/permissions.mdx b/packages/web/src/content/docs/bs/permissions.mdx index b6a194ad28..e27fa130b3 100644 --- a/packages/web/src/content/docs/bs/permissions.mdx +++ b/packages/web/src/content/docs/bs/permissions.mdx @@ -87,7 +87,7 @@ Možete koristiti `~` ili `$HOME` na početku obrasca da referencirate svoj poč ### Vanjski direktoriji -Koristite `external_directory` da dozvolite pozive alata koji dodiruju putanje izvan radnog direktorija gdje je OpenCode pokrenut. Ovo se odnosi na bilo koji alat koji uzima putanju kao ulaz (na primjer `read`, `edit`, `list`, `glob`, `grep` i mnoge `bash` komande). +Koristite `external_directory` da dozvolite pozive alata koji dodiruju putanje izvan radnog direktorija gdje je OpenCode pokrenut. Ovo se odnosi na bilo koji alat koji uzima putanju kao ulaz (na primjer `read`, `edit`, `glob`, `grep` i mnoge `bash` komande). Proširenje kuće (poput `~/...`) utiče samo na način na koji je obrazac napisan. Ne čini vanjsku stazu dijelom trenutnog radnog prostora, tako da staze izvan radnog direktorija i dalje moraju biti dozvoljene preko `external_directory`. Na primjer, ovo omogućava pristup svemu pod `~/projects/personal/`: @@ -128,7 +128,6 @@ Dozvole OpenCode su označene imenom alata, plus nekoliko sigurnosnih mjera: - `edit` — sve izmjene fajlova (pokriva `edit`, `write`, `patch`, `multiedit`) - `glob` — globbiranje fajla (odgovara glob uzorku) - `grep` — pretraga sadržaja (podudara se sa regularnim izrazom) -- `list` — lista fajlova u direktorijumu (podudara se sa putanjom direktorijuma) - `bash` — izvođenje komandi ljuske (podudara se s raščlanjenim komandama kao što je `git status --porcelain`) - `task` — pokretanje subagenta (odgovara tipu podagenta) - `skill` — učitavanje vještine (odgovara nazivu vještine) diff --git a/packages/web/src/content/docs/bs/tools.mdx b/packages/web/src/content/docs/bs/tools.mdx index d0ae9a4460..6c4d546141 100644 --- a/packages/web/src/content/docs/bs/tools.mdx +++ b/packages/web/src/content/docs/bs/tools.mdx @@ -149,22 +149,6 @@ Pronalazi datoteke po obrascima. Trazi datoteke koristeci glob obrasce kao `**/*.js` ili `src/**/*.ts`. Vraca putanje sortirane po vremenu izmjene. ---- - -### list - -Ispisuje datoteke i direktorije na zadanoj putanji. - -```json title="opencode.json" {4} -{ - "$schema": "https://opencode.ai/config.json", - "permission": { - "list": "allow" - } -} -``` - -Ovaj alat ispisuje sadrzaj direktorija. Prihvata glob obrasce za filtriranje rezultata. --- @@ -341,7 +325,7 @@ MCP (Model Context Protocol) serveri omogucavaju integraciju eksternih alata i s ## Interno -Interno, alati kao `grep`, `glob` i `list` koriste [ripgrep](https://github.com/BurntSushi/ripgrep). Po defaultu, ripgrep postuje `.gitignore` obrasce, pa se fajlovi i direktoriji iz `.gitignore` izostavljaju iz pretraga i listinga. +Interno, alati kao `grep` i `glob` koriste [ripgrep](https://github.com/BurntSushi/ripgrep). Po defaultu, ripgrep postuje `.gitignore` obrasce, pa se fajlovi i direktoriji iz `.gitignore` izostavljaju iz pretraga i listinga. --- diff --git a/packages/web/src/content/docs/da/modes.mdx b/packages/web/src/content/docs/da/modes.mdx index 34fb2b3595..a0fb87a862 100644 --- a/packages/web/src/content/docs/da/modes.mdx +++ b/packages/web/src/content/docs/da/modes.mdx @@ -233,7 +233,6 @@ Her er alle de værktøjer, der kan styres gennem tilstandskonfigurationen. | `read` | Læs filindhold | | `grep` | Søg filindhold | | `glob` | Find filer efter mønster | -| `list` | Liste biblioteksindhold | | `patch` | Anvend patches til filer | | `todowrite` | Administrer todo-lister | | `webfetch` | Hent webindhold | diff --git a/packages/web/src/content/docs/da/permissions.mdx b/packages/web/src/content/docs/da/permissions.mdx index 72ebff606c..176dd568e1 100644 --- a/packages/web/src/content/docs/da/permissions.mdx +++ b/packages/web/src/content/docs/da/permissions.mdx @@ -88,7 +88,7 @@ Du kan bruge `~` eller `$HOME` i starten af ​​et mønster til at referere ti ### Eksterne mapper -Brug `external_directory` til at tillade værktøjsopkald, der berører stier uden for den arbejdsmappe, hvor OpenCode blev startet. Dette gælder for ethvert værktøj, der tager en sti som input (for eksempel `read`, `edit`, `list`, `glob`, `grep` og mange `bash` kommandoer). +Brug `external_directory` til at tillade værktøjsopkald, der berører stier uden for den arbejdsmappe, hvor OpenCode blev startet. Dette gælder for ethvert værktøj, der tager en sti som input (for eksempel `read`, `edit`, `glob`, `grep` og mange `bash` kommandoer). Hjemmeudvidelse (som `~/...`) påvirker kun, hvordan et mønster skrives. Det gør ikke en ekstern sti til en del af det aktuelle arbejdsområde, så stier uden for arbejdsbiblioteket skal stadig være tilladt via `external_directory`. @@ -133,7 +133,6 @@ OpenCode tilladelser indtastes efter værktøjsnavn plus et par sikkerhedsafskæ - `edit` — alle filændringer (dækker `edit`, `write`, `patch`, `multiedit`) - `glob` — fil-globing (matcher glob-mønsteret) - `grep` — indholdssøgning (matcher regex-mønsteret) -- `list` — viser filer i en mappe (matcher mappestien) - `bash` — kører shell-kommandoer (matcher parsede kommandoer som `git status --porcelain`) - `task` — lancering af underagenter (matcher underagenttypen) - `skill` — indlæsning af en færdighed (matcher færdighedsnavnet) diff --git a/packages/web/src/content/docs/da/tools.mdx b/packages/web/src/content/docs/da/tools.mdx index a610e8cc39..043aabed43 100644 --- a/packages/web/src/content/docs/da/tools.mdx +++ b/packages/web/src/content/docs/da/tools.mdx @@ -149,22 +149,6 @@ Finn filer etter mønstermatching. Søk etter filer ved at bruge glob-mønstre som `**/*.js` eller `src/**/*.ts`. Returnerer samsvarende filbaner sortert etter endringstid. ---- - -### list - -List filer og kataloger i en gitt bane. - -```json title="opencode.json" {4} -{ - "$schema": "https://opencode.ai/config.json", - "permission": { - "list": "allow" - } -} -``` - -Dette verktøyet viser kataloginnhold. Den aksepterer glob-mønstre for at filtrere resultater. --- @@ -341,7 +325,7 @@ MCP (Model Context Protocol) servere lar deg integrere eksterne verktøy og tjen ## Interne -Internt bruger verktøy som `grep`, `glob` og `list` [ripgrep](https://github.com/BurntSushi/ripgrep) under panseret. Som standard respekterer ripgrep `.gitignore`-mønstre, noe som betyr at filer og kataloger som er oppført i `.gitignore` vil bli ekskludert fra søk og lister. +Internt bruger verktøy som `grep` og `glob` [ripgrep](https://github.com/BurntSushi/ripgrep) under panseret. Som standard respekterer ripgrep `.gitignore`-mønstre, noe som betyr at filer og kataloger som er oppført i `.gitignore` vil bli ekskludert fra søk og lister. --- diff --git a/packages/web/src/content/docs/de/modes.mdx b/packages/web/src/content/docs/de/modes.mdx index 38a2e34b38..11a010d6b9 100644 --- a/packages/web/src/content/docs/de/modes.mdx +++ b/packages/web/src/content/docs/de/modes.mdx @@ -233,7 +233,6 @@ Hier sind alle Tools aufgeführt, die über den Konfigurationsmodus gesteuert we | `read` | Dateiinhalt lesen | | `grep` | Dateiinhalte durchsuchen | | `glob` | Dateien nach Muster suchen | -| `list` | Verzeichnisinhalte auflisten | | `patch` | Patches auf Dateien anwenden | | `todowrite` | Aufgabenlisten verwalten | | `webfetch` | Webinhalte abrufen | diff --git a/packages/web/src/content/docs/de/permissions.mdx b/packages/web/src/content/docs/de/permissions.mdx index ba7c802040..6b647ca366 100644 --- a/packages/web/src/content/docs/de/permissions.mdx +++ b/packages/web/src/content/docs/de/permissions.mdx @@ -88,7 +88,7 @@ Sie können `~` oder `$HOME` am Anfang eines Musters verwenden, um auf Ihr Home- ### Externe Verzeichnisse -Verwenden Sie `external_directory`, um Toolaufrufe zuzulassen, die Pfade außerhalb des Arbeitsverzeichnisses berühren, in dem OpenCode gestartet wurde. Dies gilt für jedes Werkzeug, das einen Pfad als Eingabe verwendet (z. B. `read`, `edit`, `list`, `glob`, `grep` und viele `bash`-Befehle). +Verwenden Sie `external_directory`, um Toolaufrufe zuzulassen, die Pfade außerhalb des Arbeitsverzeichnisses berühren, in dem OpenCode gestartet wurde. Dies gilt für jedes Werkzeug, das einen Pfad als Eingabe verwendet (z. B. `read`, `edit`, `glob`, `grep` und viele `bash`-Befehle). Die Home-Erweiterung (wie `~/...`) wirkt sich nur darauf aus, wie ein Muster geschrieben wird. Dadurch wird ein externer Pfad nicht zum Teil des aktuellen Arbeitsbereichs, daher müssen Pfade außerhalb des Arbeitsverzeichnisses weiterhin über `external_directory` zulässig sein. @@ -133,7 +133,6 @@ OpenCode-Berechtigungen basieren auf Tool-Namen sowie einigen Sicherheitsvorkehr - `edit` – alle Dateiänderungen (umfasst `edit`, `write`, `patch`, `multiedit`) - `glob` – Datei-Globbing (entspricht dem Glob-Muster) - `grep` – Inhaltssuche (entspricht dem Regex-Muster) -- `list` – Auflistung der Dateien in einem Verzeichnis (entspricht dem Verzeichnispfad) - `bash` – Ausführen von Shell-Befehlen (entspricht analysierten Befehlen wie `git status --porcelain`) - `task` – Subagenten starten (entspricht dem Subagententyp) - `skill` – Laden einer Fertigkeit (entspricht dem Fertigkeitsnamen) diff --git a/packages/web/src/content/docs/de/tools.mdx b/packages/web/src/content/docs/de/tools.mdx index b33163df85..98f5c708c2 100644 --- a/packages/web/src/content/docs/de/tools.mdx +++ b/packages/web/src/content/docs/de/tools.mdx @@ -156,22 +156,6 @@ Findet Dateien per Musterabgleich. Sucht nach Dateien mit Glob-Mustern wie `**/*.js` oder `src/**/*.ts`. Gibt passende Dateipfade sortiert nach Aenderungsdatum zurueck. ---- - -### list - -Listet Dateien und Verzeichnisse in einem Pfad auf. - -```json title="opencode.json" {4} -{ - "$schema": "https://opencode.ai/config.json", - "permission": { - "list": "allow" - } -} -``` - -Dieses Tool listet Verzeichnisinhalte auf. Es akzeptiert Glob-Muster zum Filtern der Ergebnisse. --- @@ -350,7 +334,7 @@ Dazu gehoeren Datenbanken, API-Integrationen und Drittanbieter-Services. ## Interna -Intern verwenden Tools wie `grep`, `glob` und `list` [ripgrep](https://github.com/BurntSushi/ripgrep). +Intern verwenden Tools wie `grep` und `glob` [ripgrep](https://github.com/BurntSushi/ripgrep). Standardmaessig beachtet ripgrep `.gitignore`, daher werden dort aufgefuehrte Dateien und Ordner nicht durchsucht. --- diff --git a/packages/web/src/content/docs/es/modes.mdx b/packages/web/src/content/docs/es/modes.mdx index cefc4a4e2d..dca900dff0 100644 --- a/packages/web/src/content/docs/es/modes.mdx +++ b/packages/web/src/content/docs/es/modes.mdx @@ -233,7 +233,6 @@ Aquí están todas las herramientas que se pueden controlar a través del modo d | `read` | Leer el contenido del archivo | | `grep` | Buscar contenido del archivo | | `glob` | Buscar archivos por patrón | -| `list` | Listar el contenido del directorio | | `patch` | Aplicar parches a archivos | | `todowrite` | Administrar listas de tareas pendientes | | `webfetch` | Obtener contenido web | diff --git a/packages/web/src/content/docs/es/permissions.mdx b/packages/web/src/content/docs/es/permissions.mdx index 603b3bdb3f..6923368e40 100644 --- a/packages/web/src/content/docs/es/permissions.mdx +++ b/packages/web/src/content/docs/es/permissions.mdx @@ -88,7 +88,7 @@ Puede usar `~` o `$HOME` al comienzo de un patrón para hacer referencia a su di ### Directorios externos -Utilice `external_directory` para permitir llamadas a herramientas que toquen rutas fuera del directorio de trabajo donde se inició OpenCode. Esto se aplica a cualquier herramienta que tome una ruta como entrada (por ejemplo, `read`, `edit`, `list`, `glob`, `grep` y muchos comandos `bash`). +Utilice `external_directory` para permitir llamadas a herramientas que toquen rutas fuera del directorio de trabajo donde se inició OpenCode. Esto se aplica a cualquier herramienta que tome una ruta como entrada (por ejemplo, `read`, `edit`, `glob`, `grep` y muchos comandos `bash`). La expansión del hogar (como `~/...`) solo afecta la forma en que se escribe un patrón. No hace que una ruta externa forme parte del espacio de trabajo actual, por lo que las rutas fuera del directorio de trabajo aún deben permitirse a través de `external_directory`. @@ -133,7 +133,6 @@ Los permisos OpenCode están codificados por el nombre de la herramienta, ademá - `edit` — todas las modificaciones de archivos (cubre `edit`, `write`, `patch`, `multiedit`) - `glob` — globalización de archivos (coincide con el patrón global) - `grep` — búsqueda de contenido (coincide con el patrón de expresiones regulares) -- `list` — enumerar archivos en un directorio (coincide con la ruta del directorio) - `bash`: ejecuta comandos de shell (coincide con comandos analizados como `git status --porcelain`) - `task` — lanzamiento de subagentes (coincide con el tipo de subagente) - `skill` — cargar una habilidad (coincide con el nombre de la habilidad) diff --git a/packages/web/src/content/docs/es/tools.mdx b/packages/web/src/content/docs/es/tools.mdx index f3a050c03b..7d594a1c9f 100644 --- a/packages/web/src/content/docs/es/tools.mdx +++ b/packages/web/src/content/docs/es/tools.mdx @@ -149,22 +149,6 @@ Encuentre archivos por coincidencia de patrones. Busque archivos usando patrones globales como `**/*.js` o `src/**/*.ts`. Devuelve rutas de archivos coincidentes ordenadas por hora de modificación. ---- - -### list - -Enumere archivos y directorios en una ruta determinada. - -```json title="opencode.json" {4} -{ - "$schema": "https://opencode.ai/config.json", - "permission": { - "list": "allow" - } -} -``` - -Esta herramienta enumera el contenido del directorio. Acepta patrones globales para filtrar resultados. --- @@ -341,7 +325,7 @@ Los servidores MCP (Model Context Protocol) le permiten integrar herramientas y ## Internos -Internamente, herramientas como `grep`, `glob` y `list` usan [ripgrep](https://github.com/BurntSushi/ripgrep) bajo el capó. De forma predeterminada, ripgrep respeta los patrones `.gitignore`, lo que significa que los archivos y directorios enumerados en su `.gitignore` se excluirán de las búsquedas y listados. +Internamente, herramientas como `grep` y `glob` usan [ripgrep](https://github.com/BurntSushi/ripgrep) bajo el capó. De forma predeterminada, ripgrep respeta los patrones `.gitignore`, lo que significa que los archivos y directorios enumerados en su `.gitignore` se excluirán de las búsquedas y listados. --- diff --git a/packages/web/src/content/docs/fr/modes.mdx b/packages/web/src/content/docs/fr/modes.mdx index 8c3ad62e41..6985dbd57d 100644 --- a/packages/web/src/content/docs/fr/modes.mdx +++ b/packages/web/src/content/docs/fr/modes.mdx @@ -231,7 +231,6 @@ Voici tous les outils pouvant être contrôlés via le mode config. | `read` | Lire le contenu du fichier | | `grep` | Rechercher le contenu du fichier | | `glob` | Rechercher des fichiers par modèle | -| `list` | Liste du contenu du répertoire | | `patch` | Appliquer des correctifs aux fichiers | | `todowrite` | Gérer les listes de tâches | | `webfetch` | Récupérer du contenu Web | diff --git a/packages/web/src/content/docs/fr/permissions.mdx b/packages/web/src/content/docs/fr/permissions.mdx index 176fa34ad2..b1c1d6800f 100644 --- a/packages/web/src/content/docs/fr/permissions.mdx +++ b/packages/web/src/content/docs/fr/permissions.mdx @@ -88,7 +88,7 @@ Vous pouvez utiliser `~` ou `$HOME` au début d'un modèle pour référencer vot ### Répertoires externes -Utilisez `external_directory` pour autoriser les appels d'outils qui touchent des chemins en dehors du répertoire de travail où OpenCode a été démarré. Cela s'applique à tout outil qui prend un chemin en entrée (par exemple `read`, `edit`, `list`, `glob`, `grep` et de nombreuses commandes `bash`). +Utilisez `external_directory` pour autoriser les appels d'outils qui touchent des chemins en dehors du répertoire de travail où OpenCode a été démarré. Cela s'applique à tout outil qui prend un chemin en entrée (par exemple `read`, `edit`, `glob`, `grep` et de nombreuses commandes `bash`). L'expansion du répertoire personnel (comme `~/...`) n'affecte que la façon dont un modèle est écrit. Cela n'intègre pas un chemin externe à l'espace de travail actuel, donc les chemins en dehors du répertoire de travail doivent toujours être autorisés via `external_directory`. @@ -133,7 +133,6 @@ Les autorisations OpenCode sont classées par nom d'outil, plus quelques garde-f - `edit` — toutes les modifications de fichiers (couvre `edit`, `write`, `patch`, `multiedit`) - `glob` — globalisation de fichiers (correspond au modèle global) - `grep` — recherche de contenu (correspond au modèle regex) -- `list` — listant les fichiers dans un répertoire (correspond au chemin du répertoire) - `bash` - exécution de commandes shell (correspond aux commandes analysées comme `git status --porcelain`) - `task` — lancement de sous-agents (correspond au type de sous-agent) - `skill` — chargement d'une compétence (correspond au nom de la compétence) diff --git a/packages/web/src/content/docs/fr/tools.mdx b/packages/web/src/content/docs/fr/tools.mdx index 62579c2bf8..483a953443 100644 --- a/packages/web/src/content/docs/fr/tools.mdx +++ b/packages/web/src/content/docs/fr/tools.mdx @@ -149,22 +149,6 @@ Recherchez des fichiers par correspondance de modèles. Recherchez des fichiers à l'aide de modèles globaux tels que `**/*.js` ou `src/**/*.ts`. Renvoie les chemins de fichiers correspondants triés par heure de modification. ---- - -### liste - -Répertoriez les fichiers et les répertoires dans un chemin donné. - -```json title="opencode.json" {4} -{ - "$schema": "https://opencode.ai/config.json", - "permission": { - "list": "allow" - } -} -``` - -Cet outil répertorie le contenu du répertoire. Il accepte les modèles globaux pour filtrer les résultats. --- @@ -341,7 +325,7 @@ Les serveurs MCP (Model Context Protocol) vous permettent d'intégrer des outils ## Internes -En interne, des outils comme `grep`, `glob` et `list` utilisent [ripgrep](https://github.com/BurntSushi/ripgrep) sous le capot. Par défaut, ripgrep respecte les modèles `.gitignore`, ce qui signifie que les fichiers et répertoires répertoriés dans votre `.gitignore` seront exclus des recherches et des listes. +En interne, des outils comme `grep` et `glob` utilisent [ripgrep](https://github.com/BurntSushi/ripgrep) sous le capot. Par défaut, ripgrep respecte les modèles `.gitignore`, ce qui signifie que les fichiers et répertoires répertoriés dans votre `.gitignore` seront exclus des recherches et des listes. --- diff --git a/packages/web/src/content/docs/it/modes.mdx b/packages/web/src/content/docs/it/modes.mdx index 8f5c22d6e4..b72b388fb0 100644 --- a/packages/web/src/content/docs/it/modes.mdx +++ b/packages/web/src/content/docs/it/modes.mdx @@ -232,7 +232,6 @@ Ecco tutti gli strumenti che possono essere controllati tramite la configurazion | `read` | Legge contenuti dei file | | `grep` | Cerca nei contenuti dei file | | `glob` | Trova file per pattern | -| `list` | Elenca contenuti di una directory | | `patch` | Applica patch ai file | | `todowrite` | Gestisce liste todo | | `webfetch` | Recupera contenuti web | diff --git a/packages/web/src/content/docs/it/permissions.mdx b/packages/web/src/content/docs/it/permissions.mdx index 3f255c89dd..49f0e8e4d3 100644 --- a/packages/web/src/content/docs/it/permissions.mdx +++ b/packages/web/src/content/docs/it/permissions.mdx @@ -88,7 +88,7 @@ Puoi usare `~` o `$HOME` all'inizio di un pattern per riferirti alla tua home di ### Directory esterne -Usa `external_directory` per consentire chiamate a strumenti che toccano percorsi al di fuori della directory di lavoro da cui e' stato avviato OpenCode. Si applica a qualsiasi strumento che accetta un path come input (ad esempio `read`, `edit`, `list`, `glob`, `grep` e molti comandi `bash`). +Usa `external_directory` per consentire chiamate a strumenti che toccano percorsi al di fuori della directory di lavoro da cui e' stato avviato OpenCode. Si applica a qualsiasi strumento che accetta un path come input (ad esempio `read`, `edit`, `glob`, `grep` e molti comandi `bash`). L'espansione della home (come `~/...`) influisce solo su come viene scritto un pattern. Non rende un percorso esterno parte della workspace corrente, quindi i path fuori dalla directory di lavoro devono comunque essere consentiti tramite `external_directory`. @@ -133,7 +133,6 @@ I permessi di OpenCode sono indicizzati per nome dello strumento, piu' un paio d - `edit` — tutte le modifiche ai file (include `edit`, `write`, `patch`, `multiedit`) - `glob` — ricerca file tramite glob (corrisponde al pattern glob) - `grep` — ricerca nel contenuto (corrisponde al pattern regex) -- `list` — elenco file in una directory (corrisponde al path della directory) - `bash` — esecuzione comandi di shell (corrisponde a comandi parsati come `git status --porcelain`) - `task` — avvio subagenti (corrisponde al tipo di subagente) - `skill` — caricamento di una skill (corrisponde al nome della skill) diff --git a/packages/web/src/content/docs/it/tools.mdx b/packages/web/src/content/docs/it/tools.mdx index 50609fd616..0bf00ffc6f 100644 --- a/packages/web/src/content/docs/it/tools.mdx +++ b/packages/web/src/content/docs/it/tools.mdx @@ -149,22 +149,6 @@ Trova file tramite pattern matching. Cerca file usando pattern glob come `**/*.js` o `src/**/*.ts`. Restituisce i percorsi corrispondenti ordinati per data di modifica. ---- - -### list - -Elenca file e directory in un percorso specifico. - -```json title="opencode.json" {4} -{ - "$schema": "https://opencode.ai/config.json", - "permission": { - "list": "allow" - } -} -``` - -Questo strumento elenca il contenuto di una directory. Accetta pattern glob per filtrare i risultati. --- @@ -341,7 +325,7 @@ I server MCP (Model Context Protocol) permettono di integrare strumenti e serviz ## Interni -Internamente, strumenti come `grep`, `glob` e `list` usano [ripgrep](https://github.com/BurntSushi/ripgrep) sotto al cofano. Di default, ripgrep rispetta i pattern di `.gitignore`, quindi i file e le directory elencati in `.gitignore` vengono esclusi da ricerche ed elenchi. +Internamente, strumenti come `grep` e `glob` usano [ripgrep](https://github.com/BurntSushi/ripgrep) sotto al cofano. Di default, ripgrep rispetta i pattern di `.gitignore`, quindi i file e le directory elencati in `.gitignore` vengono esclusi da ricerche ed elenchi. --- diff --git a/packages/web/src/content/docs/ja/modes.mdx b/packages/web/src/content/docs/ja/modes.mdx index c9f2a4d5ee..623c19552d 100644 --- a/packages/web/src/content/docs/ja/modes.mdx +++ b/packages/web/src/content/docs/ja/modes.mdx @@ -231,7 +231,6 @@ Markdown ファイル名はモード名になります (例: `review.md` は `re | `read` | ファイルの内容を読み取る | | `grep` | ファイルの内容を検索 | | `glob` | パターンでファイルを検索 | -| `list` | ディレクトリの内容をリストする | | `patch` | ファイルにパッチを適用する | | `todowrite` | ToDo リストを管理する | | `webfetch` | Web コンテンツを取得する | diff --git a/packages/web/src/content/docs/ja/permissions.mdx b/packages/web/src/content/docs/ja/permissions.mdx index 5f5df6675c..f2b0978259 100644 --- a/packages/web/src/content/docs/ja/permissions.mdx +++ b/packages/web/src/content/docs/ja/permissions.mdx @@ -88,7 +88,7 @@ OpenCode は `permission` 設定を使用して、特定のアクションを自 ### 外部ディレクトリ -`external_directory` を使用して、OpenCode が開始された作業ディレクトリの外部のパスに触れるツール呼び出しを許可します。これは、パスを入力として受け取るすべてのツール (`read`、`edit`、`list`、`glob`、`grep`、および多くの `bash` コマンドなど) に適用されます。 +`external_directory` を使用して、OpenCode が開始された作業ディレクトリの外部のパスに触れるツール呼び出しを許可します。これは、パスを入力として受け取るすべてのツール (`read`、`edit`、`glob`、`grep`、および多くの `bash` コマンドなど) に適用されます。 ホーム展開 (`~/...` など) は、パターンの記述方法にのみ影響します。外部パスは現在のワークスペースの一部にはならないため、作業ディレクトリの外部のパスも `external_directory` 経由で許可する必要があります。 @@ -133,7 +133,6 @@ OpenCode の権限は、ツール名に加えて、いくつかの安全対策 - `edit` — すべてのファイル変更 (`edit`、`write`、`patch`、`multiedit` をカバー) - `glob` — ファイルのグロビング (グロブパターンと一致) - `grep` — コンテンツ検索 (正規表現パターンと一致) -- `list` — ディレクトリ内のファイルのリスト (ディレクトリパスと一致) - `bash` — シェルコマンドの実行 (`git status --porcelain` などの解析されたコマンドと一致します) - `task` — サブエージェントの起動 (サブエージェントのタイプと一致) - `skill` — スキルをロードしています(スキル名と一致します) diff --git a/packages/web/src/content/docs/ja/tools.mdx b/packages/web/src/content/docs/ja/tools.mdx index 0e0f8fe951..ae409aa7db 100644 --- a/packages/web/src/content/docs/ja/tools.mdx +++ b/packages/web/src/content/docs/ja/tools.mdx @@ -149,22 +149,6 @@ OpenCode で利用可能なすべての組み込みツールを次に示しま `**/*.js` や `src/**/*.ts` などの glob パターンを使用してファイルを検索します。一致するファイルパスを変更時間順に並べて返します。 ---- - -### list - -指定されたパス内のファイルとディレクトリを一覧表示します。 - -```json title="opencode.json" {4} -{ - "$schema": "https://opencode.ai/config.json", - "permission": { - "list": "allow" - } -} -``` - -このツールはディレクトリの内容を一覧表示します。結果をフィルタリングするための glob パターンを受け入れます。 --- @@ -341,7 +325,7 @@ MCP (Model Context Protocol) サーバーを使用すると、外部ツールと ## 内部動作 -内部的には、`grep`、`glob`、`list` などのツールは内部で [ripgrep](https://github.com/BurntSushi/ripgrep) を使用します。デフォルトでは、ripgrep は `.gitignore` パターンを尊重します。つまり、`.gitignore` にリストされているファイルとディレクトリは検索とリストから除外されます。 +内部的には、`grep`、`glob` などのツールは内部で [ripgrep](https://github.com/BurntSushi/ripgrep) を使用します。デフォルトでは、ripgrep は `.gitignore` パターンを尊重します。つまり、`.gitignore` にリストされているファイルとディレクトリは検索とリストから除外されます。 --- diff --git a/packages/web/src/content/docs/ko/modes.mdx b/packages/web/src/content/docs/ko/modes.mdx index 35bc4d2264..32746d1d05 100644 --- a/packages/web/src/content/docs/ko/modes.mdx +++ b/packages/web/src/content/docs/ko/modes.mdx @@ -232,7 +232,6 @@ Markdown 파일 이름은 모드 이름 (예 : `review.md`는 `review` 모드를 | `read` | 읽는 파일 내용 | | `grep` | 파일 검색 | | `glob` | 패턴으로 찾기 | -| `list` | 디렉토리 내용 보기 | | `patch` | 파일에 패치 적용 | | `todowrite` | 할 일(Todo) 목록 관리 | | `webfetch` | 웹사이트 가져오기 | diff --git a/packages/web/src/content/docs/ko/permissions.mdx b/packages/web/src/content/docs/ko/permissions.mdx index ec129f45c0..0742089d6b 100644 --- a/packages/web/src/content/docs/ko/permissions.mdx +++ b/packages/web/src/content/docs/ko/permissions.mdx @@ -88,7 +88,7 @@ Permission 본 사용 간단한 wildcard 일치: ## 외부 디렉터리 -`external_directory`를 사용하여 도구가 opencode가 시작된 작업 디렉토리 밖에 터치 경로가 호출되도록합니다. 이것은 입력 (예 : `read`, `edit`, `list`, `glob`, `glob`, `grep` 및 많은 `bash` 명령)로 경로를 수행하는 모든 도구에 적용됩니다. +`external_directory`를 사용하여 도구가 opencode가 시작된 작업 디렉토리 밖에 터치 경로가 호출되도록합니다. 이것은 입력 (예 : `read`, `edit`, `glob`, `grep` 및 많은 `bash` 명령)로 경로를 수행하는 모든 도구에 적용됩니다. 홈 확장 (`~/...`와 같은) 패턴이 작성된 방법에 영향을 미칩니다. 그것은 현재의 작업 공간의 외부 경로 부분을 만들지 않습니다, 그래서 작업 디렉토리 외부 경로는 여전히 `external_directory`를 통해 허용해야합니다. @@ -133,7 +133,6 @@ opencode 권한은 도구 이름에 의해 키 입력되며, 두 개의 안전 - `edit` - 모든 파일 수정 (covers `edit`, `write`, `patch`, `multiedit`) - `glob` - 파일 globbing (glob 패턴 매칭) - `grep` - 콘텐츠 검색 ( regex 패턴 매칭) -- `list` - 디렉토리의 목록 파일 (폴더 경로 매칭) - `bash` - shell 명령 실행 (`git status --porcelain`와 같은 팟 명령) - `task` - 에이전트 실행 (작업 에이전트 유형) - `skill` - 기술을 로딩 (기술 이름을 매칭) diff --git a/packages/web/src/content/docs/ko/tools.mdx b/packages/web/src/content/docs/ko/tools.mdx index 33976b66ff..b98578b58e 100644 --- a/packages/web/src/content/docs/ko/tools.mdx +++ b/packages/web/src/content/docs/ko/tools.mdx @@ -149,22 +149,6 @@ Codebase에서 빠른 콘텐츠 검색. 전체 regex 문법 및 파일 패턴 `**/*.js` 또는 `src/**/*.ts`와 같은 glob 패턴을 사용하여 파일 검색. 수정 시간에 의해 정렬 된 파일 경로 반환. ---- - -### list - -주어진 경로의 파일 및 디렉토리 목록. - -```json title="opencode.json" {4} -{ - "$schema": "https://opencode.ai/config.json", - "permission": { - "list": "allow" - } -} -``` - -이 도구는 디렉토리 내용을 나열합니다. 그것은 glob 패턴을 필터 결과에 받아들입니다. --- @@ -341,7 +325,7 @@ MCP 서버 구성에 대한 [Learn more](/docs/mcp-servers). ## 내부 -내부, 도구 `grep`, `glob`, 그리고 `list` 사용 [ripgrep](https://github.com/BurntSushi/ripgrep) 후드 아래에. 기본적으로 ripgrep은 `.gitignore` 패턴을 존중하며 `.gitignore`에 나열된 파일 및 디렉토리를 검색 및 목록에서 제외됩니다. +내부, 도구 `grep` 그리고 `glob` 사용 [ripgrep](https://github.com/BurntSushi/ripgrep) 후드 아래에. 기본적으로 ripgrep은 `.gitignore` 패턴을 존중하며 `.gitignore`에 나열된 파일 및 디렉토리를 검색 및 목록에서 제외됩니다. --- diff --git a/packages/web/src/content/docs/modes.mdx b/packages/web/src/content/docs/modes.mdx index 5f23df2540..8ce2c0d13e 100644 --- a/packages/web/src/content/docs/modes.mdx +++ b/packages/web/src/content/docs/modes.mdx @@ -233,7 +233,6 @@ Here are all the tools can be controlled through the mode config. | `read` | Read file contents | | `grep` | Search file contents | | `glob` | Find files by pattern | -| `list` | List directory contents | | `patch` | Apply patches to files | | `todowrite` | Manage todo lists | | `webfetch` | Fetch web content | diff --git a/packages/web/src/content/docs/nb/modes.mdx b/packages/web/src/content/docs/nb/modes.mdx index bf73ff040f..e99c511be6 100644 --- a/packages/web/src/content/docs/nb/modes.mdx +++ b/packages/web/src/content/docs/nb/modes.mdx @@ -232,7 +232,6 @@ Her er alle verktøyene som kan kontrolleres gjennom moduskonfigurasjonen. | `read` | Les filinnhold | | `grep` | Søk i filinnhold | | `glob` | Finn filer etter mønster | -| `list` | List opp kataloginnhold | | `patch` | Bruk patcher på filer | | `todowrite` | Administrer gjøremålslister | | `webfetch` | Hent webinnhold | diff --git a/packages/web/src/content/docs/nb/permissions.mdx b/packages/web/src/content/docs/nb/permissions.mdx index 6437555a2f..5c63b251e3 100644 --- a/packages/web/src/content/docs/nb/permissions.mdx +++ b/packages/web/src/content/docs/nb/permissions.mdx @@ -88,7 +88,7 @@ Du kan bruke `~` eller `$HOME` i starten av et mønster for å referere til hjem ### Eksterne kataloger -Bruk `external_directory` for å tillate verktøyanrop som berører stier utenfor arbeidskatalogen der OpenCode ble startet. Dette gjelder alle verktøy som tar en bane som input (for eksempel `read`, `edit`, `list`, `glob`, `grep` og mange `bash`-kommandoer). +Bruk `external_directory` for å tillate verktøyanrop som berører stier utenfor arbeidskatalogen der OpenCode ble startet. Dette gjelder alle verktøy som tar en bane som input (for eksempel `read`, `edit`, `glob`, `grep` og mange `bash`-kommandoer). Hjemmeutvidelse (som `~/...`) påvirker bare hvordan et mønster skrives. Den gjør ikke en ekstern bane til en del av det gjeldende arbeidsområdet, så stier utenfor arbeidskatalogen må fortsatt tillates via `external_directory`. @@ -133,7 +133,6 @@ OpenCode-tillatelser tastes inn etter verktøynavn, pluss et par sikkerhetsvakte - `edit` — alle filendringer (dekker `edit`, `write`, `patch`, `multiedit`) - `glob` — fil-globing (tilsvarer glob-mønsteret) - `grep` — innholdssøk (samsvarer med regex-mønsteret) -- `list` — viser filer i en katalog (tilsvarer katalogbanen) - `bash` — kjører skallkommandoer (matcher analyserte kommandoer som `git status --porcelain`) - `task` — start av subagenter (tilsvarer subagenttypen) - `skill` — laster en ferdighet (tilsvarer navnet på ferdigheten) diff --git a/packages/web/src/content/docs/nb/tools.mdx b/packages/web/src/content/docs/nb/tools.mdx index be80a0e2ba..2a67378e0a 100644 --- a/packages/web/src/content/docs/nb/tools.mdx +++ b/packages/web/src/content/docs/nb/tools.mdx @@ -149,22 +149,6 @@ Finn filer etter mønstermatching. Søk etter filer ved å bruke glob-mønstre som `**/*.js` eller `src/**/*.ts`. Returnerer samsvarende filbaner sortert etter endringstid. ---- - -### list - -List filer og kataloger i en gitt bane. - -```json title="opencode.json" {4} -{ - "$schema": "https://opencode.ai/config.json", - "permission": { - "list": "allow" - } -} -``` - -Dette verktøyet viser kataloginnhold. Den aksepterer glob-mønstre for å filtrere resultater. --- @@ -341,7 +325,7 @@ MCP (Model Context Protocol) servere lar deg integrere eksterne verktøy og tjen ## Internaler -Internt bruker verktøy som `grep`, `glob` og `list` [ripgrep](https://github.com/BurntSushi/ripgrep) i bakgrunnen. Som standard respekterer ripgrep `.gitignore`-mønstre, noe som betyr at filer og kataloger som er oppført i `.gitignore` vil bli ekskludert fra søk og oppføringer. +Internt bruker verktøy som `grep` og `glob` [ripgrep](https://github.com/BurntSushi/ripgrep) i bakgrunnen. Som standard respekterer ripgrep `.gitignore`-mønstre, noe som betyr at filer og kataloger som er oppført i `.gitignore` vil bli ekskludert fra søk og oppføringer. --- diff --git a/packages/web/src/content/docs/permissions.mdx b/packages/web/src/content/docs/permissions.mdx index a470fddd76..6383b2a3f2 100644 --- a/packages/web/src/content/docs/permissions.mdx +++ b/packages/web/src/content/docs/permissions.mdx @@ -88,7 +88,7 @@ You can use `~` or `$HOME` at the start of a pattern to reference your home dire ### External Directories -Use `external_directory` to allow tool calls that touch paths outside the working directory where OpenCode was started. This applies to any tool that takes a path as input (for example `read`, `edit`, `list`, `glob`, `grep`, and many `bash` commands). +Use `external_directory` to allow tool calls that touch paths outside the working directory where OpenCode was started. This applies to any tool that takes a path as input (for example `read`, `edit`, `glob`, `grep`, and many `bash` commands). Home expansion (like `~/...`) only affects how a pattern is written. It does not make an external path part of the current workspace, so paths outside the working directory must still be allowed via `external_directory`. @@ -133,7 +133,6 @@ OpenCode permissions are keyed by tool name, plus a couple of safety guards: - `edit` — all file modifications (covers `edit`, `write`, `patch`, `multiedit`) - `glob` — file globbing (matches the glob pattern) - `grep` — content search (matches the regex pattern) -- `list` — listing files in a directory (matches the directory path) - `bash` — running shell commands (matches parsed commands like `git status --porcelain`) - `task` — launching subagents (matches the subagent type) - `skill` — loading a skill (matches the skill name) diff --git a/packages/web/src/content/docs/pl/modes.mdx b/packages/web/src/content/docs/pl/modes.mdx index b28b160866..8d7c2568d3 100644 --- a/packages/web/src/content/docs/pl/modes.mdx +++ b/packages/web/src/content/docs/pl/modes.mdx @@ -233,7 +233,6 @@ Oto wszystkie narzędzia, które można sterować za pomocą konfiguracji trybó | `read` | Przeczytaj zawartość pliku | | `grep` | Wyszukaj zawartość pliku | | `glob` | Znajdź pliki według wzorca | -| `list` | Lista zawartości katalogu | | `patch` | Zastosuj poprawki do plików | | `todowrite` | Zarządzaj listami rzeczy do wykonania | | `webfetch` | Pobierz zawartość internetową | diff --git a/packages/web/src/content/docs/pl/permissions.mdx b/packages/web/src/content/docs/pl/permissions.mdx index 6a7840ac72..a5c05b6dc6 100644 --- a/packages/web/src/content/docs/pl/permissions.mdx +++ b/packages/web/src/content/docs/pl/permissions.mdx @@ -88,7 +88,7 @@ Możesz używać `~` lub `$HOME` na początku wzorca, aby zastosować się do sw ### Katalogi zewnętrzne -Użycie `external_directory`, aby zezwolić na wywołanie narzędzia, które obsługuje obsługę poza katalogiem roboczym, z uruchomieniem opencode. Dotyczy każdego narzędzia, które jako dane wejściowe zostało przyjęte (na przykład `read`, `edit`, `list`, `glob`, `grep` i wiele założycieli `bash`). +Użycie `external_directory`, aby zezwolić na wywołanie narzędzia, które obsługuje obsługę poza katalogiem roboczym, z uruchomieniem opencode. Dotyczy każdego narzędzia, które jako dane wejściowe zostało przyjęte (na przykład `read`, `edit`, `glob`, `grep` i wiele założycieli `bash`). Rozszerzenie domu (jak `~/...`) wpływa tylko na sposób za zwyczajowy wzorca. Nie powoduje to, że strategie zewnętrzne stają się stosowane przez `external_directory`. @@ -133,7 +133,6 @@ Uprawnienia opencode są określane na podstawie nazwy narzędzia i kilku zabezp - `edit` — wszystkie modyfikacje plików (obejmuje `edit`, `write`, `patch`, `multiedit`) - `glob` — maglowanie plików (pasuje do wzorców globowania) - `grep` — wyszukiwanie treści (pasuje do wzorca regularnego) -- `list` — wyświetlanie listy plików w katalogu (pasuje do katalogu) - `bash` — uruchamianie poleceń shell (pasuje do poleceń przeanalizowanych, takich jak `git status --porcelain`) - `task` — uruchamianie podagentów (odpowiada typowi podagenta) - `skill` — ładowanie umiejętności (pasuje do nazwy umiejętności) diff --git a/packages/web/src/content/docs/pl/tools.mdx b/packages/web/src/content/docs/pl/tools.mdx index 649c744e04..0690fb18c3 100644 --- a/packages/web/src/content/docs/pl/tools.mdx +++ b/packages/web/src/content/docs/pl/tools.mdx @@ -149,22 +149,6 @@ Znajduj pliki na podstawie wzorców. Szukaj plików przy użyciu wzorców glob, takich jak `**/*.js` lub `src/**/*.ts`. Zwraca pasujące ścieżki plików posortowane według czasu modyfikacji. ---- - -### list - -Wyświetla listę plików i katalogów w podanej ścieżce. - -```json title="opencode.json" {4} -{ - "$schema": "https://opencode.ai/config.json", - "permission": { - "list": "allow" - } -} -``` - -To narzędzie wyświetla zawartość katalogu. Akceptuje wzorce glob do filtrowania wyników. --- @@ -341,7 +325,7 @@ Serwery MCP (Model Context Protocol) umożliwiają integrację zewnętrznych nar ## Szczegóły techniczne -Wewnętrznie narzędzia takie jak `grep`, `glob` i `list` używają [ripgrep](https://github.com/BurntSushi/ripgrep). Domyślnie ripgrep respektuje wzorce `.gitignore`, co oznacza, że pliki i katalogi wymienione w Twoim `.gitignore` zostaną wykluczone z wyszukiwań i list. +Wewnętrznie narzędzia takie jak `grep` i `glob` używają [ripgrep](https://github.com/BurntSushi/ripgrep). Domyślnie ripgrep respektuje wzorce `.gitignore`, co oznacza, że pliki i katalogi wymienione w Twoim `.gitignore` zostaną wykluczone z wyszukiwań i list. --- diff --git a/packages/web/src/content/docs/pt-br/modes.mdx b/packages/web/src/content/docs/pt-br/modes.mdx index b549d69ded..b53fb4fcb4 100644 --- a/packages/web/src/content/docs/pt-br/modes.mdx +++ b/packages/web/src/content/docs/pt-br/modes.mdx @@ -230,7 +230,6 @@ Aqui estão todas as ferramentas que podem ser controladas através da configura | `read` | Ler conteúdos de arquivos | | `grep` | Pesquisar conteúdos de arquivos | | `glob` | Encontrar arquivos por padrão | -| `list` | Listar conteúdos de diretório | | `patch` | Aplicar patches a arquivos | | `todowrite` | Gerenciar listas de tarefas | | `webfetch` | Buscar conteúdo da web | diff --git a/packages/web/src/content/docs/pt-br/permissions.mdx b/packages/web/src/content/docs/pt-br/permissions.mdx index c3850c00ca..4facc9f72b 100644 --- a/packages/web/src/content/docs/pt-br/permissions.mdx +++ b/packages/web/src/content/docs/pt-br/permissions.mdx @@ -88,7 +88,7 @@ Você pode usar `~` ou `$HOME` no início de um padrão para referenciar seu dir ### Diretórios Externos -Use `external_directory` para permitir chamadas de ferramentas que tocam em caminhos fora do diretório de trabalho onde o opencode foi iniciado. Isso se aplica a qualquer ferramenta que aceite um caminho como entrada (por exemplo, `read`, `edit`, `list`, `glob`, `grep` e muitos comandos `bash`). +Use `external_directory` para permitir chamadas de ferramentas que tocam em caminhos fora do diretório de trabalho onde o opencode foi iniciado. Isso se aplica a qualquer ferramenta que aceite um caminho como entrada (por exemplo, `read`, `edit`, `glob`, `grep` e muitos comandos `bash`). A expansão do home (como `~/...`) afeta apenas como um padrão é escrito. Não torna um caminho externo parte do espaço de trabalho atual, então caminhos fora do diretório de trabalho ainda devem ser permitidos via `external_directory`. @@ -133,7 +133,6 @@ As permissões do opencode são indexadas pelo nome da ferramenta, além de algu - `edit` — todas as modificações de arquivo (cobre `edit`, `write`, `patch`, `multiedit`) - `glob` — globbing de arquivos (corresponde ao padrão glob) - `grep` — busca de conteúdo (corresponde ao padrão regex) -- `list` — listagem de arquivos em um diretório (corresponde ao caminho do diretório) - `bash` — execução de comandos de shell (corresponde a comandos analisados como `git status --porcelain`) - `task` — lançamento de subagentes (corresponde ao tipo de subagente) - `skill` — carregamento de uma habilidade (corresponde ao nome da habilidade) diff --git a/packages/web/src/content/docs/pt-br/tools.mdx b/packages/web/src/content/docs/pt-br/tools.mdx index d762fdf145..43099ab52c 100644 --- a/packages/web/src/content/docs/pt-br/tools.mdx +++ b/packages/web/src/content/docs/pt-br/tools.mdx @@ -149,22 +149,6 @@ Encontre arquivos por correspondência de padrões. Pesquise arquivos usando padrões glob como `**/*.js` ou `src/**/*.ts`. Retorna caminhos de arquivos correspondentes ordenados por tempo de modificação. ---- - -### list - -Liste arquivos e diretórios em um determinado caminho. - -```json title="opencode.json" {4} -{ - "$schema": "https://opencode.ai/config.json", - "permission": { - "list": "allow" - } -} -``` - -Esta ferramenta lista o conteúdo do diretório. Aceita padrões glob para filtrar resultados. --- @@ -341,7 +325,7 @@ Servidores MCP (Model Context Protocol) permitem que você integre ferramentas e ## Internos -Internamente, ferramentas como `grep`, `glob` e `list` usam [ripgrep](https://github.com/BurntSushi/ripgrep) por trás dos panos. Por padrão, o ripgrep respeita padrões `.gitignore`, o que significa que arquivos e diretórios listados em seu `.gitignore` serão excluídos de buscas e listagens. +Internamente, ferramentas como `grep` e `glob` usam [ripgrep](https://github.com/BurntSushi/ripgrep) por trás dos panos. Por padrão, o ripgrep respeita padrões `.gitignore`, o que significa que arquivos e diretórios listados em seu `.gitignore` serão excluídos de buscas e listagens. --- diff --git a/packages/web/src/content/docs/ru/modes.mdx b/packages/web/src/content/docs/ru/modes.mdx index f1ebca386d..6a4a74ceda 100644 --- a/packages/web/src/content/docs/ru/modes.mdx +++ b/packages/web/src/content/docs/ru/modes.mdx @@ -233,7 +233,6 @@ Provide constructive feedback without making direct changes. | `read` | Read file contents | | `grep` | Search file contents | | `glob` | Find files by pattern | -| `list` | List directory contents | | `patch` | Apply patches to files | | `todowrite` | Manage todo lists | | `webfetch` | Fetch web content | diff --git a/packages/web/src/content/docs/ru/permissions.mdx b/packages/web/src/content/docs/ru/permissions.mdx index 70f3a804a2..961a068243 100644 --- a/packages/web/src/content/docs/ru/permissions.mdx +++ b/packages/web/src/content/docs/ru/permissions.mdx @@ -88,7 +88,7 @@ opencode использует конфигурацию `permission`, чтобы ### Внешние каталоги -Используйте `external_directory`, чтобы разрешить вызовы инструментов, затрагивающие пути за пределами рабочего каталога, в котором был запущен opencode. Это применимо к любому инструменту, который принимает путь в качестве входных данных (например, `read`, `edit`, `list`, `glob`, `grep` и многие команды `bash`). +Используйте `external_directory`, чтобы разрешить вызовы инструментов, затрагивающие пути за пределами рабочего каталога, в котором был запущен opencode. Это применимо к любому инструменту, который принимает путь в качестве входных данных (например, `read`, `edit`, `glob`, `grep` и многие команды `bash`). Расширение дома (например, `~/...`) влияет только на запись шаблона. Он не делает внешний путь частью текущего рабочего пространства, поэтому пути за пределами рабочего каталога все равно должны быть разрешены через `external_directory`. @@ -133,7 +133,6 @@ opencode использует конфигурацию `permission`, чтобы - `edit` — все модификации файлов (охватывает `edit`, `write`, `patch`, `multiedit`) - `glob` — подстановка файла (соответствует шаблону подстановки) - `grep` — поиск по контенту (соответствует шаблону регулярного выражения) -- `list` — список файлов в каталоге (соответствует пути к каталогу) - `bash` — запуск shell-команд (соответствует проанализированным командам, например `git status --porcelain`) - `task` — запуск субагентов (соответствует типу субагента) - `skill` — загрузка навыка (соответствует названию навыка) diff --git a/packages/web/src/content/docs/ru/tools.mdx b/packages/web/src/content/docs/ru/tools.mdx index def6663fc1..8d4b5bf99e 100644 --- a/packages/web/src/content/docs/ru/tools.mdx +++ b/packages/web/src/content/docs/ru/tools.mdx @@ -149,22 +149,6 @@ description: Управляйте инструментами, которые м Ищите файлы, используя шаблоны glob, например `**/*.js` или `src/**/*.ts`. Возвращает соответствующие пути к файлам, отсортированные по времени изменения. ---- - -### list - -Список файлов и каталогов по заданному пути. - -```json title="opencode.json" {4} -{ - "$schema": "https://opencode.ai/config.json", - "permission": { - "list": "allow" - } -} -``` - -Этот инструмент отображает содержимое каталога. Он принимает шаблоны glob для фильтрации результатов. --- @@ -341,7 +325,7 @@ OPENCODE_ENABLE_EXA=1 opencode ## Внутреннее устройство -Внутренне такие инструменты, как `grep`, `glob` и `list`, используют [ripgrep](https://github.com/BurntSushi/ripgrep). По умолчанию ripgrep учитывает шаблоны `.gitignore`, что означает, что файлы и каталоги, перечисленные в вашем `.gitignore`, будут исключены из поиска и списков. +Внутренне такие инструменты, как `grep` и `glob`, используют [ripgrep](https://github.com/BurntSushi/ripgrep). По умолчанию ripgrep учитывает шаблоны `.gitignore`, что означает, что файлы и каталоги, перечисленные в вашем `.gitignore`, будут исключены из поиска и списков. --- diff --git a/packages/web/src/content/docs/th/modes.mdx b/packages/web/src/content/docs/th/modes.mdx index 2cbb05a26b..1569a5ad12 100644 --- a/packages/web/src/content/docs/th/modes.mdx +++ b/packages/web/src/content/docs/th/modes.mdx @@ -233,7 +233,6 @@ Provide constructive feedback without making direct changes. | `read` | อ่านเนื้อหาไฟล์ | | `grep` | ค้นหาเนื้อหาไฟล์ | | `glob` | ค้นหาไฟล์ตามรูปแบบ | -| `list` | แสดงรายการเนื้อหาไดเร็กทอรี | | `patch` | ใช้แพทช์กับไฟล์ | | `todowrite` | จัดการรายการสิ่งที่ต้องทำ | | `webfetch` | ดึงเนื้อหาเว็บ | diff --git a/packages/web/src/content/docs/th/permissions.mdx b/packages/web/src/content/docs/th/permissions.mdx index adf381dee3..5fed616159 100644 --- a/packages/web/src/content/docs/th/permissions.mdx +++ b/packages/web/src/content/docs/th/permissions.mdx @@ -88,7 +88,7 @@ OpenCode ใช้การกำหนดค่า `permission` เพื่อ ### ไดเรกทอรีภายนอก -ใช้ `external_directory` เพื่ออนุญาตการเรียกใช้เครื่องมือที่สัมผัสเส้นทางนอกไดเร็กทอรีการทำงานที่ OpenCode เริ่มทำงาน สิ่งนี้ใช้ได้กับเครื่องมือใดๆ ที่ใช้เส้นทางเป็นอินพุต (เช่น `read`, `edit`, `list`, `glob`, `grep` และคำสั่ง `bash` จำนวนมาก) +ใช้ `external_directory` เพื่ออนุญาตการเรียกใช้เครื่องมือที่สัมผัสเส้นทางนอกไดเร็กทอรีการทำงานที่ OpenCode เริ่มทำงาน สิ่งนี้ใช้ได้กับเครื่องมือใดๆ ที่ใช้เส้นทางเป็นอินพุต (เช่น `read`, `edit`, `glob`, `grep` และคำสั่ง `bash` จำนวนมาก) การขยายบ้าน (เช่น `~/...`) ส่งผลต่อวิธีการเขียนรูปแบบเท่านั้น ไม่ได้ทำให้เส้นทางภายนอกเป็นส่วนหนึ่งของพื้นที่ทำงานปัจจุบัน ดังนั้นเส้นทางภายนอกไดเรกทอรีการทำงานยังต้องได้รับอนุญาตผ่าน `external_directory` @@ -133,7 +133,6 @@ OpenCode ใช้การกำหนดค่า `permission` เพื่อ - `edit` — การแก้ไขไฟล์ทั้งหมด (ครอบคลุมถึง `edit`, `write`, `patch`, `multiedit`) - `glob` — ไฟล์ globbing (ตรงกับรูปแบบ glob) - `grep` — การค้นหาเนื้อหา (ตรงกับรูปแบบ regex) -- `list` — แสดงรายการไฟล์ในไดเร็กทอรี (ตรงกับเส้นทางไดเร็กทอรี) - `bash` — การรันคำสั่ง shell (ตรงกับคำสั่งที่แยกวิเคราะห์เช่น `git status --porcelain`) - `task` — การเปิดตัวตัวแทนย่อย (ตรงกับประเภทตัวแทนย่อย) - `skill` — กำลังโหลดทักษะ (ตรงกับชื่อทักษะ) diff --git a/packages/web/src/content/docs/th/tools.mdx b/packages/web/src/content/docs/th/tools.mdx index 17dbd9fdb3..3c9b88c0a1 100644 --- a/packages/web/src/content/docs/th/tools.mdx +++ b/packages/web/src/content/docs/th/tools.mdx @@ -149,22 +149,6 @@ description: จัดการเครื่องมือที่ LLM ส ค้นหาไฟล์โดยใช้รูปแบบ glob เช่น `**/*.js` หรือ `src/**/*.ts` ส่งคืนเส้นทางไฟล์ที่ตรงกันโดยจัดเรียงตามเวลาแก้ไข ---- - -### list - -แสดงรายการไฟล์และไดเร็กทอรีในพาธที่กำหนด - -```json title="opencode.json" {4} -{ - "$schema": "https://opencode.ai/config.json", - "permission": { - "list": "allow" - } -} -``` - -เครื่องมือนี้แสดงรายการเนื้อหาไดเร็กทอรี ยอมรับรูปแบบ glob เพื่อกรองผลลัพธ์ --- @@ -341,7 +325,7 @@ OPENCODE_ENABLE_EXA=1 opencode ## ภายใน -ภายใน เครื่องมือต่างๆ เช่น `grep`, `glob` และ `list` ใช้ [ripgrep](https://github.com/BurntSushi/ripgrep) ภายใต้ประทุน ตามค่าเริ่มต้น ripgrep เคารพรูปแบบ `.gitignore` ซึ่งหมายความว่าไฟล์และไดเร็กทอรีที่อยู่ใน `.gitignore` ของคุณจะถูกแยกออกจากการค้นหาและรายการ +ภายใน เครื่องมือต่างๆ เช่น `grep` และ `glob` ใช้ [ripgrep](https://github.com/BurntSushi/ripgrep) ภายใต้ประทุน ตามค่าเริ่มต้น ripgrep เคารพรูปแบบ `.gitignore` ซึ่งหมายความว่าไฟล์และไดเร็กทอรีที่อยู่ใน `.gitignore` ของคุณจะถูกแยกออกจากการค้นหาและรายการ --- diff --git a/packages/web/src/content/docs/tools.mdx b/packages/web/src/content/docs/tools.mdx index abd486aeb6..f05e980b8c 100644 --- a/packages/web/src/content/docs/tools.mdx +++ b/packages/web/src/content/docs/tools.mdx @@ -151,23 +151,6 @@ Search for files using glob patterns like `**/*.js` or `src/**/*.ts`. Returns ma --- -### list - -List files and directories in a given path. - -```json title="opencode.json" {4} -{ - "$schema": "https://opencode.ai/config.json", - "permission": { - "list": "allow" - } -} -``` - -This tool lists directory contents. It accepts glob patterns to filter results. - ---- - ### lsp (experimental) Interact with your configured LSP servers to get code intelligence features like definitions, references, hover info, and call hierarchy. @@ -345,7 +328,7 @@ MCP (Model Context Protocol) servers allow you to integrate external tools and s ## Internals -Internally, tools like `grep`, `glob`, and `list` use [ripgrep](https://github.com/BurntSushi/ripgrep) under the hood. By default, ripgrep respects `.gitignore` patterns, which means files and directories listed in your `.gitignore` will be excluded from searches and listings. +Internally, tools like `grep` and `glob` use [ripgrep](https://github.com/BurntSushi/ripgrep) under the hood. By default, ripgrep respects `.gitignore` patterns, which means files and directories listed in your `.gitignore` will be excluded from searches and listings. --- diff --git a/packages/web/src/content/docs/tr/modes.mdx b/packages/web/src/content/docs/tr/modes.mdx index 09538e788a..8f722ec228 100644 --- a/packages/web/src/content/docs/tr/modes.mdx +++ b/packages/web/src/content/docs/tr/modes.mdx @@ -233,7 +233,6 @@ Hiçbir araç belirtilmezse tüm araçlar varsayılan olarak etkindir. | `read` | Dosya içeriğini oku | | `grep` | Dosya içeriğini ara | | `glob` | Dosyaları desene göre bul | -| `list` | Dizinin içeriğini listele | | `patch` | Dosyalara yama uygula | | `todowrite` | Yapılacaklar listelerini yönet | | `webfetch` | Web içeriğini getir | diff --git a/packages/web/src/content/docs/tr/permissions.mdx b/packages/web/src/content/docs/tr/permissions.mdx index f608ce7e0d..976ee0a7ff 100644 --- a/packages/web/src/content/docs/tr/permissions.mdx +++ b/packages/web/src/content/docs/tr/permissions.mdx @@ -88,7 +88,7 @@ Ana dizininize referans vermek için bir modelin başlangıcında `~` veya `$HOM ### Harici Dizinler -opencode'un başlatıldığı çalışma dizini dışındaki yollara dokunan araç çağrılarına izin vermek için `external_directory` kullanın. Bu, girdi olarak bir yolu alan tüm araçlar için geçerlidir (örneğin `read`, `edit`, `list`, `glob`, `grep` ve birçok `bash` komutu). +opencode'un başlatıldığı çalışma dizini dışındaki yollara dokunan araç çağrılarına izin vermek için `external_directory` kullanın. Bu, girdi olarak bir yolu alan tüm araçlar için geçerlidir (örneğin `read`, `edit`, `glob`, `grep` ve birçok `bash` komutu). Ana sayfa genişletmesi (`~/...` gibi) yalnızca bir kalıbın nasıl yazıldığını etkiler. Geçerli çalışma alanının harici bir yolunu oluşturmaz, dolayısıyla çalışma dizini dışındaki yollara yine de `external_directory` aracılığıyla izin verilmesi gerekir. @@ -133,7 +133,6 @@ opencode izinleri araç adına ve birkaç güvenlik önlemine göre anahtarlanı - `edit` — tüm dosya değişiklikleri (`edit`, `write`, `patch`, `multiedit`'yi kapsar) - `glob` — dosya genellemesi (glob düzeniyle eşleşir) - `grep` — içerik arama (regex modeliyle eşleşir) -- `list` — bir dizideki dosyaları listeleme (dizin yoluyla eşleşir) - `bash` — kabuk komutlarını çalıştırma (`git status --porcelain` gibi ayrıştırılmış komutlarla eşleşir) - `task` — alt agent'ların başlatılması (alt agent türüyle eşleşir) - `skill` — bir skill yükleniyor (skill adıyla eşleşir) diff --git a/packages/web/src/content/docs/tr/tools.mdx b/packages/web/src/content/docs/tr/tools.mdx index e65ffec3a2..633abb4a6e 100644 --- a/packages/web/src/content/docs/tr/tools.mdx +++ b/packages/web/src/content/docs/tr/tools.mdx @@ -149,22 +149,6 @@ Desen eşleştirme ile dosya bulur. `**/*.js` veya `src/**/*.ts` gibi glob desenleriyle dosya arar. Eşleşen dosya yollarını değişim zamanına göre sıralar. ---- - -### list - -Verilen yoldaki dosya ve dizinleri listeler. - -```json title="opencode.json" {4} -{ - "$schema": "https://opencode.ai/config.json", - "permission": { - "list": "allow" - } -} -``` - -Bu araç dizin içeriğini listeler. Sonuçları filtrelemek için glob desenlerini kabul eder. --- @@ -341,7 +325,7 @@ MCP sunucularını yapılandırma için [daha fazla bilgi alın](/docs/mcp-serve ## Dahili detaylar -Dahilde `grep`, `glob` ve `list` gibi araçlar [ripgrep](https://github.com/BurntSushi/ripgrep) kullanır. Varsayılan olarak ripgrep `.gitignore` desenlerine uyar; yani `.gitignore` içindeki dosya ve dizinler arama ve listeleme sonucuna dahil edilmez. +Dahilde `grep` ve `glob` gibi araçlar [ripgrep](https://github.com/BurntSushi/ripgrep) kullanır. Varsayılan olarak ripgrep `.gitignore` desenlerine uyar; yani `.gitignore` içindeki dosya ve dizinler arama ve listeleme sonucuna dahil edilmez. --- diff --git a/packages/web/src/content/docs/zh-cn/modes.mdx b/packages/web/src/content/docs/zh-cn/modes.mdx index 4570c801c7..256cffe811 100644 --- a/packages/web/src/content/docs/zh-cn/modes.mdx +++ b/packages/web/src/content/docs/zh-cn/modes.mdx @@ -230,7 +230,6 @@ Markdown 文件名即为模式名称(例如,`review.md` 创建一个名为 ` | `read` | 读取文件内容 | | `grep` | 搜索文件内容 | | `glob` | 按模式查找文件 | -| `list` | 列出目录内容 | | `patch` | 对文件应用补丁 | | `todowrite` | 管理待办事项列表 | | `webfetch` | 获取网页内容 | diff --git a/packages/web/src/content/docs/zh-cn/permissions.mdx b/packages/web/src/content/docs/zh-cn/permissions.mdx index 24104e2a26..f928554f2a 100644 --- a/packages/web/src/content/docs/zh-cn/permissions.mdx +++ b/packages/web/src/content/docs/zh-cn/permissions.mdx @@ -88,7 +88,7 @@ OpenCode 使用 `permission` 配置来决定某个操作是否应自动运行、 ### 外部目录 -使用 `external_directory` 允许工具调用访问 OpenCode 启动时工作目录之外的路径。这适用于任何接受路径作为输入的工具(例如 `read`、`edit`、`list`、`glob`、`grep` 以及许多 `bash` 命令)。 +使用 `external_directory` 允许工具调用访问 OpenCode 启动时工作目录之外的路径。这适用于任何接受路径作为输入的工具(例如 `read`、`edit`、`glob`、`grep` 以及许多 `bash` 命令)。 主目录展开(如 `~/...`)仅影响模式的书写方式。它不会将外部路径纳入当前工作空间,因此工作目录之外的路径仍然必须通过 `external_directory` 来允许。 @@ -133,7 +133,6 @@ OpenCode 的权限以工具名称为键,外加几个安全防护项: - `edit` — 所有文件修改(涵盖 `edit`、`write`、`patch`、`multiedit`) - `glob` — 文件通配(匹配通配模式) - `grep` — 内容搜索(匹配正则表达式模式) -- `list` — 列出目录中的文件(匹配目录路径) - `bash` — 运行 shell 命令(匹配解析后的命令,如 `git status --porcelain`) - `task` — 启动子代理(匹配子代理类型) - `skill` — 加载技能(匹配技能名称) diff --git a/packages/web/src/content/docs/zh-cn/tools.mdx b/packages/web/src/content/docs/zh-cn/tools.mdx index 4f68a9cf35..c6d6d71940 100644 --- a/packages/web/src/content/docs/zh-cn/tools.mdx +++ b/packages/web/src/content/docs/zh-cn/tools.mdx @@ -149,22 +149,6 @@ description: 管理 LLM 可以使用的工具。 使用 `**/*.js` 或 `src/**/*.ts` 等 glob 模式搜索文件。返回按修改时间排序的匹配文件路径。 ---- - -### list - -列出指定路径下的文件和目录。 - -```json title="opencode.json" {4} -{ - "$schema": "https://opencode.ai/config.json", - "permission": { - "list": "allow" - } -} -``` - -该工具用于列出目录内容。它接受 glob 模式来过滤结果。 --- @@ -341,7 +325,7 @@ MCP(Model Context Protocol)服务器允许您集成外部工具和服务, ## 内部机制 -在内部,`grep`、`glob` 和 `list` 等工具底层使用 [ripgrep](https://github.com/BurntSushi/ripgrep)。默认情况下,ripgrep 遵循 `.gitignore` 中的模式,这意味着 `.gitignore` 中列出的文件和目录将被排除在搜索和列表结果之外。 +在内部,`grep` 和 `glob` 等工具底层使用 [ripgrep](https://github.com/BurntSushi/ripgrep)。默认情况下,ripgrep 遵循 `.gitignore` 中的模式,这意味着 `.gitignore` 中列出的文件和目录将被排除在搜索和列表结果之外。 --- diff --git a/packages/web/src/content/docs/zh-tw/modes.mdx b/packages/web/src/content/docs/zh-tw/modes.mdx index c97aeb61b5..625b4d0219 100644 --- a/packages/web/src/content/docs/zh-tw/modes.mdx +++ b/packages/web/src/content/docs/zh-tw/modes.mdx @@ -230,7 +230,6 @@ Markdown 檔案名稱即為模式名稱(例如,`review.md` 建立一個名 | `read` | 讀取檔案內容 | | `grep` | 搜尋檔案內容 | | `glob` | 按模式尋找檔案 | -| `list` | 列出目錄內容 | | `patch` | 對檔案套用補丁 | | `todowrite` | 管理待辦事項清單 | | `webfetch` | 擷取網頁內容 | diff --git a/packages/web/src/content/docs/zh-tw/permissions.mdx b/packages/web/src/content/docs/zh-tw/permissions.mdx index 05b522e9c7..bacd87c1ed 100644 --- a/packages/web/src/content/docs/zh-tw/permissions.mdx +++ b/packages/web/src/content/docs/zh-tw/permissions.mdx @@ -88,7 +88,7 @@ OpenCode 使用 `permission` 設定來決定某個操作是否應自動執行、 ### 外部目錄 -使用 `external_directory` 允許工具呼叫存取 OpenCode 啟動時工作目錄之外的路徑。這適用於任何接受路徑作為輸入的工具(例如 `read`、`edit`、`list`、`glob`、`grep` 以及許多 `bash` 指令)。 +使用 `external_directory` 允許工具呼叫存取 OpenCode 啟動時工作目錄之外的路徑。這適用於任何接受路徑作為輸入的工具(例如 `read`、`edit`、`glob`、`grep` 以及許多 `bash` 指令)。 主目錄展開(如 `~/...`)僅影響模式的書寫方式。它不會將外部路徑納入當前工作空間,因此工作目錄之外的路徑仍然必須透過 `external_directory` 來允許。 @@ -133,7 +133,6 @@ OpenCode 的權限以工具名稱為鍵,外加幾個安全防護項: - `edit` — 所有檔案修改(涵蓋 `edit`、`write`、`patch`、`multiedit`) - `glob` — 檔案萬用字元比對(比對萬用字元模式) - `grep` — 內容搜尋(比對正規表示式模式) -- `list` — 列出目錄中的檔案(比對目錄路徑) - `bash` — 執行 shell 指令(比對解析後的指令,如 `git status --porcelain`) - `task` — 啟動子代理(比對子代理類型) - `skill` — 載入技能(比對技能名稱) diff --git a/packages/web/src/content/docs/zh-tw/tools.mdx b/packages/web/src/content/docs/zh-tw/tools.mdx index 80e27ea0cc..cba32793d4 100644 --- a/packages/web/src/content/docs/zh-tw/tools.mdx +++ b/packages/web/src/content/docs/zh-tw/tools.mdx @@ -149,22 +149,6 @@ description: 管理 LLM 可以使用的工具。 使用 `**/*.js` 或 `src/**/*.ts` 等 glob 模式搜尋檔案。回傳按修改時間排序的匹配檔案路徑。 ---- - -### list - -列出指定路徑下的檔案和目錄。 - -```json title="opencode.json" {4} -{ - "$schema": "https://opencode.ai/config.json", - "permission": { - "list": "allow" - } -} -``` - -該工具用於列出目錄內容。它接受 glob 模式來過濾結果。 --- @@ -341,7 +325,7 @@ MCP(Model Context Protocol)伺服器允許您整合外部工具和服務, ## 內部機制 -在內部,`grep`、`glob` 和 `list` 等工具底層使用 [ripgrep](https://github.com/BurntSushi/ripgrep)。預設情況下,ripgrep 遵循 `.gitignore` 中的模式,這意味著 `.gitignore` 中列出的檔案和目錄將被排除在搜尋和列表結果之外。 +在內部,`grep` 和 `glob` 等工具底層使用 [ripgrep](https://github.com/BurntSushi/ripgrep)。預設情況下,ripgrep 遵循 `.gitignore` 中的模式,這意味著 `.gitignore` 中列出的檔案和目錄將被排除在搜尋和列表結果之外。 --- From 916131be19893b84f17902825a163a0b67274249 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Thu, 16 Apr 2026 06:44:55 +0800 Subject: [PATCH 154/300] core: move plugin intialisation to config layer override (#22620) --- packages/opencode/src/effect/app-runtime.ts | 20 +++++++++++++++++++- packages/opencode/src/project/bootstrap.ts | 1 - 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/effect/app-runtime.ts b/packages/opencode/src/effect/app-runtime.ts index 257922dafe..668c89b60b 100644 --- a/packages/opencode/src/effect/app-runtime.ts +++ b/packages/opencode/src/effect/app-runtime.ts @@ -47,13 +47,31 @@ import { Pty } from "@/pty" import { Installation } from "@/installation" import { ShareNext } from "@/share/share-next" import { SessionShare } from "@/share/session" +import * as Effect from "effect/Effect" + +// Adjusts the default Config layer to ensure that plugins are always initialised before +// any other layers read the current config +const ConfigWithPluginPriority = Layer.effect( + Config.Service, + Effect.gen(function* () { + const config = yield* Config.Service + const plugin = yield* Plugin.Service + + return { + ...config, + get: () => Effect.andThen(plugin.init(), config.get), + getGlobal: () => Effect.andThen(plugin.init(), config.getGlobal), + getConsoleState: () => Effect.andThen(plugin.init(), config.getConsoleState), + } + }), +).pipe(Layer.provide(Layer.merge(Plugin.defaultLayer, Config.defaultLayer))) export const AppLayer = Layer.mergeAll( AppFileSystem.defaultLayer, Bus.defaultLayer, Auth.defaultLayer, Account.defaultLayer, - Config.defaultLayer, + ConfigWithPluginPriority, Git.defaultLayer, Ripgrep.defaultLayer, FileTime.defaultLayer, diff --git a/packages/opencode/src/project/bootstrap.ts b/packages/opencode/src/project/bootstrap.ts index a1f2a8cb02..0babdfe13b 100644 --- a/packages/opencode/src/project/bootstrap.ts +++ b/packages/opencode/src/project/bootstrap.ts @@ -15,7 +15,6 @@ import * as Effect from "effect/Effect" export const InstanceBootstrap = Effect.gen(function* () { Log.Default.info("bootstrapping", { directory: Instance.directory }) - yield* Plugin.Service.use((svc) => svc.init()) yield* Effect.all( [ LSP.Service, From 83e257b468d75f6361f9ce50d930f110dfd37365 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Wed, 15 Apr 2026 22:45:54 +0000 Subject: [PATCH 155/300] chore: generate --- packages/web/src/content/docs/ar/tools.mdx | 1 - packages/web/src/content/docs/bs/tools.mdx | 1 - packages/web/src/content/docs/da/tools.mdx | 1 - packages/web/src/content/docs/de/tools.mdx | 1 - packages/web/src/content/docs/es/tools.mdx | 1 - packages/web/src/content/docs/fr/tools.mdx | 1 - packages/web/src/content/docs/it/modes.mdx | 22 +++++++++---------- packages/web/src/content/docs/it/tools.mdx | 1 - packages/web/src/content/docs/ja/modes.mdx | 22 +++++++++---------- packages/web/src/content/docs/ja/tools.mdx | 1 - packages/web/src/content/docs/ko/tools.mdx | 1 - packages/web/src/content/docs/modes.mdx | 22 +++++++++---------- packages/web/src/content/docs/nb/tools.mdx | 1 - packages/web/src/content/docs/pl/tools.mdx | 1 - packages/web/src/content/docs/pt-br/tools.mdx | 1 - packages/web/src/content/docs/ru/modes.mdx | 22 +++++++++---------- packages/web/src/content/docs/ru/tools.mdx | 1 - packages/web/src/content/docs/th/modes.mdx | 22 +++++++++---------- packages/web/src/content/docs/th/tools.mdx | 1 - packages/web/src/content/docs/tr/tools.mdx | 1 - packages/web/src/content/docs/zh-cn/tools.mdx | 1 - packages/web/src/content/docs/zh-tw/tools.mdx | 1 - 22 files changed, 55 insertions(+), 72 deletions(-) diff --git a/packages/web/src/content/docs/ar/tools.mdx b/packages/web/src/content/docs/ar/tools.mdx index f1477a08c2..3f3c9ee068 100644 --- a/packages/web/src/content/docs/ar/tools.mdx +++ b/packages/web/src/content/docs/ar/tools.mdx @@ -149,7 +149,6 @@ description: إدارة الأدوات التي يمكن لـ LLM استخدام ابحث عن الملفات باستخدام أنماط glob مثل `**/*.js` أو `src/**/*.ts`. يعيد مسارات الملفات المطابقة مرتبة حسب وقت التعديل. - --- ### lsp (experimental) diff --git a/packages/web/src/content/docs/bs/tools.mdx b/packages/web/src/content/docs/bs/tools.mdx index 6c4d546141..db04295fd2 100644 --- a/packages/web/src/content/docs/bs/tools.mdx +++ b/packages/web/src/content/docs/bs/tools.mdx @@ -149,7 +149,6 @@ Pronalazi datoteke po obrascima. Trazi datoteke koristeci glob obrasce kao `**/*.js` ili `src/**/*.ts`. Vraca putanje sortirane po vremenu izmjene. - --- ### lsp (eksperimentalno) diff --git a/packages/web/src/content/docs/da/tools.mdx b/packages/web/src/content/docs/da/tools.mdx index 043aabed43..6f6f95c9c5 100644 --- a/packages/web/src/content/docs/da/tools.mdx +++ b/packages/web/src/content/docs/da/tools.mdx @@ -149,7 +149,6 @@ Finn filer etter mønstermatching. Søk etter filer ved at bruge glob-mønstre som `**/*.js` eller `src/**/*.ts`. Returnerer samsvarende filbaner sortert etter endringstid. - --- ### lsp (experimental) diff --git a/packages/web/src/content/docs/de/tools.mdx b/packages/web/src/content/docs/de/tools.mdx index 98f5c708c2..6012148c6a 100644 --- a/packages/web/src/content/docs/de/tools.mdx +++ b/packages/web/src/content/docs/de/tools.mdx @@ -156,7 +156,6 @@ Findet Dateien per Musterabgleich. Sucht nach Dateien mit Glob-Mustern wie `**/*.js` oder `src/**/*.ts`. Gibt passende Dateipfade sortiert nach Aenderungsdatum zurueck. - --- ### lsp (experimentell) diff --git a/packages/web/src/content/docs/es/tools.mdx b/packages/web/src/content/docs/es/tools.mdx index 7d594a1c9f..83d61f5325 100644 --- a/packages/web/src/content/docs/es/tools.mdx +++ b/packages/web/src/content/docs/es/tools.mdx @@ -149,7 +149,6 @@ Encuentre archivos por coincidencia de patrones. Busque archivos usando patrones globales como `**/*.js` o `src/**/*.ts`. Devuelve rutas de archivos coincidentes ordenadas por hora de modificación. - --- ### lsp (experimental) diff --git a/packages/web/src/content/docs/fr/tools.mdx b/packages/web/src/content/docs/fr/tools.mdx index 483a953443..4f3f180469 100644 --- a/packages/web/src/content/docs/fr/tools.mdx +++ b/packages/web/src/content/docs/fr/tools.mdx @@ -149,7 +149,6 @@ Recherchez des fichiers par correspondance de modèles. Recherchez des fichiers à l'aide de modèles globaux tels que `**/*.js` ou `src/**/*.ts`. Renvoie les chemins de fichiers correspondants triés par heure de modification. - --- ### lsp (expérimental) diff --git a/packages/web/src/content/docs/it/modes.mdx b/packages/web/src/content/docs/it/modes.mdx index b72b388fb0..052808f7ac 100644 --- a/packages/web/src/content/docs/it/modes.mdx +++ b/packages/web/src/content/docs/it/modes.mdx @@ -224,17 +224,17 @@ Se non specifichi gli strumenti, tutti gli strumenti sono abilitati per impostaz Ecco tutti gli strumenti che possono essere controllati tramite la configurazione della modalita. -| Strumento | Descrizione | -| ----------- | --------------------------------- | -| `bash` | Esegue comandi shell | -| `edit` | Modifica file esistenti | -| `write` | Crea nuovi file | -| `read` | Legge contenuti dei file | -| `grep` | Cerca nei contenuti dei file | -| `glob` | Trova file per pattern | -| `patch` | Applica patch ai file | -| `todowrite` | Gestisce liste todo | -| `webfetch` | Recupera contenuti web | +| Strumento | Descrizione | +| ----------- | ---------------------------- | +| `bash` | Esegue comandi shell | +| `edit` | Modifica file esistenti | +| `write` | Crea nuovi file | +| `read` | Legge contenuti dei file | +| `grep` | Cerca nei contenuti dei file | +| `glob` | Trova file per pattern | +| `patch` | Applica patch ai file | +| `todowrite` | Gestisce liste todo | +| `webfetch` | Recupera contenuti web | --- diff --git a/packages/web/src/content/docs/it/tools.mdx b/packages/web/src/content/docs/it/tools.mdx index 0bf00ffc6f..c1e69f8beb 100644 --- a/packages/web/src/content/docs/it/tools.mdx +++ b/packages/web/src/content/docs/it/tools.mdx @@ -149,7 +149,6 @@ Trova file tramite pattern matching. Cerca file usando pattern glob come `**/*.js` o `src/**/*.ts`. Restituisce i percorsi corrispondenti ordinati per data di modifica. - --- ### lsp (experimental) diff --git a/packages/web/src/content/docs/ja/modes.mdx b/packages/web/src/content/docs/ja/modes.mdx index 623c19552d..8ebe7f5e68 100644 --- a/packages/web/src/content/docs/ja/modes.mdx +++ b/packages/web/src/content/docs/ja/modes.mdx @@ -223,17 +223,17 @@ Markdown ファイル名はモード名になります (例: `review.md` は `re ここでは、モード設定を通じて制御できるすべてのツールを示します。 -| ツール | 説明 | -| ----------- | ------------------------------ | -| `bash` | シェルコマンドを実行する | -| `edit` | 既存のファイルを変更する | -| `write` | 新しいファイルを作成する | -| `read` | ファイルの内容を読み取る | -| `grep` | ファイルの内容を検索 | -| `glob` | パターンでファイルを検索 | -| `patch` | ファイルにパッチを適用する | -| `todowrite` | ToDo リストを管理する | -| `webfetch` | Web コンテンツを取得する | +| ツール | 説明 | +| ----------- | -------------------------- | +| `bash` | シェルコマンドを実行する | +| `edit` | 既存のファイルを変更する | +| `write` | 新しいファイルを作成する | +| `read` | ファイルの内容を読み取る | +| `grep` | ファイルの内容を検索 | +| `glob` | パターンでファイルを検索 | +| `patch` | ファイルにパッチを適用する | +| `todowrite` | ToDo リストを管理する | +| `webfetch` | Web コンテンツを取得する | --- diff --git a/packages/web/src/content/docs/ja/tools.mdx b/packages/web/src/content/docs/ja/tools.mdx index ae409aa7db..3945063936 100644 --- a/packages/web/src/content/docs/ja/tools.mdx +++ b/packages/web/src/content/docs/ja/tools.mdx @@ -149,7 +149,6 @@ OpenCode で利用可能なすべての組み込みツールを次に示しま `**/*.js` や `src/**/*.ts` などの glob パターンを使用してファイルを検索します。一致するファイルパスを変更時間順に並べて返します。 - --- ### lsp (実験的) diff --git a/packages/web/src/content/docs/ko/tools.mdx b/packages/web/src/content/docs/ko/tools.mdx index b98578b58e..49bea93cb2 100644 --- a/packages/web/src/content/docs/ko/tools.mdx +++ b/packages/web/src/content/docs/ko/tools.mdx @@ -149,7 +149,6 @@ Codebase에서 빠른 콘텐츠 검색. 전체 regex 문법 및 파일 패턴 `**/*.js` 또는 `src/**/*.ts`와 같은 glob 패턴을 사용하여 파일 검색. 수정 시간에 의해 정렬 된 파일 경로 반환. - --- ### lsp (실험적) diff --git a/packages/web/src/content/docs/modes.mdx b/packages/web/src/content/docs/modes.mdx index 8ce2c0d13e..b8ea697399 100644 --- a/packages/web/src/content/docs/modes.mdx +++ b/packages/web/src/content/docs/modes.mdx @@ -225,17 +225,17 @@ If no tools are specified, all tools are enabled by default. Here are all the tools can be controlled through the mode config. -| Tool | Description | -| ----------- | ----------------------- | -| `bash` | Execute shell commands | -| `edit` | Modify existing files | -| `write` | Create new files | -| `read` | Read file contents | -| `grep` | Search file contents | -| `glob` | Find files by pattern | -| `patch` | Apply patches to files | -| `todowrite` | Manage todo lists | -| `webfetch` | Fetch web content | +| Tool | Description | +| ----------- | ---------------------- | +| `bash` | Execute shell commands | +| `edit` | Modify existing files | +| `write` | Create new files | +| `read` | Read file contents | +| `grep` | Search file contents | +| `glob` | Find files by pattern | +| `patch` | Apply patches to files | +| `todowrite` | Manage todo lists | +| `webfetch` | Fetch web content | --- diff --git a/packages/web/src/content/docs/nb/tools.mdx b/packages/web/src/content/docs/nb/tools.mdx index 2a67378e0a..8c871f11c9 100644 --- a/packages/web/src/content/docs/nb/tools.mdx +++ b/packages/web/src/content/docs/nb/tools.mdx @@ -149,7 +149,6 @@ Finn filer etter mønstermatching. Søk etter filer ved å bruke glob-mønstre som `**/*.js` eller `src/**/*.ts`. Returnerer samsvarende filbaner sortert etter endringstid. - --- ### lsp (eksperimentell) diff --git a/packages/web/src/content/docs/pl/tools.mdx b/packages/web/src/content/docs/pl/tools.mdx index 0690fb18c3..180e043cd5 100644 --- a/packages/web/src/content/docs/pl/tools.mdx +++ b/packages/web/src/content/docs/pl/tools.mdx @@ -149,7 +149,6 @@ Znajduj pliki na podstawie wzorców. Szukaj plików przy użyciu wzorców glob, takich jak `**/*.js` lub `src/**/*.ts`. Zwraca pasujące ścieżki plików posortowane według czasu modyfikacji. - --- ### LSP (eksperymentalne) diff --git a/packages/web/src/content/docs/pt-br/tools.mdx b/packages/web/src/content/docs/pt-br/tools.mdx index 43099ab52c..4c7b371971 100644 --- a/packages/web/src/content/docs/pt-br/tools.mdx +++ b/packages/web/src/content/docs/pt-br/tools.mdx @@ -149,7 +149,6 @@ Encontre arquivos por correspondência de padrões. Pesquise arquivos usando padrões glob como `**/*.js` ou `src/**/*.ts`. Retorna caminhos de arquivos correspondentes ordenados por tempo de modificação. - --- ### lsp (experimental) diff --git a/packages/web/src/content/docs/ru/modes.mdx b/packages/web/src/content/docs/ru/modes.mdx index 6a4a74ceda..e63c91ace4 100644 --- a/packages/web/src/content/docs/ru/modes.mdx +++ b/packages/web/src/content/docs/ru/modes.mdx @@ -225,17 +225,17 @@ Provide constructive feedback without making direct changes. Вот всеми инструментами можно управлять через конфигурацию режима. -| Инструмент | Описание | -| ----------- | ----------------------- | -| `bash` | Execute shell commands | -| `edit` | Modify existing files | -| `write` | Create new files | -| `read` | Read file contents | -| `grep` | Search file contents | -| `glob` | Find files by pattern | -| `patch` | Apply patches to files | -| `todowrite` | Manage todo lists | -| `webfetch` | Fetch web content | +| Инструмент | Описание | +| ----------- | ---------------------- | +| `bash` | Execute shell commands | +| `edit` | Modify existing files | +| `write` | Create new files | +| `read` | Read file contents | +| `grep` | Search file contents | +| `glob` | Find files by pattern | +| `patch` | Apply patches to files | +| `todowrite` | Manage todo lists | +| `webfetch` | Fetch web content | --- diff --git a/packages/web/src/content/docs/ru/tools.mdx b/packages/web/src/content/docs/ru/tools.mdx index 8d4b5bf99e..35958e036c 100644 --- a/packages/web/src/content/docs/ru/tools.mdx +++ b/packages/web/src/content/docs/ru/tools.mdx @@ -149,7 +149,6 @@ description: Управляйте инструментами, которые м Ищите файлы, используя шаблоны glob, например `**/*.js` или `src/**/*.ts`. Возвращает соответствующие пути к файлам, отсортированные по времени изменения. - --- ### lsp (экспериментальный) diff --git a/packages/web/src/content/docs/th/modes.mdx b/packages/web/src/content/docs/th/modes.mdx index 1569a5ad12..6cca309987 100644 --- a/packages/web/src/content/docs/th/modes.mdx +++ b/packages/web/src/content/docs/th/modes.mdx @@ -225,17 +225,17 @@ Provide constructive feedback without making direct changes. นี่คือเครื่องมือทั้งหมดที่สามารถควบคุมได้ผ่านการกำหนดค่าโหมด -| เครื่องมือ | คำอธิบาย | -| ----------- | --------------------------- | -| `bash` | ดำเนินการคำสั่ง shell | -| `edit` | แก้ไขไฟล์ที่มีอยู่ | -| `write` | สร้างไฟล์ใหม่ | -| `read` | อ่านเนื้อหาไฟล์ | -| `grep` | ค้นหาเนื้อหาไฟล์ | -| `glob` | ค้นหาไฟล์ตามรูปแบบ | -| `patch` | ใช้แพทช์กับไฟล์ | -| `todowrite` | จัดการรายการสิ่งที่ต้องทำ | -| `webfetch` | ดึงเนื้อหาเว็บ | +| เครื่องมือ | คำอธิบาย | +| ----------- | ------------------------- | +| `bash` | ดำเนินการคำสั่ง shell | +| `edit` | แก้ไขไฟล์ที่มีอยู่ | +| `write` | สร้างไฟล์ใหม่ | +| `read` | อ่านเนื้อหาไฟล์ | +| `grep` | ค้นหาเนื้อหาไฟล์ | +| `glob` | ค้นหาไฟล์ตามรูปแบบ | +| `patch` | ใช้แพทช์กับไฟล์ | +| `todowrite` | จัดการรายการสิ่งที่ต้องทำ | +| `webfetch` | ดึงเนื้อหาเว็บ | --- diff --git a/packages/web/src/content/docs/th/tools.mdx b/packages/web/src/content/docs/th/tools.mdx index 3c9b88c0a1..0ead638461 100644 --- a/packages/web/src/content/docs/th/tools.mdx +++ b/packages/web/src/content/docs/th/tools.mdx @@ -149,7 +149,6 @@ description: จัดการเครื่องมือที่ LLM ส ค้นหาไฟล์โดยใช้รูปแบบ glob เช่น `**/*.js` หรือ `src/**/*.ts` ส่งคืนเส้นทางไฟล์ที่ตรงกันโดยจัดเรียงตามเวลาแก้ไข - --- ### lsp (ขั้นทดลอง) diff --git a/packages/web/src/content/docs/tr/tools.mdx b/packages/web/src/content/docs/tr/tools.mdx index 633abb4a6e..2beb190094 100644 --- a/packages/web/src/content/docs/tr/tools.mdx +++ b/packages/web/src/content/docs/tr/tools.mdx @@ -149,7 +149,6 @@ Desen eşleştirme ile dosya bulur. `**/*.js` veya `src/**/*.ts` gibi glob desenleriyle dosya arar. Eşleşen dosya yollarını değişim zamanına göre sıralar. - --- ### lsp (deneysel) diff --git a/packages/web/src/content/docs/zh-cn/tools.mdx b/packages/web/src/content/docs/zh-cn/tools.mdx index c6d6d71940..4c60370590 100644 --- a/packages/web/src/content/docs/zh-cn/tools.mdx +++ b/packages/web/src/content/docs/zh-cn/tools.mdx @@ -149,7 +149,6 @@ description: 管理 LLM 可以使用的工具。 使用 `**/*.js` 或 `src/**/*.ts` 等 glob 模式搜索文件。返回按修改时间排序的匹配文件路径。 - --- ### lsp(实验性) diff --git a/packages/web/src/content/docs/zh-tw/tools.mdx b/packages/web/src/content/docs/zh-tw/tools.mdx index cba32793d4..6ce68d9fb5 100644 --- a/packages/web/src/content/docs/zh-tw/tools.mdx +++ b/packages/web/src/content/docs/zh-tw/tools.mdx @@ -149,7 +149,6 @@ description: 管理 LLM 可以使用的工具。 使用 `**/*.js` 或 `src/**/*.ts` 等 glob 模式搜尋檔案。回傳按修改時間排序的匹配檔案路徑。 - --- ### lsp(實驗性) From e16589f8b535c216939d690d721cd1eefc3c1c2a Mon Sep 17 00:00:00 2001 From: David Hill <1879069+iamdavidhill@users.noreply.github.com> Date: Wed, 15 Apr 2026 23:58:05 +0100 Subject: [PATCH 156/300] tweak(ui): session spacing (#20839) Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com> Co-authored-by: Brendan Allan --- packages/opencode/.opencode/package-lock.json | 31 ++++++++++++++ packages/ui/src/components/collapsible.css | 2 +- packages/ui/src/components/markdown.css | 30 +++++++------- packages/ui/src/components/message-part.css | 19 +++++---- packages/ui/src/components/session-turn.css | 5 ++- .../timeline-playground.stories.tsx | 41 ++++++++++++++++--- session.json | 0 7 files changed, 97 insertions(+), 31 deletions(-) create mode 100644 packages/opencode/.opencode/package-lock.json create mode 100644 session.json diff --git a/packages/opencode/.opencode/package-lock.json b/packages/opencode/.opencode/package-lock.json new file mode 100644 index 0000000000..cd3c011efc --- /dev/null +++ b/packages/opencode/.opencode/package-lock.json @@ -0,0 +1,31 @@ +{ + "name": ".opencode", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@opencode-ai/plugin": "*" + } + }, + "node_modules/@opencode-ai/plugin": { + "version": "1.2.6", + "license": "MIT", + "dependencies": { + "@opencode-ai/sdk": "1.2.6", + "zod": "4.1.8" + } + }, + "node_modules/@opencode-ai/sdk": { + "version": "1.2.6", + "license": "MIT" + }, + "node_modules/zod": { + "version": "4.1.8", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/packages/ui/src/components/collapsible.css b/packages/ui/src/components/collapsible.css index 608ca6e0eb..82c133f738 100644 --- a/packages/ui/src/components/collapsible.css +++ b/packages/ui/src/components/collapsible.css @@ -9,7 +9,7 @@ overflow: visible; &.tool-collapsible { - --tool-content-gap: 8px; + --tool-content-gap: 4px; gap: var(--tool-content-gap); } diff --git a/packages/ui/src/components/markdown.css b/packages/ui/src/components/markdown.css index f82723807d..26c9efd475 100644 --- a/packages/ui/src/components/markdown.css +++ b/packages/ui/src/components/markdown.css @@ -6,7 +6,7 @@ color: var(--text-strong); font-family: var(--font-family-sans); font-size: var(--font-size-base); /* 14px */ - line-height: var(--line-height-x-large); + line-height: 160%; /* Spacing for flow */ > *:first-child { @@ -23,11 +23,11 @@ h4, h5, h6 { - font-size: var(--font-size-base); + font-size: 14px; color: var(--text-strong); font-weight: var(--font-weight-medium); - margin-top: 2rem; - margin-bottom: 0.75rem; + margin-top: 0px; + margin-bottom: 24px; line-height: var(--line-height-large); } @@ -40,7 +40,7 @@ /* Paragraphs */ p { - margin-bottom: 1rem; + margin-bottom: 12px; } /* Links */ @@ -58,10 +58,10 @@ /* Lists */ ul, ol { - margin-top: 0.5rem; - margin-bottom: 1rem; + margin-top: 8px; + margin-bottom: 12px; margin-left: 0; - padding-left: 1.5rem; + padding-left: 32px; list-style-position: outside; } @@ -75,7 +75,7 @@ } li { - margin-bottom: 0.5rem; + margin-bottom: 8px; } li > p:first-child { @@ -117,12 +117,12 @@ hr { border: none; height: 0; - margin: 2.5rem 0; + margin: 40px 0; } .shiki { font-size: 13px; - padding: 8px 12px; + padding: 12px; border-radius: 6px; border: 0.5px solid var(--border-weak-base); } @@ -201,8 +201,8 @@ } pre { - margin-top: 2rem; - margin-bottom: 2rem; + margin-top: 12px; + margin-bottom: 32px; overflow: auto; scrollbar-width: none; @@ -229,7 +229,7 @@ table { width: 100%; border-collapse: collapse; - margin: 1.5rem 0; + margin: 24px 0; font-size: var(--font-size-base); display: block; overflow-x: auto; @@ -239,7 +239,7 @@ td { /* Minimal borders for structure, matching TUI "lines" roughly but keeping it web-clean */ border-bottom: 1px solid var(--border-weaker-base); - padding: 0.75rem 0.5rem; + padding: 12px; text-align: left; vertical-align: top; } diff --git a/packages/ui/src/components/message-part.css b/packages/ui/src/components/message-part.css index d9893503fb..c84a368922 100644 --- a/packages/ui/src/components/message-part.css +++ b/packages/ui/src/components/message-part.css @@ -283,9 +283,9 @@ line-height: var(--line-height-normal); [data-component="markdown"] { - margin-top: 24px; + margin-top: 16px; font-style: normal; - font-size: var(--font-size-base); + font-size: 13px; color: var(--text-weak); strong, @@ -556,9 +556,12 @@ [data-component="exa-tool-output"] { width: 100%; - padding-top: 8px; display: flex; flex-direction: column; + font-family: var(--font-family-sans); + font-size: var(--font-size-base); + line-height: var(--line-height-large); + color: var(--text-base); } [data-slot="basic-tool-tool-subtitle"].exa-tool-query { @@ -578,6 +581,8 @@ [data-slot="exa-tool-link"] { display: block; max-width: 100%; + font: inherit; + line-height: inherit; color: var(--text-interactive-base); text-decoration: underline; text-underline-offset: 2px; @@ -636,13 +641,13 @@ } [data-component="context-tool-group-list"] { - padding-top: 6px; + padding-top: 0; padding-right: 0; - padding-bottom: 4px; - padding-left: 13px; + padding-bottom: 0; + padding-left: 12px; display: flex; flex-direction: column; - gap: 8px; + gap: 4px; [data-slot="context-tool-group-item"] { min-width: 0; diff --git a/packages/ui/src/components/session-turn.css b/packages/ui/src/components/session-turn.css index b01343a01d..54076f3f89 100644 --- a/packages/ui/src/components/session-turn.css +++ b/packages/ui/src/components/session-turn.css @@ -26,7 +26,7 @@ align-items: flex-start; align-self: stretch; min-width: 0; - gap: 18px; + gap: 0px; overflow-anchor: none; } @@ -47,6 +47,7 @@ display: flex; align-items: center; gap: 8px; + margin-top: 12px; width: 100%; min-width: 0; color: var(--text-weak); @@ -226,5 +227,5 @@ } [data-slot="session-turn-list"] { - gap: 48px; + gap: 24px; } diff --git a/packages/ui/src/components/timeline-playground.stories.tsx b/packages/ui/src/components/timeline-playground.stories.tsx index e79e97a3ab..98cdf85001 100644 --- a/packages/ui/src/components/timeline-playground.stories.tsx +++ b/packages/ui/src/components/timeline-playground.stories.tsx @@ -568,6 +568,7 @@ const MD = "markdown.css" const MP = "message-part.css" const ST = "session-turn.css" const CL = "collapsible.css" +const BT = "basic-tool.css" /** * Source mapping for a CSS control. @@ -607,10 +608,10 @@ const CSS_CONTROLS: CSSControl[] = [ // --- Timeline spacing --- { key: "turn-gap", - label: "Turn gap", + label: "Above user messages", group: "Timeline Spacing", type: "range", - initial: "48", + initial: "32", selector: '[data-slot="session-turn-list"]', property: "gap", min: "0", @@ -621,10 +622,10 @@ const CSS_CONTROLS: CSSControl[] = [ }, { key: "container-gap", - label: "Container gap", + label: "Below user messages", group: "Timeline Spacing", type: "range", - initial: "18", + initial: "0", selector: '[data-slot="session-turn-message-container"]', property: "gap", min: "0", @@ -1040,12 +1041,40 @@ const CSS_CONTROLS: CSSControl[] = [ }, // --- Tool parts --- + { + key: "tool-subtitle-font-size", + label: "Subtitle font size", + group: "Tool Parts", + type: "range", + initial: "14", + selector: '[data-slot="basic-tool-tool-subtitle"]', + property: "font-size", + min: "10", + max: "22", + step: "1", + unit: "px", + source: { file: BT, anchor: '[data-slot="basic-tool-tool-subtitle"]', prop: "font-size", format: px }, + }, + { + key: "exa-output-font-size", + label: "Search output font size", + group: "Tool Parts", + type: "range", + initial: "14", + selector: '[data-component="exa-tool-output"]', + property: "font-size", + min: "10", + max: "22", + step: "1", + unit: "px", + source: { file: MP, anchor: '[data-component="exa-tool-output"]', prop: "font-size", format: px }, + }, { key: "tool-content-gap", label: "Trigger/content gap", group: "Tool Parts", type: "range", - initial: "8", + initial: "4", selector: '[data-component="collapsible"].tool-collapsible', property: "--tool-content-gap", min: "0", @@ -1059,7 +1088,7 @@ const CSS_CONTROLS: CSSControl[] = [ label: "Explored tool gap", group: "Explored Group", type: "range", - initial: "14", + initial: "4", selector: '[data-component="context-tool-group-list"]', property: "gap", min: "0", diff --git a/session.json b/session.json new file mode 100644 index 0000000000..e69de29bb2 From 672ee28635f471c5fcdc7e77c518d4465678d786 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 19:20:25 -0400 Subject: [PATCH 157/300] fix(opencode): avoid org lookup during config startup (#22670) --- AGENTS.md | 26 -------------------------- packages/opencode/src/config/config.ts | 20 +++++++++++--------- 2 files changed, 11 insertions(+), 35 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 0b080ac4e2..a7895c831f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,36 +11,10 @@ - Keep things in one function unless composable or reusable - Avoid `try`/`catch` where possible - Avoid using the `any` type -- Prefer single word variable names where possible - Use Bun APIs when possible, like `Bun.file()` - Rely on type inference when possible; avoid explicit type annotations or interfaces unless necessary for exports or clarity - Prefer functional array methods (flatMap, filter, map) over for loops; use type guards on filter to maintain type inference downstream -### Naming - -Prefer single word names for variables and functions. Only use multiple words if necessary. - -### Naming Enforcement (Read This) - -THIS RULE IS MANDATORY FOR AGENT WRITTEN CODE. - -- Use single word names by default for new locals, params, and helper functions. -- Multi-word names are allowed only when a single word would be unclear or ambiguous. -- Do not introduce new camelCase compounds when a short single-word alternative is clear. -- Before finishing edits, review touched lines and shorten newly introduced identifiers where possible. -- Good short names to prefer: `pid`, `cfg`, `err`, `opts`, `dir`, `root`, `child`, `state`, `timeout`. -- Examples to avoid unless truly required: `inputPID`, `existingClient`, `connectTimeout`, `workerPath`. - -```ts -// Good -const foo = 1 -function journal(dir: string) {} - -// Bad -const fooBar = 1 -function prepareJournal(dir: string) {} -``` - Reduce total variable count by inlining when a value is only used once. ```ts diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 915e604e90..6aee4e1dc8 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -1277,7 +1277,7 @@ export namespace Config { return yield* cachedGlobal }) - const install = Effect.fnUntraced(function* (dir: string) { + const install = Effect.fn("Config.install")(function* (dir: string) { const pkg = path.join(dir, "package.json") const gitignore = path.join(dir, ".gitignore") const plugin = path.join(dir, "node_modules", "@opencode-ai", "plugin", "package.json") @@ -1345,7 +1345,7 @@ export namespace Config { ) }) - const loadInstanceState = Effect.fnUntraced(function* (ctx: InstanceContext) { + const loadInstanceState = Effect.fn("Config.loadInstanceState")(function* (ctx: InstanceContext) { const auth = yield* authSvc.all().pipe(Effect.orDie) let result: Info = {} @@ -1468,13 +1468,16 @@ export namespace Config { log.debug("loaded custom config from OPENCODE_CONFIG_CONTENT") } - const activeOrg = Option.getOrUndefined( - yield* accountSvc.activeOrg().pipe(Effect.catch(() => Effect.succeed(Option.none()))), + const activeAccount = Option.getOrUndefined( + yield* accountSvc.active().pipe(Effect.catch(() => Effect.succeed(Option.none()))), ) - if (activeOrg) { + if (activeAccount?.active_org_id) { + const accountID = activeAccount.id + const orgID = activeAccount.active_org_id + const url = activeAccount.url yield* Effect.gen(function* () { const [configOpt, tokenOpt] = yield* Effect.all( - [accountSvc.config(activeOrg.account.id, activeOrg.org.id), accountSvc.token(activeOrg.account.id)], + [accountSvc.config(accountID, orgID), accountSvc.token(accountID)], { concurrency: 2 }, ) if (Option.isSome(tokenOpt)) { @@ -1482,10 +1485,8 @@ export namespace Config { yield* env.set("OPENCODE_CONSOLE_TOKEN", tokenOpt.value) } - activeOrgName = activeOrg.org.name - if (Option.isSome(configOpt)) { - const source = `${activeOrg.account.url}/api/config` + const source = `${url}/api/config` const next = yield* loadConfig(JSON.stringify(configOpt.value), { dir: path.dirname(source), source, @@ -1496,6 +1497,7 @@ export namespace Config { yield* merge(source, next, "global") } }).pipe( + Effect.withSpan("Config.loadActiveOrgConfig"), Effect.catch((err) => { log.debug("failed to fetch remote account config", { error: err instanceof Error ? err.message : String(err), From 4dd0d1f67e6f600f629617e3b7c7881f4d4a1a6e Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 19:36:30 -0400 Subject: [PATCH 158/300] refactor(opencode): use AppFileSystem path helpers (#22637) --- packages/opencode/src/config/tui.ts | 5 ++--- packages/opencode/src/file/time.ts | 9 ++++----- packages/opencode/src/project/instance.ts | 10 +++++----- packages/opencode/src/server/instance/middleware.ts | 4 ++-- packages/opencode/src/tool/edit.ts | 3 +-- 5 files changed, 14 insertions(+), 17 deletions(-) diff --git a/packages/opencode/src/config/tui.ts b/packages/opencode/src/config/tui.ts index 87c39e700a..12bd7e0dac 100644 --- a/packages/opencode/src/config/tui.ts +++ b/packages/opencode/src/config/tui.ts @@ -10,7 +10,6 @@ import { Flag } from "@/flag/flag" import { Log } from "@/util/log" import { isRecord } from "@/util/record" import { Global } from "@/global" -import { Filesystem } from "@/util/filesystem" import { InstanceState } from "@/effect/instance-state" import { makeRuntime } from "@/effect/run-service" import { AppFileSystem } from "@opencode-ai/shared/filesystem" @@ -42,8 +41,8 @@ export namespace TuiConfig { export class Service extends Context.Service()("@opencode/TuiConfig") {} function pluginScope(file: string, ctx: { directory: string; worktree: string }): Config.PluginScope { - if (Filesystem.contains(ctx.directory, file)) return "local" - if (ctx.worktree !== "/" && Filesystem.contains(ctx.worktree, file)) return "local" + if (AppFileSystem.contains(ctx.directory, file)) return "local" + if (ctx.worktree !== "/" && AppFileSystem.contains(ctx.worktree, file)) return "local" return "global" } diff --git a/packages/opencode/src/file/time.ts b/packages/opencode/src/file/time.ts index 5537526730..853da3bd98 100644 --- a/packages/opencode/src/file/time.ts +++ b/packages/opencode/src/file/time.ts @@ -3,7 +3,6 @@ import { InstanceState } from "@/effect/instance-state" import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { Flag } from "@/flag/flag" import type { SessionID } from "@/session/schema" -import { Filesystem } from "@/util/filesystem" import { Log } from "../util/log" export namespace FileTime { @@ -62,7 +61,7 @@ export namespace FileTime { ) const getLock = Effect.fn("FileTime.lock")(function* (filepath: string) { - filepath = Filesystem.normalizePath(filepath) + filepath = AppFileSystem.normalizePath(filepath) const locks = (yield* InstanceState.get(state)).locks const lock = locks.get(filepath) if (lock) return lock @@ -73,21 +72,21 @@ export namespace FileTime { }) const read = Effect.fn("FileTime.read")(function* (sessionID: SessionID, file: string) { - file = Filesystem.normalizePath(file) + file = AppFileSystem.normalizePath(file) const reads = (yield* InstanceState.get(state)).reads log.info("read", { sessionID, file }) session(reads, sessionID).set(file, yield* stamp(file)) }) const get = Effect.fn("FileTime.get")(function* (sessionID: SessionID, file: string) { - file = Filesystem.normalizePath(file) + file = AppFileSystem.normalizePath(file) const reads = (yield* InstanceState.get(state)).reads return reads.get(sessionID)?.get(file)?.read }) const assert = Effect.fn("FileTime.assert")(function* (sessionID: SessionID, filepath: string) { if (disableCheck) return - filepath = Filesystem.normalizePath(filepath) + filepath = AppFileSystem.normalizePath(filepath) const reads = (yield* InstanceState.get(state)).reads const time = reads.get(sessionID)?.get(filepath) diff --git a/packages/opencode/src/project/instance.ts b/packages/opencode/src/project/instance.ts index 51ae669dc8..2a20ecac97 100644 --- a/packages/opencode/src/project/instance.ts +++ b/packages/opencode/src/project/instance.ts @@ -1,7 +1,7 @@ import { GlobalBus } from "@/bus/global" import { disposeInstance } from "@/effect/instance-registry" import { makeRuntime } from "@/effect/run-service" -import { Filesystem } from "@/util/filesystem" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { iife } from "@/util/iife" import { Log } from "@/util/log" import { LocalContext } from "../util/local-context" @@ -56,7 +56,7 @@ function track(directory: string, next: Promise) { export const Instance = { async provide(input: { directory: string; init?: () => Promise; fn: () => R }): Promise { - const directory = Filesystem.resolve(input.directory) + const directory = AppFileSystem.resolve(input.directory) let existing = cache.get(directory) if (!existing) { Log.Default.info("creating instance", { directory }) @@ -93,11 +93,11 @@ export const Instance = { */ containsPath(filepath: string, ctx?: InstanceContext) { const instance = ctx ?? Instance - if (Filesystem.contains(instance.directory, filepath)) return true + if (AppFileSystem.contains(instance.directory, filepath)) return true // Non-git projects set worktree to "/" which would match ANY absolute path. // Skip worktree check in this case to preserve external_directory permissions. if (Instance.worktree === "/") return false - return Filesystem.contains(instance.worktree, filepath) + return AppFileSystem.contains(instance.worktree, filepath) }, /** * Captures the current instance ALS context and returns a wrapper that @@ -117,7 +117,7 @@ export const Instance = { return context.provide(ctx, fn) }, async reload(input: { directory: string; init?: () => Promise; project?: Project.Info; worktree?: string }) { - const directory = Filesystem.resolve(input.directory) + const directory = AppFileSystem.resolve(input.directory) Log.Default.info("reloading instance", { directory }) await disposeInstance(directory) cache.delete(directory) diff --git a/packages/opencode/src/server/instance/middleware.ts b/packages/opencode/src/server/instance/middleware.ts index 824c265efe..549fb38d5d 100644 --- a/packages/opencode/src/server/instance/middleware.ts +++ b/packages/opencode/src/server/instance/middleware.ts @@ -4,7 +4,6 @@ import { getAdaptor } from "@/control-plane/adaptors" import { WorkspaceID } from "@/control-plane/schema" import { Workspace } from "@/control-plane/workspace" import { ServerProxy } from "../proxy" -import { Filesystem } from "@/util/filesystem" import { Instance } from "@/project/instance" import { InstanceBootstrap } from "@/project/bootstrap" import { Session } from "@/session" @@ -12,6 +11,7 @@ import { SessionID } from "@/session/schema" import { WorkspaceContext } from "@/control-plane/workspace-context" import { AppRuntime } from "@/effect/app-runtime" import { Log } from "@/util/log" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" type Rule = { method?: string; path: string; exact?: boolean; action: "local" | "forward" } @@ -53,7 +53,7 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware return async (c, next) => { const raw = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd() - const directory = Filesystem.resolve( + const directory = AppFileSystem.resolve( (() => { try { return decodeURIComponent(raw) diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index bc8478e39f..5c82463945 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -15,7 +15,6 @@ import { FileWatcher } from "../file/watcher" import { Bus } from "../bus" import { Format } from "../format" import { FileTime } from "../file/time" -import { Filesystem } from "../util/filesystem" import { Instance } from "../project/instance" import { Snapshot } from "@/snapshot" import { assertExternalDirectoryEffect } from "./external-directory" @@ -169,7 +168,7 @@ export const EditTool = Tool.define( let output = "Edit applied successfully." yield* lsp.touchFile(filePath, true) const diagnostics = yield* lsp.diagnostics() - const normalizedFilePath = Filesystem.normalizePath(filePath) + const normalizedFilePath = AppFileSystem.normalizePath(filePath) const block = LSP.Diagnostic.report(filePath, diagnostics[normalizedFilePath] ?? []) if (block) output += `\n\nLSP errors detected in this file, please fix:\n${block}` From a554fad2327c68b2dc562a19e62a96415028b6d8 Mon Sep 17 00:00:00 2001 From: Carlo Wood Date: Thu, 16 Apr 2026 01:41:35 +0200 Subject: [PATCH 159/300] fix(tui): Don't overwrite the agent that was specified on the command line (#20554) --- packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 5a3e1d451d..d0f5b481cb 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -37,6 +37,7 @@ import { useToast } from "../../ui/toast" import { useKV } from "../../context/kv" import { useTextareaKeybindings } from "../textarea-keybindings" import { DialogSkill } from "../dialog-skill" +import { useArgs } from "@tui/context/args" export type PromptProps = { sessionID?: string @@ -81,6 +82,7 @@ export function Prompt(props: PromptProps) { const keybind = useKeybind() const local = useLocal() + const args = useArgs() const sdk = useSDK() const route = useRoute() const sync = useSync() @@ -202,7 +204,8 @@ export function Prompt(props: PromptProps) { // Only set agent if it's a primary agent (not a subagent) const isPrimaryAgent = local.agent.list().some((x) => x.name === msg.agent) if (msg.agent && isPrimaryAgent) { - local.agent.set(msg.agent) + // Keep command line --agent if specified. + if (!args.agent) local.agent.set(msg.agent) if (msg.model) { local.model.set(msg.model) local.model.variant.set(msg.model.variant) From 3d6f90cb536ec30ff5091e1cbe3b1e619a93e1b0 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 20:45:19 -0400 Subject: [PATCH 160/300] feat: add oxlint with correctness defaults (#22682) --- .oxlintrc.json | 10 +++++ bun.lock | 41 +++++++++++++++++++ package.json | 2 + packages/app/src/components/file-tree.tsx | 2 +- packages/app/src/components/terminal.tsx | 2 +- .../src/context/global-sync/child-store.ts | 4 +- packages/app/src/context/layout.tsx | 4 +- .../app/src/routes/zen/util/handler.ts | 2 +- packages/console/core/src/key.ts | 8 +--- packages/desktop-electron/src/main/apps.ts | 2 +- .../desktop-electron/src/main/shell-env.ts | 2 +- packages/opencode/script/build.ts | 4 +- packages/opencode/src/agent/agent.ts | 2 +- packages/opencode/src/cli/cmd/providers.ts | 4 +- packages/opencode/src/config/tui.ts | 2 +- packages/opencode/src/lsp/launch.ts | 2 +- packages/opencode/src/plugin/cloudflare.ts | 6 +-- packages/opencode/src/plugin/meta.ts | 2 +- packages/opencode/src/provider/provider.ts | 4 +- .../opencode/src/server/instance/session.ts | 2 +- packages/opencode/src/session/prompt.ts | 2 +- packages/opencode/src/tool/bash.ts | 2 +- .../opencode/test/cli/tui/theme-store.test.ts | 2 +- .../test/plugin/loader-shared.test.ts | 10 ++--- .../test/plugin/workspace-adaptor.test.ts | 2 +- packages/shared/src/util/path.ts | 8 ++-- packages/ui/src/components/accordion.tsx | 10 ++--- packages/ui/src/components/app-icon.tsx | 2 +- packages/ui/src/components/avatar.tsx | 2 +- packages/ui/src/components/button.tsx | 2 +- packages/ui/src/components/card.tsx | 8 ++-- packages/ui/src/components/collapsible.tsx | 2 +- packages/ui/src/components/context-menu.tsx | 32 +++++++-------- packages/ui/src/components/dialog.tsx | 2 +- packages/ui/src/components/dock-surface.tsx | 6 +-- packages/ui/src/components/dropdown-menu.tsx | 32 +++++++-------- packages/ui/src/components/file-icon.tsx | 2 +- packages/ui/src/components/file-ssr.tsx | 4 +- packages/ui/src/components/file.tsx | 2 +- packages/ui/src/components/hover-card.tsx | 2 +- packages/ui/src/components/icon-button.tsx | 2 +- packages/ui/src/components/icon.tsx | 2 +- packages/ui/src/components/keybind.tsx | 2 +- packages/ui/src/components/markdown.tsx | 4 +- packages/ui/src/components/popover.tsx | 2 +- .../ui/src/components/progress-circle.tsx | 2 +- packages/ui/src/components/progress.tsx | 2 +- packages/ui/src/components/provider-icon.tsx | 2 +- packages/ui/src/components/radio-group.tsx | 2 +- packages/ui/src/components/resize-handle.tsx | 2 +- packages/ui/src/components/select.tsx | 6 +-- packages/ui/src/components/session-turn.tsx | 2 +- packages/ui/src/components/spinner.tsx | 2 +- .../components/sticky-accordion-header.tsx | 2 +- packages/ui/src/components/tabs.tsx | 8 ++-- packages/ui/src/components/tag.tsx | 2 +- packages/ui/src/components/toast.tsx | 2 +- 57 files changed, 165 insertions(+), 122 deletions(-) create mode 100644 .oxlintrc.json diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000000..0875f38326 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://raw.githubusercontent.com/nicolo-ribaudo/oxc-project.github.io/refs/heads/json-schema/src/public/.oxlintrc.schema.json", + "rules": { + // Effect uses `function*` with Effect.gen/Effect.fnUntraced that don't always yield + "require-yield": "off", + // SolidJS uses `let ref: T | undefined` for JSX ref bindings assigned at runtime + "no-unassigned-vars": "off" + }, + "ignorePatterns": ["**/node_modules", "**/dist", "**/.build", "**/.sst", "**/*.d.ts"] +} diff --git a/bun.lock b/bun.lock index aeab042cf3..48243e652e 100644 --- a/bun.lock +++ b/bun.lock @@ -19,6 +19,7 @@ "@typescript/native-preview": "catalog:", "glob": "13.0.5", "husky": "9.1.7", + "oxlint": "1.60.0", "prettier": "3.6.2", "semver": "^7.6.0", "sst": "3.18.10", @@ -1693,6 +1694,44 @@ "@oxc-transform/binding-win32-x64-msvc": ["@oxc-transform/binding-win32-x64-msvc@0.96.0", "", { "os": "win32", "cpu": "x64" }, "sha512-0fI0P0W7bSO/GCP/N5dkmtB9vBqCA4ggo1WmXTnxNJVmFFOtcA1vYm1I9jl8fxo+sucW2WnlpnI4fjKdo3JKxA=="], + "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.60.0", "", { "os": "android", "cpu": "arm" }, "sha512-YdeJKaZckDQL1qa62a1aKq/goyq48aX3yOxaaWqWb4sau4Ee4IiLbamftNLU3zbePky6QsDj6thnSSzHRBjDfA=="], + + "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.60.0", "", { "os": "android", "cpu": "arm64" }, "sha512-7ANS7PpXCfq84xZQ8E5WPs14gwcuPcl+/8TFNXfpSu0CQBXz3cUo2fDpHT8v8HJN+Ut02eacvMAzTnc9s6X4tw=="], + + "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.60.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-pJsgd9AfplLGBm1fIr25V6V14vMrayhx4uIQvlfH7jWs2SZwSrvi3TfgfJySB8T+hvyEH8K2zXljQiUnkgUnfQ=="], + + "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.60.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ue1aXHX49ivwflKqGJc7zcd/LeLgbhaTcDCQStgx5x06AXgjEAZmvrlMuIkWd4AL4FHQe6QJ9f33z04Cg448VQ=="], + + "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.60.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-YCyQzsQtusQw+gNRW9rRTifSO+Dt/+dtCl2NHoDMZqJlRTEZ/Oht9YnuporI9yiTx7+cB+eqzX3MtHHVHGIWhg=="], + + "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.60.0", "", { "os": "linux", "cpu": "arm" }, "sha512-c7dxM2Zksa45Qw16i2iGY3Fti2NirJ38FrsBsKw+qcJ0OtqTsBgKJLF0xV+yLG56UH01Z8WRPgsw31e0MoRoGQ=="], + + "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.60.0", "", { "os": "linux", "cpu": "arm" }, "sha512-ZWALoA42UYqBEP1Tbw9OWURgFGS1nWj2AAvLdY6ZcGx/Gj93qVCBKjcvwXMupZibYwFbi9s/rzqkZseb/6gVtQ=="], + + "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.60.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-tpy+1w4p9hN5CicMCxqNy6ymfRtV5ayE573vFNjp1k1TN/qhLFgflveZoE/0++RlkHikBz2vY545NWm/hp7big=="], + + "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.60.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-eDYDXZGhQAXyn6GwtwiX/qcLS0HlOLPJ/+iiIY8RYr+3P8oKBmgKxADLlniL6FtWfE7pPk7IGN9/xvDEvDvFeg=="], + + "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.60.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nxehly5XYBHUWI9VJX1bqCf9j/B43DaK/aS/T1fcxCpX3PA4Rm9BB54nPD1CKayT8xg6REN1ao+01hSRNgy8OA=="], + + "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-j1qf/NaUfOWQutjeoooNG1Q0zsK0XGmSu1uDLq3cctquRF3j7t9Hxqf/76ehCc5GEUAanth2W4Fa+XT1RFg/nw=="], + + "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-YELKPRefQ/q/h3RUmeRfPCUhh2wBvgV1RyZ/F9M9u8cDyXsQW2ojv1DeWQTt466yczDITjZnIOg/s05pk7Ve2A=="], + + "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.60.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-JkO3C6Gki7Y6h/MiIkFKvHFOz98/YWvQ4WYbK9DLXACMP2rjULzkeGyAzorJE5S1dzLQGFgeqvN779kSFwoV1g=="], + + "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.60.0", "", { "os": "linux", "cpu": "x64" }, "sha512-XjKHdFVCpZZZSWBCKyyqCq65s2AKXykMXkjLoKYODrD+f5toLhlwsMESscu8FbgnJQ4Y/dpR/zdazsahmgBJIA=="], + + "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.60.0", "", { "os": "linux", "cpu": "x64" }, "sha512-js29ZWIuPhNWzY8NC7KoffEMEeWG105vbmm+8EOJsC+T/jHBiKIJEUF78+F/IrgEWMMP9N0kRND4Pp75+xAhKg=="], + + "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.60.0", "", { "os": "none", "cpu": "arm64" }, "sha512-H+PUITKHk04stFpWj3x3Kg08Afp/bcXSBi0EhasR5a0Vw7StXHTzdl655PUI0fB4qdh2Wsu6Dsi+3ACxPoyQnA=="], + + "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.60.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-WA/yc7f7ZfCefBXVzNHn1Ztulb1EFwNBb4jMZ6pjML0zz6pHujlF3Q3jySluz3XHl/GNeMTntG1seUBWVMlMag=="], + + "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.60.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-33YxL1sqwYNZXtn3MD/4dno6s0xeedXOJlT1WohkVD565WvohClZUr7vwKdAk954n4xiEWJkewiCr+zLeq7AeA=="], + + "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.60.0", "", { "os": "win32", "cpu": "x64" }, "sha512-JOro4ZcfBLamJCyfURQmOQByoorgOdx3ZjAkSqnb/CyG/i+lN3KoV5LAgk5ZAW6DPq7/Cx7n23f8DuTWXTWgyQ=="], + "@pagefind/darwin-arm64": ["@pagefind/darwin-arm64@1.5.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MXpI+7HsAdPkvJ0gk9xj9g541BCqBZOBbdwj9g6lB5LCj6kSV6nqDSjzcAJwvOsfu0fjwvC8hQU+ecfhp+MpiQ=="], "@pagefind/darwin-x64": ["@pagefind/darwin-x64@1.5.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-IojxFWMEJe0RQ7PQ3KXQsPIImNsbpPYpoZ+QUDrL8fAl/O27IX+LVLs74/UzEZy5uA2LD8Nz1AiwKr72vrkZQw=="], @@ -4073,6 +4112,8 @@ "oxc-transform": ["oxc-transform@0.96.0", "", { "optionalDependencies": { "@oxc-transform/binding-android-arm64": "0.96.0", "@oxc-transform/binding-darwin-arm64": "0.96.0", "@oxc-transform/binding-darwin-x64": "0.96.0", "@oxc-transform/binding-freebsd-x64": "0.96.0", "@oxc-transform/binding-linux-arm-gnueabihf": "0.96.0", "@oxc-transform/binding-linux-arm-musleabihf": "0.96.0", "@oxc-transform/binding-linux-arm64-gnu": "0.96.0", "@oxc-transform/binding-linux-arm64-musl": "0.96.0", "@oxc-transform/binding-linux-riscv64-gnu": "0.96.0", "@oxc-transform/binding-linux-s390x-gnu": "0.96.0", "@oxc-transform/binding-linux-x64-gnu": "0.96.0", "@oxc-transform/binding-linux-x64-musl": "0.96.0", "@oxc-transform/binding-wasm32-wasi": "0.96.0", "@oxc-transform/binding-win32-arm64-msvc": "0.96.0", "@oxc-transform/binding-win32-x64-msvc": "0.96.0" } }, "sha512-dQPNIF+gHpSkmC0+Vg9IktNyhcn28Y8R3eTLyzn52UNymkasLicl3sFAtz7oEVuFmCpgGjaUTKkwk+jW2cHpDQ=="], + "oxlint": ["oxlint@1.60.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.60.0", "@oxlint/binding-android-arm64": "1.60.0", "@oxlint/binding-darwin-arm64": "1.60.0", "@oxlint/binding-darwin-x64": "1.60.0", "@oxlint/binding-freebsd-x64": "1.60.0", "@oxlint/binding-linux-arm-gnueabihf": "1.60.0", "@oxlint/binding-linux-arm-musleabihf": "1.60.0", "@oxlint/binding-linux-arm64-gnu": "1.60.0", "@oxlint/binding-linux-arm64-musl": "1.60.0", "@oxlint/binding-linux-ppc64-gnu": "1.60.0", "@oxlint/binding-linux-riscv64-gnu": "1.60.0", "@oxlint/binding-linux-riscv64-musl": "1.60.0", "@oxlint/binding-linux-s390x-gnu": "1.60.0", "@oxlint/binding-linux-x64-gnu": "1.60.0", "@oxlint/binding-linux-x64-musl": "1.60.0", "@oxlint/binding-openharmony-arm64": "1.60.0", "@oxlint/binding-win32-arm64-msvc": "1.60.0", "@oxlint/binding-win32-ia32-msvc": "1.60.0", "@oxlint/binding-win32-x64-msvc": "1.60.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.18.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-tnRzTWiWJ9pg3ftRWnD0+Oqh78L6ZSwcEudvCZaER0PIqiAnNyXj5N1dPwjmNpDalkKS9m/WMLN1CTPUBPmsgw=="], + "p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="], "p-defer": ["p-defer@3.0.0", "", {}, "sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw=="], diff --git a/package.json b/package.json index abe1b5d362..8c5ae91955 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dev:web": "bun --cwd packages/app dev", "dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev", "dev:storybook": "bun --cwd packages/storybook storybook", + "lint": "oxlint", "typecheck": "bun turbo typecheck", "postinstall": "bun run --cwd packages/opencode fix-node-pty", "prepare": "husky", @@ -85,6 +86,7 @@ "@typescript/native-preview": "catalog:", "glob": "13.0.5", "husky": "9.1.7", + "oxlint": "1.60.0", "prettier": "3.6.2", "semver": "^7.6.0", "sst": "3.18.10", diff --git a/packages/app/src/components/file-tree.tsx b/packages/app/src/components/file-tree.tsx index 930832fb65..8fbecf6712 100644 --- a/packages/app/src/components/file-tree.tsx +++ b/packages/app/src/components/file-tree.tsx @@ -149,7 +149,7 @@ const FileTreeNode = ( classList={{ "w-full min-w-0 h-6 flex items-center justify-start gap-x-1.5 rounded-md px-1.5 py-0 text-left hover:bg-surface-raised-base-hover active:bg-surface-base-active transition-colors cursor-pointer": true, "bg-surface-base-active": local.node.path === local.active, - ...(local.classList ?? {}), + ...local.classList, [local.class ?? ""]: !!local.class, [local.nodeClass ?? ""]: !!local.nodeClass, }} diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx index 96a865b9e8..9b7ef83b28 100644 --- a/packages/app/src/components/terminal.tsx +++ b/packages/app/src/components/terminal.tsx @@ -634,7 +634,7 @@ export const Terminal = (props: TerminalProps) => { tabIndex={-1} style={{ "background-color": terminalColors().background }} classList={{ - ...(local.classList ?? {}), + ...local.classList, "select-text": true, "size-full px-6 py-3 font-mono relative overflow-hidden": true, [local.class ?? ""]: !!local.class, diff --git a/packages/app/src/context/global-sync/child-store.ts b/packages/app/src/context/global-sync/child-store.ts index 5678491f89..3fe67e4fbe 100644 --- a/packages/app/src/context/global-sync/child-store.ts +++ b/packages/app/src/context/global-sync/child-store.ts @@ -243,8 +243,8 @@ export function createChildStoreManager(input: { const cached = metaCache.get(directory) if (!cached) return const previous = store.projectMeta ?? {} - const icon = patch.icon ? { ...(previous.icon ?? {}), ...patch.icon } : previous.icon - const commands = patch.commands ? { ...(previous.commands ?? {}), ...patch.commands } : previous.commands + const icon = patch.icon ? { ...previous.icon, ...patch.icon } : previous.icon + const commands = patch.commands ? { ...previous.commands, ...patch.commands } : previous.commands const next = { ...previous, ...patch, diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx index bab3d39f38..87f11d2b64 100644 --- a/packages/app/src/context/layout.tsx +++ b/packages/app/src/context/layout.tsx @@ -344,7 +344,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( return } - setStore("sessionView", sessionKey, "scroll", (prev) => ({ ...(prev ?? {}), ...next })) + setStore("sessionView", sessionKey, "scroll", (prev) => ({ ...prev, ...next })) prune(keep) }, }) @@ -399,7 +399,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( local?.icon?.color !== undefined const base = { - ...(metadata ?? {}), + ...metadata, ...project, icon: { url: metadata?.icon?.url, diff --git a/packages/console/app/src/routes/zen/util/handler.ts b/packages/console/app/src/routes/zen/util/handler.ts index 58df618094..358d8736c4 100644 --- a/packages/console/app/src/routes/zen/util/handler.ts +++ b/packages/console/app/src/routes/zen/util/handler.ts @@ -144,7 +144,7 @@ export async function handler( providerInfo.modifyBody({ ...createBodyConverter(opts.format, providerInfo.format)(body), model: providerInfo.model, - ...(providerInfo.payloadModifier ?? {}), + ...providerInfo.payloadModifier, ...Object.fromEntries( Object.entries(providerInfo.payloadMappings ?? {}) .map(([k, v]) => [k, input.request.headers.get(v)]) diff --git a/packages/console/core/src/key.ts b/packages/console/core/src/key.ts index 688f19b3d8..d1aae15240 100644 --- a/packages/console/core/src/key.ts +++ b/packages/console/core/src/key.ts @@ -24,11 +24,9 @@ export namespace Key { .innerJoin(AuthTable, and(eq(UserTable.accountID, AuthTable.accountID), eq(AuthTable.provider, "email"))) .where( and( - ...[ - eq(KeyTable.workspaceID, Actor.workspace()), + eq(KeyTable.workspaceID, Actor.workspace()), isNull(KeyTable.timeDeleted), ...(Actor.userRole() === "admin" ? [] : [eq(KeyTable.userID, Actor.userID())]), - ], ), ) .orderBy(sql`${KeyTable.name} DESC`), @@ -84,11 +82,9 @@ export namespace Key { }) .where( and( - ...[ - eq(KeyTable.id, input.id), + eq(KeyTable.id, input.id), eq(KeyTable.workspaceID, Actor.workspace()), ...(Actor.userRole() === "admin" ? [] : [eq(KeyTable.userID, Actor.userID())]), - ], ), ), ) diff --git a/packages/desktop-electron/src/main/apps.ts b/packages/desktop-electron/src/main/apps.ts index 2b46037894..d21b6cc9e3 100644 --- a/packages/desktop-electron/src/main/apps.ts +++ b/packages/desktop-electron/src/main/apps.ts @@ -20,7 +20,7 @@ export function wslPath(path: string, mode: "windows" | "linux" | null): string try { if (path.startsWith("~")) { const suffix = path.slice(1) - const cmd = `wslpath ${flag} \"$HOME${suffix.replace(/\"/g, '\\"')}\"` + const cmd = `wslpath ${flag} "$HOME${suffix.replace(/"/g, '\\"')}"` const output = execFileSync("wsl", ["-e", "sh", "-lc", cmd]) return output.toString().trim() } diff --git a/packages/desktop-electron/src/main/shell-env.ts b/packages/desktop-electron/src/main/shell-env.ts index 8453a5730d..f57677323c 100644 --- a/packages/desktop-electron/src/main/shell-env.ts +++ b/packages/desktop-electron/src/main/shell-env.ts @@ -82,7 +82,7 @@ export function loadShellEnv(shell: string) { export function mergeShellEnv(shell: Record | null, env: Record) { return { - ...(shell || {}), + ...shell, ...env, } } diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts index 6d1087f287..d2628974fa 100755 --- a/packages/opencode/script/build.ts +++ b/packages/opencode/script/build.ts @@ -211,9 +211,7 @@ for (const item of targets) { execArgv: [`--user-agent=opencode/${Script.version}`, "--use-system-ca", "--"], windows: {}, }, - files: { - ...(embeddedFileMap ? { "opencode-web-ui.gen.ts": embeddedFileMap } : {}), - }, + files: (embeddedFileMap ? { "opencode-web-ui.gen.ts": embeddedFileMap } : {}), entrypoints: [ "./src/index.ts", parserWorker, diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 8857696b05..ba38c8efe3 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -368,7 +368,7 @@ export namespace Agent { )), { role: "user", - content: `Create an agent configuration based on this request: \"${input.description}\".\n\nIMPORTANT: The following identifiers already exist and must NOT be used: ${existing.map((i) => i.name).join(", ")}\n Return ONLY the JSON object, no other text, do not wrap in backticks`, + content: `Create an agent configuration based on this request: "${input.description}".\n\nIMPORTANT: The following identifiers already exist and must NOT be used: ${existing.map((i) => i.name).join(", ")}\n Return ONLY the JSON object, no other text, do not wrap in backticks`, }, ], model: language, diff --git a/packages/opencode/src/cli/cmd/providers.ts b/packages/opencode/src/cli/cmd/providers.ts index d2afbabfb0..06f9fdf1d5 100644 --- a/packages/opencode/src/cli/cmd/providers.ts +++ b/packages/opencode/src/cli/cmd/providers.ts @@ -40,12 +40,10 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string, } else if (plugin.auth.methods.length > 1) { const method = await prompts.select({ message: "Login method", - options: [ - ...plugin.auth.methods.map((x, index) => ({ + options: plugin.auth.methods.map((x, index) => ({ label: x.label, value: index.toString(), })), - ], }) if (prompts.isCancel(method)) throw new UI.CancelledError() index = parseInt(method) diff --git a/packages/opencode/src/config/tui.ts b/packages/opencode/src/config/tui.ts index 12bd7e0dac..e64b226c14 100644 --- a/packages/opencode/src/config/tui.ts +++ b/packages/opencode/src/config/tui.ts @@ -125,7 +125,7 @@ export namespace TuiConfig { } } - const keybinds = { ...(acc.result.keybinds ?? {}) } + const keybinds = { ...acc.result.keybinds } if (process.platform === "win32") { // Native Windows terminals do not support POSIX suspend, so prefer prompt undo. keybinds.terminal_suspend = "none" diff --git a/packages/opencode/src/lsp/launch.ts b/packages/opencode/src/lsp/launch.ts index b7dca446f5..51a7c209b4 100644 --- a/packages/opencode/src/lsp/launch.ts +++ b/packages/opencode/src/lsp/launch.ts @@ -9,7 +9,7 @@ export function spawn(cmd: string, argsOrOpts?: string[] | Process.Options, opts const args = Array.isArray(argsOrOpts) ? [...argsOrOpts] : [] const cfg = Array.isArray(argsOrOpts) ? opts : argsOrOpts const proc = Process.spawn([cmd, ...args], { - ...(cfg ?? {}), + ...cfg, stdin: "pipe", stdout: "pipe", stderr: "pipe", diff --git a/packages/opencode/src/plugin/cloudflare.ts b/packages/opencode/src/plugin/cloudflare.ts index e20a488a36..267d1ed2f2 100644 --- a/packages/opencode/src/plugin/cloudflare.ts +++ b/packages/opencode/src/plugin/cloudflare.ts @@ -1,8 +1,7 @@ import type { Hooks, PluginInput } from "@opencode-ai/plugin" export async function CloudflareWorkersAuthPlugin(_input: PluginInput): Promise { - const prompts = [ - ...(!process.env.CLOUDFLARE_ACCOUNT_ID + const prompts = (!process.env.CLOUDFLARE_ACCOUNT_ID ? [ { type: "text" as const, @@ -11,8 +10,7 @@ export async function CloudflareWorkersAuthPlugin(_input: PluginInput): Promise< placeholder: "e.g. 1234567890abcdef1234567890abcdef", }, ] - : []), - ] + : []) return { auth: { diff --git a/packages/opencode/src/plugin/meta.ts b/packages/opencode/src/plugin/meta.ts index f408954690..3f02f543ef 100644 --- a/packages/opencode/src/plugin/meta.ts +++ b/packages/opencode/src/plugin/meta.ts @@ -174,7 +174,7 @@ export namespace PluginMeta { const entry = store[id] if (!entry) return entry.themes = { - ...(entry.themes ?? {}), + ...entry.themes, [name]: theme, } await Filesystem.writeJson(file, store) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 9ec5dfc6b5..c029e5c5c6 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -551,13 +551,13 @@ export namespace Provider { const aiGatewayHeaders = { "User-Agent": `opencode/${Installation.VERSION} gitlab-ai-provider/${GITLAB_PROVIDER_VERSION} (${os.platform()} ${os.release()}; ${os.arch()})`, "anthropic-beta": "context-1m-2025-08-07", - ...(providerConfig?.options?.aiGatewayHeaders || {}), + ...providerConfig?.options?.aiGatewayHeaders, } const featureFlags = { duo_agent_platform_agentic_chat: true, duo_agent_platform: true, - ...(providerConfig?.options?.featureFlags || {}), + ...providerConfig?.options?.featureFlags, } return { diff --git a/packages/opencode/src/server/instance/session.ts b/packages/opencode/src/server/instance/session.ts index 0bce3085e0..c606af8544 100644 --- a/packages/opencode/src/server/instance/session.ts +++ b/packages/opencode/src/server/instance/session.ts @@ -695,7 +695,7 @@ export const SessionRoutes = lazy(() => url.searchParams.set("limit", query.limit.toString()) url.searchParams.set("before", page.cursor) c.header("Access-Control-Expose-Headers", "Link, X-Next-Cursor") - c.header("Link", `<${url.toString()}>; rel=\"next\"`) + c.header("Link", `<${url.toString()}>; rel="next"`) c.header("X-Next-Cursor", page.cursor) } return c.json(page.items) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index ffd074d3f8..f2a160e268 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -497,7 +497,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the const truncated = yield* truncate.output(textParts.join("\n\n"), {}, input.agent) const metadata = { - ...(result.metadata ?? {}), + ...result.metadata, truncated: truncated.truncated, ...(truncated.truncated && { outputPath: truncated.outputPath }), } diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index 3bb936944c..7a124dadae 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -176,7 +176,7 @@ function dynamic(text: string, ps: boolean) { } function prefix(text: string) { - const match = /[?*\[]/.exec(text) + const match = /[?*[]/.exec(text) if (!match) return text if (match.index === 0) return return text.slice(0, match.index) diff --git a/packages/opencode/test/cli/tui/theme-store.test.ts b/packages/opencode/test/cli/tui/theme-store.test.ts index 936e3e6f7c..9ebfc4320e 100644 --- a/packages/opencode/test/cli/tui/theme-store.test.ts +++ b/packages/opencode/test/cli/tui/theme-store.test.ts @@ -41,7 +41,7 @@ test("hasTheme checks theme presence", () => { test("resolveTheme rejects circular color refs", () => { const item = structuredClone(DEFAULT_THEMES.opencode) item.defs = { - ...(item.defs ?? {}), + ...item.defs, one: "two", two: "one", } diff --git a/packages/opencode/test/plugin/loader-shared.test.ts b/packages/opencode/test/plugin/loader-shared.test.ts index 5f1e2b1686..4265e83c55 100644 --- a/packages/opencode/test/plugin/loader-shared.test.ts +++ b/packages/opencode/test/plugin/loader-shared.test.ts @@ -48,7 +48,7 @@ describe("plugin.loader.shared", () => { file, [ "export default async () => {", - ` await Bun.write(${JSON.stringify(mark)}, \"called\")`, + ` await Bun.write(${JSON.stringify(mark)}, "called")`, " return {}", "}", "", @@ -78,8 +78,8 @@ describe("plugin.loader.shared", () => { file, [ "const run = async () => {", - ` const text = await Bun.file(${JSON.stringify(mark)}).text().catch(() => \"\")`, - ` await Bun.write(${JSON.stringify(mark)}, text + \"1\")`, + ` const text = await Bun.file(${JSON.stringify(mark)}).text().catch(() => "")`, + ` await Bun.write(${JSON.stringify(mark)}, text + "1")`, " return {}", "}", "export default run", @@ -715,7 +715,7 @@ describe("plugin.loader.shared", () => { "const plugin = {", ' id: "demo.object",', " server: async () => {", - ` await Bun.write(${JSON.stringify(mark)}, \"called\")`, + ` await Bun.write(${JSON.stringify(mark)}, "called")`, " return {}", " },", "}", @@ -833,7 +833,7 @@ export default { "export default {", ' id: "demo.pure",', " server: async () => {", - ` await Bun.write(${JSON.stringify(mark)}, \"called\")`, + ` await Bun.write(${JSON.stringify(mark)}, "called")`, " return {}", " },", "}", diff --git a/packages/opencode/test/plugin/workspace-adaptor.test.ts b/packages/opencode/test/plugin/workspace-adaptor.test.ts index a5f56df5e9..669a822a2f 100644 --- a/packages/opencode/test/plugin/workspace-adaptor.test.ts +++ b/packages/opencode/test/plugin/workspace-adaptor.test.ts @@ -39,7 +39,7 @@ describe("plugin.workspace", () => { ' name: "plug",', ' description: "plugin workspace adaptor",', " configure(input) {", - ` return { ...input, name: \"plug\", branch: \"plug/main\", directory: ${JSON.stringify(space)} }`, + ` return { ...input, name: "plug", branch: "plug/main", directory: ${JSON.stringify(space)} }`, " },", " async create(input) {", ` await Bun.write(${JSON.stringify(mark)}, JSON.stringify(input))`, diff --git a/packages/shared/src/util/path.ts b/packages/shared/src/util/path.ts index bb191f5120..b87316358f 100644 --- a/packages/shared/src/util/path.ts +++ b/packages/shared/src/util/path.ts @@ -1,14 +1,14 @@ export function getFilename(path: string | undefined) { if (!path) return "" - const trimmed = path.replace(/[\/\\]+$/, "") - const parts = trimmed.split(/[\/\\]/) + const trimmed = path.replace(/[/\\]+$/, "") + const parts = trimmed.split(/[/\\]/) return parts[parts.length - 1] ?? "" } export function getDirectory(path: string | undefined) { if (!path) return "" - const trimmed = path.replace(/[\/\\]+$/, "") - const parts = trimmed.split(/[\/\\]/) + const trimmed = path.replace(/[/\\]+$/, "") + const parts = trimmed.split(/[/\\]/) return parts.slice(0, parts.length - 1).join("/") + "/" } diff --git a/packages/ui/src/components/accordion.tsx b/packages/ui/src/components/accordion.tsx index 535d38e3d0..3179b8a153 100644 --- a/packages/ui/src/components/accordion.tsx +++ b/packages/ui/src/components/accordion.tsx @@ -15,7 +15,7 @@ function AccordionRoot(props: AccordionProps) { {...rest} data-component="accordion" classList={{ - ...(split.classList ?? {}), + ...split.classList, [split.class ?? ""]: !!split.class, }} /> @@ -29,7 +29,7 @@ function AccordionItem(props: AccordionItemProps) { {...rest} data-slot="accordion-item" classList={{ - ...(split.classList ?? {}), + ...split.classList, [split.class ?? ""]: !!split.class, }} /> @@ -43,7 +43,7 @@ function AccordionHeader(props: ParentProps) { {...rest} data-slot="accordion-header" classList={{ - ...(split.classList ?? {}), + ...split.classList, [split.class ?? ""]: !!split.class, }} > @@ -59,7 +59,7 @@ function AccordionTrigger(props: ParentProps) { {...rest} data-slot="accordion-trigger" classList={{ - ...(split.classList ?? {}), + ...split.classList, [split.class ?? ""]: !!split.class, }} > @@ -75,7 +75,7 @@ function AccordionContent(props: ParentProps) { {...rest} data-slot="accordion-content" classList={{ - ...(split.classList ?? {}), + ...split.classList, [split.class ?? ""]: !!split.class, }} > diff --git a/packages/ui/src/components/app-icon.tsx b/packages/ui/src/components/app-icon.tsx index f8b587ff26..541dfc5708 100644 --- a/packages/ui/src/components/app-icon.tsx +++ b/packages/ui/src/components/app-icon.tsx @@ -77,7 +77,7 @@ export const AppIcon: Component = (props) => { alt={local.alt ?? ""} draggable={local.draggable ?? false} classList={{ - ...(local.classList ?? {}), + ...local.classList, [local.class ?? ""]: !!local.class, }} /> diff --git a/packages/ui/src/components/avatar.tsx b/packages/ui/src/components/avatar.tsx index c1617b265c..035c2d3041 100644 --- a/packages/ui/src/components/avatar.tsx +++ b/packages/ui/src/components/avatar.tsx @@ -38,7 +38,7 @@ export function Avatar(props: AvatarProps) { data-size={split.size || "normal"} data-has-image={src ? "" : undefined} classList={{ - ...(split.classList ?? {}), + ...split.classList, [split.class ?? ""]: !!split.class, }} style={{ diff --git a/packages/ui/src/components/button.tsx b/packages/ui/src/components/button.tsx index 7f974b2f76..d1652145f5 100644 --- a/packages/ui/src/components/button.tsx +++ b/packages/ui/src/components/button.tsx @@ -20,7 +20,7 @@ export function Button(props: ButtonProps) { data-variant={split.variant || "secondary"} data-icon={split.icon} classList={{ - ...(split.classList ?? {}), + ...split.classList, [split.class ?? ""]: !!split.class, }} > diff --git a/packages/ui/src/components/card.tsx b/packages/ui/src/components/card.tsx index 7a1bd5e45b..320aba718c 100644 --- a/packages/ui/src/components/card.tsx +++ b/packages/ui/src/components/card.tsx @@ -53,7 +53,7 @@ export function Card(props: CardProps) { data-variant={variant()} style={mix(split.style, accent())} classList={{ - ...(split.classList ?? {}), + ...split.classList, [split.class ?? ""]: !!split.class, }} > @@ -76,7 +76,7 @@ export function CardTitle(props: CardTitleProps) { {...rest} data-slot="card-title" classList={{ - ...(split.classList ?? {}), + ...split.classList, [split.class ?? ""]: !!split.class, }} > @@ -97,7 +97,7 @@ export function CardDescription(props: ComponentProps<"div">) { {...rest} data-slot="card-description" classList={{ - ...(split.classList ?? {}), + ...split.classList, [split.class ?? ""]: !!split.class, }} > @@ -113,7 +113,7 @@ export function CardActions(props: ComponentProps<"div">) { {...rest} data-slot="card-actions" classList={{ - ...(split.classList ?? {}), + ...split.classList, [split.class ?? ""]: !!split.class, }} > diff --git a/packages/ui/src/components/collapsible.tsx b/packages/ui/src/components/collapsible.tsx index 8b5cd825ce..b2a6032646 100644 --- a/packages/ui/src/components/collapsible.tsx +++ b/packages/ui/src/components/collapsible.tsx @@ -15,7 +15,7 @@ function CollapsibleRoot(props: CollapsibleProps) { data-component="collapsible" data-variant={local.variant || "normal"} classList={{ - ...(local.classList ?? {}), + ...local.classList, [local.class ?? ""]: !!local.class, }} {...others} diff --git a/packages/ui/src/components/context-menu.tsx b/packages/ui/src/components/context-menu.tsx index afdaff7b80..f4566a17a9 100644 --- a/packages/ui/src/components/context-menu.tsx +++ b/packages/ui/src/components/context-menu.tsx @@ -33,7 +33,7 @@ function ContextMenuTrigger(props: ParentProps) { {...rest} data-slot="context-menu-trigger" classList={{ - ...(local.classList ?? {}), + ...local.classList, [local.class ?? ""]: !!local.class, }} > @@ -49,7 +49,7 @@ function ContextMenuIcon(props: ParentProps) { {...rest} data-slot="context-menu-icon" classList={{ - ...(local.classList ?? {}), + ...local.classList, [local.class ?? ""]: !!local.class, }} > @@ -69,7 +69,7 @@ function ContextMenuContent(props: ParentProps) { {...rest} data-component="context-menu-content" classList={{ - ...(local.classList ?? {}), + ...local.classList, [local.class ?? ""]: !!local.class, }} > @@ -85,7 +85,7 @@ function ContextMenuArrow(props: ContextMenuArrowProps) { {...rest} data-slot="context-menu-arrow" classList={{ - ...(local.classList ?? {}), + ...local.classList, [local.class ?? ""]: !!local.class, }} /> @@ -99,7 +99,7 @@ function ContextMenuSeparator(props: ContextMenuSeparatorProps) { {...rest} data-slot="context-menu-separator" classList={{ - ...(local.classList ?? {}), + ...local.classList, [local.class ?? ""]: !!local.class, }} /> @@ -113,7 +113,7 @@ function ContextMenuGroup(props: ParentProps) { {...rest} data-slot="context-menu-group" classList={{ - ...(local.classList ?? {}), + ...local.classList, [local.class ?? ""]: !!local.class, }} > @@ -129,7 +129,7 @@ function ContextMenuGroupLabel(props: ParentProps) { {...rest} data-slot="context-menu-group-label" classList={{ - ...(local.classList ?? {}), + ...local.classList, [local.class ?? ""]: !!local.class, }} > @@ -145,7 +145,7 @@ function ContextMenuItem(props: ParentProps) { {...rest} data-slot="context-menu-item" classList={{ - ...(local.classList ?? {}), + ...local.classList, [local.class ?? ""]: !!local.class, }} > @@ -161,7 +161,7 @@ function ContextMenuItemLabel(props: ParentProps) { {...rest} data-slot="context-menu-item-label" classList={{ - ...(local.classList ?? {}), + ...local.classList, [local.class ?? ""]: !!local.class, }} > @@ -177,7 +177,7 @@ function ContextMenuItemDescription(props: ParentProps @@ -193,7 +193,7 @@ function ContextMenuItemIndicator(props: ParentProps @@ -209,7 +209,7 @@ function ContextMenuRadioGroup(props: ParentProps) { {...rest} data-slot="context-menu-radio-group" classList={{ - ...(local.classList ?? {}), + ...local.classList, [local.class ?? ""]: !!local.class, }} > @@ -225,7 +225,7 @@ function ContextMenuRadioItem(props: ParentProps) { {...rest} data-slot="context-menu-radio-item" classList={{ - ...(local.classList ?? {}), + ...local.classList, [local.class ?? ""]: !!local.class, }} > @@ -241,7 +241,7 @@ function ContextMenuCheckboxItem(props: ParentProps @@ -261,7 +261,7 @@ function ContextMenuSubTrigger(props: ParentProps) { {...rest} data-slot="context-menu-sub-trigger" classList={{ - ...(local.classList ?? {}), + ...local.classList, [local.class ?? ""]: !!local.class, }} > @@ -277,7 +277,7 @@ function ContextMenuSubContent(props: ParentProps) { {...rest} data-component="context-menu-sub-content" classList={{ - ...(local.classList ?? {}), + ...local.classList, [local.class ?? ""]: !!local.class, }} > diff --git a/packages/ui/src/components/dialog.tsx b/packages/ui/src/components/dialog.tsx index ce7704f37e..981e3f45d7 100644 --- a/packages/ui/src/components/dialog.tsx +++ b/packages/ui/src/components/dialog.tsx @@ -28,7 +28,7 @@ export function Dialog(props: DialogProps) { data-slot="dialog-content" data-no-header={!props.title && !props.action ? "" : undefined} classList={{ - ...(props.classList ?? {}), + ...props.classList, [props.class ?? ""]: !!props.class, }} onOpenAutoFocus={(e) => { diff --git a/packages/ui/src/components/dock-surface.tsx b/packages/ui/src/components/dock-surface.tsx index 1c4af2ed5e..06cf2a5eba 100644 --- a/packages/ui/src/components/dock-surface.tsx +++ b/packages/ui/src/components/dock-surface.tsx @@ -11,7 +11,7 @@ export function DockShell(props: ComponentProps<"div">) { {...rest} data-dock-surface="shell" classList={{ - ...(split.classList ?? {}), + ...split.classList, [split.class ?? ""]: !!split.class, }} > @@ -27,7 +27,7 @@ export function DockShellForm(props: ComponentProps<"form">) { {...rest} data-dock-surface="shell" classList={{ - ...(split.classList ?? {}), + ...split.classList, [split.class ?? ""]: !!split.class, }} > @@ -44,7 +44,7 @@ export function DockTray(props: DockTrayProps) { data-dock-surface="tray" data-dock-attach={split.attach || "none"} classList={{ - ...(split.classList ?? {}), + ...split.classList, [split.class ?? ""]: !!split.class, }} > diff --git a/packages/ui/src/components/dropdown-menu.tsx b/packages/ui/src/components/dropdown-menu.tsx index efb2b45cae..259cb791ab 100644 --- a/packages/ui/src/components/dropdown-menu.tsx +++ b/packages/ui/src/components/dropdown-menu.tsx @@ -33,7 +33,7 @@ function DropdownMenuTrigger(props: ParentProps) { {...rest} data-slot="dropdown-menu-trigger" classList={{ - ...(local.classList ?? {}), + ...local.classList, [local.class ?? ""]: !!local.class, }} > @@ -49,7 +49,7 @@ function DropdownMenuIcon(props: ParentProps) { {...rest} data-slot="dropdown-menu-icon" classList={{ - ...(local.classList ?? {}), + ...local.classList, [local.class ?? ""]: !!local.class, }} > @@ -69,7 +69,7 @@ function DropdownMenuContent(props: ParentProps) { {...rest} data-component="dropdown-menu-content" classList={{ - ...(local.classList ?? {}), + ...local.classList, [local.class ?? ""]: !!local.class, }} > @@ -85,7 +85,7 @@ function DropdownMenuArrow(props: DropdownMenuArrowProps) { {...rest} data-slot="dropdown-menu-arrow" classList={{ - ...(local.classList ?? {}), + ...local.classList, [local.class ?? ""]: !!local.class, }} /> @@ -99,7 +99,7 @@ function DropdownMenuSeparator(props: DropdownMenuSeparatorProps) { {...rest} data-slot="dropdown-menu-separator" classList={{ - ...(local.classList ?? {}), + ...local.classList, [local.class ?? ""]: !!local.class, }} /> @@ -113,7 +113,7 @@ function DropdownMenuGroup(props: ParentProps) { {...rest} data-slot="dropdown-menu-group" classList={{ - ...(local.classList ?? {}), + ...local.classList, [local.class ?? ""]: !!local.class, }} > @@ -129,7 +129,7 @@ function DropdownMenuGroupLabel(props: ParentProps) {...rest} data-slot="dropdown-menu-group-label" classList={{ - ...(local.classList ?? {}), + ...local.classList, [local.class ?? ""]: !!local.class, }} > @@ -145,7 +145,7 @@ function DropdownMenuItem(props: ParentProps) { {...rest} data-slot="dropdown-menu-item" classList={{ - ...(local.classList ?? {}), + ...local.classList, [local.class ?? ""]: !!local.class, }} > @@ -161,7 +161,7 @@ function DropdownMenuItemLabel(props: ParentProps) { {...rest} data-slot="dropdown-menu-item-label" classList={{ - ...(local.classList ?? {}), + ...local.classList, [local.class ?? ""]: !!local.class, }} > @@ -177,7 +177,7 @@ function DropdownMenuItemDescription(props: ParentProps @@ -193,7 +193,7 @@ function DropdownMenuItemIndicator(props: ParentProps @@ -209,7 +209,7 @@ function DropdownMenuRadioGroup(props: ParentProps) {...rest} data-slot="dropdown-menu-radio-group" classList={{ - ...(local.classList ?? {}), + ...local.classList, [local.class ?? ""]: !!local.class, }} > @@ -225,7 +225,7 @@ function DropdownMenuRadioItem(props: ParentProps) { {...rest} data-slot="dropdown-menu-radio-item" classList={{ - ...(local.classList ?? {}), + ...local.classList, [local.class ?? ""]: !!local.class, }} > @@ -241,7 +241,7 @@ function DropdownMenuCheckboxItem(props: ParentProps @@ -261,7 +261,7 @@ function DropdownMenuSubTrigger(props: ParentProps) {...rest} data-slot="dropdown-menu-sub-trigger" classList={{ - ...(local.classList ?? {}), + ...local.classList, [local.class ?? ""]: !!local.class, }} > @@ -277,7 +277,7 @@ function DropdownMenuSubContent(props: ParentProps) {...rest} data-component="dropdown-menu-sub-content" classList={{ - ...(local.classList ?? {}), + ...local.classList, [local.class ?? ""]: !!local.class, }} > diff --git a/packages/ui/src/components/file-icon.tsx b/packages/ui/src/components/file-icon.tsx index 133cb169c7..d66ee1c250 100644 --- a/packages/ui/src/components/file-icon.tsx +++ b/packages/ui/src/components/file-icon.tsx @@ -18,7 +18,7 @@ export const FileIcon: Component = (props) => { data-component="file-icon" {...rest} classList={{ - ...(local.classList ?? {}), + ...local.classList, [local.class ?? ""]: !!local.class, }} > diff --git a/packages/ui/src/components/file-ssr.tsx b/packages/ui/src/components/file-ssr.tsx index fed5c89315..ad05555bdf 100644 --- a/packages/ui/src/components/file-ssr.tsx +++ b/packages/ui/src/components/file-ssr.tsx @@ -99,7 +99,7 @@ function DiffSSRViewer(props: SSRDiffFileProps) { { ...createDefaultOptions(props.diffStyle), ...others, - ...(local.preloadedDiff.options ?? {}), + ...local.preloadedDiff.options, }, virtualizer, virtualMetrics, @@ -109,7 +109,7 @@ function DiffSSRViewer(props: SSRDiffFileProps) { { ...createDefaultOptions(props.diffStyle), ...others, - ...(local.preloadedDiff.options ?? {}), + ...local.preloadedDiff.options, }, workerPool, ) diff --git a/packages/ui/src/components/file.tsx b/packages/ui/src/components/file.tsx index 51c2892737..fd902b2e08 100644 --- a/packages/ui/src/components/file.tsx +++ b/packages/ui/src/components/file.tsx @@ -655,7 +655,7 @@ function ViewerShell(props: { style={styleVariables} class="relative outline-none" classList={{ - ...(props.classList || {}), + ...props.classList, [props.class ?? ""]: !!props.class, }} ref={(el) => (props.viewer.wrapper = el)} diff --git a/packages/ui/src/components/hover-card.tsx b/packages/ui/src/components/hover-card.tsx index 8330375aa3..4e6647313f 100644 --- a/packages/ui/src/components/hover-card.tsx +++ b/packages/ui/src/components/hover-card.tsx @@ -20,7 +20,7 @@ export function HoverCard(props: HoverCardProps) { diff --git a/packages/ui/src/components/icon-button.tsx b/packages/ui/src/components/icon-button.tsx index 89ab00fcd3..457283aa03 100644 --- a/packages/ui/src/components/icon-button.tsx +++ b/packages/ui/src/components/icon-button.tsx @@ -19,7 +19,7 @@ export function IconButton(props: ComponentProps<"button"> & IconButtonProps) { data-size={split.size || "normal"} data-variant={split.variant || "secondary"} classList={{ - ...(split.classList ?? {}), + ...split.classList, [split.class ?? ""]: !!split.class, }} > diff --git a/packages/ui/src/components/icon.tsx b/packages/ui/src/components/icon.tsx index e2eaf107a6..08726d0ff2 100644 --- a/packages/ui/src/components/icon.tsx +++ b/packages/ui/src/components/icon.tsx @@ -117,7 +117,7 @@ export function Icon(props: IconProps) { diff --git a/packages/ui/src/components/markdown.tsx b/packages/ui/src/components/markdown.tsx index f3037da8bc..28653512e5 100644 --- a/packages/ui/src/components/markdown.tsx +++ b/packages/ui/src/components/markdown.tsx @@ -50,7 +50,7 @@ function escape(text: string) { .replace(/&/g, "&") .replace(//g, ">") - .replace(/\"/g, """) + .replace(/"/g, """) .replace(/'/g, "'") } @@ -338,7 +338,7 @@ export function Markdown(
(props: PopoverProps ref={(el: HTMLElement | undefined) => setState("contentRef", el)} data-component="popover-content" classList={{ - ...(local.classList ?? {}), + ...local.classList, [local.class ?? ""]: !!local.class, }} style={local.style} diff --git a/packages/ui/src/components/progress-circle.tsx b/packages/ui/src/components/progress-circle.tsx index 02bd36bb71..992fb62e83 100644 --- a/packages/ui/src/components/progress-circle.tsx +++ b/packages/ui/src/components/progress-circle.tsx @@ -32,7 +32,7 @@ export function ProgressCircle(props: ProgressCircleProps) { fill="none" data-component="progress-circle" classList={{ - ...(split.classList ?? {}), + ...split.classList, [split.class ?? ""]: !!split.class, }} > diff --git a/packages/ui/src/components/progress.tsx b/packages/ui/src/components/progress.tsx index bfe10a1d1e..7cbe5d6bcb 100644 --- a/packages/ui/src/components/progress.tsx +++ b/packages/ui/src/components/progress.tsx @@ -15,7 +15,7 @@ export function Progress(props: ProgressProps) { {...others} data-component="progress" classList={{ - ...(local.classList ?? {}), + ...local.classList, [local.class ?? ""]: !!local.class, }} > diff --git a/packages/ui/src/components/provider-icon.tsx b/packages/ui/src/components/provider-icon.tsx index edfdd03571..7c0eb3d047 100644 --- a/packages/ui/src/components/provider-icon.tsx +++ b/packages/ui/src/components/provider-icon.tsx @@ -15,7 +15,7 @@ export const ProviderIcon: Component = (props) => { data-component="provider-icon" {...rest} classList={{ - ...(local.classList ?? {}), + ...local.classList, [local.class ?? ""]: !!local.class, }} > diff --git a/packages/ui/src/components/radio-group.tsx b/packages/ui/src/components/radio-group.tsx index 544e852e47..9151a24b0f 100644 --- a/packages/ui/src/components/radio-group.tsx +++ b/packages/ui/src/components/radio-group.tsx @@ -56,7 +56,7 @@ export function RadioGroup(props: RadioGroupProps) { data-fill={local.fill ? "" : undefined} data-pad={local.pad ?? "normal"} classList={{ - ...(local.classList ?? {}), + ...local.classList, [local.class ?? ""]: !!local.class, }} value={local.current ? getValue(local.current) : undefined} diff --git a/packages/ui/src/components/resize-handle.tsx b/packages/ui/src/components/resize-handle.tsx index e2eed1bb7c..d7774a684b 100644 --- a/packages/ui/src/components/resize-handle.tsx +++ b/packages/ui/src/components/resize-handle.tsx @@ -73,7 +73,7 @@ export function ResizeHandle(props: ResizeHandleProps) { data-direction={local.direction} data-edge={local.edge ?? (local.direction === "vertical" ? "start" : "end")} classList={{ - ...(local.classList ?? {}), + ...local.classList, [local.class ?? ""]: !!local.class, }} onMouseDown={handleMouseDown} diff --git a/packages/ui/src/components/select.tsx b/packages/ui/src/components/select.tsx index 61804a9519..67becf2d9c 100644 --- a/packages/ui/src/components/select.tsx +++ b/packages/ui/src/components/select.tsx @@ -104,7 +104,7 @@ export function Select(props: SelectProps & Omit) {...itemProps} data-slot="select-select-item" classList={{ - ...(local.classList ?? {}), + ...local.classList, [local.class ?? ""]: !!local.class, }} onPointerEnter={() => move(itemProps.item.rawValue)} @@ -141,7 +141,7 @@ export function Select(props: SelectProps & Omit) variant={props.variant} style={local.triggerStyle} classList={{ - ...(local.classList ?? {}), + ...local.classList, [local.class ?? ""]: !!local.class, }} > @@ -160,7 +160,7 @@ export function Select(props: SelectProps & Omit) diff --git a/packages/ui/src/components/tabs.tsx b/packages/ui/src/components/tabs.tsx index 396504dd72..f46e9bfb30 100644 --- a/packages/ui/src/components/tabs.tsx +++ b/packages/ui/src/components/tabs.tsx @@ -27,7 +27,7 @@ function TabsRoot(props: TabsProps) { data-variant={split.variant || "normal"} data-orientation={split.orientation || "horizontal"} classList={{ - ...(split.classList ?? {}), + ...split.classList, [split.class ?? ""]: !!split.class, }} /> @@ -41,7 +41,7 @@ function TabsList(props: TabsListProps) { {...rest} data-slot="tabs-list" classList={{ - ...(split.classList ?? {}), + ...split.classList, [split.class ?? ""]: !!split.class, }} /> @@ -63,7 +63,7 @@ function TabsTrigger(props: ParentProps) { data-slot="tabs-trigger-wrapper" data-value={props.value} classList={{ - ...(split.classList ?? {}), + ...split.classList, [split.class ?? ""]: !!split.class, }} onMouseDown={(e) => { @@ -104,7 +104,7 @@ function TabsContent(props: ParentProps) { {...rest} data-slot="tabs-content" classList={{ - ...(split.classList ?? {}), + ...split.classList, [split.class ?? ""]: !!split.class, }} > diff --git a/packages/ui/src/components/tag.tsx b/packages/ui/src/components/tag.tsx index 428eedd0f3..c54e4d4747 100644 --- a/packages/ui/src/components/tag.tsx +++ b/packages/ui/src/components/tag.tsx @@ -12,7 +12,7 @@ export function Tag(props: TagProps) { data-component="tag" data-size={split.size || "normal"} classList={{ - ...(split.classList ?? {}), + ...split.classList, [split.class ?? ""]: !!split.class, }} > diff --git a/packages/ui/src/components/toast.tsx b/packages/ui/src/components/toast.tsx index e8062a2a8b..599cf2a9ea 100644 --- a/packages/ui/src/components/toast.tsx +++ b/packages/ui/src/components/toast.tsx @@ -30,7 +30,7 @@ function ToastRoot(props: ToastRootComponentProps) { Date: Thu, 16 Apr 2026 00:46:18 +0000 Subject: [PATCH 161/300] chore: generate --- packages/console/core/src/key.ts | 8 ++++---- packages/opencode/script/build.ts | 2 +- packages/opencode/src/cli/cmd/providers.ts | 6 +++--- packages/opencode/src/plugin/cloudflare.ts | 20 ++++++++++---------- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/console/core/src/key.ts b/packages/console/core/src/key.ts index d1aae15240..aef4298c90 100644 --- a/packages/console/core/src/key.ts +++ b/packages/console/core/src/key.ts @@ -25,8 +25,8 @@ export namespace Key { .where( and( eq(KeyTable.workspaceID, Actor.workspace()), - isNull(KeyTable.timeDeleted), - ...(Actor.userRole() === "admin" ? [] : [eq(KeyTable.userID, Actor.userID())]), + isNull(KeyTable.timeDeleted), + ...(Actor.userRole() === "admin" ? [] : [eq(KeyTable.userID, Actor.userID())]), ), ) .orderBy(sql`${KeyTable.name} DESC`), @@ -83,8 +83,8 @@ export namespace Key { .where( and( eq(KeyTable.id, input.id), - eq(KeyTable.workspaceID, Actor.workspace()), - ...(Actor.userRole() === "admin" ? [] : [eq(KeyTable.userID, Actor.userID())]), + eq(KeyTable.workspaceID, Actor.workspace()), + ...(Actor.userRole() === "admin" ? [] : [eq(KeyTable.userID, Actor.userID())]), ), ), ) diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts index d2628974fa..5aa14d52cd 100755 --- a/packages/opencode/script/build.ts +++ b/packages/opencode/script/build.ts @@ -211,7 +211,7 @@ for (const item of targets) { execArgv: [`--user-agent=opencode/${Script.version}`, "--use-system-ca", "--"], windows: {}, }, - files: (embeddedFileMap ? { "opencode-web-ui.gen.ts": embeddedFileMap } : {}), + files: embeddedFileMap ? { "opencode-web-ui.gen.ts": embeddedFileMap } : {}, entrypoints: [ "./src/index.ts", parserWorker, diff --git a/packages/opencode/src/cli/cmd/providers.ts b/packages/opencode/src/cli/cmd/providers.ts index 06f9fdf1d5..6ab927e253 100644 --- a/packages/opencode/src/cli/cmd/providers.ts +++ b/packages/opencode/src/cli/cmd/providers.ts @@ -41,9 +41,9 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string, const method = await prompts.select({ message: "Login method", options: plugin.auth.methods.map((x, index) => ({ - label: x.label, - value: index.toString(), - })), + label: x.label, + value: index.toString(), + })), }) if (prompts.isCancel(method)) throw new UI.CancelledError() index = parseInt(method) diff --git a/packages/opencode/src/plugin/cloudflare.ts b/packages/opencode/src/plugin/cloudflare.ts index 267d1ed2f2..2ccf5168d8 100644 --- a/packages/opencode/src/plugin/cloudflare.ts +++ b/packages/opencode/src/plugin/cloudflare.ts @@ -1,16 +1,16 @@ import type { Hooks, PluginInput } from "@opencode-ai/plugin" export async function CloudflareWorkersAuthPlugin(_input: PluginInput): Promise { - const prompts = (!process.env.CLOUDFLARE_ACCOUNT_ID - ? [ - { - type: "text" as const, - key: "accountId", - message: "Enter your Cloudflare Account ID", - placeholder: "e.g. 1234567890abcdef1234567890abcdef", - }, - ] - : []) + const prompts = !process.env.CLOUDFLARE_ACCOUNT_ID + ? [ + { + type: "text" as const, + key: "accountId", + message: "Enter your Cloudflare Account ID", + placeholder: "e.g. 1234567890abcdef1234567890abcdef", + }, + ] + : [] return { auth: { From a147ad68e6aed8a6a3eeaf2ce1e56f73fab7fa31 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 20:55:14 -0400 Subject: [PATCH 162/300] feat(shared): add Effect-idiomatic file lock (EffectFlock) (#22681) --- packages/shared/src/util/effect-flock.ts | 278 +++++++++++++ .../test/fixture/effect-flock-worker.ts | 64 +++ .../shared/test/util/effect-flock.test.ts | 388 ++++++++++++++++++ 3 files changed, 730 insertions(+) create mode 100644 packages/shared/src/util/effect-flock.ts create mode 100644 packages/shared/test/fixture/effect-flock-worker.ts create mode 100644 packages/shared/test/util/effect-flock.test.ts diff --git a/packages/shared/src/util/effect-flock.ts b/packages/shared/src/util/effect-flock.ts new file mode 100644 index 0000000000..d728c0ef15 --- /dev/null +++ b/packages/shared/src/util/effect-flock.ts @@ -0,0 +1,278 @@ +import path from "path" +import os from "os" +import { randomUUID } from "crypto" +import { Context, Effect, Function, Layer, Option, Schedule, Schema } from "effect" +import type { FileSystem, Scope } from "effect" +import type { PlatformError } from "effect/PlatformError" +import { AppFileSystem } from "../filesystem" +import { Global } from "../global" +import { Hash } from "./hash" + +export namespace EffectFlock { + // --------------------------------------------------------------------------- + // Errors + // --------------------------------------------------------------------------- + + export class LockTimeoutError extends Schema.TaggedErrorClass()("LockTimeoutError", { + key: Schema.String, + }) {} + + export class LockCompromisedError extends Schema.TaggedErrorClass()("LockCompromisedError", { + detail: Schema.String, + }) {} + + class ReleaseError extends Schema.TaggedErrorClass()("ReleaseError", { + detail: Schema.String, + cause: Schema.optional(Schema.Defect), + }) { + override get message() { + return this.detail + } + } + + /** Internal: signals "lock is held, retry later". Never leaks to callers. */ + class NotAcquired extends Schema.TaggedErrorClass()("NotAcquired", {}) {} + + export type LockError = LockTimeoutError | LockCompromisedError + + // --------------------------------------------------------------------------- + // Timing (baked in — no caller ever overrides these) + // --------------------------------------------------------------------------- + + const STALE_MS = 60_000 + const TIMEOUT_MS = 5 * 60_000 + const BASE_DELAY_MS = 100 + const MAX_DELAY_MS = 2_000 + const HEARTBEAT_MS = Math.max(100, Math.floor(STALE_MS / 3)) + + const retrySchedule = Schedule.exponential(BASE_DELAY_MS, 1.7).pipe( + Schedule.either(Schedule.spaced(MAX_DELAY_MS)), + Schedule.jittered, + Schedule.while((meta) => meta.elapsed < TIMEOUT_MS), + ) + + // --------------------------------------------------------------------------- + // Lock metadata schema + // --------------------------------------------------------------------------- + + const LockMetaJson = Schema.fromJsonString( + Schema.Struct({ + token: Schema.String, + pid: Schema.Number, + hostname: Schema.String, + createdAt: Schema.String, + }), + ) + + const decodeMeta = Schema.decodeUnknownSync(LockMetaJson) + const encodeMeta = Schema.encodeSync(LockMetaJson) + + // --------------------------------------------------------------------------- + // Service + // --------------------------------------------------------------------------- + + export interface Interface { + readonly acquire: (key: string, dir?: string) => Effect.Effect + readonly withLock: { + (key: string, dir?: string): (body: Effect.Effect) => Effect.Effect + (body: Effect.Effect, key: string, dir?: string): Effect.Effect + } + } + + export class Service extends Context.Service()("EffectFlock") {} + + // --------------------------------------------------------------------------- + // Layer + // --------------------------------------------------------------------------- + + function wall() { + return performance.timeOrigin + performance.now() + } + + const mtimeMs = (info: FileSystem.File.Info) => Option.getOrElse(info.mtime, () => new Date(0)).getTime() + + const isPathGone = (e: PlatformError) => e.reason._tag === "NotFound" || e.reason._tag === "Unknown" + + export const layer: Layer.Layer = Layer.effect( + Service, + Effect.gen(function* () { + const global = yield* Global.Service + const fs = yield* AppFileSystem.Service + const lockRoot = path.join(global.state, "locks") + const hostname = os.hostname() + const ensuredDirs = new Set() + + // -- helpers (close over fs) -- + + const safeStat = (file: string) => + fs.stat(file).pipe( + Effect.catchIf(isPathGone, () => Effect.void), + Effect.orDie, + ) + + const forceRemove = (target: string) => fs.remove(target, { recursive: true }).pipe(Effect.ignore) + + /** Atomic mkdir — returns true if created, false if already exists, dies on other errors. */ + const atomicMkdir = (dir: string) => + fs.makeDirectory(dir, { mode: 0o700 }).pipe( + Effect.as(true), + Effect.catchIf( + (e) => e.reason._tag === "AlreadyExists", + () => Effect.succeed(false), + ), + Effect.orDie, + ) + + /** Write with exclusive create — compromised error if file already exists. */ + const exclusiveWrite = (filePath: string, content: string, lockDir: string, detail: string) => + fs.writeFileString(filePath, content, { flag: "wx" }).pipe( + Effect.catch(() => + Effect.gen(function* () { + yield* forceRemove(lockDir) + return yield* new LockCompromisedError({ detail }) + }), + ), + ) + + const cleanStaleBreaker = Effect.fnUntraced(function* (breakerPath: string) { + const bs = yield* safeStat(breakerPath) + if (bs && wall() - mtimeMs(bs) > STALE_MS) yield* forceRemove(breakerPath) + return false + }) + + const ensureDir = Effect.fnUntraced(function* (dir: string) { + if (ensuredDirs.has(dir)) return + yield* fs.makeDirectory(dir, { recursive: true }).pipe(Effect.orDie) + ensuredDirs.add(dir) + }) + + const isStale = Effect.fnUntraced(function* (lockDir: string, heartbeatPath: string, metaPath: string) { + const now = wall() + + const hb = yield* safeStat(heartbeatPath) + if (hb) return now - mtimeMs(hb) > STALE_MS + + const meta = yield* safeStat(metaPath) + if (meta) return now - mtimeMs(meta) > STALE_MS + + const dir = yield* safeStat(lockDir) + if (!dir) return false + + return now - mtimeMs(dir) > STALE_MS + }) + + // -- single lock attempt -- + + type Handle = { token: string; metaPath: string; heartbeatPath: string; lockDir: string } + + const tryAcquireLockDir = Effect.fn("EffectFlock.tryAcquire")(function* (lockDir: string) { + const token = randomUUID() + const metaPath = path.join(lockDir, "meta.json") + const heartbeatPath = path.join(lockDir, "heartbeat") + + // Atomic mkdir — the POSIX lock primitive + const created = yield* atomicMkdir(lockDir) + + if (!created) { + if (!(yield* isStale(lockDir, heartbeatPath, metaPath))) return yield* new NotAcquired() + + // Stale — race for breaker ownership + const breakerPath = lockDir + ".breaker" + + const claimed = yield* fs.makeDirectory(breakerPath, { mode: 0o700 }).pipe( + Effect.as(true), + Effect.catchIf( + (e) => e.reason._tag === "AlreadyExists", + () => cleanStaleBreaker(breakerPath), + ), + Effect.catchIf(isPathGone, () => Effect.succeed(false)), + Effect.orDie, + ) + + if (!claimed) return yield* new NotAcquired() + + // We own the breaker — double-check staleness, nuke, recreate + const recreated = yield* Effect.gen(function* () { + if (!(yield* isStale(lockDir, heartbeatPath, metaPath))) return false + yield* forceRemove(lockDir) + return yield* atomicMkdir(lockDir) + }).pipe(Effect.ensuring(forceRemove(breakerPath))) + + if (!recreated) return yield* new NotAcquired() + } + + // We own the lock dir — write heartbeat + meta with exclusive create + yield* exclusiveWrite(heartbeatPath, "", lockDir, "heartbeat already existed") + + const metaJson = encodeMeta({ token, pid: process.pid, hostname, createdAt: new Date().toISOString() }) + yield* exclusiveWrite(metaPath, metaJson, lockDir, "meta.json already existed") + + return { token, metaPath, heartbeatPath, lockDir } satisfies Handle + }) + + // -- retry wrapper (preserves Handle type) -- + + const acquireHandle = (lockfile: string, key: string): Effect.Effect => + tryAcquireLockDir(lockfile).pipe( + Effect.retry({ + while: (err) => err._tag === "NotAcquired", + schedule: retrySchedule, + }), + Effect.catchTag("NotAcquired", () => Effect.fail(new LockTimeoutError({ key }))), + ) + + // -- release -- + + const release = (handle: Handle) => + Effect.gen(function* () { + const raw = yield* fs.readFileString(handle.metaPath).pipe( + Effect.catch((err) => { + if (isPathGone(err)) return Effect.die(new ReleaseError({ detail: "metadata missing" })) + return Effect.die(err) + }), + ) + + const parsed = yield* Effect.try({ + try: () => decodeMeta(raw), + catch: (cause) => new ReleaseError({ detail: "metadata invalid", cause }), + }).pipe(Effect.orDie) + + if (parsed.token !== handle.token) return yield* Effect.die(new ReleaseError({ detail: "token mismatch" })) + + yield* forceRemove(handle.lockDir) + }) + + // -- build service -- + + const acquire = Effect.fn("EffectFlock.acquire")(function* (key: string, dir?: string) { + const lockDir = dir ?? lockRoot + yield* ensureDir(lockDir) + + const lockfile = path.join(lockDir, Hash.fast(key) + ".lock") + + // acquireRelease: acquire is uninterruptible, release is guaranteed + const handle = yield* Effect.acquireRelease(acquireHandle(lockfile, key), (handle) => release(handle)) + + // Heartbeat fiber — scoped, so it's interrupted before release runs + yield* fs + .utimes(handle.heartbeatPath, new Date(), new Date()) + .pipe(Effect.ignore, Effect.repeat(Schedule.spaced(HEARTBEAT_MS)), Effect.forkScoped) + }) + + const withLock: Interface["withLock"] = Function.dual( + (args) => Effect.isEffect(args[0]), + (body: Effect.Effect, key: string, dir?: string): Effect.Effect => + Effect.scoped( + Effect.gen(function* () { + yield* acquire(key, dir) + return yield* body + }), + ), + ) + + return Service.of({ acquire, withLock }) + }), + ) + + export const live = layer.pipe(Layer.provide(AppFileSystem.defaultLayer)) +} diff --git a/packages/shared/test/fixture/effect-flock-worker.ts b/packages/shared/test/fixture/effect-flock-worker.ts new file mode 100644 index 0000000000..7fd2e144a2 --- /dev/null +++ b/packages/shared/test/fixture/effect-flock-worker.ts @@ -0,0 +1,64 @@ +import fs from "fs/promises" +import path from "path" +import os from "os" +import { Effect, Layer } from "effect" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { EffectFlock } from "@opencode-ai/shared/util/effect-flock" +import { Global } from "@opencode-ai/shared/global" + +type Msg = { + key: string + dir: string + holdMs?: number + ready?: string + active?: string + done?: string +} + +function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)) +} + +const msg: Msg = JSON.parse(process.argv[2]!) + +const testGlobal = Layer.succeed( + Global.Service, + Global.Service.of({ + home: os.homedir(), + data: os.tmpdir(), + cache: os.tmpdir(), + config: os.tmpdir(), + state: os.tmpdir(), + bin: os.tmpdir(), + log: os.tmpdir(), + }), +) + +const testLayer = EffectFlock.layer.pipe(Layer.provide(testGlobal), Layer.provide(AppFileSystem.defaultLayer)) + +async function job() { + if (msg.ready) await fs.writeFile(msg.ready, String(process.pid)) + if (msg.active) await fs.writeFile(msg.active, String(process.pid), { flag: "wx" }) + + try { + if (msg.holdMs && msg.holdMs > 0) await sleep(msg.holdMs) + if (msg.done) await fs.appendFile(msg.done, "1\n") + } finally { + if (msg.active) await fs.rm(msg.active, { force: true }) + } +} + +await Effect.runPromise( + Effect.gen(function* () { + const flock = yield* EffectFlock.Service + yield* flock.withLock( + Effect.promise(() => job()), + msg.key, + msg.dir, + ) + }).pipe(Effect.provide(testLayer)), +).catch((err) => { + const text = err instanceof Error ? (err.stack ?? err.message) : String(err) + process.stderr.write(text) + process.exit(1) +}) diff --git a/packages/shared/test/util/effect-flock.test.ts b/packages/shared/test/util/effect-flock.test.ts new file mode 100644 index 0000000000..6e094c2e1c --- /dev/null +++ b/packages/shared/test/util/effect-flock.test.ts @@ -0,0 +1,388 @@ +import { describe, expect } from "bun:test" +import { spawn } from "child_process" +import fs from "fs/promises" +import path from "path" +import os from "os" +import { Cause, Effect, Exit, Layer } from "effect" +import { testEffect } from "../lib/effect" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { EffectFlock } from "@opencode-ai/shared/util/effect-flock" +import { Global } from "@opencode-ai/shared/global" +import { Hash } from "@opencode-ai/shared/util/hash" + +function lock(dir: string, key: string) { + return path.join(dir, Hash.fast(key) + ".lock") +} + +function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)) +} + +async function exists(file: string) { + return fs + .stat(file) + .then(() => true) + .catch(() => false) +} + +async function readJson(p: string): Promise { + return JSON.parse(await fs.readFile(p, "utf8")) +} + +// --------------------------------------------------------------------------- +// Worker subprocess helpers +// --------------------------------------------------------------------------- + +type Msg = { + key: string + dir: string + holdMs?: number + ready?: string + active?: string + done?: string +} + +const root = path.join(import.meta.dir, "../..") +const worker = path.join(import.meta.dir, "../fixture/effect-flock-worker.ts") + +function run(msg: Msg) { + return new Promise<{ code: number; stdout: Buffer; stderr: Buffer }>((resolve) => { + const proc = spawn(process.execPath, [worker, JSON.stringify(msg)], { cwd: root }) + const stdout: Buffer[] = [] + const stderr: Buffer[] = [] + proc.stdout?.on("data", (data) => stdout.push(Buffer.from(data))) + proc.stderr?.on("data", (data) => stderr.push(Buffer.from(data))) + proc.on("close", (code) => { + resolve({ code: code ?? 1, stdout: Buffer.concat(stdout), stderr: Buffer.concat(stderr) }) + }) + }) +} + +function spawnWorker(msg: Msg) { + return spawn(process.execPath, [worker, JSON.stringify(msg)], { + cwd: root, + stdio: ["ignore", "pipe", "pipe"], + }) +} + +function stopWorker(proc: ReturnType) { + if (proc.exitCode !== null || proc.signalCode !== null) return Promise.resolve() + if (process.platform !== "win32" || !proc.pid) { + proc.kill() + return Promise.resolve() + } + return new Promise((resolve) => { + const killProc = spawn("taskkill", ["/pid", String(proc.pid), "/T", "/F"]) + killProc.on("close", () => { + proc.kill() + resolve() + }) + }) +} + +async function waitForFile(file: string, timeout = 3_000) { + const stop = Date.now() + timeout + while (Date.now() < stop) { + if (await exists(file)) return + await sleep(20) + } + throw new Error(`Timed out waiting for file: ${file}`) +} + +// --------------------------------------------------------------------------- +// Test layer +// --------------------------------------------------------------------------- + +const testGlobal = Layer.succeed( + Global.Service, + Global.Service.of({ + home: os.homedir(), + data: os.tmpdir(), + cache: os.tmpdir(), + config: os.tmpdir(), + state: os.tmpdir(), + bin: os.tmpdir(), + log: os.tmpdir(), + }), +) + +const testLayer = EffectFlock.layer.pipe(Layer.provide(testGlobal), Layer.provide(AppFileSystem.defaultLayer)) + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +describe("util.effect-flock", () => { + const it = testEffect(testLayer) + + it.live( + "acquire and release via scoped Effect", + Effect.gen(function* () { + const flock = yield* EffectFlock.Service + const tmp = yield* Effect.promise(() => fs.mkdtemp(path.join(os.tmpdir(), "eflock-test-"))) + const dir = path.join(tmp, "locks") + const lockDir = lock(dir, "eflock:acquire") + + yield* Effect.scoped(flock.acquire("eflock:acquire", dir)) + + expect(yield* Effect.promise(() => exists(lockDir))).toBe(false) + yield* Effect.promise(() => fs.rm(tmp, { recursive: true, force: true })) + }), + ) + + it.live( + "withLock data-first", + Effect.gen(function* () { + const flock = yield* EffectFlock.Service + const tmp = yield* Effect.promise(() => fs.mkdtemp(path.join(os.tmpdir(), "eflock-test-"))) + const dir = path.join(tmp, "locks") + + let hit = false + yield* flock.withLock( + Effect.sync(() => { + hit = true + }), + "eflock:df", + dir, + ) + expect(hit).toBe(true) + yield* Effect.promise(() => fs.rm(tmp, { recursive: true, force: true })) + }), + ) + + it.live( + "withLock pipeable", + Effect.gen(function* () { + const flock = yield* EffectFlock.Service + const tmp = yield* Effect.promise(() => fs.mkdtemp(path.join(os.tmpdir(), "eflock-test-"))) + const dir = path.join(tmp, "locks") + + let hit = false + yield* Effect.sync(() => { + hit = true + }).pipe(flock.withLock("eflock:pipe", dir)) + expect(hit).toBe(true) + yield* Effect.promise(() => fs.rm(tmp, { recursive: true, force: true })) + }), + ) + + it.live( + "writes owner metadata", + Effect.gen(function* () { + const flock = yield* EffectFlock.Service + const tmp = yield* Effect.promise(() => fs.mkdtemp(path.join(os.tmpdir(), "eflock-test-"))) + const dir = path.join(tmp, "locks") + const key = "eflock:meta" + const file = path.join(lock(dir, key), "meta.json") + + yield* Effect.scoped( + Effect.gen(function* () { + yield* flock.acquire(key, dir) + const json = yield* Effect.promise(() => + readJson<{ token?: unknown; pid?: unknown; hostname?: unknown; createdAt?: unknown }>(file), + ) + expect(typeof json.token).toBe("string") + expect(typeof json.pid).toBe("number") + expect(typeof json.hostname).toBe("string") + expect(typeof json.createdAt).toBe("string") + }), + ) + yield* Effect.promise(() => fs.rm(tmp, { recursive: true, force: true })) + }), + ) + + it.live( + "breaks stale lock dirs", + Effect.gen(function* () { + const flock = yield* EffectFlock.Service + const tmp = yield* Effect.promise(() => fs.mkdtemp(path.join(os.tmpdir(), "eflock-test-"))) + const dir = path.join(tmp, "locks") + const key = "eflock:stale" + const lockDir = lock(dir, key) + + yield* Effect.promise(async () => { + await fs.mkdir(lockDir, { recursive: true }) + const old = new Date(Date.now() - 120_000) + await fs.utimes(lockDir, old, old) + }) + + let hit = false + yield* flock.withLock( + Effect.sync(() => { + hit = true + }), + key, + dir, + ) + expect(hit).toBe(true) + yield* Effect.promise(() => fs.rm(tmp, { recursive: true, force: true })) + }), + ) + + it.live( + "recovers from stale breaker", + Effect.gen(function* () { + const flock = yield* EffectFlock.Service + const tmp = yield* Effect.promise(() => fs.mkdtemp(path.join(os.tmpdir(), "eflock-test-"))) + const dir = path.join(tmp, "locks") + const key = "eflock:stale-breaker" + const lockDir = lock(dir, key) + const breaker = lockDir + ".breaker" + + yield* Effect.promise(async () => { + await fs.mkdir(lockDir, { recursive: true }) + await fs.mkdir(breaker) + const old = new Date(Date.now() - 120_000) + await fs.utimes(lockDir, old, old) + await fs.utimes(breaker, old, old) + }) + + let hit = false + yield* flock.withLock( + Effect.sync(() => { + hit = true + }), + key, + dir, + ) + expect(hit).toBe(true) + expect(yield* Effect.promise(() => exists(breaker))).toBe(false) + yield* Effect.promise(() => fs.rm(tmp, { recursive: true, force: true })) + }), + ) + + it.live( + "detects compromise when lock dir removed", + Effect.gen(function* () { + const flock = yield* EffectFlock.Service + const tmp = yield* Effect.promise(() => fs.mkdtemp(path.join(os.tmpdir(), "eflock-test-"))) + const dir = path.join(tmp, "locks") + const key = "eflock:compromised" + const lockDir = lock(dir, key) + + const result = yield* flock + .withLock( + Effect.promise(() => fs.rm(lockDir, { recursive: true, force: true })), + key, + dir, + ) + .pipe(Effect.exit) + + expect(Exit.isFailure(result)).toBe(true) + expect(Exit.isFailure(result) ? Cause.pretty(result.cause) : "").toContain("missing") + yield* Effect.promise(() => fs.rm(tmp, { recursive: true, force: true })) + }), + ) + + it.live( + "detects token mismatch", + Effect.gen(function* () { + const flock = yield* EffectFlock.Service + const tmp = yield* Effect.promise(() => fs.mkdtemp(path.join(os.tmpdir(), "eflock-test-"))) + const dir = path.join(tmp, "locks") + const key = "eflock:token" + const lockDir = lock(dir, key) + const meta = path.join(lockDir, "meta.json") + + const result = yield* flock + .withLock( + Effect.promise(async () => { + const json = await readJson<{ token?: string }>(meta) + json.token = "tampered" + await fs.writeFile(meta, JSON.stringify(json, null, 2)) + }), + key, + dir, + ) + .pipe(Effect.exit) + + expect(Exit.isFailure(result)).toBe(true) + expect(Exit.isFailure(result) ? Cause.pretty(result.cause) : "").toContain("token mismatch") + expect(yield* Effect.promise(() => exists(lockDir))).toBe(true) + yield* Effect.promise(() => fs.rm(tmp, { recursive: true, force: true })) + }), + ) + + it.live( + "fails on unwritable lock roots", + Effect.gen(function* () { + if (process.platform === "win32") return + const flock = yield* EffectFlock.Service + const tmp = yield* Effect.promise(() => fs.mkdtemp(path.join(os.tmpdir(), "eflock-test-"))) + const dir = path.join(tmp, "locks") + + yield* Effect.promise(async () => { + await fs.mkdir(dir, { recursive: true }) + await fs.chmod(dir, 0o500) + }) + + const result = yield* flock.withLock(Effect.void, "eflock:perm", dir).pipe(Effect.exit) + expect(String(result)).toContain("PermissionDenied") + yield* Effect.promise(() => fs.chmod(dir, 0o700).then(() => fs.rm(tmp, { recursive: true, force: true }))) + }), + ) + + it.live( + "enforces mutual exclusion under process contention", + () => + Effect.promise(async () => { + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "eflock-stress-")) + const dir = path.join(tmp, "locks") + const done = path.join(tmp, "done.log") + const active = path.join(tmp, "active") + const n = 16 + + try { + const out = await Promise.all( + Array.from({ length: n }, () => run({ key: "eflock:stress", dir, done, active, holdMs: 30 })), + ) + + expect(out.map((x) => x.code)).toEqual(Array.from({ length: n }, () => 0)) + expect(out.map((x) => x.stderr.toString()).filter(Boolean)).toEqual([]) + + const lines = (await fs.readFile(done, "utf8")) + .split("\n") + .map((x) => x.trim()) + .filter(Boolean) + expect(lines.length).toBe(n) + } finally { + await fs.rm(tmp, { recursive: true, force: true }) + } + }), + 60_000, + ) + + it.live( + "recovers after a crashed lock owner", + () => + Effect.promise(async () => { + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "eflock-crash-")) + const dir = path.join(tmp, "locks") + const ready = path.join(tmp, "ready") + + const proc = spawnWorker({ key: "eflock:crash", dir, ready, holdMs: 120_000 }) + + try { + await waitForFile(ready, 5_000) + await stopWorker(proc) + await new Promise((resolve) => proc.on("close", resolve)) + + // Backdate lock files so they're past STALE_MS (60s) + const lockDir = lock(dir, "eflock:crash") + const old = new Date(Date.now() - 120_000) + await fs.utimes(lockDir, old, old).catch(() => {}) + await fs.utimes(path.join(lockDir, "heartbeat"), old, old).catch(() => {}) + await fs.utimes(path.join(lockDir, "meta.json"), old, old).catch(() => {}) + + const done = path.join(tmp, "done.log") + const result = await run({ key: "eflock:crash", dir, done, holdMs: 10 }) + expect(result.code).toBe(0) + expect(result.stderr.toString()).toBe("") + } finally { + await stopWorker(proc).catch(() => {}) + await fs.rm(tmp, { recursive: true, force: true }) + } + }), + 30_000, + ) +}) From 4ca809ef4e71ee6d62990c815c82c7ee57395a8b Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 20:58:48 -0400 Subject: [PATCH 163/300] fix(session): retry 5xx server errors even when isRetryable is unset (#22511) --- packages/opencode/src/session/retry.ts | 5 ++- packages/opencode/test/session/retry.test.ts | 41 ++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/session/retry.ts b/packages/opencode/src/session/retry.ts index 39eb8cfb74..6aad55f3f8 100644 --- a/packages/opencode/src/session/retry.ts +++ b/packages/opencode/src/session/retry.ts @@ -56,7 +56,10 @@ export namespace SessionRetry { // context overflow errors should not be retried if (MessageV2.ContextOverflowError.isInstance(error)) return undefined if (MessageV2.APIError.isInstance(error)) { - if (!error.data.isRetryable) return undefined + const status = error.data.statusCode + // 5xx errors are transient server failures and should always be retried, + // even when the provider SDK doesn't explicitly mark them as retryable. + if (!error.data.isRetryable && !(status !== undefined && status >= 500)) return undefined if (error.data.responseBody?.includes("FreeUsageLimitError")) return GO_UPSELL_MESSAGE return error.data.message.includes("Overloaded") ? "Provider is overloaded" : error.data.message } diff --git a/packages/opencode/test/session/retry.test.ts b/packages/opencode/test/session/retry.test.ts index 314306ba62..2d01a8f354 100644 --- a/packages/opencode/test/session/retry.test.ts +++ b/packages/opencode/test/session/retry.test.ts @@ -178,6 +178,47 @@ describe("session.retry.retryable", () => { expect(SessionRetry.retryable(error)).toBeUndefined() }) + test("retries 500 errors even when isRetryable is false", () => { + const error = new MessageV2.APIError({ + message: "Internal server error", + isRetryable: false, + statusCode: 500, + responseBody: '{"type":"api_error","message":"Internal server error"}', + }).toObject() as MessageV2.APIError + + expect(SessionRetry.retryable(error)).toBe("Internal server error") + }) + + test("retries 502 bad gateway errors", () => { + const error = new MessageV2.APIError({ + message: "Bad gateway", + isRetryable: false, + statusCode: 502, + }).toObject() as MessageV2.APIError + + expect(SessionRetry.retryable(error)).toBe("Bad gateway") + }) + + test("retries 503 service unavailable errors", () => { + const error = new MessageV2.APIError({ + message: "Service unavailable", + isRetryable: false, + statusCode: 503, + }).toObject() as MessageV2.APIError + + expect(SessionRetry.retryable(error)).toBe("Service unavailable") + }) + + test("does not retry 4xx errors when isRetryable is false", () => { + const error = new MessageV2.APIError({ + message: "Bad request", + isRetryable: false, + statusCode: 400, + }).toObject() as MessageV2.APIError + + expect(SessionRetry.retryable(error)).toBeUndefined() + }) + test("retries ZlibError decompression failures", () => { const error = new MessageV2.APIError({ message: "Response decompression failed", From 074ef032eef2cb6a9a9b8dde5626ad5c0080d808 Mon Sep 17 00:00:00 2001 From: James Long Date: Wed, 15 Apr 2026 21:04:37 -0400 Subject: [PATCH 164/300] feat(core): add fence to make all methods strongly consistent when syncing (#22679) --- packages/opencode/src/bus/global.ts | 16 +-- packages/opencode/src/control-plane/util.ts | 37 +++++++ .../opencode/src/control-plane/workspace.ts | 103 ++++++++++++++++-- packages/opencode/src/flag/flag.ts | 4 +- packages/opencode/src/server/fence.ts | 81 ++++++++++++++ .../src/server/instance/middleware.ts | 9 +- packages/opencode/src/server/proxy.ts | 47 ++++++-- packages/opencode/src/server/server.ts | 18 +++ .../test/plugin/workspace-adaptor.test.ts | 13 ++- 9 files changed, 289 insertions(+), 39 deletions(-) create mode 100644 packages/opencode/src/control-plane/util.ts create mode 100644 packages/opencode/src/server/fence.ts diff --git a/packages/opencode/src/bus/global.ts b/packages/opencode/src/bus/global.ts index e751b59faf..b5392a81b9 100644 --- a/packages/opencode/src/bus/global.ts +++ b/packages/opencode/src/bus/global.ts @@ -1,12 +1,12 @@ import { EventEmitter } from "events" +export type GlobalEvent = { + directory?: string + project?: string + workspace?: string + payload: any +} + export const GlobalBus = new EventEmitter<{ - event: [ - { - directory?: string - project?: string - workspace?: string - payload: any - }, - ] + event: [GlobalEvent] }>() diff --git a/packages/opencode/src/control-plane/util.ts b/packages/opencode/src/control-plane/util.ts new file mode 100644 index 0000000000..023c2ae150 --- /dev/null +++ b/packages/opencode/src/control-plane/util.ts @@ -0,0 +1,37 @@ +import { GlobalBus, type GlobalEvent } from "@/bus/global" + +export function waitEvent(input: { timeout: number; signal?: AbortSignal; fn: (event: GlobalEvent) => boolean }) { + if (input.signal?.aborted) return Promise.reject(input.signal.reason ?? new Error("Request aborted")) + + return new Promise((resolve, reject) => { + const abort = () => { + cleanup() + reject(input.signal?.reason ?? new Error("Request aborted")) + } + + const handler = (event: GlobalEvent) => { + try { + if (!input.fn(event)) return + cleanup() + resolve() + } catch (error) { + cleanup() + reject(error) + } + } + + const cleanup = () => { + clearTimeout(timeout) + GlobalBus.off("event", handler) + input.signal?.removeEventListener("abort", abort) + } + + const timeout = setTimeout(() => { + cleanup() + reject(new Error("Timed out waiting for global event")) + }, input.timeout) + + GlobalBus.on("event", handler) + input.signal?.addEventListener("abort", abort, { once: true }) + }) +} diff --git a/packages/opencode/src/control-plane/workspace.ts b/packages/opencode/src/control-plane/workspace.ts index b9ac0a6b43..67583107fc 100644 --- a/packages/opencode/src/control-plane/workspace.ts +++ b/packages/opencode/src/control-plane/workspace.ts @@ -1,7 +1,7 @@ import z from "zod" import { setTimeout as sleep } from "node:timers/promises" import { fn } from "@/util/fn" -import { Database, asc, eq } from "@/storage/db" +import { Database, asc, eq, inArray } from "@/storage/db" import { Project } from "@/project/project" import { BusEvent } from "@/bus/bus-event" import { GlobalBus } from "@/bus/global" @@ -22,6 +22,8 @@ import { SessionTable } from "@/session/session.sql" import { SessionID } from "@/session/schema" import { errorData } from "@/util/error" import { AppRuntime } from "@/effect/app-runtime" +import { EventSequenceTable } from "@/sync/event.sql" +import { waitEvent } from "./util" export namespace Workspace { export const Info = WorkspaceInfo.meta({ @@ -114,6 +116,17 @@ export namespace Workspace { startSync(info) + await waitEvent({ + timeout: TIMEOUT, + fn(event) { + if (event.workspace === info.id && event.payload.type === Event.Status.type) { + const { status } = event.payload.properties + return status === "error" || status === "connected" + } + return false + }, + }) + return info }) @@ -285,10 +298,15 @@ export namespace Workspace { return spaces } - export const get = fn(WorkspaceID.zod, async (id) => { + function lookup(id: WorkspaceID) { const row = Database.use((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, id)).get()) if (!row) return - const space = fromRow(row) + return fromRow(row) + } + + export const get = fn(WorkspaceID.zod, async (id) => { + const space = lookup(id) + if (!space) return startSync(space) return space }) @@ -320,12 +338,18 @@ export namespace Workspace { const connections = new Map() const aborts = new Map() + const TIMEOUT = 5000 function setStatus(id: WorkspaceID, status: ConnectionStatus["status"], error?: string) { const prev = connections.get(id) if (prev?.status === status && prev?.error === error) return const next = { workspaceID: id, status, error } connections.set(id, next) + + if (status === "error") { + aborts.delete(id) + } + GlobalBus.emit("event", { directory: "global", workspace: id, @@ -340,6 +364,52 @@ export namespace Workspace { return [...connections.values()] } + function synced(state: Record) { + const ids = Object.keys(state) + if (ids.length === 0) return true + + const done = Object.fromEntries( + Database.use((db) => + db + .select({ + id: EventSequenceTable.aggregate_id, + seq: EventSequenceTable.seq, + }) + .from(EventSequenceTable) + .where(inArray(EventSequenceTable.aggregate_id, ids)) + .all(), + ).map((row) => [row.id, row.seq]), + ) as Record + + return ids.every((id) => { + return (done[id] ?? -1) >= state[id] + }) + } + + export async function isSyncing(workspaceID: WorkspaceID) { + return aborts.has(workspaceID) + } + + export async function waitForSync(workspaceID: WorkspaceID, state: Record, signal?: AbortSignal) { + if (synced(state)) return + + try { + await waitEvent({ + timeout: TIMEOUT, + signal, + fn(event) { + if (event.workspace !== workspaceID && event.payload.type !== "sync") { + return false + } + return synced(state) + }, + }) + } catch (error) { + if (signal?.aborted) throw signal.reason ?? new Error("Request aborted") + throw new Error(`Timed out waiting for sync fence: ${JSON.stringify(state)}`) + } + } + const log = Log.create({ service: "workspace-sync" }) function route(url: string | URL, path: string) { @@ -353,6 +423,7 @@ export namespace Workspace { async function syncWorkspace(space: Info, signal: AbortSignal) { while (!signal.aborted) { log.info("connecting to global sync", { workspace: space.name }) + setStatus(space.id, "connecting") const adaptor = await getAdaptor(space.projectID, space.type) const target = await adaptor.target(space) @@ -364,7 +435,7 @@ export namespace Workspace { headers: target.headers, signal, }).catch((err: unknown) => { - setStatus(space.id, "error") + setStatus(space.id, "error", err instanceof Error ? err.message : String(err)) log.info("failed to connect to global sync", { workspace: space.name, @@ -374,8 +445,9 @@ export namespace Workspace { }) if (!res || !res.ok || !res.body) { - log.info("failed to connect to global sync", { workspace: space.name }) - setStatus(space.id, "error") + const error = !res ? "No response from global sync" : `Global sync HTTP ${res.status}` + log.info("failed to connect to global sync", { workspace: space.name, error }) + setStatus(space.id, "error", error) await sleep(1000) continue } @@ -414,22 +486,29 @@ export namespace Workspace { } } - function startSync(space: Info) { + async function startSync(space: Info) { if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) return - if (space.type === "worktree") { - void Filesystem.exists(space.directory!).then((exists) => { + const adaptor = await getAdaptor(space.projectID, space.type) + const target = await adaptor.target(space) + + if (target.type === "local") { + void Filesystem.exists(target.directory).then((exists) => { setStatus(space.id, exists ? "connected" : "error", exists ? undefined : "directory does not exist") }) return } - if (aborts.has(space.id)) return - const abort = new AbortController() - aborts.set(space.id, abort) + if (aborts.has(space.id)) return true + setStatus(space.id, "disconnected") + const abort = new AbortController() + aborts.set(space.id, abort) + void syncWorkspace(space, abort.signal).catch((error) => { + aborts.delete(space.id) + setStatus(space.id, "error", String(error)) log.warn("workspace listener failed", { workspaceID: space.id, diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index f091fa02a9..a63f8d1c66 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -74,7 +74,6 @@ export namespace Flag { Config.withDefault(false), ) export const OPENCODE_EXPERIMENTAL_PLAN_MODE = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_PLAN_MODE") - export const OPENCODE_EXPERIMENTAL_WORKSPACES = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_WORKSPACES") export const OPENCODE_EXPERIMENTAL_MARKDOWN = !falsy("OPENCODE_EXPERIMENTAL_MARKDOWN") export const OPENCODE_MODELS_URL = process.env["OPENCODE_MODELS_URL"] export const OPENCODE_MODELS_PATH = process.env["OPENCODE_MODELS_PATH"] @@ -84,6 +83,9 @@ export namespace Flag { export const OPENCODE_SKIP_MIGRATIONS = truthy("OPENCODE_SKIP_MIGRATIONS") export const OPENCODE_STRICT_CONFIG_DEPS = truthy("OPENCODE_STRICT_CONFIG_DEPS") + export const OPENCODE_WORKSPACE_ID = process.env["OPENCODE_WORKSPACE_ID"] + export const OPENCODE_EXPERIMENTAL_WORKSPACES = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_WORKSPACES") + function number(key: string) { const value = process.env[key] if (!value) return undefined diff --git a/packages/opencode/src/server/fence.ts b/packages/opencode/src/server/fence.ts new file mode 100644 index 0000000000..bb41bd7a43 --- /dev/null +++ b/packages/opencode/src/server/fence.ts @@ -0,0 +1,81 @@ +import type { MiddlewareHandler } from "hono" +import { Database, inArray } from "@/storage/db" +import { EventSequenceTable } from "@/sync/event.sql" +import { Workspace } from "@/control-plane/workspace" +import type { WorkspaceID } from "@/control-plane/schema" +import { Log } from "@/util/log" + +const HEADER = "x-opencode-sync" +type State = Record +const log = Log.create({ service: "fence" }) + +export function load(ids?: string[]) { + const rows = Database.use((db) => { + if (!ids?.length) { + return db.select().from(EventSequenceTable).all() + } + + return db.select().from(EventSequenceTable).where(inArray(EventSequenceTable.aggregate_id, ids)).all() + }) + + return Object.fromEntries(rows.map((row) => [row.aggregate_id, row.seq])) as State +} + +export function diff(prev: State, next: State) { + const ids = new Set([...Object.keys(prev), ...Object.keys(next)]) + return Object.fromEntries( + [...ids] + .map((id) => [id, next[id] ?? -1] as const) + .filter(([id, seq]) => { + return (prev[id] ?? -1) !== seq + }), + ) as State +} + +export function parse(headers: Headers) { + const raw = headers.get(HEADER) + if (!raw) return + + let data + + try { + data = JSON.parse(raw) + } catch (err) { + return + } + + if (!data || typeof data !== "object") return + + return Object.fromEntries( + Object.entries(data).filter(([id, seq]) => { + return typeof id === "string" && Number.isInteger(seq) + }), + ) as State +} + +export async function wait(workspaceID: WorkspaceID, state: State, signal?: AbortSignal) { + log.info("waiting for state", { + workspaceID, + state, + }) + await Workspace.waitForSync(workspaceID, state, signal) + log.info("state fully synced", { + workspaceID, + state, + }) +} + +export const FenceMiddleware: MiddlewareHandler = async (c, next) => { + if (c.req.method === "GET" || c.req.method === "HEAD" || c.req.method === "OPTIONS") return next() + + const prev = load() + await next() + const current = diff(prev, load()) + + if (Object.keys(current).length > 0) { + log.info("header", { + diff: current, + }) + c.res.headers.set(HEADER, JSON.stringify(current)) + } +} diff --git a/packages/opencode/src/server/instance/middleware.ts b/packages/opencode/src/server/instance/middleware.ts index 549fb38d5d..0e29daa9ee 100644 --- a/packages/opencode/src/server/instance/middleware.ts +++ b/packages/opencode/src/server/instance/middleware.ts @@ -6,6 +6,7 @@ import { Workspace } from "@/control-plane/workspace" import { ServerProxy } from "../proxy" import { Instance } from "@/project/instance" import { InstanceBootstrap } from "@/project/bootstrap" +import { Flag } from "@/flag/flag" import { Session } from "@/session" import { SessionID } from "@/session/schema" import { WorkspaceContext } from "@/control-plane/workspace-context" @@ -68,10 +69,10 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware const sessionWorkspaceID = await getSessionWorkspace(url) const workspaceID = sessionWorkspaceID || url.searchParams.get("workspace") - if (!workspaceID || url.pathname.startsWith("/console") || OPENCODE_WORKSPACE) { - if (OPENCODE_WORKSPACE) { + if (!workspaceID || url.pathname.startsWith("/console") || Flag.OPENCODE_WORKSPACE_ID) { + if (Flag.OPENCODE_WORKSPACE_ID) { return WorkspaceContext.provide({ - workspaceID: WorkspaceID.make(OPENCODE_WORKSPACE), + workspaceID: WorkspaceID.make(Flag.OPENCODE_WORKSPACE_ID), async fn() { return Instance.provide({ directory, @@ -148,6 +149,6 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware headers.delete("x-opencode-workspace") const req = new Request(c.req.raw, { headers }) - return ServerProxy.http(proxyURL, target.headers, req) + return ServerProxy.http(proxyURL, target.headers, req, workspace.id) } } diff --git a/packages/opencode/src/server/proxy.ts b/packages/opencode/src/server/proxy.ts index 0c0deba20c..5effa5d05f 100644 --- a/packages/opencode/src/server/proxy.ts +++ b/packages/opencode/src/server/proxy.ts @@ -1,6 +1,9 @@ import { Hono } from "hono" import type { UpgradeWebSocket } from "hono/ws" import { Log } from "@/util/log" +import * as Fence from "./fence" +import type { WorkspaceID } from "@/control-plane/schema" +import { Workspace } from "@/control-plane/workspace" const hop = new Set([ "connection", @@ -101,12 +104,27 @@ const app = (upgrade: UpgradeWebSocket) => export namespace ServerProxy { const log = Log.Default.clone().tag("service", "server-proxy") - export function http(url: string | URL, extra: HeadersInit | undefined, req: Request) { + export async function http( + url: string | URL, + extra: HeadersInit | undefined, + req: Request, + workspaceID: WorkspaceID, + ) { console.log("proxy http request", { method: req.method, request: req.url, url: String(url), }) + + if (!Workspace.isSyncing(workspaceID)) { + return new Response(`broken sync connection for workspace: ${workspaceID}`, { + status: 503, + headers: { + "content-type": "text/plain; charset=utf-8", + }, + }) + } + return fetch( new Request(url, { method: req.method, @@ -116,21 +134,26 @@ export namespace ServerProxy { signal: req.signal, }), ).then((res) => { + const sync = Fence.parse(res.headers) const next = new Headers(res.headers) next.delete("content-encoding") next.delete("content-length") - console.log("proxy http response", { - method: req.method, - request: req.url, - url: String(url), - status: res.status, - statusText: res.statusText, - }) - return new Response(res.body, { - status: res.status, - statusText: res.statusText, - headers: next, + const done = sync ? Fence.wait(workspaceID, sync, req.signal) : Promise.resolve() + + return done.then(async () => { + console.log("proxy http response", { + method: req.method, + request: req.url, + url: String(url), + status: res.status, + statusText: res.statusText, + }) + return new Response(res.body, { + status: res.status, + statusText: res.statusText, + headers: next, + }) }) }) } diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 02ec7356ec..c6c37ee438 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -4,9 +4,11 @@ import { adapter } from "#hono" import { MDNS } from "./mdns" import { lazy } from "@/util/lazy" import { AuthMiddleware, CompressionMiddleware, CorsMiddleware, ErrorMiddleware, LoggerMiddleware } from "./middleware" +import { FenceMiddleware } from "./fence" import { InstanceRoutes } from "./instance" import { initProjectors } from "./projectors" import { Log } from "@/util/log" +import { Flag } from "@/flag/flag" import { ControlPlaneRoutes } from "./control" import { UIRoutes } from "./ui" @@ -30,6 +32,22 @@ export namespace Server { function create(opts: { cors?: string[] }) { const app = new Hono() const runtime = adapter.create(app) + + if (Flag.OPENCODE_WORKSPACE_ID) { + return { + app: app + .onError(ErrorMiddleware) + .use(AuthMiddleware) + .use(LoggerMiddleware) + .use(CompressionMiddleware) + .use(CorsMiddleware(opts)) + .use(FenceMiddleware) + .route("/", ControlPlaneRoutes()) + .route("/", InstanceRoutes(runtime.upgradeWebSocket)), + runtime, + } + } + return { app: app .onError(ErrorMiddleware) diff --git a/packages/opencode/test/plugin/workspace-adaptor.test.ts b/packages/opencode/test/plugin/workspace-adaptor.test.ts index 669a822a2f..ff8df7490d 100644 --- a/packages/opencode/test/plugin/workspace-adaptor.test.ts +++ b/packages/opencode/test/plugin/workspace-adaptor.test.ts @@ -7,10 +7,16 @@ import { tmpdir } from "../fixture/fixture" const disableDefault = process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS = "1" +const { Flag } = await import("../../src/flag/flag") const { Plugin } = await import("../../src/plugin/index") const { Workspace } = await import("../../src/control-plane/workspace") const { Instance } = await import("../../src/project/instance") +const experimental = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES + +// @ts-expect-error tests override the flag directly +Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = true + afterEach(async () => { await Instance.disposeAll() }) @@ -18,9 +24,12 @@ afterEach(async () => { afterAll(() => { if (disableDefault === undefined) { delete process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS - return + } else { + process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS = disableDefault } - process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS = disableDefault + + // @ts-expect-error restore original test flag value + Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = experimental }) describe("plugin.workspace", () => { From 307251bf3cc80131b4df4877d5c0cefc127828dd Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Wed, 15 Apr 2026 20:09:06 -0500 Subject: [PATCH 165/300] fix: bash memory usage (#22660) --- packages/opencode/src/tool/bash.ts | 121 +++++++++++++++++- packages/opencode/src/tool/truncate.ts | 15 ++- .../test/session/prompt-effect.test.ts | 4 +- packages/opencode/test/tool/bash.test.ts | 8 +- 4 files changed, 132 insertions(+), 16 deletions(-) diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index 7a124dadae..0ab1301305 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -1,5 +1,6 @@ import z from "zod" import os from "os" +import { createWriteStream } from "node:fs" import { Tool } from "./tool" import path from "path" import DESCRIPTION from "./bash.txt" @@ -76,6 +77,11 @@ type Scan = { always: Set } +type Chunk = { + text: string + size: number +} + export const log = Log.create({ service: "bash-tool" }) const resolveWasm = (asset: string) => { @@ -211,7 +217,39 @@ function pathArgs(list: Part[], ps: boolean) { function preview(text: string) { if (text.length <= MAX_METADATA_LENGTH) return text - return text.slice(0, MAX_METADATA_LENGTH) + "\n\n..." + return "...\n\n" + text.slice(-MAX_METADATA_LENGTH) +} + +function tail(text: string, maxLines: number, maxBytes: number) { + const lines = text.split("\n") + if (lines.length <= maxLines && Buffer.byteLength(text, "utf-8") <= maxBytes) { + return { + text, + cut: false, + } + } + + const out: string[] = [] + let bytes = 0 + for (let i = lines.length - 1; i >= 0 && out.length < maxLines; i--) { + const size = Buffer.byteLength(lines[i], "utf-8") + (out.length > 0 ? 1 : 0) + if (bytes + size > maxBytes) { + if (out.length === 0) { + const buf = Buffer.from(lines[i], "utf-8") + let start = buf.length - maxBytes + if (start < 0) start = 0 + while (start < buf.length && (buf[start] & 0xc0) === 0x80) start++ + out.unshift(buf.subarray(start).toString("utf-8")) + } + break + } + out.unshift(lines[i]) + bytes += size + } + return { + text: out.join("\n"), + cut: true, + } } const parse = Effect.fn("BashTool.parse")(function* (command: string, ps: boolean) { @@ -295,6 +333,7 @@ export const BashTool = Tool.define( Effect.gen(function* () { const spawner = yield* ChildProcessSpawner const fs = yield* AppFileSystem.Service + const trunc = yield* Truncate.Service const plugin = yield* Plugin.Service const cygpath = Effect.fn("BashTool.cygpath")(function* (shell: string, text: string) { @@ -381,7 +420,16 @@ export const BashTool = Tool.define( }, ctx: Tool.Context, ) { - let output = "" + const bytes = Truncate.MAX_BYTES + const lines = Truncate.MAX_LINES + const keep = bytes * 2 + let full = "" + let last = "" + const list: Chunk[] = [] + let used = 0 + let file = "" + let sink: ReturnType | undefined + let cut = false let expired = false let aborted = false @@ -398,10 +446,47 @@ export const BashTool = Tool.define( yield* Effect.forkScoped( Stream.runForEach(Stream.decodeText(handle.all), (chunk) => { - output += chunk + const size = Buffer.byteLength(chunk, "utf-8") + list.push({ text: chunk, size }) + used += size + while (used > keep && list.length > 1) { + const item = list.shift() + if (!item) break + used -= item.size + cut = true + } + + last = preview(last + chunk) + + if (file) { + sink?.write(chunk) + } else { + full += chunk + if (Buffer.byteLength(full, "utf-8") > bytes) { + return trunc.write(full).pipe( + Effect.andThen((next) => + Effect.sync(() => { + file = next + cut = true + sink = createWriteStream(next, { flags: "a" }) + full = "" + }), + ), + Effect.andThen( + ctx.metadata({ + metadata: { + output: last, + description: input.description, + }, + }), + ), + ) + } + } + return ctx.metadata({ metadata: { - output: preview(output), + output: last, description: input.description, }, }) @@ -443,16 +528,42 @@ export const BashTool = Tool.define( ) } if (aborted) meta.push("User aborted the command") + const raw = list.map((item) => item.text).join("") + const end = tail(raw, lines, bytes) + if (end.cut) cut = true + if (!file && end.cut) { + file = yield* trunc.write(raw) + } + + let output = end.text + if (!output) output = "(no output)" + + if (cut && file) { + output = `...output truncated...\n\nFull output saved to: ${file}\n\n` + output + } + if (meta.length > 0) { output += "\n\n\n" + meta.join("\n") + "\n" } + if (sink) { + const stream = sink + yield* Effect.promise( + () => + new Promise((resolve) => { + stream.end(() => resolve()) + stream.on("error", () => resolve()) + }), + ) + } return { title: input.description, metadata: { - output: preview(output), + output: last || preview(output), exit: code, description: input.description, + truncated: cut, + ...(cut && file ? { outputPath: file } : {}), }, output, } diff --git a/packages/opencode/src/tool/truncate.ts b/packages/opencode/src/tool/truncate.ts index a7bd8a4b16..d607e22f28 100644 --- a/packages/opencode/src/tool/truncate.ts +++ b/packages/opencode/src/tool/truncate.ts @@ -33,6 +33,7 @@ export namespace Truncate { export interface Interface { readonly cleanup: () => Effect.Effect + readonly write: (text: string) => Effect.Effect /** * Returns output unchanged when it fits within the limits, otherwise writes the full text * to the truncation directory and returns a preview plus a hint to inspect the saved file. @@ -61,6 +62,13 @@ export namespace Truncate { } }) + const write = Effect.fn("Truncate.write")(function* (text: string) { + const file = path.join(TRUNCATION_DIR, ToolID.ascending()) + yield* fs.ensureDir(TRUNCATION_DIR).pipe(Effect.orDie) + yield* fs.writeFileString(file, text).pipe(Effect.orDie) + return file + }) + const output = Effect.fn("Truncate.output")(function* (text: string, options: Options = {}, agent?: Agent.Info) { const maxLines = options.maxLines ?? MAX_LINES const maxBytes = options.maxBytes ?? MAX_BYTES @@ -102,10 +110,7 @@ export namespace Truncate { const removed = hitBytes ? totalBytes - bytes : lines.length - out.length const unit = hitBytes ? "bytes" : "lines" const preview = out.join("\n") - const file = path.join(TRUNCATION_DIR, ToolID.ascending()) - - yield* fs.ensureDir(TRUNCATION_DIR).pipe(Effect.orDie) - yield* fs.writeFileString(file, text).pipe(Effect.orDie) + const file = yield* write(text) const hint = hasTaskTool(agent) ? `The tool call succeeded but the output was truncated. Full output saved to: ${file}\nUse the Task tool to have explore agent process this file with Grep and Read (with offset/limit). Do NOT read the full file yourself - delegate to save context.` @@ -131,7 +136,7 @@ export namespace Truncate { Effect.forkScoped, ) - return Service.of({ cleanup, output }) + return Service.of({ cleanup, write, output }) }), ) diff --git a/packages/opencode/test/session/prompt-effect.test.ts b/packages/opencode/test/session/prompt-effect.test.ts index 94561206e2..31727e3df9 100644 --- a/packages/opencode/test/session/prompt-effect.test.ts +++ b/packages/opencode/test/session/prompt-effect.test.ts @@ -1362,8 +1362,8 @@ unix( expect(tool.state.metadata.truncated).toBe(true) expect(typeof tool.state.metadata.outputPath).toBe("string") - expect(tool.state.output).toContain("The tool call succeeded but the output was truncated.") - expect(tool.state.output).toContain("Full output saved to:") + expect(tool.state.output).toMatch(/\.\.\.output truncated\.\.\./) + expect(tool.state.output).toMatch(/Full output saved to:\s+\S+/) expect(tool.state.output).not.toContain("Tool execution aborted") }), { git: true, config: providerCfg }, diff --git a/packages/opencode/test/tool/bash.test.ts b/packages/opencode/test/tool/bash.test.ts index 3b03da57ee..19135ba98b 100644 --- a/packages/opencode/test/tool/bash.test.ts +++ b/packages/opencode/test/tool/bash.test.ts @@ -1116,8 +1116,8 @@ describe("tool.bash truncation", () => { ), ) mustTruncate(result) - expect(result.output).toContain("truncated") - expect(result.output).toContain("The tool call succeeded but the output was truncated") + expect(result.output).toMatch(/\.\.\.output truncated\.\.\./) + expect(result.output).toMatch(/Full output saved to:\s+\S+/) }, }) }) @@ -1138,8 +1138,8 @@ describe("tool.bash truncation", () => { ), ) mustTruncate(result) - expect(result.output).toContain("truncated") - expect(result.output).toContain("The tool call succeeded but the output was truncated") + expect(result.output).toMatch(/\.\.\.output truncated\.\.\./) + expect(result.output).toMatch(/Full output saved to:\s+\S+/) }, }) }) From 6d42f976447f1b150dd6582e38dd29e0f7100c1b Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 21:14:39 -0400 Subject: [PATCH 166/300] fix: revert "core: move plugin initialisation to config layer override" (#22686) --- packages/opencode/src/effect/app-runtime.ts | 20 +------------------- packages/opencode/src/project/bootstrap.ts | 1 + 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/packages/opencode/src/effect/app-runtime.ts b/packages/opencode/src/effect/app-runtime.ts index 668c89b60b..257922dafe 100644 --- a/packages/opencode/src/effect/app-runtime.ts +++ b/packages/opencode/src/effect/app-runtime.ts @@ -47,31 +47,13 @@ import { Pty } from "@/pty" import { Installation } from "@/installation" import { ShareNext } from "@/share/share-next" import { SessionShare } from "@/share/session" -import * as Effect from "effect/Effect" - -// Adjusts the default Config layer to ensure that plugins are always initialised before -// any other layers read the current config -const ConfigWithPluginPriority = Layer.effect( - Config.Service, - Effect.gen(function* () { - const config = yield* Config.Service - const plugin = yield* Plugin.Service - - return { - ...config, - get: () => Effect.andThen(plugin.init(), config.get), - getGlobal: () => Effect.andThen(plugin.init(), config.getGlobal), - getConsoleState: () => Effect.andThen(plugin.init(), config.getConsoleState), - } - }), -).pipe(Layer.provide(Layer.merge(Plugin.defaultLayer, Config.defaultLayer))) export const AppLayer = Layer.mergeAll( AppFileSystem.defaultLayer, Bus.defaultLayer, Auth.defaultLayer, Account.defaultLayer, - ConfigWithPluginPriority, + Config.defaultLayer, Git.defaultLayer, Ripgrep.defaultLayer, FileTime.defaultLayer, diff --git a/packages/opencode/src/project/bootstrap.ts b/packages/opencode/src/project/bootstrap.ts index 0babdfe13b..a1f2a8cb02 100644 --- a/packages/opencode/src/project/bootstrap.ts +++ b/packages/opencode/src/project/bootstrap.ts @@ -15,6 +15,7 @@ import * as Effect from "effect/Effect" export const InstanceBootstrap = Effect.gen(function* () { Log.Default.info("bootstrapping", { directory: Instance.directory }) + yield* Plugin.Service.use((svc) => svc.init()) yield* Effect.all( [ LSP.Service, From 02f2cf439e58d3f3db2e7927ecad9c0d4e1bca16 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 21:18:36 -0400 Subject: [PATCH 167/300] =?UTF-8?q?feat:=20namespace=20=E2=86=92=20flat=20?= =?UTF-8?q?export=20migration=20(Bus=20proof-of-concept)=20(#22685)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/opencode/script/unwrap-namespace.ts | 190 ++++++++ .../specs/effect/namespace-treeshake.md | 444 ++++++++++++++++++ packages/opencode/src/bus/bus.ts | 192 ++++++++ packages/opencode/src/bus/index.ts | 195 +------- 4 files changed, 827 insertions(+), 194 deletions(-) create mode 100644 packages/opencode/script/unwrap-namespace.ts create mode 100644 packages/opencode/specs/effect/namespace-treeshake.md create mode 100644 packages/opencode/src/bus/bus.ts diff --git a/packages/opencode/script/unwrap-namespace.ts b/packages/opencode/script/unwrap-namespace.ts new file mode 100644 index 0000000000..65ce498be8 --- /dev/null +++ b/packages/opencode/script/unwrap-namespace.ts @@ -0,0 +1,190 @@ +#!/usr/bin/env bun +/** + * Unwrap a TypeScript `export namespace` into flat exports + barrel. + * + * Usage: + * bun script/unwrap-namespace.ts src/bus/index.ts + * bun script/unwrap-namespace.ts src/bus/index.ts --dry-run + * + * What it does: + * 1. Reads the file and finds the `export namespace Foo { ... }` block + * (uses ast-grep for accurate AST-based boundary detection) + * 2. Removes the namespace wrapper and dedents the body + * 3. If the file is index.ts, renames it to .ts + * 4. Creates/updates index.ts with `export * as Foo from "./"` + * 5. Prints the import rewrite commands to run across the codebase + * + * Does NOT auto-rewrite imports — prints the commands so you can review them. + * + * Requires: ast-grep (`brew install ast-grep` or `cargo install ast-grep`) + */ + +import path from "path" +import fs from "fs" + +const args = process.argv.slice(2) +const dryRun = args.includes("--dry-run") +const filePath = args.find((a) => !a.startsWith("--")) + +if (!filePath) { + console.error("Usage: bun script/unwrap-namespace.ts [--dry-run]") + process.exit(1) +} + +const absPath = path.resolve(filePath) +if (!fs.existsSync(absPath)) { + console.error(`File not found: ${absPath}`) + process.exit(1) +} + +const src = fs.readFileSync(absPath, "utf-8") +const lines = src.split("\n") + +// Use ast-grep to find the namespace boundaries accurately. +// This avoids false matches from braces in strings, templates, comments, etc. +const astResult = Bun.spawnSync( + ["ast-grep", "run", "--pattern", "export namespace $NAME { $$$BODY }", "--lang", "typescript", "--json", absPath], + { stdout: "pipe", stderr: "pipe" }, +) + +if (astResult.exitCode !== 0) { + console.error("ast-grep failed:", astResult.stderr.toString()) + process.exit(1) +} + +const matches = JSON.parse(astResult.stdout.toString()) as Array<{ + text: string + range: { start: { line: number; column: number }; end: { line: number; column: number } } + metaVariables: { single: Record; multi: Record> } +}> + +if (matches.length === 0) { + console.error("No `export namespace Foo { ... }` found in file") + process.exit(1) +} + +if (matches.length > 1) { + console.error(`Found ${matches.length} namespaces — this script handles one at a time`) + console.error("Namespaces found:") + for (const m of matches) console.error(` ${m.metaVariables.single.NAME.text} (line ${m.range.start.line + 1})`) + process.exit(1) +} + +const match = matches[0] +const nsName = match.metaVariables.single.NAME.text +const nsLine = match.range.start.line // 0-indexed +const closeLine = match.range.end.line // 0-indexed, the line with closing `}` + +console.log(`Found: export namespace ${nsName} { ... }`) +console.log(` Lines ${nsLine + 1}–${closeLine + 1} (${closeLine - nsLine + 1} lines)`) + +// Build the new file content: +// 1. Everything before the namespace declaration (imports, etc.) +// 2. The namespace body, dedented by one level (2 spaces) +// 3. Everything after the closing brace (rare, but possible) +const before = lines.slice(0, nsLine) +const body = lines.slice(nsLine + 1, closeLine) +const after = lines.slice(closeLine + 1) + +// Dedent: remove exactly 2 leading spaces from each line +const dedented = body.map((line) => { + if (line === "") return "" + if (line.startsWith(" ")) return line.slice(2) + return line // don't touch lines that aren't indented (shouldn't happen) +}) + +const newContent = [...before, ...dedented, ...after].join("\n") + +// Figure out file naming +const dir = path.dirname(absPath) +const basename = path.basename(absPath, ".ts") +const isIndex = basename === "index" + +// The implementation file name (lowercase namespace name if currently index.ts) +const implName = isIndex ? nsName.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase() : basename +const implFile = path.join(dir, `${implName}.ts`) +const indexFile = path.join(dir, "index.ts") + +// The barrel line +const barrelLine = `export * as ${nsName} from "./${implName}"\n` + +console.log("") +if (isIndex) { + console.log(`Plan: rename ${basename}.ts → ${implName}.ts, create new index.ts barrel`) +} else { + console.log(`Plan: rewrite ${basename}.ts in place, create index.ts barrel`) +} +console.log("") + +if (dryRun) { + console.log("--- DRY RUN ---") + console.log("") + console.log(`=== ${implName}.ts (first 30 lines) ===`) + newContent + .split("\n") + .slice(0, 30) + .forEach((l, i) => console.log(` ${i + 1}: ${l}`)) + console.log(" ...") + console.log("") + console.log(`=== index.ts ===`) + console.log(` ${barrelLine.trim()}`) +} else { + // Write the implementation file + if (isIndex) { + // Rename: write new content to implFile, then overwrite index.ts with barrel + fs.writeFileSync(implFile, newContent) + fs.writeFileSync(indexFile, barrelLine) + console.log(`Wrote ${implName}.ts (${newContent.split("\n").length} lines)`) + console.log(`Wrote index.ts (barrel)`) + } else { + // Rewrite in place, create index.ts + fs.writeFileSync(absPath, newContent) + if (fs.existsSync(indexFile)) { + // Append to existing barrel + const existing = fs.readFileSync(indexFile, "utf-8") + if (!existing.includes(`export * as ${nsName}`)) { + fs.appendFileSync(indexFile, barrelLine) + console.log(`Appended to existing index.ts`) + } else { + console.log(`index.ts already has ${nsName} export`) + } + } else { + fs.writeFileSync(indexFile, barrelLine) + console.log(`Wrote index.ts (barrel)`) + } + console.log(`Rewrote ${basename}.ts (${newContent.split("\n").length} lines)`) + } +} + +// Print the import rewrite guidance +const relDir = path.relative(path.resolve("src"), dir) + +console.log("") +console.log("=== Import rewrites ===") +console.log("") + +if (!isIndex) { + // Non-index files: imports like "../provider/provider" need to become "../provider" + const oldTail = `${relDir}/${basename}` + + console.log(`# Find all imports to rewrite:`) + console.log(`rg 'from.*${oldTail}' src/ --files-with-matches`) + console.log("") + + // Auto-rewrite with sed (safe: only rewrites the import path, not other occurrences) + console.log("# Auto-rewrite (review diff afterward):") + console.log(`rg -l 'from.*${oldTail}' src/ | xargs sed -i '' 's|${oldTail}"|${relDir}"|g'`) + console.log("") + console.log("# What changes:") + console.log(`# import { ${nsName} } from ".../${oldTail}"`) + console.log(`# import { ${nsName} } from ".../${relDir}"`) +} else { + console.log("# File was index.ts — import paths already resolve correctly.") + console.log("# No import rewrites needed!") +} + +console.log("") +console.log("=== Verify ===") +console.log("") +console.log("bun typecheck # from packages/opencode") +console.log("bun run test # run tests") diff --git a/packages/opencode/specs/effect/namespace-treeshake.md b/packages/opencode/specs/effect/namespace-treeshake.md new file mode 100644 index 0000000000..8a9cf94fd4 --- /dev/null +++ b/packages/opencode/specs/effect/namespace-treeshake.md @@ -0,0 +1,444 @@ +# Namespace → flat export migration + +Migrate `export namespace` to the `export * as` / flat-export pattern used by +effect-smol. Primary goal: tree-shakeability. Secondary: consistency with Effect +conventions, LLM-friendliness for future migrations. + +## What changes and what doesn't + +The **consumer API stays the same**. You still write `Provider.ModelNotFoundError`, +`Config.JsonError`, `Bus.publish`, etc. The namespace ergonomics are preserved. + +What changes is **how** the namespace is constructed — the TypeScript +`export namespace` keyword is replaced by `export * as` in a barrel file. This +is a mechanical change: unwrap the namespace body into flat exports, add a +one-line barrel. Consumers that import `{ Provider }` don't notice. + +Import paths actually get **nicer**. Today most consumers import from the +explicit file (`"../provider/provider"`). After the migration, each module has a +barrel `index.ts`, so imports become `"../provider"` or `"@/provider"`: + +```ts +// BEFORE — points at the file directly +import { Provider } from "../provider/provider" + +// AFTER — resolves to provider/index.ts, same Provider namespace +import { Provider } from "../provider" +``` + +## Why this matters right now + +The CLI binary startup time (TOI) is too slow. Profiling shows we're loading +massive dependency graphs that are never actually used at runtime — because +bundlers cannot tree-shake TypeScript `export namespace` bodies. + +### The problem in one sentence + +`cli/error.ts` needs 6 lightweight `.isInstance()` checks on error classes, but +importing `{ Provider }` from `provider.ts` forces the bundler to include **all +20+ `@ai-sdk/*` packages**, `@aws-sdk/credential-providers`, +`google-auth-library`, and every other top-level import in that 1709-line file. + +### Why `export namespace` defeats tree-shaking + +TypeScript compiles `export namespace Foo { ... }` to an IIFE: + +```js +// TypeScript output +export var Provider; +(function (Provider) { + Provider.ModelNotFoundError = NamedError.create(...) + // ... 1600 more lines of assignments ... +})(Provider || (Provider = {})) +``` + +This is **opaque to static analysis**. The bundler sees one big function call +whose return value populates an object. It cannot determine which properties are +used downstream, so it keeps everything. Every `import` statement at the top of +`provider.ts` executes unconditionally — that's 20+ AI SDK packages loaded into +memory just so the CLI can check `Provider.ModelNotFoundError.isInstance(x)`. + +### What `export * as` does differently + +`export * as Provider from "./provider"` compiles to a static re-export. The +bundler knows the exact shape of `Provider` at compile time — it's the named +export list of `./provider.ts`. When it sees `Provider.ModelNotFoundError` used +but `Provider.layer` unused, it can trace that `ModelNotFoundError` doesn't +reference `createAnthropic` or any AI SDK import, and drop them. The namespace +object still exists at runtime — same API — but the bundler can see inside it. + +### Concrete impact + +The worst import chain in the codebase: + +``` +src/index.ts (entry point) + └── FormatError from src/cli/error.ts + ├── { Provider } from provider/provider.ts (1709 lines) + │ ├── 20+ @ai-sdk/* packages + │ ├── @aws-sdk/credential-providers + │ ├── google-auth-library + │ ├── gitlab-ai-provider, venice-ai-sdk-provider + │ └── fuzzysort, remeda, etc. + ├── { Config } from config/config.ts (1663 lines) + │ ├── jsonc-parser + │ ├── LSPServer (all server definitions) + │ └── Plugin, Auth, Env, Account, etc. + └── { MCP } from mcp/index.ts (930 lines) + ├── @modelcontextprotocol/sdk (3 transports) + └── open (browser launcher) +``` + +All of this gets pulled in to check `.isInstance()` on 6 error classes — code +that needs maybe 200 bytes total. This inflates the binary, increases startup +memory, and slows down initial module evaluation. + +### Why this also hurts memory + +Every module-level import is eagerly evaluated. Even with Bun's fast module +loader, evaluating 20+ AI SDK factory functions, the AWS credential chain, and +Google's auth library allocates objects, closures, and prototype chains that +persist for the lifetime of the process. Most CLI commands never use a provider +at all. + +## What effect-smol does + +effect-smol achieves tree-shakeable namespaced APIs via three structural choices. + +### 1. Each module is a separate file with flat named exports + +```ts +// Effect.ts — no namespace wrapper, just flat exports +export const gen: { ... } = internal.gen +export const fail: (error: E) => Effect = internal.fail +export const succeed: (value: A) => Effect = internal.succeed +// ... 230+ individual named exports +``` + +### 2. Barrel file uses `export * as` (not `export namespace`) + +```ts +// index.ts +export * as Effect from "./Effect.ts" +export * as Schema from "./Schema.ts" +export * as Stream from "./Stream.ts" +// ~134 modules +``` + +This creates a namespace-like API (`Effect.gen`, `Schema.parse`) but the +bundler knows the **exact shape** at compile time — it's the static export list +of that file. It can trace property accesses (`Effect.gen` → keep `gen`, +drop `timeout` if unused). With `export namespace`, the IIFE is opaque and +nothing can be dropped. + +### 3. `sideEffects: []` and deep imports + +```jsonc +// package.json +{ "sideEffects": [] } +``` + +Plus `"./*": "./src/*.ts"` in the exports map, enabling +`import * as Effect from "effect/Effect"` to bypass the barrel entirely. + +### 4. Errors as flat exports, not class declarations + +```ts +// Cause.ts +export const NoSuchElementErrorTypeId = core.NoSuchElementErrorTypeId +export interface NoSuchElementError extends YieldableError { ... } +export const NoSuchElementError: new(msg?: string) => NoSuchElementError = core.NoSuchElementError +export const isNoSuchElementError: (u: unknown) => u is NoSuchElementError = core.isNoSuchElementError +``` + +Each error is 4 independent exports: TypeId, interface, constructor (as const), +type guard. All individually shakeable. + +## The plan + +The core migration is **Phase 1** — convert `export namespace` to +`export * as`. Once that's done, the bundler can tree-shake individual exports +within each module. You do NOT need to break things into subfiles for +tree-shaking to work — the bundler traces which exports you actually access on +the namespace object and drops the rest, including their transitive imports. + +Splitting errors/schemas into separate files (Phase 0) is optional — it's a +lower-risk warmup step that can be done before or after the main conversion, and +it provides extra resilience against bundler edge cases. But the big win comes +from Phase 1. + +### Phase 0 (optional): Pre-split errors into subfiles + +This is a low-risk warmup that provides immediate benefit even before the full +`export * as` conversion. It's optional because Phase 1 alone is sufficient for +tree-shaking. But it's a good starting point if you want incremental progress: + +**For each namespace that defines errors** (15 files, ~30 error classes total): + +1. Create a sibling `errors.ts` file (e.g. `provider/errors.ts`) with the error + definitions as top-level named exports: + + ```ts + // provider/errors.ts + import z from "zod" + import { NamedError } from "@opencode-ai/shared/util/error" + import { ProviderID, ModelID } from "./schema" + + export const ModelNotFoundError = NamedError.create( + "ProviderModelNotFoundError", + z.object({ + providerID: ProviderID.zod, + modelID: ModelID.zod, + suggestions: z.array(z.string()).optional(), + }), + ) + + export const InitError = NamedError.create("ProviderInitError", z.object({ providerID: ProviderID.zod })) + ``` + +2. In the namespace file, re-export from the errors file to maintain backward + compatibility: + + ```ts + // provider/provider.ts — inside the namespace + export { ModelNotFoundError, InitError } from "./errors" + ``` + +3. Update `cli/error.ts` (and any other light consumers) to import directly: + + ```ts + // BEFORE + import { Provider } from "../provider/provider" + Provider.ModelNotFoundError.isInstance(input) + + // AFTER + import { ModelNotFoundError as ProviderModelNotFoundError } from "../provider/errors" + ProviderModelNotFoundError.isInstance(input) + ``` + +**Files to split (Phase 0):** + +| Current file | New errors file | Errors to extract | +| ----------------------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | +| `provider/provider.ts` | `provider/errors.ts` | ModelNotFoundError, InitError | +| `provider/auth.ts` | `provider/auth-errors.ts` | OauthMissing, OauthCodeMissing, OauthCallbackFailed, ValidationFailed | +| `config/config.ts` | (already has `config/paths.ts`) | ConfigDirectoryTypoError → move to paths.ts | +| `config/markdown.ts` | `config/markdown-errors.ts` | FrontmatterError | +| `mcp/index.ts` | `mcp/errors.ts` | Failed | +| `session/message-v2.ts` | `session/message-errors.ts` | OutputLengthError, AbortedError, StructuredOutputError, AuthError, APIError, ContextOverflowError | +| `session/message.ts` | (shares with message-v2) | OutputLengthError, AuthError | +| `cli/ui.ts` | `cli/ui-errors.ts` | CancelledError | +| `skill/index.ts` | `skill/errors.ts` | InvalidError, NameMismatchError | +| `worktree/index.ts` | `worktree/errors.ts` | NotGitError, NameGenerationFailedError, CreateFailedError, StartCommandFailedError, RemoveFailedError, ResetFailedError | +| `storage/storage.ts` | `storage/errors.ts` | NotFoundError | +| `npm/index.ts` | `npm/errors.ts` | InstallFailedError | +| `ide/index.ts` | `ide/errors.ts` | AlreadyInstalledError, InstallFailedError | +| `lsp/client.ts` | `lsp/errors.ts` | InitializeError | + +### Phase 1: The real migration — `export namespace` → `export * as` + +This is the phase that actually fixes tree-shaking. For each module: + +1. **Unwrap** the `export namespace Foo { ... }` — remove the namespace wrapper, + keep all the members as top-level `export const` / `export function` / etc. +2. **Rename** the file if it's currently `index.ts` (e.g. `bus/index.ts` → + `bus/bus.ts`), so the barrel can take `index.ts`. +3. **Create the barrel** `index.ts` with one line: `export * as Foo from "./foo"` + +The file structure change for a module that's currently a single file: + +``` +# BEFORE +provider/ + provider.ts ← 1709-line file with `export namespace Provider { ... }` + +# AFTER +provider/ + index.ts ← NEW: `export * as Provider from "./provider"` + provider.ts ← SAME file, same name, just unwrap the namespace +``` + +And the code change is purely removing the wrapper: + +```ts +// BEFORE: provider/provider.ts +export namespace Provider { + export class Service extends Context.Service<...>()("@opencode/Provider") {} + export const layer = Layer.effect(Service, ...) + export const ModelNotFoundError = NamedError.create(...) + export function parseModel(model: string) { ... } +} + +// AFTER: provider/provider.ts — identical exports, no namespace keyword +export class Service extends Context.Service<...>()("@opencode/Provider") {} +export const layer = Layer.effect(Service, ...) +export const ModelNotFoundError = NamedError.create(...) +export function parseModel(model: string) { ... } +``` + +```ts +// NEW: provider/index.ts +export * as Provider from "./provider" +``` + +Consumer code barely changes — import path gets shorter: + +```ts +// BEFORE +import { Provider } from "../provider/provider" + +// AFTER — resolves to provider/index.ts, same Provider object +import { Provider } from "../provider" +``` + +All access like `Provider.ModelNotFoundError`, `Provider.Service`, +`Provider.layer` works exactly as before. The difference is invisible to +consumers but lets the bundler see inside the namespace. + +**Once this is done, you don't need to break anything into subfiles for +tree-shaking.** The bundler traces that `Provider.ModelNotFoundError` only +depends on `NamedError` + `zod` + the schema file, and drops +`Provider.layer` + all 20 AI SDK imports when they're unused. This works because +`export * as` gives the bundler a static export list it can do inner-graph +analysis on — it knows which exports reference which imports. + +**Order of conversion** (by risk / size, do small modules first): + +1. Tiny utilities: `Archive`, `Color`, `Token`, `Rpc`, `LocalContext` (~7-66 lines each) +2. Small services: `Auth`, `Env`, `BusEvent`, `SessionStatus`, `SessionRunState`, `Editor`, `Selection` (~25-91 lines) +3. Medium services: `Bus`, `Format`, `FileTime`, `FileWatcher`, `Command`, `Question`, `Permission`, `Vcs`, `Project` +4. Large services: `Config`, `Provider`, `MCP`, `Session`, `SessionProcessor`, `SessionPrompt`, `ACP` + +### Phase 2: Build configuration + +After the module structure supports tree-shaking: + +1. Add `"sideEffects": []` to `packages/opencode/package.json` (or + `"sideEffects": false`) — this is safe because our services use explicit + layer composition, not import-time side effects. +2. Verify Bun's bundler respects the new structure. If Bun's tree-shaking is + insufficient, evaluate whether the compiled binary path needs an esbuild + pre-pass. +3. Consider adding `/*#__PURE__*/` annotations to `NamedError.create(...)` calls + — these are factory functions that return classes, and bundlers may not know + they're side-effect-free without the annotation. + +## Automation + +The transformation is scripted. From `packages/opencode`: + +```bash +bun script/unwrap-namespace.ts [--dry-run] +``` + +The script uses ast-grep for accurate AST-based namespace boundary detection +(no false matches from braces in strings/templates/comments), then: + +1. Removes the `export namespace Foo {` line and its closing `}` +2. Dedents the body by one indent level (2 spaces) +3. If the file is `index.ts`, renames it to `.ts` and creates a new + `index.ts` barrel +4. If the file is NOT `index.ts`, rewrites it in place and creates `index.ts` +5. Prints the exact commands to find and rewrite import paths + +### Walkthrough: converting a module + +Using `Provider` as an example: + +```bash +# 1. Preview what will change +bun script/unwrap-namespace.ts src/provider/provider.ts --dry-run + +# 2. Apply the transformation +bun script/unwrap-namespace.ts src/provider/provider.ts + +# 3. Rewrite import paths (script prints the exact command) +rg -l 'from.*provider/provider' src/ | xargs sed -i '' 's|provider/provider"|provider"|g' + +# 4. Verify +bun typecheck +bun run test +``` + +**What changes on disk:** + +``` +# BEFORE +provider/ + provider.ts ← 1709 lines, `export namespace Provider { ... }` + +# AFTER +provider/ + index.ts ← NEW: `export * as Provider from "./provider"` + provider.ts ← same file, namespace unwrapped to flat exports +``` + +**What changes in consumer code:** + +```ts +// BEFORE +import { Provider } from "../provider/provider" + +// AFTER — shorter path, same Provider object +import { Provider } from "../provider" +``` + +All property access (`Provider.Service`, `Provider.ModelNotFoundError`, etc.) +stays identical. + +### Two cases the script handles + +**Case A: file is NOT `index.ts`** (e.g. `provider/provider.ts`) + +- Rewrites the file in place (unwrap + dedent) +- Creates `provider/index.ts` as the barrel +- Import paths change: `"../provider/provider"` → `"../provider"` + +**Case B: file IS `index.ts`** (e.g. `bus/index.ts`) + +- Renames `index.ts` → `bus.ts` (kebab-case of namespace name) +- Creates new `index.ts` as the barrel +- **No import rewrites needed** — `"@/bus"` already resolves to `bus/index.ts` + +## Do I need to split errors/schemas into subfiles? + +**No.** Once you do the `export * as` conversion, the bundler can tree-shake +individual exports within the file. If `cli/error.ts` only accesses +`Provider.ModelNotFoundError`, the bundler traces that `ModelNotFoundError` +doesn't reference `createAnthropic` and drops the AI SDK imports. + +Splitting into subfiles (errors.ts, schema.ts) is still a fine idea for **code +organization** — smaller files are easier to read and review. But it's not +required for tree-shaking. The `export * as` conversion alone is sufficient. + +The one case where subfile splitting provides extra tree-shake value is if an +imported package has module-level side effects that the bundler can't prove are +unused. In practice this is rare — most npm packages are side-effect-free — and +adding `"sideEffects": []` to package.json handles the common cases. + +## Scope + +| Metric | Count | +| ----------------------------------------------- | --------------- | +| Files with `export namespace` | 106 | +| Total namespace declarations | 118 (12 nested) | +| Files with `NamedError.create` inside namespace | 15 | +| Total error classes to extract | ~30 | +| Files using `export * as` today | 0 | + +Phase 1 (the `export * as` conversion) is the main change. It's mechanical and +LLM-friendly but touches every import site, so it should be done module by +module with type-checking between each step. Each module is an independent PR. + +## Rules for new code + +Going forward: + +- **No new `export namespace`**. Use a file with flat named exports and + `export * as` in the barrel. +- Keep the service, layer, errors, schemas, and runtime wiring together in one + file if you want — that's fine now. The `export * as` barrel makes everything + individually shakeable regardless of file structure. +- If a file grows large enough that it's hard to navigate, split by concern + (errors.ts, schema.ts, etc.) for readability. Not for tree-shaking — the + bundler handles that. diff --git a/packages/opencode/src/bus/bus.ts b/packages/opencode/src/bus/bus.ts new file mode 100644 index 0000000000..c5e31e6c20 --- /dev/null +++ b/packages/opencode/src/bus/bus.ts @@ -0,0 +1,192 @@ +import z from "zod" +import { Effect, Exit, Layer, PubSub, Scope, Context, Stream } from "effect" +import { EffectBridge } from "@/effect/bridge" +import { Log } from "../util/log" +import { BusEvent } from "./bus-event" +import { GlobalBus } from "./global" +import { WorkspaceContext } from "@/control-plane/workspace-context" +import { InstanceState } from "@/effect/instance-state" +import { makeRuntime } from "@/effect/run-service" + +const log = Log.create({ service: "bus" }) + +export const InstanceDisposed = BusEvent.define( + "server.instance.disposed", + z.object({ + directory: z.string(), + }), +) + +type Payload = { + type: D["type"] + properties: z.infer +} + +type State = { + wildcard: PubSub.PubSub + typed: Map> +} + +export interface Interface { + readonly publish: ( + def: D, + properties: z.output, + ) => Effect.Effect + readonly subscribe: (def: D) => Stream.Stream> + readonly subscribeAll: () => Stream.Stream + readonly subscribeCallback: ( + def: D, + callback: (event: Payload) => unknown, + ) => Effect.Effect<() => void> + readonly subscribeAllCallback: (callback: (event: any) => unknown) => Effect.Effect<() => void> +} + +export class Service extends Context.Service()("@opencode/Bus") {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const state = yield* InstanceState.make( + Effect.fn("Bus.state")(function* (ctx) { + const wildcard = yield* PubSub.unbounded() + const typed = new Map>() + + yield* Effect.addFinalizer(() => + Effect.gen(function* () { + // Publish InstanceDisposed before shutting down so subscribers see it + yield* PubSub.publish(wildcard, { + type: InstanceDisposed.type, + properties: { directory: ctx.directory }, + }) + yield* PubSub.shutdown(wildcard) + for (const ps of typed.values()) { + yield* PubSub.shutdown(ps) + } + }), + ) + + return { wildcard, typed } + }), + ) + + function getOrCreate(state: State, def: D) { + return Effect.gen(function* () { + let ps = state.typed.get(def.type) + if (!ps) { + ps = yield* PubSub.unbounded() + state.typed.set(def.type, ps) + } + return ps as unknown as PubSub.PubSub> + }) + } + + function publish(def: D, properties: z.output) { + return Effect.gen(function* () { + const s = yield* InstanceState.get(state) + const payload: Payload = { type: def.type, properties } + log.info("publishing", { type: def.type }) + + const ps = s.typed.get(def.type) + if (ps) yield* PubSub.publish(ps, payload) + yield* PubSub.publish(s.wildcard, payload) + + const dir = yield* InstanceState.directory + const context = yield* InstanceState.context + const workspace = yield* InstanceState.workspaceID + + GlobalBus.emit("event", { + directory: dir, + project: context.project.id, + workspace, + payload, + }) + }) + } + + function subscribe(def: D): Stream.Stream> { + log.info("subscribing", { type: def.type }) + return Stream.unwrap( + Effect.gen(function* () { + const s = yield* InstanceState.get(state) + const ps = yield* getOrCreate(s, def) + return Stream.fromPubSub(ps) + }), + ).pipe(Stream.ensuring(Effect.sync(() => log.info("unsubscribing", { type: def.type })))) + } + + function subscribeAll(): Stream.Stream { + log.info("subscribing", { type: "*" }) + return Stream.unwrap( + Effect.gen(function* () { + const s = yield* InstanceState.get(state) + return Stream.fromPubSub(s.wildcard) + }), + ).pipe(Stream.ensuring(Effect.sync(() => log.info("unsubscribing", { type: "*" })))) + } + + function on(pubsub: PubSub.PubSub, type: string, callback: (event: T) => unknown) { + return Effect.gen(function* () { + log.info("subscribing", { type }) + const bridge = yield* EffectBridge.make() + const scope = yield* Scope.make() + const subscription = yield* Scope.provide(scope)(PubSub.subscribe(pubsub)) + + yield* Scope.provide(scope)( + Stream.fromSubscription(subscription).pipe( + Stream.runForEach((msg) => + Effect.tryPromise({ + try: () => Promise.resolve().then(() => callback(msg)), + catch: (cause) => { + log.error("subscriber failed", { type, cause }) + }, + }).pipe(Effect.ignore), + ), + Effect.forkScoped, + ), + ) + + return () => { + log.info("unsubscribing", { type }) + bridge.fork(Scope.close(scope, Exit.void)) + } + }) + } + + const subscribeCallback = Effect.fn("Bus.subscribeCallback")(function* ( + def: D, + callback: (event: Payload) => unknown, + ) { + const s = yield* InstanceState.get(state) + const ps = yield* getOrCreate(s, def) + return yield* on(ps, def.type, callback) + }) + + const subscribeAllCallback = Effect.fn("Bus.subscribeAllCallback")(function* (callback: (event: any) => unknown) { + const s = yield* InstanceState.get(state) + return yield* on(s.wildcard, "*", callback) + }) + + return Service.of({ publish, subscribe, subscribeAll, subscribeCallback, subscribeAllCallback }) + }), +) + +export const defaultLayer = layer + +const { runPromise, runSync } = makeRuntime(Service, layer) + +// runSync is safe here because the subscribe chain (InstanceState.get, PubSub.subscribe, +// Scope.make, Effect.forkScoped) is entirely synchronous. If any step becomes async, this will throw. +export async function publish(def: D, properties: z.output) { + return runPromise((svc) => svc.publish(def, properties)) +} + +export function subscribe( + def: D, + callback: (event: { type: D["type"]; properties: z.infer }) => unknown, +) { + return runSync((svc) => svc.subscribeCallback(def, callback)) +} + +export function subscribeAll(callback: (event: any) => unknown) { + return runSync((svc) => svc.subscribeAllCallback(callback)) +} diff --git a/packages/opencode/src/bus/index.ts b/packages/opencode/src/bus/index.ts index 3a1eea5c73..3c21d7c7d1 100644 --- a/packages/opencode/src/bus/index.ts +++ b/packages/opencode/src/bus/index.ts @@ -1,194 +1 @@ -import z from "zod" -import { Effect, Exit, Layer, PubSub, Scope, Context, Stream } from "effect" -import { EffectBridge } from "@/effect/bridge" -import { Log } from "../util/log" -import { BusEvent } from "./bus-event" -import { GlobalBus } from "./global" -import { WorkspaceContext } from "@/control-plane/workspace-context" -import { InstanceState } from "@/effect/instance-state" -import { makeRuntime } from "@/effect/run-service" - -export namespace Bus { - const log = Log.create({ service: "bus" }) - - export const InstanceDisposed = BusEvent.define( - "server.instance.disposed", - z.object({ - directory: z.string(), - }), - ) - - type Payload = { - type: D["type"] - properties: z.infer - } - - type State = { - wildcard: PubSub.PubSub - typed: Map> - } - - export interface Interface { - readonly publish: ( - def: D, - properties: z.output, - ) => Effect.Effect - readonly subscribe: (def: D) => Stream.Stream> - readonly subscribeAll: () => Stream.Stream - readonly subscribeCallback: ( - def: D, - callback: (event: Payload) => unknown, - ) => Effect.Effect<() => void> - readonly subscribeAllCallback: (callback: (event: any) => unknown) => Effect.Effect<() => void> - } - - export class Service extends Context.Service()("@opencode/Bus") {} - - export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const state = yield* InstanceState.make( - Effect.fn("Bus.state")(function* (ctx) { - const wildcard = yield* PubSub.unbounded() - const typed = new Map>() - - yield* Effect.addFinalizer(() => - Effect.gen(function* () { - // Publish InstanceDisposed before shutting down so subscribers see it - yield* PubSub.publish(wildcard, { - type: InstanceDisposed.type, - properties: { directory: ctx.directory }, - }) - yield* PubSub.shutdown(wildcard) - for (const ps of typed.values()) { - yield* PubSub.shutdown(ps) - } - }), - ) - - return { wildcard, typed } - }), - ) - - function getOrCreate(state: State, def: D) { - return Effect.gen(function* () { - let ps = state.typed.get(def.type) - if (!ps) { - ps = yield* PubSub.unbounded() - state.typed.set(def.type, ps) - } - return ps as unknown as PubSub.PubSub> - }) - } - - function publish(def: D, properties: z.output) { - return Effect.gen(function* () { - const s = yield* InstanceState.get(state) - const payload: Payload = { type: def.type, properties } - log.info("publishing", { type: def.type }) - - const ps = s.typed.get(def.type) - if (ps) yield* PubSub.publish(ps, payload) - yield* PubSub.publish(s.wildcard, payload) - - const dir = yield* InstanceState.directory - const context = yield* InstanceState.context - const workspace = yield* InstanceState.workspaceID - - GlobalBus.emit("event", { - directory: dir, - project: context.project.id, - workspace, - payload, - }) - }) - } - - function subscribe(def: D): Stream.Stream> { - log.info("subscribing", { type: def.type }) - return Stream.unwrap( - Effect.gen(function* () { - const s = yield* InstanceState.get(state) - const ps = yield* getOrCreate(s, def) - return Stream.fromPubSub(ps) - }), - ).pipe(Stream.ensuring(Effect.sync(() => log.info("unsubscribing", { type: def.type })))) - } - - function subscribeAll(): Stream.Stream { - log.info("subscribing", { type: "*" }) - return Stream.unwrap( - Effect.gen(function* () { - const s = yield* InstanceState.get(state) - return Stream.fromPubSub(s.wildcard) - }), - ).pipe(Stream.ensuring(Effect.sync(() => log.info("unsubscribing", { type: "*" })))) - } - - function on(pubsub: PubSub.PubSub, type: string, callback: (event: T) => unknown) { - return Effect.gen(function* () { - log.info("subscribing", { type }) - const bridge = yield* EffectBridge.make() - const scope = yield* Scope.make() - const subscription = yield* Scope.provide(scope)(PubSub.subscribe(pubsub)) - - yield* Scope.provide(scope)( - Stream.fromSubscription(subscription).pipe( - Stream.runForEach((msg) => - Effect.tryPromise({ - try: () => Promise.resolve().then(() => callback(msg)), - catch: (cause) => { - log.error("subscriber failed", { type, cause }) - }, - }).pipe(Effect.ignore), - ), - Effect.forkScoped, - ), - ) - - return () => { - log.info("unsubscribing", { type }) - bridge.fork(Scope.close(scope, Exit.void)) - } - }) - } - - const subscribeCallback = Effect.fn("Bus.subscribeCallback")(function* ( - def: D, - callback: (event: Payload) => unknown, - ) { - const s = yield* InstanceState.get(state) - const ps = yield* getOrCreate(s, def) - return yield* on(ps, def.type, callback) - }) - - const subscribeAllCallback = Effect.fn("Bus.subscribeAllCallback")(function* (callback: (event: any) => unknown) { - const s = yield* InstanceState.get(state) - return yield* on(s.wildcard, "*", callback) - }) - - return Service.of({ publish, subscribe, subscribeAll, subscribeCallback, subscribeAllCallback }) - }), - ) - - export const defaultLayer = layer - - const { runPromise, runSync } = makeRuntime(Service, layer) - - // runSync is safe here because the subscribe chain (InstanceState.get, PubSub.subscribe, - // Scope.make, Effect.forkScoped) is entirely synchronous. If any step becomes async, this will throw. - export async function publish(def: D, properties: z.output) { - return runPromise((svc) => svc.publish(def, properties)) - } - - export function subscribe( - def: D, - callback: (event: { type: D["type"]; properties: z.infer }) => unknown, - ) { - return runSync((svc) => svc.subscribeCallback(def, callback)) - } - - export function subscribeAll(callback: (event: any) => unknown) { - return runSync((svc) => svc.subscribeAllCallback(callback)) - } -} +export * as Bus from "./bus" From 0fb0135e514216732b982a9b302ee5e10f3c8e51 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 21:22:18 -0400 Subject: [PATCH 168/300] refactor: remove makeRuntime facades from File and Ripgrep (#22513) --- packages/opencode/src/cli/cmd/debug/file.ts | 33 +-- .../opencode/src/cli/cmd/debug/ripgrep.ts | 29 ++- packages/opencode/src/file/index.ts | 24 +- packages/opencode/src/file/ripgrep.ts | 16 +- packages/opencode/test/file/ripgrep.test.ts | 241 +++++++----------- 5 files changed, 130 insertions(+), 213 deletions(-) diff --git a/packages/opencode/src/cli/cmd/debug/file.ts b/packages/opencode/src/cli/cmd/debug/file.ts index d5e24a0cfa..8e4eaa4e4d 100644 --- a/packages/opencode/src/cli/cmd/debug/file.ts +++ b/packages/opencode/src/cli/cmd/debug/file.ts @@ -1,10 +1,9 @@ import { EOL } from "os" -import { Effect } from "effect" import { AppRuntime } from "@/effect/app-runtime" import { File } from "../../../file" +import { Ripgrep } from "@/file/ripgrep" import { bootstrap } from "../../bootstrap" import { cmd } from "../cmd" -import { Ripgrep } from "@/file/ripgrep" const FileSearchCommand = cmd({ command: "search ", @@ -17,11 +16,7 @@ const FileSearchCommand = cmd({ }), async handler(args) { await bootstrap(process.cwd(), async () => { - const results = await AppRuntime.runPromise( - Effect.gen(function* () { - return yield* File.Service.use((svc) => svc.search({ query: args.query })) - }), - ) + const results = await AppRuntime.runPromise(File.Service.use((svc) => svc.search({ query: args.query }))) process.stdout.write(results.join(EOL) + EOL) }) }, @@ -38,11 +33,7 @@ const FileReadCommand = cmd({ }), async handler(args) { await bootstrap(process.cwd(), async () => { - const content = await AppRuntime.runPromise( - Effect.gen(function* () { - return yield* File.Service.use((svc) => svc.read(args.path)) - }), - ) + const content = await AppRuntime.runPromise(File.Service.use((svc) => svc.read(args.path))) process.stdout.write(JSON.stringify(content, null, 2) + EOL) }) }, @@ -54,11 +45,7 @@ const FileStatusCommand = cmd({ builder: (yargs) => yargs, async handler() { await bootstrap(process.cwd(), async () => { - const status = await AppRuntime.runPromise( - Effect.gen(function* () { - return yield* File.Service.use((svc) => svc.status()) - }), - ) + const status = await AppRuntime.runPromise(File.Service.use((svc) => svc.status())) process.stdout.write(JSON.stringify(status, null, 2) + EOL) }) }, @@ -75,11 +62,7 @@ const FileListCommand = cmd({ }), async handler(args) { await bootstrap(process.cwd(), async () => { - const files = await AppRuntime.runPromise( - Effect.gen(function* () { - return yield* File.Service.use((svc) => svc.list(args.path)) - }), - ) + const files = await AppRuntime.runPromise(File.Service.use((svc) => svc.list(args.path))) process.stdout.write(JSON.stringify(files, null, 2) + EOL) }) }, @@ -95,8 +78,10 @@ const FileTreeCommand = cmd({ default: process.cwd(), }), async handler(args) { - const files = await Ripgrep.tree({ cwd: args.dir, limit: 200 }) - console.log(JSON.stringify(files, null, 2)) + await bootstrap(process.cwd(), async () => { + const tree = await AppRuntime.runPromise(Ripgrep.Service.use((svc) => svc.tree({ cwd: args.dir, limit: 200 }))) + console.log(JSON.stringify(tree, null, 2)) + }) }, }) diff --git a/packages/opencode/src/cli/cmd/debug/ripgrep.ts b/packages/opencode/src/cli/cmd/debug/ripgrep.ts index 8c994d6e52..9b7e826915 100644 --- a/packages/opencode/src/cli/cmd/debug/ripgrep.ts +++ b/packages/opencode/src/cli/cmd/debug/ripgrep.ts @@ -1,4 +1,5 @@ import { EOL } from "os" +import { Effect, Stream } from "effect" import { AppRuntime } from "../../../effect/app-runtime" import { Ripgrep } from "../../../file/ripgrep" import { Instance } from "../../../project/instance" @@ -21,7 +22,10 @@ const TreeCommand = cmd({ }), async handler(args) { await bootstrap(process.cwd(), async () => { - process.stdout.write((await Ripgrep.tree({ cwd: Instance.directory, limit: args.limit })) + EOL) + const tree = await AppRuntime.runPromise( + Ripgrep.Service.use((svc) => svc.tree({ cwd: Instance.directory, limit: args.limit })), + ) + process.stdout.write(tree + EOL) }) }, }) @@ -45,14 +49,21 @@ const FilesCommand = cmd({ }), async handler(args) { await bootstrap(process.cwd(), async () => { - const files: string[] = [] - for await (const file of await Ripgrep.files({ - cwd: Instance.directory, - glob: args.glob ? [args.glob] : undefined, - })) { - files.push(file) - if (args.limit && files.length >= args.limit) break - } + const files = await AppRuntime.runPromise( + Effect.gen(function* () { + const rg = yield* Ripgrep.Service + return yield* rg + .files({ + cwd: Instance.directory, + glob: args.glob ? [args.glob] : undefined, + }) + .pipe( + Stream.take(args.limit ?? Infinity), + Stream.runCollect, + Effect.map((c) => [...c]), + ) + }), + ) process.stdout.write(files.join(EOL) + EOL) }) }, diff --git a/packages/opencode/src/file/index.ts b/packages/opencode/src/file/index.ts index 113dc59096..909f1e61d2 100644 --- a/packages/opencode/src/file/index.ts +++ b/packages/opencode/src/file/index.ts @@ -1,6 +1,6 @@ import { BusEvent } from "@/bus/bus-event" import { InstanceState } from "@/effect/instance-state" -import { makeRuntime } from "@/effect/run-service" + import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { Git } from "@/git" import { Effect, Layer, Context } from "effect" @@ -653,26 +653,4 @@ export namespace File { Layer.provide(AppFileSystem.defaultLayer), Layer.provide(Git.defaultLayer), ) - - const { runPromise } = makeRuntime(Service, defaultLayer) - - export function init() { - return runPromise((svc) => svc.init()) - } - - export async function status() { - return runPromise((svc) => svc.status()) - } - - export async function read(file: string): Promise { - return runPromise((svc) => svc.read(file)) - } - - export async function list(dir?: string) { - return runPromise((svc) => svc.list(dir)) - } - - export async function search(input: { query: string; limit?: number; dirs?: boolean; type?: "file" | "directory" }) { - return runPromise((svc) => svc.search(input)) - } } diff --git a/packages/opencode/src/file/ripgrep.ts b/packages/opencode/src/file/ripgrep.ts index abf7438dcc..fee9cf4430 100644 --- a/packages/opencode/src/file/ripgrep.ts +++ b/packages/opencode/src/file/ripgrep.ts @@ -4,7 +4,7 @@ import { fileURLToPath } from "url" import z from "zod" import { Cause, Context, Effect, Layer, Queue, Stream } from "effect" import { ripgrep } from "ripgrep" -import { makeRuntime } from "@/effect/run-service" + import { Filesystem } from "@/util/filesystem" import { Log } from "@/util/log" @@ -572,18 +572,4 @@ export namespace Ripgrep { ) export const defaultLayer = layer - - const { runPromise } = makeRuntime(Service, defaultLayer) - - export function files(input: FilesInput) { - return runPromise((svc) => Stream.toAsyncIterableEffect(svc.files(input))) - } - - export function tree(input: TreeInput) { - return runPromise((svc) => svc.tree(input)) - } - - export function search(input: SearchInput) { - return runPromise((svc) => svc.search(input)) - } } diff --git a/packages/opencode/test/file/ripgrep.test.ts b/packages/opencode/test/file/ripgrep.test.ts index c3575fdf85..a76c7ebe26 100644 --- a/packages/opencode/test/file/ripgrep.test.ts +++ b/packages/opencode/test/file/ripgrep.test.ts @@ -6,20 +6,8 @@ import path from "path" import { tmpdir } from "../fixture/fixture" import { Ripgrep } from "../../src/file/ripgrep" -async function seed(dir: string, count: number, size = 16) { - const txt = "a".repeat(size) - await Promise.all(Array.from({ length: count }, (_, i) => Bun.write(path.join(dir, `file-${i}.txt`), `${txt}${i}\n`))) -} - -function env(name: string, value: string | undefined) { - const prev = process.env[name] - if (value === undefined) delete process.env[name] - else process.env[name] = value - return () => { - if (prev === undefined) delete process.env[name] - else process.env[name] = prev - } -} +const run = (effect: Effect.Effect) => + effect.pipe(Effect.provide(Ripgrep.defaultLayer), Effect.runPromise) describe("file.ripgrep", () => { test("defaults to include hidden", async () => { @@ -31,7 +19,14 @@ describe("file.ripgrep", () => { }, }) - const files = await Array.fromAsync(await Ripgrep.files({ cwd: tmp.path })) + const files = await run( + Ripgrep.Service.use((rg) => + rg.files({ cwd: tmp.path }).pipe( + Stream.runCollect, + Effect.map((c) => [...c]), + ), + ), + ) expect(files.includes("visible.txt")).toBe(true) expect(files.includes(path.join(".opencode", "thing.json"))).toBe(true) }) @@ -45,7 +40,14 @@ describe("file.ripgrep", () => { }, }) - const files = await Array.fromAsync(await Ripgrep.files({ cwd: tmp.path, hidden: false })) + const files = await run( + Ripgrep.Service.use((rg) => + rg.files({ cwd: tmp.path, hidden: false }).pipe( + Stream.runCollect, + Effect.map((c) => [...c]), + ), + ), + ) expect(files.includes("visible.txt")).toBe(true) expect(files.includes(path.join(".opencode", "thing.json"))).toBe(false) }) @@ -57,7 +59,7 @@ describe("file.ripgrep", () => { }, }) - const result = await Ripgrep.search({ cwd: tmp.path, pattern: "needle" }) + const result = await run(Ripgrep.Service.use((rg) => rg.search({ cwd: tmp.path, pattern: "needle" }))) expect(result.partial).toBe(false) expect(result.items).toEqual([]) }) @@ -70,7 +72,7 @@ describe("file.ripgrep", () => { }, }) - const result = await Ripgrep.search({ cwd: tmp.path, pattern: "needle" }) + const result = await run(Ripgrep.Service.use((rg) => rg.search({ cwd: tmp.path, pattern: "needle" }))) expect(result.partial).toBe(false) expect(result.items).toHaveLength(1) expect(result.items[0]?.path.text).toBe(path.join("src", "match.ts")) @@ -78,99 +80,7 @@ describe("file.ripgrep", () => { expect(result.items[0]?.lines.text).toContain("needle") }) - test("files returns empty when glob matches no files in worker mode", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - await fs.mkdir(path.join(dir, "packages", "console"), { recursive: true }) - await Bun.write(path.join(dir, "packages", "console", "package.json"), "{}") - }, - }) - - const ctl = new AbortController() - const files = await Array.fromAsync( - await Ripgrep.files({ - cwd: tmp.path, - glob: ["packages/*"], - signal: ctl.signal, - }), - ) - - expect(files).toEqual([]) - }) - - test("ignores RIPGREP_CONFIG_PATH in direct mode", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write(path.join(dir, "match.ts"), "const needle = 1\n") - }, - }) - - const restore = env("RIPGREP_CONFIG_PATH", path.join(tmp.path, "missing-ripgreprc")) - try { - const result = await Ripgrep.search({ cwd: tmp.path, pattern: "needle" }) - expect(result.items).toHaveLength(1) - } finally { - restore() - } - }) - - test("ignores RIPGREP_CONFIG_PATH in worker mode", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write(path.join(dir, "match.ts"), "const needle = 1\n") - }, - }) - - const restore = env("RIPGREP_CONFIG_PATH", path.join(tmp.path, "missing-ripgreprc")) - try { - const ctl = new AbortController() - const result = await Ripgrep.search({ - cwd: tmp.path, - pattern: "needle", - signal: ctl.signal, - }) - expect(result.items).toHaveLength(1) - } finally { - restore() - } - }) - - test("aborts files scan in worker mode", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - await seed(dir, 4000) - }, - }) - - const ctl = new AbortController() - const iter = await Ripgrep.files({ cwd: tmp.path, signal: ctl.signal }) - const pending = Array.fromAsync(iter) - setTimeout(() => ctl.abort(), 0) - - const err = await pending.catch((err) => err) - expect(err).toBeInstanceOf(Error) - expect(err.name).toBe("AbortError") - }, 15_000) - - test("aborts search in worker mode", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - await seed(dir, 512, 64 * 1024) - }, - }) - - const ctl = new AbortController() - const pending = Ripgrep.search({ cwd: tmp.path, pattern: "needle", signal: ctl.signal }) - setTimeout(() => ctl.abort(), 0) - - const err = await pending.catch((err) => err) - expect(err).toBeInstanceOf(Error) - expect(err.name).toBe("AbortError") - }, 15_000) -}) - -describe("Ripgrep.Service", () => { - test("search returns matched rows", async () => { + test("search returns matched rows with glob filter", async () => { await using tmp = await tmpdir({ init: async (dir) => { await Bun.write(path.join(dir, "match.ts"), "const value = 'needle'\n") @@ -178,11 +88,9 @@ describe("Ripgrep.Service", () => { }, }) - const result = await Effect.gen(function* () { - const rg = yield* Ripgrep.Service - return yield* rg.search({ cwd: tmp.path, pattern: "needle", glob: ["*.ts"] }) - }).pipe(Effect.provide(Ripgrep.defaultLayer), Effect.runPromise) - + const result = await run( + Ripgrep.Service.use((rg) => rg.search({ cwd: tmp.path, pattern: "needle", glob: ["*.ts"] })), + ) expect(result.partial).toBe(false) expect(result.items).toHaveLength(1) expect(result.items[0]?.path.text).toContain("match.ts") @@ -198,16 +106,31 @@ describe("Ripgrep.Service", () => { }) const file = path.join(tmp.path, "match.ts") - const result = await Effect.gen(function* () { - const rg = yield* Ripgrep.Service - return yield* rg.search({ cwd: tmp.path, pattern: "needle", file: [file] }) - }).pipe(Effect.provide(Ripgrep.defaultLayer), Effect.runPromise) - + const result = await run(Ripgrep.Service.use((rg) => rg.search({ cwd: tmp.path, pattern: "needle", file: [file] }))) expect(result.partial).toBe(false) expect(result.items).toHaveLength(1) expect(result.items[0]?.path.text).toBe(file) }) + test("files returns empty when glob matches no files", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await fs.mkdir(path.join(dir, "packages", "console"), { recursive: true }) + await Bun.write(path.join(dir, "packages", "console", "package.json"), "{}") + }, + }) + + const files = await run( + Ripgrep.Service.use((rg) => + rg.files({ cwd: tmp.path, glob: ["packages/*"] }).pipe( + Stream.runCollect, + Effect.map((c) => [...c]), + ), + ), + ) + expect(files).toEqual([]) + }) + test("files returns stream of filenames", async () => { await using tmp = await tmpdir({ init: async (dir) => { @@ -216,14 +139,14 @@ describe("Ripgrep.Service", () => { }, }) - const files = await Effect.gen(function* () { - const rg = yield* Ripgrep.Service - return yield* rg.files({ cwd: tmp.path }).pipe( - Stream.runCollect, - Effect.map((chunk) => [...chunk].sort()), - ) - }).pipe(Effect.provide(Ripgrep.defaultLayer), Effect.runPromise) - + const files = await run( + Ripgrep.Service.use((rg) => + rg.files({ cwd: tmp.path }).pipe( + Stream.runCollect, + Effect.map((c) => [...c].sort()), + ), + ), + ) expect(files).toEqual(["a.txt", "b.txt"]) }) @@ -235,23 +158,57 @@ describe("Ripgrep.Service", () => { }, }) - const files = await Effect.gen(function* () { - const rg = yield* Ripgrep.Service - return yield* rg.files({ cwd: tmp.path, glob: ["*.ts"] }).pipe( - Stream.runCollect, - Effect.map((chunk) => [...chunk]), - ) - }).pipe(Effect.provide(Ripgrep.defaultLayer), Effect.runPromise) - + const files = await run( + Ripgrep.Service.use((rg) => + rg.files({ cwd: tmp.path, glob: ["*.ts"] }).pipe( + Stream.runCollect, + Effect.map((c) => [...c]), + ), + ), + ) expect(files).toEqual(["keep.ts"]) }) test("files dies on nonexistent directory", async () => { - const exit = await Effect.gen(function* () { - const rg = yield* Ripgrep.Service - return yield* rg.files({ cwd: "/tmp/nonexistent-dir-12345" }).pipe(Stream.runCollect) - }).pipe(Effect.provide(Ripgrep.defaultLayer), Effect.runPromiseExit) - + const exit = await Ripgrep.Service.use((rg) => + rg.files({ cwd: "/tmp/nonexistent-dir-12345" }).pipe(Stream.runCollect), + ).pipe(Effect.provide(Ripgrep.defaultLayer), Effect.runPromiseExit) expect(exit._tag).toBe("Failure") }) + + test("ignores RIPGREP_CONFIG_PATH in direct mode", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write(path.join(dir, "match.ts"), "const needle = 1\n") + }, + }) + + const prev = process.env["RIPGREP_CONFIG_PATH"] + process.env["RIPGREP_CONFIG_PATH"] = path.join(tmp.path, "missing-ripgreprc") + try { + const result = await run(Ripgrep.Service.use((rg) => rg.search({ cwd: tmp.path, pattern: "needle" }))) + expect(result.items).toHaveLength(1) + } finally { + if (prev === undefined) delete process.env["RIPGREP_CONFIG_PATH"] + else process.env["RIPGREP_CONFIG_PATH"] = prev + } + }) + + test("ignores RIPGREP_CONFIG_PATH in worker mode", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write(path.join(dir, "match.ts"), "const needle = 1\n") + }, + }) + + const prev = process.env["RIPGREP_CONFIG_PATH"] + process.env["RIPGREP_CONFIG_PATH"] = path.join(tmp.path, "missing-ripgreprc") + try { + const result = await run(Ripgrep.Service.use((rg) => rg.search({ cwd: tmp.path, pattern: "needle" }))) + expect(result.items).toHaveLength(1) + } finally { + if (prev === undefined) delete process.env["RIPGREP_CONFIG_PATH"] + else process.env["RIPGREP_CONFIG_PATH"] = prev + } + }) }) From bbdbc107ae1f935f9694fc36b79c833643ee87a4 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 21:26:24 -0400 Subject: [PATCH 169/300] feat: unwrap Config namespace to flat exports + barrel (#22689) --- packages/opencode/script/schema.ts | 2 +- packages/opencode/src/acp/agent.ts | 2 +- packages/opencode/src/agent/agent.ts | 2 +- packages/opencode/src/cli/cmd/debug/config.ts | 2 +- packages/opencode/src/cli/cmd/mcp.ts | 2 +- packages/opencode/src/cli/cmd/providers.ts | 2 +- .../src/cli/cmd/tui/plugin/runtime.ts | 2 +- packages/opencode/src/cli/cmd/tui/worker.ts | 2 +- packages/opencode/src/cli/error.ts | 2 +- packages/opencode/src/cli/network.ts | 2 +- packages/opencode/src/cli/upgrade.ts | 2 +- packages/opencode/src/command/index.ts | 2 +- packages/opencode/src/config/config.ts | 2125 ++++++++--------- packages/opencode/src/config/index.ts | 1 + packages/opencode/src/config/tui-schema.ts | 2 +- packages/opencode/src/config/tui.ts | 2 +- packages/opencode/src/effect/app-runtime.ts | 2 +- packages/opencode/src/file/watcher.ts | 2 +- packages/opencode/src/format/index.ts | 2 +- packages/opencode/src/lsp/index.ts | 2 +- packages/opencode/src/mcp/index.ts | 2 +- packages/opencode/src/node.ts | 2 +- packages/opencode/src/permission/index.ts | 2 +- packages/opencode/src/plugin/index.ts | 2 +- packages/opencode/src/plugin/loader.ts | 2 +- packages/opencode/src/provider/provider.ts | 2 +- .../opencode/src/server/instance/config.ts | 2 +- .../src/server/instance/experimental.ts | 2 +- .../opencode/src/server/instance/global.ts | 2 +- packages/opencode/src/server/instance/mcp.ts | 2 +- .../opencode/src/server/instance/provider.ts | 2 +- packages/opencode/src/session/compaction.ts | 2 +- packages/opencode/src/session/instruction.ts | 2 +- packages/opencode/src/session/llm.ts | 2 +- packages/opencode/src/session/overflow.ts | 2 +- packages/opencode/src/session/processor.ts | 2 +- packages/opencode/src/share/session.ts | 2 +- packages/opencode/src/share/share-next.ts | 2 +- packages/opencode/src/skill/index.ts | 2 +- packages/opencode/src/snapshot/index.ts | 2 +- packages/opencode/src/tool/registry.ts | 2 +- packages/opencode/src/tool/task.ts | 2 +- .../opencode/test/config/agent-color.test.ts | 2 +- packages/opencode/test/config/config.test.ts | 2 +- packages/opencode/test/config/tui.test.ts | 2 +- packages/opencode/test/file/watcher.test.ts | 2 +- packages/opencode/test/fixture/fixture.ts | 2 +- .../opencode/test/permission-task.test.ts | 2 +- .../opencode/test/session/compaction.test.ts | 2 +- .../test/session/processor-effect.test.ts | 2 +- .../test/session/prompt-effect.test.ts | 2 +- .../test/session/snapshot-tool-race.test.ts | 2 +- .../opencode/test/share/share-next.test.ts | 2 +- packages/opencode/test/tool/task.test.ts | 2 +- 54 files changed, 1105 insertions(+), 1125 deletions(-) create mode 100644 packages/opencode/src/config/index.ts diff --git a/packages/opencode/script/schema.ts b/packages/opencode/script/schema.ts index 61d11ea7c9..4ea68d9bbb 100755 --- a/packages/opencode/script/schema.ts +++ b/packages/opencode/script/schema.ts @@ -1,7 +1,7 @@ #!/usr/bin/env bun import { z } from "zod" -import { Config } from "../src/config/config" +import { Config } from "../src/config" import { TuiConfig } from "../src/config/tui" function generate(schema: z.ZodType) { diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index 5cbf4ed1f9..c065c64ffc 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -43,7 +43,7 @@ import { Agent as AgentModule } from "../agent/agent" import { AppRuntime } from "@/effect/app-runtime" import { Installation } from "@/installation" import { MessageV2 } from "@/session/message-v2" -import { Config } from "@/config/config" +import { Config } from "@/config" import { Todo } from "@/session/todo" import { z } from "zod" import { LoadAPIKeyError } from "ai" diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index ba38c8efe3..5887ee28e3 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -1,4 +1,4 @@ -import { Config } from "../config/config" +import { Config } from "../config" import z from "zod" import { Provider } from "../provider/provider" import { ModelID, ProviderID } from "../provider/schema" diff --git a/packages/opencode/src/cli/cmd/debug/config.ts b/packages/opencode/src/cli/cmd/debug/config.ts index 59e29c4a38..b1f1c25e9c 100644 --- a/packages/opencode/src/cli/cmd/debug/config.ts +++ b/packages/opencode/src/cli/cmd/debug/config.ts @@ -1,5 +1,5 @@ import { EOL } from "os" -import { Config } from "../../../config/config" +import { Config } from "../../../config" import { AppRuntime } from "@/effect/app-runtime" import { bootstrap } from "../../bootstrap" import { cmd } from "../cmd" diff --git a/packages/opencode/src/cli/cmd/mcp.ts b/packages/opencode/src/cli/cmd/mcp.ts index 3afedb356d..b9e4b04219 100644 --- a/packages/opencode/src/cli/cmd/mcp.ts +++ b/packages/opencode/src/cli/cmd/mcp.ts @@ -7,7 +7,7 @@ import { UI } from "../ui" import { MCP } from "../../mcp" import { McpAuth } from "../../mcp/auth" import { McpOAuthProvider } from "../../mcp/oauth-provider" -import { Config } from "../../config/config" +import { Config } from "../../config" import { Instance } from "../../project/instance" import { Installation } from "../../installation" import path from "path" diff --git a/packages/opencode/src/cli/cmd/providers.ts b/packages/opencode/src/cli/cmd/providers.ts index 6ab927e253..5b7f5a1a0d 100644 --- a/packages/opencode/src/cli/cmd/providers.ts +++ b/packages/opencode/src/cli/cmd/providers.ts @@ -7,7 +7,7 @@ import { ModelsDev } from "../../provider/models" import { map, pipe, sortBy, values } from "remeda" import path from "path" import os from "os" -import { Config } from "../../config/config" +import { Config } from "../../config" import { Global } from "../../global" import { Plugin } from "../../plugin" import { Instance } from "../../project/instance" diff --git a/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts b/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts index 7f12106b2c..bd7eac7713 100644 --- a/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts +++ b/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts @@ -13,7 +13,7 @@ import { import path from "path" import { fileURLToPath } from "url" -import { Config } from "@/config/config" +import { Config } from "@/config" import { TuiConfig } from "@/config/tui" import { Log } from "@/util/log" import { errorData, errorMessage } from "@/util/error" diff --git a/packages/opencode/src/cli/cmd/tui/worker.ts b/packages/opencode/src/cli/cmd/tui/worker.ts index 4e1bdabcdd..da9e3985b5 100644 --- a/packages/opencode/src/cli/cmd/tui/worker.ts +++ b/packages/opencode/src/cli/cmd/tui/worker.ts @@ -5,7 +5,7 @@ import { Instance } from "@/project/instance" import { InstanceBootstrap } from "@/project/bootstrap" import { Rpc } from "@/util/rpc" import { upgrade } from "@/cli/upgrade" -import { Config } from "@/config/config" +import { Config } from "@/config" import { GlobalBus } from "@/bus/global" import { Flag } from "@/flag/flag" import { writeHeapSnapshot } from "node:v8" diff --git a/packages/opencode/src/cli/error.ts b/packages/opencode/src/cli/error.ts index cd67635e9b..1277f5046c 100644 --- a/packages/opencode/src/cli/error.ts +++ b/packages/opencode/src/cli/error.ts @@ -1,7 +1,7 @@ import { AccountServiceError, AccountTransportError } from "@/account" import { ConfigMarkdown } from "@/config/markdown" import { errorFormat } from "@/util/error" -import { Config } from "../config/config" +import { Config } from "../config" import { MCP } from "../mcp" import { Provider } from "../provider/provider" import { UI } from "./ui" diff --git a/packages/opencode/src/cli/network.ts b/packages/opencode/src/cli/network.ts index cea49affa5..6321c056d0 100644 --- a/packages/opencode/src/cli/network.ts +++ b/packages/opencode/src/cli/network.ts @@ -1,5 +1,5 @@ import type { Argv, InferredOptionTypes } from "yargs" -import { Config } from "../config/config" +import { Config } from "../config" import { AppRuntime } from "@/effect/app-runtime" const options = { diff --git a/packages/opencode/src/cli/upgrade.ts b/packages/opencode/src/cli/upgrade.ts index f67b662455..2628f9673f 100644 --- a/packages/opencode/src/cli/upgrade.ts +++ b/packages/opencode/src/cli/upgrade.ts @@ -1,5 +1,5 @@ import { Bus } from "@/bus" -import { Config } from "@/config/config" +import { Config } from "@/config" import { AppRuntime } from "@/effect/app-runtime" import { Flag } from "@/flag/flag" import { Installation } from "@/installation" diff --git a/packages/opencode/src/command/index.ts b/packages/opencode/src/command/index.ts index 91a9e1b405..28fb37f272 100644 --- a/packages/opencode/src/command/index.ts +++ b/packages/opencode/src/command/index.ts @@ -5,7 +5,7 @@ import type { InstanceContext } from "@/project/instance" import { SessionID, MessageID } from "@/session/schema" import { Effect, Layer, Context } from "effect" import z from "zod" -import { Config } from "../config/config" +import { Config } from "../config" import { MCP } from "../mcp" import { Skill } from "../skill" import { Log } from "../util/log" diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 6aee4e1dc8..f35e8c83df 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -39,1133 +39,1113 @@ import { isPathPluginSpec, parsePluginSpecifier, resolvePathPluginTarget } from import { Npm } from "../npm" import { InstanceRef } from "@/effect/instance-ref" -export namespace Config { - const ModelId = z.string().meta({ $ref: "https://models.dev/model-schema.json#/$defs/Model" }) - const PluginOptions = z.record(z.string(), z.unknown()) - export const PluginSpec = z.union([z.string(), z.tuple([z.string(), PluginOptions])]) +const ModelId = z.string().meta({ $ref: "https://models.dev/model-schema.json#/$defs/Model" }) +const PluginOptions = z.record(z.string(), z.unknown()) +export const PluginSpec = z.union([z.string(), z.tuple([z.string(), PluginOptions])]) - export type PluginOptions = z.infer - export type PluginSpec = z.infer - export type PluginScope = "global" | "local" - export type PluginOrigin = { - spec: PluginSpec - source: string - scope: PluginScope +export type PluginOptions = z.infer +export type PluginSpec = z.infer +export type PluginScope = "global" | "local" +export type PluginOrigin = { + spec: PluginSpec + source: string + scope: PluginScope +} + +const log = Log.create({ service: "config" }) + +// Managed settings directory for enterprise deployments (highest priority, admin-controlled) +// These settings override all user and project settings +function systemManagedConfigDir(): string { + switch (process.platform) { + case "darwin": + return "/Library/Application Support/opencode" + case "win32": + return path.join(process.env.ProgramData || "C:\\ProgramData", "opencode") + default: + return "/etc/opencode" } +} - const log = Log.create({ service: "config" }) +export function managedConfigDir() { + return process.env.OPENCODE_TEST_MANAGED_CONFIG_DIR || systemManagedConfigDir() +} - // Managed settings directory for enterprise deployments (highest priority, admin-controlled) - // These settings override all user and project settings - function systemManagedConfigDir(): string { - switch (process.platform) { - case "darwin": - return "/Library/Application Support/opencode" - case "win32": - return path.join(process.env.ProgramData || "C:\\ProgramData", "opencode") - default: - return "/etc/opencode" +const managedDir = managedConfigDir() + +const MANAGED_PLIST_DOMAIN = "ai.opencode.managed" + +// Keys injected by macOS/MDM into the managed plist that are not OpenCode config +const PLIST_META = new Set([ + "PayloadDisplayName", + "PayloadIdentifier", + "PayloadType", + "PayloadUUID", + "PayloadVersion", + "_manualProfile", +]) + +/** + * Parse raw JSON (from plutil conversion of a managed plist) into OpenCode config. + * Strips MDM metadata keys before parsing through the config schema. + * Pure function — no OS interaction, safe to unit test directly. + */ +export function parseManagedPlist(json: string, source: string): Info { + const raw = JSON.parse(json) + for (const key of Object.keys(raw)) { + if (PLIST_META.has(key)) delete raw[key] + } + return parseConfig(JSON.stringify(raw), source) +} + +/** + * Read macOS managed preferences deployed via .mobileconfig / MDM (Jamf, Kandji, etc). + * MDM-installed profiles write to /Library/Managed Preferences/ which is only writable by root. + * User-scoped plists are checked first, then machine-scoped. + */ +async function readManagedPreferences(): Promise { + if (process.platform !== "darwin") return {} + + const domain = MANAGED_PLIST_DOMAIN + const user = os.userInfo().username + const paths = [ + path.join("/Library/Managed Preferences", user, `${domain}.plist`), + path.join("/Library/Managed Preferences", `${domain}.plist`), + ] + + for (const plist of paths) { + if (!existsSync(plist)) continue + log.info("reading macOS managed preferences", { path: plist }) + const result = await Process.run(["plutil", "-convert", "json", "-o", "-", plist], { nothrow: true }) + if (result.code !== 0) { + log.warn("failed to convert managed preferences plist", { path: plist }) + continue } + return parseManagedPlist(result.stdout.toString(), `mobileconfig:${plist}`) } + return {} +} - export function managedConfigDir() { - return process.env.OPENCODE_TEST_MANAGED_CONFIG_DIR || systemManagedConfigDir() +// Custom merge function that concatenates array fields instead of replacing them +function mergeConfigConcatArrays(target: Info, source: Info): Info { + const merged = mergeDeep(target, source) + if (target.instructions && source.instructions) { + merged.instructions = Array.from(new Set([...target.instructions, ...source.instructions])) } + return merged +} - const managedDir = managedConfigDir() +export type InstallInput = { + waitTick?: (input: { dir: string; attempt: number; delay: number; waited: number }) => void | Promise +} - const MANAGED_PLIST_DOMAIN = "ai.opencode.managed" +type Package = { + dependencies?: Record +} - // Keys injected by macOS/MDM into the managed plist that are not OpenCode config - const PLIST_META = new Set([ - "PayloadDisplayName", - "PayloadIdentifier", - "PayloadType", - "PayloadUUID", - "PayloadVersion", - "_manualProfile", - ]) - - /** - * Parse raw JSON (from plutil conversion of a managed plist) into OpenCode config. - * Strips MDM metadata keys before parsing through the config schema. - * Pure function — no OS interaction, safe to unit test directly. - */ - export function parseManagedPlist(json: string, source: string): Info { - const raw = JSON.parse(json) - for (const key of Object.keys(raw)) { - if (PLIST_META.has(key)) delete raw[key] - } - return parseConfig(JSON.stringify(raw), source) +function rel(item: string, patterns: string[]) { + const normalizedItem = item.replaceAll("\\", "/") + for (const pattern of patterns) { + const index = normalizedItem.indexOf(pattern) + if (index === -1) continue + return normalizedItem.slice(index + pattern.length) } +} - /** - * Read macOS managed preferences deployed via .mobileconfig / MDM (Jamf, Kandji, etc). - * MDM-installed profiles write to /Library/Managed Preferences/ which is only writable by root. - * User-scoped plists are checked first, then machine-scoped. - */ - async function readManagedPreferences(): Promise { - if (process.platform !== "darwin") return {} +function trim(file: string) { + const ext = path.extname(file) + return ext.length ? file.slice(0, -ext.length) : file +} - const domain = MANAGED_PLIST_DOMAIN - const user = os.userInfo().username - const paths = [ - path.join("/Library/Managed Preferences", user, `${domain}.plist`), - path.join("/Library/Managed Preferences", `${domain}.plist`), - ] - - for (const plist of paths) { - if (!existsSync(plist)) continue - log.info("reading macOS managed preferences", { path: plist }) - const result = await Process.run(["plutil", "-convert", "json", "-o", "-", plist], { nothrow: true }) - if (result.code !== 0) { - log.warn("failed to convert managed preferences plist", { path: plist }) - continue - } - return parseManagedPlist(result.stdout.toString(), `mobileconfig:${plist}`) - } - return {} - } - - // Custom merge function that concatenates array fields instead of replacing them - function mergeConfigConcatArrays(target: Info, source: Info): Info { - const merged = mergeDeep(target, source) - if (target.instructions && source.instructions) { - merged.instructions = Array.from(new Set([...target.instructions, ...source.instructions])) - } - return merged - } - - export type InstallInput = { - waitTick?: (input: { dir: string; attempt: number; delay: number; waited: number }) => void | Promise - } - - type Package = { - dependencies?: Record - } - - function rel(item: string, patterns: string[]) { - const normalizedItem = item.replaceAll("\\", "/") - for (const pattern of patterns) { - const index = normalizedItem.indexOf(pattern) - if (index === -1) continue - return normalizedItem.slice(index + pattern.length) - } - } - - function trim(file: string) { - const ext = path.extname(file) - return ext.length ? file.slice(0, -ext.length) : file - } - - async function loadCommand(dir: string) { - const result: Record = {} - for (const item of await Glob.scan("{command,commands}/**/*.md", { - cwd: dir, - absolute: true, - dot: true, - symlink: true, - })) { - const md = await ConfigMarkdown.parse(item).catch(async (err) => { - const message = ConfigMarkdown.FrontmatterError.isInstance(err) - ? err.data.message - : `Failed to parse command ${item}` - const { Session } = await import("@/session") - Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() }) - log.error("failed to load command", { command: item, err }) - return undefined - }) - if (!md) continue - - const patterns = ["/.opencode/command/", "/.opencode/commands/", "/command/", "/commands/"] - const file = rel(item, patterns) ?? path.basename(item) - const name = trim(file) - - const config = { - name, - ...md.data, - template: md.content.trim(), - } - const parsed = Command.safeParse(config) - if (parsed.success) { - result[config.name] = parsed.data - continue - } - throw new InvalidError({ path: item, issues: parsed.error.issues }, { cause: parsed.error }) - } - return result - } - - async function loadAgent(dir: string) { - const result: Record = {} - - for (const item of await Glob.scan("{agent,agents}/**/*.md", { - cwd: dir, - absolute: true, - dot: true, - symlink: true, - })) { - const md = await ConfigMarkdown.parse(item).catch(async (err) => { - const message = ConfigMarkdown.FrontmatterError.isInstance(err) - ? err.data.message - : `Failed to parse agent ${item}` - const { Session } = await import("@/session") - Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() }) - log.error("failed to load agent", { agent: item, err }) - return undefined - }) - if (!md) continue - - const patterns = ["/.opencode/agent/", "/.opencode/agents/", "/agent/", "/agents/"] - const file = rel(item, patterns) ?? path.basename(item) - const agentName = trim(file) - - const config = { - name: agentName, - ...md.data, - prompt: md.content.trim(), - } - const parsed = Agent.safeParse(config) - if (parsed.success) { - result[config.name] = parsed.data - continue - } - throw new InvalidError({ path: item, issues: parsed.error.issues }, { cause: parsed.error }) - } - return result - } - - async function loadMode(dir: string) { - const result: Record = {} - for (const item of await Glob.scan("{mode,modes}/*.md", { - cwd: dir, - absolute: true, - dot: true, - symlink: true, - })) { - const md = await ConfigMarkdown.parse(item).catch(async (err) => { - const message = ConfigMarkdown.FrontmatterError.isInstance(err) - ? err.data.message - : `Failed to parse mode ${item}` - const { Session } = await import("@/session") - Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() }) - log.error("failed to load mode", { mode: item, err }) - return undefined - }) - if (!md) continue - - const config = { - name: path.basename(item, ".md"), - ...md.data, - prompt: md.content.trim(), - } - const parsed = Agent.safeParse(config) - if (parsed.success) { - result[config.name] = { - ...parsed.data, - mode: "primary" as const, - } - continue - } - } - return result - } - - async function loadPlugin(dir: string) { - const plugins: PluginSpec[] = [] - - for (const item of await Glob.scan("{plugin,plugins}/*.{ts,js}", { - cwd: dir, - absolute: true, - dot: true, - symlink: true, - })) { - plugins.push(pathToFileURL(item).href) - } - return plugins - } - - export function pluginSpecifier(plugin: PluginSpec): string { - return Array.isArray(plugin) ? plugin[0] : plugin - } - - export function pluginOptions(plugin: PluginSpec): PluginOptions | undefined { - return Array.isArray(plugin) ? plugin[1] : undefined - } - - export async function resolvePluginSpec(plugin: PluginSpec, configFilepath: string): Promise { - const spec = pluginSpecifier(plugin) - if (!isPathPluginSpec(spec)) return plugin - - const base = path.dirname(configFilepath) - const file = (() => { - if (spec.startsWith("file://")) return spec - if (path.isAbsolute(spec) || /^[A-Za-z]:[\\/]/.test(spec)) return pathToFileURL(spec).href - return pathToFileURL(path.resolve(base, spec)).href - })() - - const resolved = await resolvePathPluginTarget(file).catch(() => file) - - if (Array.isArray(plugin)) return [resolved, plugin[1]] - return resolved - } - - export function deduplicatePluginOrigins(plugins: PluginOrigin[]): PluginOrigin[] { - const seen = new Set() - const list: PluginOrigin[] = [] - - for (const plugin of plugins.toReversed()) { - const spec = pluginSpecifier(plugin.spec) - const name = spec.startsWith("file://") ? spec : parsePluginSpecifier(spec).pkg - if (seen.has(name)) continue - seen.add(name) - list.push(plugin) - } - - return list.toReversed() - } - - export const McpLocal = z - .object({ - type: z.literal("local").describe("Type of MCP server connection"), - command: z.string().array().describe("Command and arguments to run the MCP server"), - environment: z - .record(z.string(), z.string()) - .optional() - .describe("Environment variables to set when running the MCP server"), - enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"), - timeout: z - .number() - .int() - .positive() - .optional() - .describe("Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified."), - }) - .strict() - .meta({ - ref: "McpLocalConfig", +async function loadCommand(dir: string) { + const result: Record = {} + for (const item of await Glob.scan("{command,commands}/**/*.md", { + cwd: dir, + absolute: true, + dot: true, + symlink: true, + })) { + const md = await ConfigMarkdown.parse(item).catch(async (err) => { + const message = ConfigMarkdown.FrontmatterError.isInstance(err) + ? err.data.message + : `Failed to parse command ${item}` + const { Session } = await import("@/session") + Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() }) + log.error("failed to load command", { command: item, err }) + return undefined }) + if (!md) continue - export const McpOAuth = z - .object({ - clientId: z - .string() - .optional() - .describe("OAuth client ID. If not provided, dynamic client registration (RFC 7591) will be attempted."), - clientSecret: z.string().optional().describe("OAuth client secret (if required by the authorization server)"), - scope: z.string().optional().describe("OAuth scopes to request during authorization"), - redirectUri: z - .string() - .optional() - .describe("OAuth redirect URI (default: http://127.0.0.1:19876/mcp/oauth/callback)."), - }) - .strict() - .meta({ - ref: "McpOAuthConfig", - }) - export type McpOAuth = z.infer + const patterns = ["/.opencode/command/", "/.opencode/commands/", "/command/", "/commands/"] + const file = rel(item, patterns) ?? path.basename(item) + const name = trim(file) - export const McpRemote = z - .object({ - type: z.literal("remote").describe("Type of MCP server connection"), - url: z.string().describe("URL of the remote MCP server"), - enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"), - headers: z.record(z.string(), z.string()).optional().describe("Headers to send with the request"), - oauth: z - .union([McpOAuth, z.literal(false)]) - .optional() - .describe( - "OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection.", - ), - timeout: z - .number() - .int() - .positive() - .optional() - .describe("Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified."), - }) - .strict() - .meta({ - ref: "McpRemoteConfig", - }) + const config = { + name, + ...md.data, + template: md.content.trim(), + } + const parsed = Command.safeParse(config) + if (parsed.success) { + result[config.name] = parsed.data + continue + } + throw new InvalidError({ path: item, issues: parsed.error.issues }, { cause: parsed.error }) + } + return result +} - export const Mcp = z.discriminatedUnion("type", [McpLocal, McpRemote]) - export type Mcp = z.infer +async function loadAgent(dir: string) { + const result: Record = {} - export const PermissionAction = z.enum(["ask", "allow", "deny"]).meta({ - ref: "PermissionActionConfig", + for (const item of await Glob.scan("{agent,agents}/**/*.md", { + cwd: dir, + absolute: true, + dot: true, + symlink: true, + })) { + const md = await ConfigMarkdown.parse(item).catch(async (err) => { + const message = ConfigMarkdown.FrontmatterError.isInstance(err) + ? err.data.message + : `Failed to parse agent ${item}` + const { Session } = await import("@/session") + Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() }) + log.error("failed to load agent", { agent: item, err }) + return undefined + }) + if (!md) continue + + const patterns = ["/.opencode/agent/", "/.opencode/agents/", "/agent/", "/agents/"] + const file = rel(item, patterns) ?? path.basename(item) + const agentName = trim(file) + + const config = { + name: agentName, + ...md.data, + prompt: md.content.trim(), + } + const parsed = Agent.safeParse(config) + if (parsed.success) { + result[config.name] = parsed.data + continue + } + throw new InvalidError({ path: item, issues: parsed.error.issues }, { cause: parsed.error }) + } + return result +} + +async function loadMode(dir: string) { + const result: Record = {} + for (const item of await Glob.scan("{mode,modes}/*.md", { + cwd: dir, + absolute: true, + dot: true, + symlink: true, + })) { + const md = await ConfigMarkdown.parse(item).catch(async (err) => { + const message = ConfigMarkdown.FrontmatterError.isInstance(err) + ? err.data.message + : `Failed to parse mode ${item}` + const { Session } = await import("@/session") + Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() }) + log.error("failed to load mode", { mode: item, err }) + return undefined + }) + if (!md) continue + + const config = { + name: path.basename(item, ".md"), + ...md.data, + prompt: md.content.trim(), + } + const parsed = Agent.safeParse(config) + if (parsed.success) { + result[config.name] = { + ...parsed.data, + mode: "primary" as const, + } + continue + } + } + return result +} + +async function loadPlugin(dir: string) { + const plugins: PluginSpec[] = [] + + for (const item of await Glob.scan("{plugin,plugins}/*.{ts,js}", { + cwd: dir, + absolute: true, + dot: true, + symlink: true, + })) { + plugins.push(pathToFileURL(item).href) + } + return plugins +} + +export function pluginSpecifier(plugin: PluginSpec): string { + return Array.isArray(plugin) ? plugin[0] : plugin +} + +export function pluginOptions(plugin: PluginSpec): PluginOptions | undefined { + return Array.isArray(plugin) ? plugin[1] : undefined +} + +export async function resolvePluginSpec(plugin: PluginSpec, configFilepath: string): Promise { + const spec = pluginSpecifier(plugin) + if (!isPathPluginSpec(spec)) return plugin + + const base = path.dirname(configFilepath) + const file = (() => { + if (spec.startsWith("file://")) return spec + if (path.isAbsolute(spec) || /^[A-Za-z]:[\\/]/.test(spec)) return pathToFileURL(spec).href + return pathToFileURL(path.resolve(base, spec)).href + })() + + const resolved = await resolvePathPluginTarget(file).catch(() => file) + + if (Array.isArray(plugin)) return [resolved, plugin[1]] + return resolved +} + +export function deduplicatePluginOrigins(plugins: PluginOrigin[]): PluginOrigin[] { + const seen = new Set() + const list: PluginOrigin[] = [] + + for (const plugin of plugins.toReversed()) { + const spec = pluginSpecifier(plugin.spec) + const name = spec.startsWith("file://") ? spec : parsePluginSpecifier(spec).pkg + if (seen.has(name)) continue + seen.add(name) + list.push(plugin) + } + + return list.toReversed() +} + +export const McpLocal = z + .object({ + type: z.literal("local").describe("Type of MCP server connection"), + command: z.string().array().describe("Command and arguments to run the MCP server"), + environment: z + .record(z.string(), z.string()) + .optional() + .describe("Environment variables to set when running the MCP server"), + enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"), + timeout: z + .number() + .int() + .positive() + .optional() + .describe("Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified."), }) - export type PermissionAction = z.infer - - export const PermissionObject = z.record(z.string(), PermissionAction).meta({ - ref: "PermissionObjectConfig", + .strict() + .meta({ + ref: "McpLocalConfig", }) - export type PermissionObject = z.infer - export const PermissionRule = z.union([PermissionAction, PermissionObject]).meta({ - ref: "PermissionRuleConfig", +export const McpOAuth = z + .object({ + clientId: z + .string() + .optional() + .describe("OAuth client ID. If not provided, dynamic client registration (RFC 7591) will be attempted."), + clientSecret: z.string().optional().describe("OAuth client secret (if required by the authorization server)"), + scope: z.string().optional().describe("OAuth scopes to request during authorization"), + redirectUri: z + .string() + .optional() + .describe("OAuth redirect URI (default: http://127.0.0.1:19876/mcp/oauth/callback)."), }) - export type PermissionRule = z.infer + .strict() + .meta({ + ref: "McpOAuthConfig", + }) +export type McpOAuth = z.infer - // Capture original key order before zod reorders, then rebuild in original order - const permissionPreprocess = (val: unknown) => { - if (typeof val === "object" && val !== null && !Array.isArray(val)) { - return { __originalKeys: Object.keys(val), ...val } - } - return val +export const McpRemote = z + .object({ + type: z.literal("remote").describe("Type of MCP server connection"), + url: z.string().describe("URL of the remote MCP server"), + enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"), + headers: z.record(z.string(), z.string()).optional().describe("Headers to send with the request"), + oauth: z + .union([McpOAuth, z.literal(false)]) + .optional() + .describe("OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection."), + timeout: z + .number() + .int() + .positive() + .optional() + .describe("Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified."), + }) + .strict() + .meta({ + ref: "McpRemoteConfig", + }) + +export const Mcp = z.discriminatedUnion("type", [McpLocal, McpRemote]) +export type Mcp = z.infer + +export const PermissionAction = z.enum(["ask", "allow", "deny"]).meta({ + ref: "PermissionActionConfig", +}) +export type PermissionAction = z.infer + +export const PermissionObject = z.record(z.string(), PermissionAction).meta({ + ref: "PermissionObjectConfig", +}) +export type PermissionObject = z.infer + +export const PermissionRule = z.union([PermissionAction, PermissionObject]).meta({ + ref: "PermissionRuleConfig", +}) +export type PermissionRule = z.infer + +// Capture original key order before zod reorders, then rebuild in original order +const permissionPreprocess = (val: unknown) => { + if (typeof val === "object" && val !== null && !Array.isArray(val)) { + return { __originalKeys: Object.keys(val), ...val } } + return val +} - const permissionTransform = (x: unknown): Record => { - if (typeof x === "string") return { "*": x as PermissionAction } - const obj = x as { __originalKeys?: string[] } & Record - const { __originalKeys, ...rest } = obj - if (!__originalKeys) return rest as Record - const result: Record = {} - for (const key of __originalKeys) { - if (key in rest) result[key] = rest[key] as PermissionRule - } - return result +const permissionTransform = (x: unknown): Record => { + if (typeof x === "string") return { "*": x as PermissionAction } + const obj = x as { __originalKeys?: string[] } & Record + const { __originalKeys, ...rest } = obj + if (!__originalKeys) return rest as Record + const result: Record = {} + for (const key of __originalKeys) { + if (key in rest) result[key] = rest[key] as PermissionRule } + return result +} - export const Permission = z - .preprocess( - permissionPreprocess, - z - .object({ - __originalKeys: z.string().array().optional(), - read: PermissionRule.optional(), - edit: PermissionRule.optional(), - glob: PermissionRule.optional(), - grep: PermissionRule.optional(), - list: PermissionRule.optional(), - bash: PermissionRule.optional(), - task: PermissionRule.optional(), - external_directory: PermissionRule.optional(), - todowrite: PermissionAction.optional(), - question: PermissionAction.optional(), - webfetch: PermissionAction.optional(), - websearch: PermissionAction.optional(), - codesearch: PermissionAction.optional(), - lsp: PermissionRule.optional(), - doom_loop: PermissionAction.optional(), - skill: PermissionRule.optional(), - }) - .catchall(PermissionRule) - .or(PermissionAction), - ) - .transform(permissionTransform) - .meta({ - ref: "PermissionConfig", - }) - export type Permission = z.infer +export const Permission = z + .preprocess( + permissionPreprocess, + z + .object({ + __originalKeys: z.string().array().optional(), + read: PermissionRule.optional(), + edit: PermissionRule.optional(), + glob: PermissionRule.optional(), + grep: PermissionRule.optional(), + list: PermissionRule.optional(), + bash: PermissionRule.optional(), + task: PermissionRule.optional(), + external_directory: PermissionRule.optional(), + todowrite: PermissionAction.optional(), + question: PermissionAction.optional(), + webfetch: PermissionAction.optional(), + websearch: PermissionAction.optional(), + codesearch: PermissionAction.optional(), + lsp: PermissionRule.optional(), + doom_loop: PermissionAction.optional(), + skill: PermissionRule.optional(), + }) + .catchall(PermissionRule) + .or(PermissionAction), + ) + .transform(permissionTransform) + .meta({ + ref: "PermissionConfig", + }) +export type Permission = z.infer - export const Command = z.object({ - template: z.string(), - description: z.string().optional(), - agent: z.string().optional(), +export const Command = z.object({ + template: z.string(), + description: z.string().optional(), + agent: z.string().optional(), + model: ModelId.optional(), + subtask: z.boolean().optional(), +}) +export type Command = z.infer + +export const Skills = z.object({ + paths: z.array(z.string()).optional().describe("Additional paths to skill folders"), + urls: z + .array(z.string()) + .optional() + .describe("URLs to fetch skills from (e.g., https://example.com/.well-known/skills/)"), +}) +export type Skills = z.infer + +export const Agent = z + .object({ model: ModelId.optional(), - subtask: z.boolean().optional(), + variant: z + .string() + .optional() + .describe("Default model variant for this agent (applies only when using the agent's configured model)."), + temperature: z.number().optional(), + top_p: z.number().optional(), + prompt: z.string().optional(), + tools: z.record(z.string(), z.boolean()).optional().describe("@deprecated Use 'permission' field instead"), + disable: z.boolean().optional(), + description: z.string().optional().describe("Description of when to use the agent"), + mode: z.enum(["subagent", "primary", "all"]).optional(), + hidden: z + .boolean() + .optional() + .describe("Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent)"), + options: z.record(z.string(), z.any()).optional(), + color: z + .union([ + z.string().regex(/^#[0-9a-fA-F]{6}$/, "Invalid hex color format"), + z.enum(["primary", "secondary", "accent", "success", "warning", "error", "info"]), + ]) + .optional() + .describe("Hex color code (e.g., #FF5733) or theme color (e.g., primary)"), + steps: z + .number() + .int() + .positive() + .optional() + .describe("Maximum number of agentic iterations before forcing text-only response"), + maxSteps: z.number().int().positive().optional().describe("@deprecated Use 'steps' field instead."), + permission: Permission.optional(), }) - export type Command = z.infer + .catchall(z.any()) + .transform((agent, ctx) => { + const knownKeys = new Set([ + "name", + "model", + "variant", + "prompt", + "description", + "temperature", + "top_p", + "mode", + "hidden", + "color", + "steps", + "maxSteps", + "options", + "permission", + "disable", + "tools", + ]) - export const Skills = z.object({ - paths: z.array(z.string()).optional().describe("Additional paths to skill folders"), - urls: z + // Extract unknown properties into options + const options: Record = { ...agent.options } + for (const [key, value] of Object.entries(agent)) { + if (!knownKeys.has(key)) options[key] = value + } + + // Convert legacy tools config to permissions + const permission: Permission = {} + for (const [tool, enabled] of Object.entries(agent.tools ?? {})) { + const action = enabled ? "allow" : "deny" + // write, edit, patch, multiedit all map to edit permission + if (tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit") { + permission.edit = action + } else { + permission[tool] = action + } + } + Object.assign(permission, agent.permission) + + // Convert legacy maxSteps to steps + const steps = agent.steps ?? agent.maxSteps + + return { ...agent, options, permission, steps } as typeof agent & { + options?: Record + permission?: Permission + steps?: number + } + }) + .meta({ + ref: "AgentConfig", + }) +export type Agent = z.infer + +export const Keybinds = z + .object({ + leader: z.string().optional().default("ctrl+x").describe("Leader key for keybind combinations"), + app_exit: z.string().optional().default("ctrl+c,ctrl+d,q").describe("Exit the application"), + editor_open: z.string().optional().default("e").describe("Open external editor"), + theme_list: z.string().optional().default("t").describe("List available themes"), + sidebar_toggle: z.string().optional().default("b").describe("Toggle sidebar"), + scrollbar_toggle: z.string().optional().default("none").describe("Toggle session scrollbar"), + username_toggle: z.string().optional().default("none").describe("Toggle username visibility"), + status_view: z.string().optional().default("s").describe("View status"), + session_export: z.string().optional().default("x").describe("Export session to editor"), + session_new: z.string().optional().default("n").describe("Create a new session"), + session_list: z.string().optional().default("l").describe("List all sessions"), + session_timeline: z.string().optional().default("g").describe("Show session timeline"), + session_fork: z.string().optional().default("none").describe("Fork session from message"), + session_rename: z.string().optional().default("ctrl+r").describe("Rename session"), + session_delete: z.string().optional().default("ctrl+d").describe("Delete session"), + stash_delete: z.string().optional().default("ctrl+d").describe("Delete stash entry"), + model_provider_list: z.string().optional().default("ctrl+a").describe("Open provider list from model dialog"), + model_favorite_toggle: z.string().optional().default("ctrl+f").describe("Toggle model favorite status"), + session_share: z.string().optional().default("none").describe("Share current session"), + session_unshare: z.string().optional().default("none").describe("Unshare current session"), + session_interrupt: z.string().optional().default("escape").describe("Interrupt current session"), + session_compact: z.string().optional().default("c").describe("Compact the session"), + messages_page_up: z.string().optional().default("pageup,ctrl+alt+b").describe("Scroll messages up by one page"), + messages_page_down: z + .string() + .optional() + .default("pagedown,ctrl+alt+f") + .describe("Scroll messages down by one page"), + messages_line_up: z.string().optional().default("ctrl+alt+y").describe("Scroll messages up by one line"), + messages_line_down: z.string().optional().default("ctrl+alt+e").describe("Scroll messages down by one line"), + messages_half_page_up: z.string().optional().default("ctrl+alt+u").describe("Scroll messages up by half page"), + messages_half_page_down: z.string().optional().default("ctrl+alt+d").describe("Scroll messages down by half page"), + messages_first: z.string().optional().default("ctrl+g,home").describe("Navigate to first message"), + messages_last: z.string().optional().default("ctrl+alt+g,end").describe("Navigate to last message"), + messages_next: z.string().optional().default("none").describe("Navigate to next message"), + messages_previous: z.string().optional().default("none").describe("Navigate to previous message"), + messages_last_user: z.string().optional().default("none").describe("Navigate to last user message"), + messages_copy: z.string().optional().default("y").describe("Copy message"), + messages_undo: z.string().optional().default("u").describe("Undo message"), + messages_redo: z.string().optional().default("r").describe("Redo message"), + messages_toggle_conceal: z + .string() + .optional() + .default("h") + .describe("Toggle code block concealment in messages"), + tool_details: z.string().optional().default("none").describe("Toggle tool details visibility"), + model_list: z.string().optional().default("m").describe("List available models"), + model_cycle_recent: z.string().optional().default("f2").describe("Next recently used model"), + model_cycle_recent_reverse: z.string().optional().default("shift+f2").describe("Previous recently used model"), + model_cycle_favorite: z.string().optional().default("none").describe("Next favorite model"), + model_cycle_favorite_reverse: z.string().optional().default("none").describe("Previous favorite model"), + command_list: z.string().optional().default("ctrl+p").describe("List available commands"), + agent_list: z.string().optional().default("a").describe("List agents"), + agent_cycle: z.string().optional().default("tab").describe("Next agent"), + agent_cycle_reverse: z.string().optional().default("shift+tab").describe("Previous agent"), + variant_cycle: z.string().optional().default("ctrl+t").describe("Cycle model variants"), + variant_list: z.string().optional().default("none").describe("List model variants"), + input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"), + input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"), + input_submit: z.string().optional().default("return").describe("Submit input"), + input_newline: z + .string() + .optional() + .default("shift+return,ctrl+return,alt+return,ctrl+j") + .describe("Insert newline in input"), + input_move_left: z.string().optional().default("left,ctrl+b").describe("Move cursor left in input"), + input_move_right: z.string().optional().default("right,ctrl+f").describe("Move cursor right in input"), + input_move_up: z.string().optional().default("up").describe("Move cursor up in input"), + input_move_down: z.string().optional().default("down").describe("Move cursor down in input"), + input_select_left: z.string().optional().default("shift+left").describe("Select left in input"), + input_select_right: z.string().optional().default("shift+right").describe("Select right in input"), + input_select_up: z.string().optional().default("shift+up").describe("Select up in input"), + input_select_down: z.string().optional().default("shift+down").describe("Select down in input"), + input_line_home: z.string().optional().default("ctrl+a").describe("Move to start of line in input"), + input_line_end: z.string().optional().default("ctrl+e").describe("Move to end of line in input"), + input_select_line_home: z.string().optional().default("ctrl+shift+a").describe("Select to start of line in input"), + input_select_line_end: z.string().optional().default("ctrl+shift+e").describe("Select to end of line in input"), + input_visual_line_home: z.string().optional().default("alt+a").describe("Move to start of visual line in input"), + input_visual_line_end: z.string().optional().default("alt+e").describe("Move to end of visual line in input"), + input_select_visual_line_home: z + .string() + .optional() + .default("alt+shift+a") + .describe("Select to start of visual line in input"), + input_select_visual_line_end: z + .string() + .optional() + .default("alt+shift+e") + .describe("Select to end of visual line in input"), + input_buffer_home: z.string().optional().default("home").describe("Move to start of buffer in input"), + input_buffer_end: z.string().optional().default("end").describe("Move to end of buffer in input"), + input_select_buffer_home: z + .string() + .optional() + .default("shift+home") + .describe("Select to start of buffer in input"), + input_select_buffer_end: z.string().optional().default("shift+end").describe("Select to end of buffer in input"), + input_delete_line: z.string().optional().default("ctrl+shift+d").describe("Delete line in input"), + input_delete_to_line_end: z.string().optional().default("ctrl+k").describe("Delete to end of line in input"), + input_delete_to_line_start: z.string().optional().default("ctrl+u").describe("Delete to start of line in input"), + input_backspace: z.string().optional().default("backspace,shift+backspace").describe("Backspace in input"), + input_delete: z.string().optional().default("ctrl+d,delete,shift+delete").describe("Delete character in input"), + input_undo: z.string().optional().default("ctrl+-,super+z").describe("Undo in input"), + input_redo: z.string().optional().default("ctrl+.,super+shift+z").describe("Redo in input"), + input_word_forward: z + .string() + .optional() + .default("alt+f,alt+right,ctrl+right") + .describe("Move word forward in input"), + input_word_backward: z + .string() + .optional() + .default("alt+b,alt+left,ctrl+left") + .describe("Move word backward in input"), + input_select_word_forward: z + .string() + .optional() + .default("alt+shift+f,alt+shift+right") + .describe("Select word forward in input"), + input_select_word_backward: z + .string() + .optional() + .default("alt+shift+b,alt+shift+left") + .describe("Select word backward in input"), + input_delete_word_forward: z + .string() + .optional() + .default("alt+d,alt+delete,ctrl+delete") + .describe("Delete word forward in input"), + input_delete_word_backward: z + .string() + .optional() + .default("ctrl+w,ctrl+backspace,alt+backspace") + .describe("Delete word backward in input"), + history_previous: z.string().optional().default("up").describe("Previous history item"), + history_next: z.string().optional().default("down").describe("Next history item"), + session_child_first: z.string().optional().default("down").describe("Go to first child session"), + session_child_cycle: z.string().optional().default("right").describe("Go to next child session"), + session_child_cycle_reverse: z.string().optional().default("left").describe("Go to previous child session"), + session_parent: z.string().optional().default("up").describe("Go to parent session"), + terminal_suspend: z.string().optional().default("ctrl+z").describe("Suspend terminal"), + terminal_title_toggle: z.string().optional().default("none").describe("Toggle terminal title"), + tips_toggle: z.string().optional().default("h").describe("Toggle tips on home screen"), + plugin_manager: z.string().optional().default("none").describe("Open plugin manager dialog"), + display_thinking: z.string().optional().default("none").describe("Toggle thinking blocks visibility"), + }) + .strict() + .meta({ + ref: "KeybindsConfig", + }) + +export const Server = z + .object({ + port: z.number().int().positive().optional().describe("Port to listen on"), + hostname: z.string().optional().describe("Hostname to listen on"), + mdns: z.boolean().optional().describe("Enable mDNS service discovery"), + mdnsDomain: z.string().optional().describe("Custom domain name for mDNS service (default: opencode.local)"), + cors: z.array(z.string()).optional().describe("Additional domains to allow for CORS"), + }) + .strict() + .meta({ + ref: "ServerConfig", + }) + +export const Layout = z.enum(["auto", "stretch"]).meta({ + ref: "LayoutConfig", +}) +export type Layout = z.infer + +export const Model = z + .object({ + id: z.string(), + name: z.string(), + family: z.string().optional(), + release_date: z.string(), + attachment: z.boolean(), + reasoning: z.boolean(), + temperature: z.boolean(), + tool_call: z.boolean(), + interleaved: z + .union([ + z.literal(true), + z + .object({ + field: z.enum(["reasoning_content", "reasoning_details"]), + }) + .strict(), + ]) + .optional(), + cost: z + .object({ + input: z.number(), + output: z.number(), + cache_read: z.number().optional(), + cache_write: z.number().optional(), + context_over_200k: z + .object({ + input: z.number(), + output: z.number(), + cache_read: z.number().optional(), + cache_write: z.number().optional(), + }) + .optional(), + }) + .optional(), + limit: z.object({ + context: z.number(), + input: z.number().optional(), + output: z.number(), + }), + modalities: z + .object({ + input: z.array(z.enum(["text", "audio", "image", "video", "pdf"])), + output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])), + }) + .optional(), + experimental: z.boolean().optional(), + status: z.enum(["alpha", "beta", "deprecated"]).optional(), + provider: z.object({ npm: z.string().optional(), api: z.string().optional() }).optional(), + options: z.record(z.string(), z.any()), + headers: z.record(z.string(), z.string()).optional(), + variants: z + .record( + z.string(), + z + .object({ + disabled: z.boolean().optional().describe("Disable this variant for the model"), + }) + .catchall(z.any()), + ) + .optional() + .describe("Variant-specific configuration"), + }) + .partial() + +export const Provider = z + .object({ + api: z.string().optional(), + name: z.string(), + env: z.array(z.string()), + id: z.string(), + npm: z.string().optional(), + whitelist: z.array(z.string()).optional(), + blacklist: z.array(z.string()).optional(), + options: z + .object({ + apiKey: z.string().optional(), + baseURL: z.string().optional(), + enterpriseUrl: z.string().optional().describe("GitHub Enterprise URL for copilot authentication"), + setCacheKey: z.boolean().optional().describe("Enable promptCacheKey for this provider (default false)"), + timeout: z + .union([ + z + .number() + .int() + .positive() + .describe( + "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.", + ), + z.literal(false).describe("Disable timeout for this provider entirely."), + ]) + .optional() + .describe( + "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.", + ), + chunkTimeout: z + .number() + .int() + .positive() + .optional() + .describe( + "Timeout in milliseconds between streamed SSE chunks for this provider. If no chunk arrives within this window, the request is aborted.", + ), + }) + .catchall(z.any()) + .optional(), + models: z.record(z.string(), Model).optional(), + }) + .partial() + .strict() + .meta({ + ref: "ProviderConfig", + }) + +export type Provider = z.infer + +export const Info = z + .object({ + $schema: z.string().optional().describe("JSON schema reference for configuration validation"), + logLevel: Log.Level.optional().describe("Log level"), + server: Server.optional().describe("Server configuration for opencode serve and web commands"), + command: z + .record(z.string(), Command) + .optional() + .describe("Command configuration, see https://opencode.ai/docs/commands"), + skills: Skills.optional().describe("Additional skill folder paths"), + watcher: z + .object({ + ignore: z.array(z.string()).optional(), + }) + .optional(), + snapshot: z + .boolean() + .optional() + .describe( + "Enable or disable snapshot tracking. When false, filesystem snapshots are not recorded and undoing or reverting will not undo/redo file changes. Defaults to true.", + ), + plugin: PluginSpec.array().optional(), + share: z + .enum(["manual", "auto", "disabled"]) + .optional() + .describe( + "Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing", + ), + autoshare: z + .boolean() + .optional() + .describe("@deprecated Use 'share' field instead. Share newly created sessions automatically"), + autoupdate: z + .union([z.boolean(), z.literal("notify")]) + .optional() + .describe( + "Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications", + ), + disabled_providers: z.array(z.string()).optional().describe("Disable providers that are loaded automatically"), + enabled_providers: z .array(z.string()) .optional() - .describe("URLs to fetch skills from (e.g., https://example.com/.well-known/skills/)"), - }) - export type Skills = z.infer - - export const Agent = z - .object({ - model: ModelId.optional(), - variant: z - .string() - .optional() - .describe("Default model variant for this agent (applies only when using the agent's configured model)."), - temperature: z.number().optional(), - top_p: z.number().optional(), - prompt: z.string().optional(), - tools: z.record(z.string(), z.boolean()).optional().describe("@deprecated Use 'permission' field instead"), - disable: z.boolean().optional(), - description: z.string().optional().describe("Description of when to use the agent"), - mode: z.enum(["subagent", "primary", "all"]).optional(), - hidden: z - .boolean() - .optional() - .describe("Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent)"), - options: z.record(z.string(), z.any()).optional(), - color: z - .union([ - z.string().regex(/^#[0-9a-fA-F]{6}$/, "Invalid hex color format"), - z.enum(["primary", "secondary", "accent", "success", "warning", "error", "info"]), - ]) - .optional() - .describe("Hex color code (e.g., #FF5733) or theme color (e.g., primary)"), - steps: z - .number() - .int() - .positive() - .optional() - .describe("Maximum number of agentic iterations before forcing text-only response"), - maxSteps: z.number().int().positive().optional().describe("@deprecated Use 'steps' field instead."), - permission: Permission.optional(), - }) - .catchall(z.any()) - .transform((agent, ctx) => { - const knownKeys = new Set([ - "name", - "model", - "variant", - "prompt", - "description", - "temperature", - "top_p", - "mode", - "hidden", - "color", - "steps", - "maxSteps", - "options", - "permission", - "disable", - "tools", - ]) - - // Extract unknown properties into options - const options: Record = { ...agent.options } - for (const [key, value] of Object.entries(agent)) { - if (!knownKeys.has(key)) options[key] = value - } - - // Convert legacy tools config to permissions - const permission: Permission = {} - for (const [tool, enabled] of Object.entries(agent.tools ?? {})) { - const action = enabled ? "allow" : "deny" - // write, edit, patch, multiedit all map to edit permission - if (tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit") { - permission.edit = action - } else { - permission[tool] = action - } - } - Object.assign(permission, agent.permission) - - // Convert legacy maxSteps to steps - const steps = agent.steps ?? agent.maxSteps - - return { ...agent, options, permission, steps } as typeof agent & { - options?: Record - permission?: Permission - steps?: number - } - }) - .meta({ - ref: "AgentConfig", - }) - export type Agent = z.infer - - export const Keybinds = z - .object({ - leader: z.string().optional().default("ctrl+x").describe("Leader key for keybind combinations"), - app_exit: z.string().optional().default("ctrl+c,ctrl+d,q").describe("Exit the application"), - editor_open: z.string().optional().default("e").describe("Open external editor"), - theme_list: z.string().optional().default("t").describe("List available themes"), - sidebar_toggle: z.string().optional().default("b").describe("Toggle sidebar"), - scrollbar_toggle: z.string().optional().default("none").describe("Toggle session scrollbar"), - username_toggle: z.string().optional().default("none").describe("Toggle username visibility"), - status_view: z.string().optional().default("s").describe("View status"), - session_export: z.string().optional().default("x").describe("Export session to editor"), - session_new: z.string().optional().default("n").describe("Create a new session"), - session_list: z.string().optional().default("l").describe("List all sessions"), - session_timeline: z.string().optional().default("g").describe("Show session timeline"), - session_fork: z.string().optional().default("none").describe("Fork session from message"), - session_rename: z.string().optional().default("ctrl+r").describe("Rename session"), - session_delete: z.string().optional().default("ctrl+d").describe("Delete session"), - stash_delete: z.string().optional().default("ctrl+d").describe("Delete stash entry"), - model_provider_list: z.string().optional().default("ctrl+a").describe("Open provider list from model dialog"), - model_favorite_toggle: z.string().optional().default("ctrl+f").describe("Toggle model favorite status"), - session_share: z.string().optional().default("none").describe("Share current session"), - session_unshare: z.string().optional().default("none").describe("Unshare current session"), - session_interrupt: z.string().optional().default("escape").describe("Interrupt current session"), - session_compact: z.string().optional().default("c").describe("Compact the session"), - messages_page_up: z.string().optional().default("pageup,ctrl+alt+b").describe("Scroll messages up by one page"), - messages_page_down: z - .string() - .optional() - .default("pagedown,ctrl+alt+f") - .describe("Scroll messages down by one page"), - messages_line_up: z.string().optional().default("ctrl+alt+y").describe("Scroll messages up by one line"), - messages_line_down: z.string().optional().default("ctrl+alt+e").describe("Scroll messages down by one line"), - messages_half_page_up: z.string().optional().default("ctrl+alt+u").describe("Scroll messages up by half page"), - messages_half_page_down: z - .string() - .optional() - .default("ctrl+alt+d") - .describe("Scroll messages down by half page"), - messages_first: z.string().optional().default("ctrl+g,home").describe("Navigate to first message"), - messages_last: z.string().optional().default("ctrl+alt+g,end").describe("Navigate to last message"), - messages_next: z.string().optional().default("none").describe("Navigate to next message"), - messages_previous: z.string().optional().default("none").describe("Navigate to previous message"), - messages_last_user: z.string().optional().default("none").describe("Navigate to last user message"), - messages_copy: z.string().optional().default("y").describe("Copy message"), - messages_undo: z.string().optional().default("u").describe("Undo message"), - messages_redo: z.string().optional().default("r").describe("Redo message"), - messages_toggle_conceal: z - .string() - .optional() - .default("h") - .describe("Toggle code block concealment in messages"), - tool_details: z.string().optional().default("none").describe("Toggle tool details visibility"), - model_list: z.string().optional().default("m").describe("List available models"), - model_cycle_recent: z.string().optional().default("f2").describe("Next recently used model"), - model_cycle_recent_reverse: z.string().optional().default("shift+f2").describe("Previous recently used model"), - model_cycle_favorite: z.string().optional().default("none").describe("Next favorite model"), - model_cycle_favorite_reverse: z.string().optional().default("none").describe("Previous favorite model"), - command_list: z.string().optional().default("ctrl+p").describe("List available commands"), - agent_list: z.string().optional().default("a").describe("List agents"), - agent_cycle: z.string().optional().default("tab").describe("Next agent"), - agent_cycle_reverse: z.string().optional().default("shift+tab").describe("Previous agent"), - variant_cycle: z.string().optional().default("ctrl+t").describe("Cycle model variants"), - variant_list: z.string().optional().default("none").describe("List model variants"), - input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"), - input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"), - input_submit: z.string().optional().default("return").describe("Submit input"), - input_newline: z - .string() - .optional() - .default("shift+return,ctrl+return,alt+return,ctrl+j") - .describe("Insert newline in input"), - input_move_left: z.string().optional().default("left,ctrl+b").describe("Move cursor left in input"), - input_move_right: z.string().optional().default("right,ctrl+f").describe("Move cursor right in input"), - input_move_up: z.string().optional().default("up").describe("Move cursor up in input"), - input_move_down: z.string().optional().default("down").describe("Move cursor down in input"), - input_select_left: z.string().optional().default("shift+left").describe("Select left in input"), - input_select_right: z.string().optional().default("shift+right").describe("Select right in input"), - input_select_up: z.string().optional().default("shift+up").describe("Select up in input"), - input_select_down: z.string().optional().default("shift+down").describe("Select down in input"), - input_line_home: z.string().optional().default("ctrl+a").describe("Move to start of line in input"), - input_line_end: z.string().optional().default("ctrl+e").describe("Move to end of line in input"), - input_select_line_home: z - .string() - .optional() - .default("ctrl+shift+a") - .describe("Select to start of line in input"), - input_select_line_end: z.string().optional().default("ctrl+shift+e").describe("Select to end of line in input"), - input_visual_line_home: z.string().optional().default("alt+a").describe("Move to start of visual line in input"), - input_visual_line_end: z.string().optional().default("alt+e").describe("Move to end of visual line in input"), - input_select_visual_line_home: z - .string() - .optional() - .default("alt+shift+a") - .describe("Select to start of visual line in input"), - input_select_visual_line_end: z - .string() - .optional() - .default("alt+shift+e") - .describe("Select to end of visual line in input"), - input_buffer_home: z.string().optional().default("home").describe("Move to start of buffer in input"), - input_buffer_end: z.string().optional().default("end").describe("Move to end of buffer in input"), - input_select_buffer_home: z - .string() - .optional() - .default("shift+home") - .describe("Select to start of buffer in input"), - input_select_buffer_end: z.string().optional().default("shift+end").describe("Select to end of buffer in input"), - input_delete_line: z.string().optional().default("ctrl+shift+d").describe("Delete line in input"), - input_delete_to_line_end: z.string().optional().default("ctrl+k").describe("Delete to end of line in input"), - input_delete_to_line_start: z.string().optional().default("ctrl+u").describe("Delete to start of line in input"), - input_backspace: z.string().optional().default("backspace,shift+backspace").describe("Backspace in input"), - input_delete: z.string().optional().default("ctrl+d,delete,shift+delete").describe("Delete character in input"), - input_undo: z.string().optional().default("ctrl+-,super+z").describe("Undo in input"), - input_redo: z.string().optional().default("ctrl+.,super+shift+z").describe("Redo in input"), - input_word_forward: z - .string() - .optional() - .default("alt+f,alt+right,ctrl+right") - .describe("Move word forward in input"), - input_word_backward: z - .string() - .optional() - .default("alt+b,alt+left,ctrl+left") - .describe("Move word backward in input"), - input_select_word_forward: z - .string() - .optional() - .default("alt+shift+f,alt+shift+right") - .describe("Select word forward in input"), - input_select_word_backward: z - .string() - .optional() - .default("alt+shift+b,alt+shift+left") - .describe("Select word backward in input"), - input_delete_word_forward: z - .string() - .optional() - .default("alt+d,alt+delete,ctrl+delete") - .describe("Delete word forward in input"), - input_delete_word_backward: z - .string() - .optional() - .default("ctrl+w,ctrl+backspace,alt+backspace") - .describe("Delete word backward in input"), - history_previous: z.string().optional().default("up").describe("Previous history item"), - history_next: z.string().optional().default("down").describe("Next history item"), - session_child_first: z.string().optional().default("down").describe("Go to first child session"), - session_child_cycle: z.string().optional().default("right").describe("Go to next child session"), - session_child_cycle_reverse: z.string().optional().default("left").describe("Go to previous child session"), - session_parent: z.string().optional().default("up").describe("Go to parent session"), - terminal_suspend: z.string().optional().default("ctrl+z").describe("Suspend terminal"), - terminal_title_toggle: z.string().optional().default("none").describe("Toggle terminal title"), - tips_toggle: z.string().optional().default("h").describe("Toggle tips on home screen"), - plugin_manager: z.string().optional().default("none").describe("Open plugin manager dialog"), - display_thinking: z.string().optional().default("none").describe("Toggle thinking blocks visibility"), - }) - .strict() - .meta({ - ref: "KeybindsConfig", - }) - - export const Server = z - .object({ - port: z.number().int().positive().optional().describe("Port to listen on"), - hostname: z.string().optional().describe("Hostname to listen on"), - mdns: z.boolean().optional().describe("Enable mDNS service discovery"), - mdnsDomain: z.string().optional().describe("Custom domain name for mDNS service (default: opencode.local)"), - cors: z.array(z.string()).optional().describe("Additional domains to allow for CORS"), - }) - .strict() - .meta({ - ref: "ServerConfig", - }) - - export const Layout = z.enum(["auto", "stretch"]).meta({ - ref: "LayoutConfig", - }) - export type Layout = z.infer - - export const Model = z - .object({ - id: z.string(), - name: z.string(), - family: z.string().optional(), - release_date: z.string(), - attachment: z.boolean(), - reasoning: z.boolean(), - temperature: z.boolean(), - tool_call: z.boolean(), - interleaved: z - .union([ - z.literal(true), + .describe("When set, ONLY these providers will be enabled. All other providers will be ignored"), + model: ModelId.describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(), + small_model: ModelId.describe( + "Small model to use for tasks like title generation in the format of provider/model", + ).optional(), + default_agent: z + .string() + .optional() + .describe( + "Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid.", + ), + username: z.string().optional().describe("Custom username to display in conversations instead of system username"), + mode: z + .object({ + build: Agent.optional(), + plan: Agent.optional(), + }) + .catchall(Agent) + .optional() + .describe("@deprecated Use `agent` field instead."), + agent: z + .object({ + // primary + plan: Agent.optional(), + build: Agent.optional(), + // subagent + general: Agent.optional(), + explore: Agent.optional(), + // specialized + title: Agent.optional(), + summary: Agent.optional(), + compaction: Agent.optional(), + }) + .catchall(Agent) + .optional() + .describe("Agent configuration, see https://opencode.ai/docs/agents"), + provider: z.record(z.string(), Provider).optional().describe("Custom provider configurations and model overrides"), + mcp: z + .record( + z.string(), + z.union([ + Mcp, z .object({ - field: z.enum(["reasoning_content", "reasoning_details"]), + enabled: z.boolean(), }) .strict(), - ]) - .optional(), - cost: z - .object({ - input: z.number(), - output: z.number(), - cache_read: z.number().optional(), - cache_write: z.number().optional(), - context_over_200k: z - .object({ - input: z.number(), - output: z.number(), - cache_read: z.number().optional(), - cache_write: z.number().optional(), - }) - .optional(), - }) - .optional(), - limit: z.object({ - context: z.number(), - input: z.number().optional(), - output: z.number(), - }), - modalities: z - .object({ - input: z.array(z.enum(["text", "audio", "image", "video", "pdf"])), - output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])), - }) - .optional(), - experimental: z.boolean().optional(), - status: z.enum(["alpha", "beta", "deprecated"]).optional(), - provider: z.object({ npm: z.string().optional(), api: z.string().optional() }).optional(), - options: z.record(z.string(), z.any()), - headers: z.record(z.string(), z.string()).optional(), - variants: z - .record( + ]), + ) + .optional() + .describe("MCP (Model Context Protocol) server configurations"), + formatter: z + .union([ + z.literal(false), + z.record( z.string(), - z - .object({ - disabled: z.boolean().optional().describe("Disable this variant for the model"), - }) - .catchall(z.any()), - ) - .optional() - .describe("Variant-specific configuration"), - }) - .partial() - - export const Provider = z - .object({ - api: z.string().optional(), - name: z.string(), - env: z.array(z.string()), - id: z.string(), - npm: z.string().optional(), - whitelist: z.array(z.string()).optional(), - blacklist: z.array(z.string()).optional(), - options: z - .object({ - apiKey: z.string().optional(), - baseURL: z.string().optional(), - enterpriseUrl: z.string().optional().describe("GitHub Enterprise URL for copilot authentication"), - setCacheKey: z.boolean().optional().describe("Enable promptCacheKey for this provider (default false)"), - timeout: z - .union([ - z - .number() - .int() - .positive() - .describe( - "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.", - ), - z.literal(false).describe("Disable timeout for this provider entirely."), - ]) - .optional() - .describe( - "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.", - ), - chunkTimeout: z - .number() - .int() - .positive() - .optional() - .describe( - "Timeout in milliseconds between streamed SSE chunks for this provider. If no chunk arrives within this window, the request is aborted.", - ), - }) - .catchall(z.any()) - .optional(), - models: z.record(z.string(), Model).optional(), - }) - .partial() - .strict() - .meta({ - ref: "ProviderConfig", - }) - - export type Provider = z.infer - - export const Info = z - .object({ - $schema: z.string().optional().describe("JSON schema reference for configuration validation"), - logLevel: Log.Level.optional().describe("Log level"), - server: Server.optional().describe("Server configuration for opencode serve and web commands"), - command: z - .record(z.string(), Command) - .optional() - .describe("Command configuration, see https://opencode.ai/docs/commands"), - skills: Skills.optional().describe("Additional skill folder paths"), - watcher: z - .object({ - ignore: z.array(z.string()).optional(), - }) - .optional(), - snapshot: z - .boolean() - .optional() - .describe( - "Enable or disable snapshot tracking. When false, filesystem snapshots are not recorded and undoing or reverting will not undo/redo file changes. Defaults to true.", + z.object({ + disabled: z.boolean().optional(), + command: z.array(z.string()).optional(), + environment: z.record(z.string(), z.string()).optional(), + extensions: z.array(z.string()).optional(), + }), ), - plugin: PluginSpec.array().optional(), - share: z - .enum(["manual", "auto", "disabled"]) - .optional() - .describe( - "Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing", - ), - autoshare: z - .boolean() - .optional() - .describe("@deprecated Use 'share' field instead. Share newly created sessions automatically"), - autoupdate: z - .union([z.boolean(), z.literal("notify")]) - .optional() - .describe( - "Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications", - ), - disabled_providers: z.array(z.string()).optional().describe("Disable providers that are loaded automatically"), - enabled_providers: z - .array(z.string()) - .optional() - .describe("When set, ONLY these providers will be enabled. All other providers will be ignored"), - model: ModelId.describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(), - small_model: ModelId.describe( - "Small model to use for tasks like title generation in the format of provider/model", - ).optional(), - default_agent: z - .string() - .optional() - .describe( - "Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid.", - ), - username: z - .string() - .optional() - .describe("Custom username to display in conversations instead of system username"), - mode: z - .object({ - build: Agent.optional(), - plan: Agent.optional(), - }) - .catchall(Agent) - .optional() - .describe("@deprecated Use `agent` field instead."), - agent: z - .object({ - // primary - plan: Agent.optional(), - build: Agent.optional(), - // subagent - general: Agent.optional(), - explore: Agent.optional(), - // specialized - title: Agent.optional(), - summary: Agent.optional(), - compaction: Agent.optional(), - }) - .catchall(Agent) - .optional() - .describe("Agent configuration, see https://opencode.ai/docs/agents"), - provider: z - .record(z.string(), Provider) - .optional() - .describe("Custom provider configurations and model overrides"), - mcp: z - .record( + ]) + .optional(), + lsp: z + .union([ + z.literal(false), + z.record( z.string(), z.union([ - Mcp, - z - .object({ - enabled: z.boolean(), - }) - .strict(), - ]), - ) - .optional() - .describe("MCP (Model Context Protocol) server configurations"), - formatter: z - .union([ - z.literal(false), - z.record( - z.string(), z.object({ - disabled: z.boolean().optional(), - command: z.array(z.string()).optional(), - environment: z.record(z.string(), z.string()).optional(), - extensions: z.array(z.string()).optional(), + disabled: z.literal(true), }), - ), - ]) - .optional(), - lsp: z - .union([ - z.literal(false), - z.record( - z.string(), - z.union([ - z.object({ - disabled: z.literal(true), - }), - z.object({ - command: z.array(z.string()), - extensions: z.array(z.string()).optional(), - disabled: z.boolean().optional(), - env: z.record(z.string(), z.string()).optional(), - initialization: z.record(z.string(), z.any()).optional(), - }), - ]), - ), - ]) - .optional() - .refine( - (data) => { - if (!data) return true - if (typeof data === "boolean") return true - const serverIds = new Set(Object.values(LSPServer).map((s) => s.id)) - - return Object.entries(data).every(([id, config]) => { - if (config.disabled) return true - if (serverIds.has(id)) return true - return Boolean(config.extensions) - }) - }, - { - error: "For custom LSP servers, 'extensions' array is required.", - }, + z.object({ + command: z.array(z.string()), + extensions: z.array(z.string()).optional(), + disabled: z.boolean().optional(), + env: z.record(z.string(), z.string()).optional(), + initialization: z.record(z.string(), z.any()).optional(), + }), + ]), ), - instructions: z.array(z.string()).optional().describe("Additional instruction files or patterns to include"), - layout: Layout.optional().describe("@deprecated Always uses stretch layout."), - permission: Permission.optional(), - tools: z.record(z.string(), z.boolean()).optional(), - enterprise: z - .object({ - url: z.string().optional().describe("Enterprise URL"), - }) - .optional(), - compaction: z - .object({ - auto: z.boolean().optional().describe("Enable automatic compaction when context is full (default: true)"), - prune: z.boolean().optional().describe("Enable pruning of old tool outputs (default: true)"), - reserved: z - .number() - .int() - .min(0) - .optional() - .describe("Token buffer for compaction. Leaves enough window to avoid overflow during compaction."), - }) - .optional(), - experimental: z - .object({ - disable_paste_summary: z.boolean().optional(), - batch_tool: z.boolean().optional().describe("Enable the batch tool"), - openTelemetry: z - .boolean() - .optional() - .describe("Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag)"), - primary_tools: z - .array(z.string()) - .optional() - .describe("Tools that should only be available to primary agents."), - continue_loop_on_deny: z.boolean().optional().describe("Continue the agent loop when a tool call is denied"), - mcp_timeout: z - .number() - .int() - .positive() - .optional() - .describe("Timeout in milliseconds for model context protocol (MCP) requests"), - }) - .optional(), - }) - .strict() - .meta({ - ref: "Config", - }) + ]) + .optional() + .refine( + (data) => { + if (!data) return true + if (typeof data === "boolean") return true + const serverIds = new Set(Object.values(LSPServer).map((s) => s.id)) - export type Info = z.output & { - plugin_origins?: PluginOrigin[] - } - - type State = { - config: Info - directories: string[] - deps: Fiber.Fiber[] - consoleState: ConsoleState - } - - export interface Interface { - readonly get: () => Effect.Effect - readonly getGlobal: () => Effect.Effect - readonly getConsoleState: () => Effect.Effect - readonly installDependencies: (dir: string, input?: InstallInput) => Effect.Effect - readonly update: (config: Info) => Effect.Effect - readonly updateGlobal: (config: Info) => Effect.Effect - readonly invalidate: (wait?: boolean) => Effect.Effect - readonly directories: () => Effect.Effect - readonly waitForDependencies: () => Effect.Effect - } - - export class Service extends Context.Service()("@opencode/Config") {} - - function globalConfigFile() { - const candidates = ["opencode.jsonc", "opencode.json", "config.json"].map((file) => - path.join(Global.Path.config, file), - ) - for (const file of candidates) { - if (existsSync(file)) return file - } - return candidates[0] - } - - function patchJsonc(input: string, patch: unknown, path: string[] = []): string { - if (!isRecord(patch)) { - const edits = modify(input, path, patch, { - formattingOptions: { - insertSpaces: true, - tabSize: 2, + return Object.entries(data).every(([id, config]) => { + if (config.disabled) return true + if (serverIds.has(id)) return true + return Boolean(config.extensions) + }) }, + { + error: "For custom LSP servers, 'extensions' array is required.", + }, + ), + instructions: z.array(z.string()).optional().describe("Additional instruction files or patterns to include"), + layout: Layout.optional().describe("@deprecated Always uses stretch layout."), + permission: Permission.optional(), + tools: z.record(z.string(), z.boolean()).optional(), + enterprise: z + .object({ + url: z.string().optional().describe("Enterprise URL"), }) - return applyEdits(input, edits) - } + .optional(), + compaction: z + .object({ + auto: z.boolean().optional().describe("Enable automatic compaction when context is full (default: true)"), + prune: z.boolean().optional().describe("Enable pruning of old tool outputs (default: true)"), + reserved: z + .number() + .int() + .min(0) + .optional() + .describe("Token buffer for compaction. Leaves enough window to avoid overflow during compaction."), + }) + .optional(), + experimental: z + .object({ + disable_paste_summary: z.boolean().optional(), + batch_tool: z.boolean().optional().describe("Enable the batch tool"), + openTelemetry: z + .boolean() + .optional() + .describe("Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag)"), + primary_tools: z + .array(z.string()) + .optional() + .describe("Tools that should only be available to primary agents."), + continue_loop_on_deny: z.boolean().optional().describe("Continue the agent loop when a tool call is denied"), + mcp_timeout: z + .number() + .int() + .positive() + .optional() + .describe("Timeout in milliseconds for model context protocol (MCP) requests"), + }) + .optional(), + }) + .strict() + .meta({ + ref: "Config", + }) - return Object.entries(patch).reduce((result, [key, value]) => { - if (value === undefined) return result - return patchJsonc(result, value, [...path, key]) - }, input) +export type Info = z.output & { + plugin_origins?: PluginOrigin[] +} + +type State = { + config: Info + directories: string[] + deps: Fiber.Fiber[] + consoleState: ConsoleState +} + +export interface Interface { + readonly get: () => Effect.Effect + readonly getGlobal: () => Effect.Effect + readonly getConsoleState: () => Effect.Effect + readonly installDependencies: (dir: string, input?: InstallInput) => Effect.Effect + readonly update: (config: Info) => Effect.Effect + readonly updateGlobal: (config: Info) => Effect.Effect + readonly invalidate: (wait?: boolean) => Effect.Effect + readonly directories: () => Effect.Effect + readonly waitForDependencies: () => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/Config") {} + +function globalConfigFile() { + const candidates = ["opencode.jsonc", "opencode.json", "config.json"].map((file) => + path.join(Global.Path.config, file), + ) + for (const file of candidates) { + if (existsSync(file)) return file + } + return candidates[0] +} + +function patchJsonc(input: string, patch: unknown, path: string[] = []): string { + if (!isRecord(patch)) { + const edits = modify(input, path, patch, { + formattingOptions: { + insertSpaces: true, + tabSize: 2, + }, + }) + return applyEdits(input, edits) } - function writable(info: Info) { - const { plugin_origins, ...next } = info - return next - } + return Object.entries(patch).reduce((result, [key, value]) => { + if (value === undefined) return result + return patchJsonc(result, value, [...path, key]) + }, input) +} - function parseConfig(text: string, filepath: string): Info { - const errors: JsoncParseError[] = [] - const data = parseJsonc(text, errors, { allowTrailingComma: true }) - if (errors.length) { - const lines = text.split("\n") - const errorDetails = errors - .map((e) => { - const beforeOffset = text.substring(0, e.offset).split("\n") - const line = beforeOffset.length - const column = beforeOffset[beforeOffset.length - 1].length + 1 - const problemLine = lines[line - 1] +function writable(info: Info) { + const { plugin_origins, ...next } = info + return next +} - const error = `${printParseErrorCode(e.error)} at line ${line}, column ${column}` - if (!problemLine) return error +function parseConfig(text: string, filepath: string): Info { + const errors: JsoncParseError[] = [] + const data = parseJsonc(text, errors, { allowTrailingComma: true }) + if (errors.length) { + const lines = text.split("\n") + const errorDetails = errors + .map((e) => { + const beforeOffset = text.substring(0, e.offset).split("\n") + const line = beforeOffset.length + const column = beforeOffset[beforeOffset.length - 1].length + 1 + const problemLine = lines[line - 1] - return `${error}\n Line ${line}: ${problemLine}\n${"".padStart(column + 9)}^` - }) - .join("\n") + const error = `${printParseErrorCode(e.error)} at line ${line}, column ${column}` + if (!problemLine) return error - throw new JsonError({ - path: filepath, - message: `\n--- JSONC Input ---\n${text}\n--- Errors ---\n${errorDetails}\n--- End ---`, + return `${error}\n Line ${line}: ${problemLine}\n${"".padStart(column + 9)}^` }) - } + .join("\n") - const parsed = Info.safeParse(data) - if (parsed.success) return parsed.data - - throw new InvalidError({ + throw new JsonError({ path: filepath, - issues: parsed.error.issues, + message: `\n--- JSONC Input ---\n${text}\n--- Errors ---\n${errorDetails}\n--- End ---`, }) } - export const { JsonError, InvalidError } = ConfigPaths + const parsed = Info.safeParse(data) + if (parsed.success) return parsed.data - export const ConfigDirectoryTypoError = NamedError.create( - "ConfigDirectoryTypoError", - z.object({ - path: z.string(), - dir: z.string(), - suggestion: z.string(), - }), - ) + throw new InvalidError({ + path: filepath, + issues: parsed.error.issues, + }) +} - export const layer: Layer.Layer< - Service, - never, - AppFileSystem.Service | Auth.Service | Account.Service | Env.Service - > = Layer.effect( +export const { JsonError, InvalidError } = ConfigPaths + +export const ConfigDirectoryTypoError = NamedError.create( + "ConfigDirectoryTypoError", + z.object({ + path: z.string(), + dir: z.string(), + suggestion: z.string(), + }), +) + +export const layer: Layer.Layer = + Layer.effect( Service, Effect.gen(function* () { const fs = yield* AppFileSystem.Service @@ -1531,9 +1511,9 @@ export namespace Config { } if (result.tools) { - const perms: Record = {} + const perms: Record = {} for (const [tool, enabled] of Object.entries(result.tools)) { - const action: Config.PermissionAction = enabled ? "allow" : "deny" + const action: PermissionAction = enabled ? "allow" : "deny" if (tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit") { perms.edit = action continue @@ -1654,10 +1634,9 @@ export namespace Config { }), ) - export const defaultLayer = layer.pipe( - Layer.provide(AppFileSystem.defaultLayer), - Layer.provide(Env.defaultLayer), - Layer.provide(Auth.defaultLayer), - Layer.provide(Account.defaultLayer), - ) -} +export const defaultLayer = layer.pipe( + Layer.provide(AppFileSystem.defaultLayer), + Layer.provide(Env.defaultLayer), + Layer.provide(Auth.defaultLayer), + Layer.provide(Account.defaultLayer), +) diff --git a/packages/opencode/src/config/index.ts b/packages/opencode/src/config/index.ts new file mode 100644 index 0000000000..60e39c3163 --- /dev/null +++ b/packages/opencode/src/config/index.ts @@ -0,0 +1 @@ +export * as Config from "./config" diff --git a/packages/opencode/src/config/tui-schema.ts b/packages/opencode/src/config/tui-schema.ts index a373b4d800..fd5cd8c88d 100644 --- a/packages/opencode/src/config/tui-schema.ts +++ b/packages/opencode/src/config/tui-schema.ts @@ -1,5 +1,5 @@ import z from "zod" -import { Config } from "./config" +import { Config } from "." const KeybindOverride = z .object( diff --git a/packages/opencode/src/config/tui.ts b/packages/opencode/src/config/tui.ts index e64b226c14..163bd4d7d7 100644 --- a/packages/opencode/src/config/tui.ts +++ b/packages/opencode/src/config/tui.ts @@ -2,7 +2,7 @@ import { existsSync } from "fs" import z from "zod" import { mergeDeep, unique } from "remeda" import { Context, Effect, Fiber, Layer } from "effect" -import { Config } from "./config" +import { Config } from "." import { ConfigPaths } from "./paths" import { migrateTuiConfig } from "./tui-migrate" import { TuiInfo } from "./tui-schema" diff --git a/packages/opencode/src/effect/app-runtime.ts b/packages/opencode/src/effect/app-runtime.ts index 257922dafe..54139eb777 100644 --- a/packages/opencode/src/effect/app-runtime.ts +++ b/packages/opencode/src/effect/app-runtime.ts @@ -6,7 +6,7 @@ import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { Bus } from "@/bus" import { Auth } from "@/auth" import { Account } from "@/account" -import { Config } from "@/config/config" +import { Config } from "@/config" import { Git } from "@/git" import { Ripgrep } from "@/file/ripgrep" import { FileTime } from "@/file/time" diff --git a/packages/opencode/src/file/watcher.ts b/packages/opencode/src/file/watcher.ts index 8737045c18..74966fd47a 100644 --- a/packages/opencode/src/file/watcher.ts +++ b/packages/opencode/src/file/watcher.ts @@ -12,7 +12,7 @@ import { Flag } from "@/flag/flag" import { Git } from "@/git" import { Instance } from "@/project/instance" import { lazy } from "@/util/lazy" -import { Config } from "../config/config" +import { Config } from "../config" import { FileIgnore } from "./ignore" import { Protected } from "./protected" import { Log } from "../util/log" diff --git a/packages/opencode/src/format/index.ts b/packages/opencode/src/format/index.ts index 1aeb2e51a4..595bb7a608 100644 --- a/packages/opencode/src/format/index.ts +++ b/packages/opencode/src/format/index.ts @@ -5,7 +5,7 @@ import { InstanceState } from "@/effect/instance-state" import path from "path" import { mergeDeep } from "remeda" import z from "zod" -import { Config } from "../config/config" +import { Config } from "../config" import { Instance } from "../project/instance" import { Log } from "../util/log" import * as Formatter from "./formatter" diff --git a/packages/opencode/src/lsp/index.ts b/packages/opencode/src/lsp/index.ts index 0c83890e55..4daacd30b8 100644 --- a/packages/opencode/src/lsp/index.ts +++ b/packages/opencode/src/lsp/index.ts @@ -6,7 +6,7 @@ import path from "path" import { pathToFileURL, fileURLToPath } from "url" import { LSPServer } from "./server" import z from "zod" -import { Config } from "../config/config" +import { Config } from "../config" import { Instance } from "../project/instance" import { Flag } from "@/flag/flag" import { Process } from "../util/process" diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index a68c6c1d8d..cbaa2c24b3 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -9,7 +9,7 @@ import { type Tool as MCPToolDef, ToolListChangedNotificationSchema, } from "@modelcontextprotocol/sdk/types.js" -import { Config } from "../config/config" +import { Config } from "../config" import { Log } from "../util/log" import { NamedError } from "@opencode-ai/shared/util/error" import z from "zod/v4" diff --git a/packages/opencode/src/node.ts b/packages/opencode/src/node.ts index 44a9f3b430..6f020576d9 100644 --- a/packages/opencode/src/node.ts +++ b/packages/opencode/src/node.ts @@ -1,4 +1,4 @@ -export { Config } from "./config/config" +export { Config } from "./config" export { Server } from "./server/server" export { bootstrap } from "./cli/bootstrap" export { Log } from "./util/log" diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts index b6a44e2582..71d321080a 100644 --- a/packages/opencode/src/permission/index.ts +++ b/packages/opencode/src/permission/index.ts @@ -1,6 +1,6 @@ import { Bus } from "@/bus" import { BusEvent } from "@/bus/bus-event" -import { Config } from "@/config/config" +import { Config } from "@/config" import { InstanceState } from "@/effect/instance-state" import { ProjectID } from "@/project/schema" import { Instance } from "@/project/instance" diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index 9f618eff8c..f31e0b9ff2 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -5,7 +5,7 @@ import type { PluginModule, WorkspaceAdaptor as PluginWorkspaceAdaptor, } from "@opencode-ai/plugin" -import { Config } from "../config/config" +import { Config } from "../config" import { Bus } from "../bus" import { Log } from "../util/log" import { createOpencodeClient } from "@opencode-ai/sdk" diff --git a/packages/opencode/src/plugin/loader.ts b/packages/opencode/src/plugin/loader.ts index 634fe6aad0..12617f9010 100644 --- a/packages/opencode/src/plugin/loader.ts +++ b/packages/opencode/src/plugin/loader.ts @@ -1,4 +1,4 @@ -import { Config } from "@/config/config" +import { Config } from "@/config" import { Installation } from "@/installation" import { checkPluginCompatibility, diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index c029e5c5c6..1dd6027db9 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -1,7 +1,7 @@ import z from "zod" import os from "os" import fuzzysort from "fuzzysort" -import { Config } from "../config/config" +import { Config } from "../config" import { mapValues, mergeDeep, omit, pickBy, sortBy } from "remeda" import { NoSuchModelError, type Provider as SDK } from "ai" import { Log } from "../util/log" diff --git a/packages/opencode/src/server/instance/config.ts b/packages/opencode/src/server/instance/config.ts index aa770726df..11845c69c9 100644 --- a/packages/opencode/src/server/instance/config.ts +++ b/packages/opencode/src/server/instance/config.ts @@ -1,7 +1,7 @@ import { Hono } from "hono" import { describeRoute, validator, resolver } from "hono-openapi" import z from "zod" -import { Config } from "../../config/config" +import { Config } from "../../config" import { Provider } from "../../provider/provider" import { mapValues } from "remeda" import { errors } from "../error" diff --git a/packages/opencode/src/server/instance/experimental.ts b/packages/opencode/src/server/instance/experimental.ts index e8e46b2e3b..6e1a47ed20 100644 --- a/packages/opencode/src/server/instance/experimental.ts +++ b/packages/opencode/src/server/instance/experimental.ts @@ -8,7 +8,7 @@ import { Instance } from "../../project/instance" import { Project } from "../../project/project" import { MCP } from "../../mcp" import { Session } from "../../session" -import { Config } from "../../config/config" +import { Config } from "../../config" import { ConsoleState } from "../../config/console-state" import { Account, AccountID, OrgID } from "../../account" import { AppRuntime } from "../../effect/app-runtime" diff --git a/packages/opencode/src/server/instance/global.ts b/packages/opencode/src/server/instance/global.ts index d462a07f74..b69f35a649 100644 --- a/packages/opencode/src/server/instance/global.ts +++ b/packages/opencode/src/server/instance/global.ts @@ -12,7 +12,7 @@ import { Instance } from "../../project/instance" import { Installation } from "@/installation" import { Log } from "../../util/log" import { lazy } from "../../util/lazy" -import { Config } from "../../config/config" +import { Config } from "../../config" import { errors } from "../error" const log = Log.create({ service: "server" }) diff --git a/packages/opencode/src/server/instance/mcp.ts b/packages/opencode/src/server/instance/mcp.ts index f1c8701c4e..695008fc4e 100644 --- a/packages/opencode/src/server/instance/mcp.ts +++ b/packages/opencode/src/server/instance/mcp.ts @@ -2,7 +2,7 @@ import { Hono } from "hono" import { describeRoute, validator, resolver } from "hono-openapi" import z from "zod" import { MCP } from "../../mcp" -import { Config } from "../../config/config" +import { Config } from "../../config" import { AppRuntime } from "../../effect/app-runtime" import { errors } from "../error" import { lazy } from "../../util/lazy" diff --git a/packages/opencode/src/server/instance/provider.ts b/packages/opencode/src/server/instance/provider.ts index 6988d56e4e..b9e39d4eff 100644 --- a/packages/opencode/src/server/instance/provider.ts +++ b/packages/opencode/src/server/instance/provider.ts @@ -1,7 +1,7 @@ import { Hono } from "hono" import { describeRoute, validator, resolver } from "hono-openapi" import z from "zod" -import { Config } from "../../config/config" +import { Config } from "../../config" import { Provider } from "../../provider/provider" import { ModelsDev } from "../../provider/models" import { ProviderAuth } from "../../provider/auth" diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index 4978ef5478..810b949743 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -11,7 +11,7 @@ import { Log } from "../util/log" import { SessionProcessor } from "./processor" import { Agent } from "@/agent/agent" import { Plugin } from "@/plugin" -import { Config } from "@/config/config" +import { Config } from "@/config" import { NotFoundError } from "@/storage/db" import { ModelID, ProviderID } from "@/provider/schema" import { Effect, Layer, Context } from "effect" diff --git a/packages/opencode/src/session/instruction.ts b/packages/opencode/src/session/instruction.ts index b4794ba5b1..23dd88ff5a 100644 --- a/packages/opencode/src/session/instruction.ts +++ b/packages/opencode/src/session/instruction.ts @@ -2,7 +2,7 @@ import os from "os" import path from "path" import { Effect, Layer, Context } from "effect" import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http" -import { Config } from "@/config/config" +import { Config } from "@/config" import { InstanceState } from "@/effect/instance-state" import { Flag } from "@/flag/flag" import { AppFileSystem } from "@opencode-ai/shared/filesystem" diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index 05d7882757..2efe4a4054 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -6,7 +6,7 @@ import { streamText, wrapLanguageModel, type ModelMessage, type Tool, tool, json import { mergeDeep, pipe } from "remeda" import { GitLabWorkflowLanguageModel } from "gitlab-ai-provider" import { ProviderTransform } from "@/provider/transform" -import { Config } from "@/config/config" +import { Config } from "@/config" import { Instance } from "@/project/instance" import type { Agent } from "@/agent/agent" import type { MessageV2 } from "./message-v2" diff --git a/packages/opencode/src/session/overflow.ts b/packages/opencode/src/session/overflow.ts index f0e52565d8..c4c6d09279 100644 --- a/packages/opencode/src/session/overflow.ts +++ b/packages/opencode/src/session/overflow.ts @@ -1,4 +1,4 @@ -import type { Config } from "@/config/config" +import type { Config } from "@/config" import type { Provider } from "@/provider/provider" import { ProviderTransform } from "@/provider/transform" import type { MessageV2 } from "./message-v2" diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index b02e7cc81c..d91b1427b0 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -2,7 +2,7 @@ import { Cause, Deferred, Effect, Layer, Context, Scope } from "effect" import * as Stream from "effect/Stream" import { Agent } from "@/agent/agent" import { Bus } from "@/bus" -import { Config } from "@/config/config" +import { Config } from "@/config" import { Permission } from "@/permission" import { Plugin } from "@/plugin" import { Snapshot } from "@/snapshot" diff --git a/packages/opencode/src/share/session.ts b/packages/opencode/src/share/session.ts index 08210de8a1..0a673f81c6 100644 --- a/packages/opencode/src/share/session.ts +++ b/packages/opencode/src/share/session.ts @@ -2,7 +2,7 @@ import { Session } from "@/session" import { SessionID } from "@/session/schema" import { SyncEvent } from "@/sync" import { Effect, Layer, Scope, Context } from "effect" -import { Config } from "../config/config" +import { Config } from "../config" import { Flag } from "../flag/flag" import { ShareNext } from "./share-next" diff --git a/packages/opencode/src/share/share-next.ts b/packages/opencode/src/share/share-next.ts index ad247f5466..667e0720c4 100644 --- a/packages/opencode/src/share/share-next.ts +++ b/packages/opencode/src/share/share-next.ts @@ -10,7 +10,7 @@ import { Session } from "@/session" import { MessageV2 } from "@/session/message-v2" import type { SessionID } from "@/session/schema" import { Database, eq } from "@/storage/db" -import { Config } from "@/config/config" +import { Config } from "@/config" import { Log } from "@/util/log" import { SessionShareTable } from "./share.sql" diff --git a/packages/opencode/src/skill/index.ts b/packages/opencode/src/skill/index.ts index 79b426c69c..4bf5d0cfed 100644 --- a/packages/opencode/src/skill/index.ts +++ b/packages/opencode/src/skill/index.ts @@ -11,7 +11,7 @@ import { Flag } from "@/flag/flag" import { Global } from "@/global" import { Permission } from "@/permission" import { AppFileSystem } from "@opencode-ai/shared/filesystem" -import { Config } from "../config/config" +import { Config } from "../config" import { ConfigMarkdown } from "../config/markdown" import { Glob } from "@opencode-ai/shared/util/glob" import { Log } from "../util/log" diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 9378e309aa..83963e3511 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -7,7 +7,7 @@ import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" import { InstanceState } from "@/effect/instance-state" import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { Hash } from "@opencode-ai/shared/util/hash" -import { Config } from "../config/config" +import { Config } from "../config" import { Global } from "../global" import { Log } from "../util/log" diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 6900feecc3..2e9971ad71 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -13,7 +13,7 @@ import { WriteTool } from "./write" import { InvalidTool } from "./invalid" import { SkillTool } from "./skill" import { Tool } from "./tool" -import { Config } from "../config/config" +import { Config } from "../config" import { type ToolContext as PluginToolContext, type ToolDefinition } from "@opencode-ai/plugin" import z from "zod" import { Plugin } from "../plugin" diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index ce99ab2992..bbb07caa40 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -6,7 +6,7 @@ import { SessionID, MessageID } from "../session/schema" import { MessageV2 } from "../session/message-v2" import { Agent } from "../agent/agent" import type { SessionPrompt } from "../session/prompt" -import { Config } from "../config/config" +import { Config } from "../config" import { Effect } from "effect" import { Log } from "@/util/log" diff --git a/packages/opencode/test/config/agent-color.test.ts b/packages/opencode/test/config/agent-color.test.ts index af9565cba8..d77782354c 100644 --- a/packages/opencode/test/config/agent-color.test.ts +++ b/packages/opencode/test/config/agent-color.test.ts @@ -3,7 +3,7 @@ import { Effect } from "effect" import path from "path" import { provideInstance, tmpdir } from "../fixture/fixture" import { Instance } from "../../src/project/instance" -import { Config } from "../../src/config/config" +import { Config } from "../../src/config" import { Agent as AgentSvc } from "../../src/agent/agent" import { Color } from "../../src/util/color" import { AppRuntime } from "../../src/effect/app-runtime" diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index ed7e689da4..88957c6141 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -1,7 +1,7 @@ import { test, expect, describe, mock, afterEach, beforeEach, spyOn } from "bun:test" import { Deferred, Effect, Fiber, Layer, Option } from "effect" import { NodeFileSystem, NodePath } from "@effect/platform-node" -import { Config } from "../../src/config/config" +import { Config } from "../../src/config" import { Instance } from "../../src/project/instance" import { Auth } from "../../src/auth" import { AccessToken, Account, AccountID, OrgID } from "../../src/account" diff --git a/packages/opencode/test/config/tui.test.ts b/packages/opencode/test/config/tui.test.ts index 529d88bce1..4767e94b01 100644 --- a/packages/opencode/test/config/tui.test.ts +++ b/packages/opencode/test/config/tui.test.ts @@ -3,7 +3,7 @@ import path from "path" import fs from "fs/promises" import { tmpdir } from "../fixture/fixture" import { Instance } from "../../src/project/instance" -import { Config } from "../../src/config/config" +import { Config } from "../../src/config" import { TuiConfig } from "../../src/config/tui" import { Global } from "../../src/global" import { Filesystem } from "../../src/util/filesystem" diff --git a/packages/opencode/test/file/watcher.test.ts b/packages/opencode/test/file/watcher.test.ts index 0c8968d94b..0c23550083 100644 --- a/packages/opencode/test/file/watcher.test.ts +++ b/packages/opencode/test/file/watcher.test.ts @@ -5,7 +5,7 @@ import path from "path" import { ConfigProvider, Deferred, Effect, Layer, ManagedRuntime, Option } from "effect" import { tmpdir } from "../fixture/fixture" import { Bus } from "../../src/bus" -import { Config } from "../../src/config/config" +import { Config } from "../../src/config" import { FileWatcher } from "../../src/file/watcher" import { Git } from "../../src/git" import { Instance } from "../../src/project/instance" diff --git a/packages/opencode/test/fixture/fixture.ts b/packages/opencode/test/fixture/fixture.ts index 7970543547..fd7f5e3808 100644 --- a/packages/opencode/test/fixture/fixture.ts +++ b/packages/opencode/test/fixture/fixture.ts @@ -6,7 +6,7 @@ import { Effect, Context } from "effect" import type * as PlatformError from "effect/PlatformError" import type * as Scope from "effect/Scope" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" -import type { Config } from "../../src/config/config" +import type { Config } from "../../src/config" import { InstanceRef } from "../../src/effect/instance-ref" import { Instance } from "../../src/project/instance" import { TestLLMServer } from "../lib/llm-server" diff --git a/packages/opencode/test/permission-task.test.ts b/packages/opencode/test/permission-task.test.ts index d415d23ebc..3c53314b6a 100644 --- a/packages/opencode/test/permission-task.test.ts +++ b/packages/opencode/test/permission-task.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, test, expect } from "bun:test" import { Permission } from "../src/permission" -import { Config } from "../src/config/config" +import { Config } from "../src/config" import { Instance } from "../src/project/instance" import { tmpdir } from "./fixture/fixture" import { AppRuntime } from "../src/effect/app-runtime" diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index 251447762d..1174cdf6a1 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/packages/opencode/test/session/compaction.test.ts @@ -5,7 +5,7 @@ import * as Stream from "effect/Stream" import path from "path" import z from "zod" import { Bus } from "../../src/bus" -import { Config } from "../../src/config/config" +import { Config } from "../../src/config" import { Agent } from "../../src/agent/agent" import { LLM } from "../../src/session/llm" import { SessionCompaction } from "../../src/session/compaction" diff --git a/packages/opencode/test/session/processor-effect.test.ts b/packages/opencode/test/session/processor-effect.test.ts index d384513087..10945be188 100644 --- a/packages/opencode/test/session/processor-effect.test.ts +++ b/packages/opencode/test/session/processor-effect.test.ts @@ -5,7 +5,7 @@ import path from "path" import type { Agent } from "../../src/agent/agent" import { Agent as AgentSvc } from "../../src/agent/agent" import { Bus } from "../../src/bus" -import { Config } from "../../src/config/config" +import { Config } from "../../src/config" import { Permission } from "../../src/permission" import { Plugin } from "../../src/plugin" import { Provider } from "../../src/provider/provider" diff --git a/packages/opencode/test/session/prompt-effect.test.ts b/packages/opencode/test/session/prompt-effect.test.ts index 31727e3df9..ec1a87e969 100644 --- a/packages/opencode/test/session/prompt-effect.test.ts +++ b/packages/opencode/test/session/prompt-effect.test.ts @@ -7,7 +7,7 @@ import z from "zod" import { Agent as AgentSvc } from "../../src/agent/agent" import { Bus } from "../../src/bus" import { Command } from "../../src/command" -import { Config } from "../../src/config/config" +import { Config } from "../../src/config" import { FileTime } from "../../src/file/time" import { LSP } from "../../src/lsp" import { MCP } from "../../src/mcp" diff --git a/packages/opencode/test/session/snapshot-tool-race.test.ts b/packages/opencode/test/session/snapshot-tool-race.test.ts index 80d74c7565..a0ea47c89c 100644 --- a/packages/opencode/test/session/snapshot-tool-race.test.ts +++ b/packages/opencode/test/session/snapshot-tool-race.test.ts @@ -32,7 +32,7 @@ import { NodeFileSystem } from "@effect/platform-node" import { Agent as AgentSvc } from "../../src/agent/agent" import { Bus } from "../../src/bus" import { Command } from "../../src/command" -import { Config } from "../../src/config/config" +import { Config } from "../../src/config" import { FileTime } from "../../src/file/time" import { LSP } from "../../src/lsp" import { MCP } from "../../src/mcp" diff --git a/packages/opencode/test/share/share-next.test.ts b/packages/opencode/test/share/share-next.test.ts index fd230f5459..135d44db09 100644 --- a/packages/opencode/test/share/share-next.test.ts +++ b/packages/opencode/test/share/share-next.test.ts @@ -8,7 +8,7 @@ import { Account } from "../../src/account" import { AccountRepo } from "../../src/account/repo" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" import { Bus } from "../../src/bus" -import { Config } from "../../src/config/config" +import { Config } from "../../src/config" import { Provider } from "../../src/provider/provider" import { Session } from "../../src/session" import type { SessionID } from "../../src/session/schema" diff --git a/packages/opencode/test/tool/task.test.ts b/packages/opencode/test/tool/task.test.ts index e7a143c9af..bc90dc0f22 100644 --- a/packages/opencode/test/tool/task.test.ts +++ b/packages/opencode/test/tool/task.test.ts @@ -1,7 +1,7 @@ import { afterEach, describe, expect } from "bun:test" import { Effect, Layer } from "effect" import { Agent } from "../../src/agent/agent" -import { Config } from "../../src/config/config" +import { Config } from "../../src/config" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" import { Instance } from "../../src/project/instance" import { Session } from "../../src/session" From f7d4665e4091c88c2fde0f9db70d6333f83b86fd Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 21:33:54 -0400 Subject: [PATCH 170/300] =?UTF-8?q?fix:=20resolve=20oxlint=20warnings=20?= =?UTF-8?q?=E2=80=94=20suppress=20false=20positives,=20remove=20unused=20i?= =?UTF-8?q?mports=20(#22687)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .oxlintrc.json | 8 +++++++- github/index.ts | 6 +++--- infra/enterprise.ts | 2 +- packages/app/src/addons/serialize.ts | 4 ++-- .../app/src/components/session/session-header.tsx | 2 +- packages/app/src/components/titlebar.tsx | 2 +- packages/app/src/context/global-sync/queue.ts | 1 + packages/app/src/pages/layout.tsx | 2 +- packages/app/src/pages/session/review-tab.tsx | 2 +- packages/console/app/src/component/email-signup.tsx | 1 - packages/console/app/src/component/header.tsx | 2 +- packages/console/app/src/context/auth.session.ts | 1 + packages/console/app/src/routes/bench/[id].tsx | 2 +- packages/console/app/src/routes/go/index.tsx | 2 +- packages/console/app/src/routes/workspace-picker.tsx | 2 +- packages/console/app/src/routes/zen/index.tsx | 2 +- packages/console/app/src/routes/zen/util/handler.ts | 2 +- .../app/src/routes/zen/util/provider/anthropic.ts | 2 +- .../app/src/routes/zen/util/provider/google.ts | 2 +- .../src/routes/zen/util/provider/openai-compatible.ts | 6 +++--- .../app/src/routes/zen/util/provider/openai.ts | 2 +- packages/console/core/script/black-cancel-waitlist.ts | 6 ++---- packages/console/core/script/black-gift.ts | 6 ++---- .../console/core/script/black-onboard-waitlist.ts | 6 ++---- .../console/core/script/black-select-workspaces.ts | 2 +- packages/console/core/src/util/env.cloudflare.ts | 1 + packages/console/core/src/util/log.ts | 2 +- packages/enterprise/test/core/share.test.ts | 2 +- packages/opencode/script/publish.ts | 2 +- packages/opencode/src/cli/cmd/debug/lsp.ts | 1 - packages/opencode/src/cli/cmd/export.ts | 2 +- packages/opencode/src/cli/cmd/github.ts | 4 ++-- packages/opencode/src/cli/cmd/serve.ts | 3 --- packages/opencode/src/cli/cmd/tui/app.tsx | 3 ++- .../src/cli/cmd/tui/component/dialog-theme-list.tsx | 2 +- .../src/cli/cmd/tui/component/prompt/index.tsx | 2 +- packages/opencode/src/cli/cmd/tui/event.ts | 1 - .../opencode/src/cli/cmd/tui/routes/session/index.tsx | 6 +++--- .../opencode/src/cli/cmd/tui/ui/dialog-select.tsx | 2 +- .../opencode/src/control-plane/workspace-context.ts | 2 +- packages/opencode/src/file/ignore.ts | 1 - packages/opencode/src/file/watcher.ts | 2 +- packages/opencode/src/format/index.ts | 1 - packages/opencode/src/global/index.ts | 2 +- packages/opencode/src/ide/index.ts | 1 - packages/opencode/src/lsp/server.ts | 2 +- packages/opencode/src/patch/index.ts | 2 +- packages/opencode/src/permission/index.ts | 1 - packages/opencode/src/plugin/codex.ts | 4 +--- .../responses/openai-responses-language-model.ts | 1 + packages/opencode/src/server/instance/config.ts | 1 - packages/opencode/src/server/instance/event.ts | 1 - packages/opencode/src/server/instance/pty.ts | 2 +- packages/opencode/src/session/compaction.ts | 1 - packages/opencode/src/session/processor.ts | 2 ++ packages/opencode/src/session/projectors.ts | 6 ++---- packages/opencode/src/session/revert.ts | 1 - packages/opencode/src/storage/json-migration.ts | 4 ++++ packages/opencode/src/sync/index.ts | 1 - packages/opencode/src/tool/edit.ts | 2 +- packages/opencode/src/tool/task.ts | 1 - packages/opencode/src/tool/webfetch.ts | 2 +- packages/opencode/src/util/lazy.ts | 11 +++-------- packages/opencode/src/v2/session.ts | 2 -- .../opencode/test/cli/tui/plugin-lifecycle.test.ts | 1 - .../opencode/test/effect/cross-spawn-spawner.test.ts | 3 +-- packages/opencode/test/effect/instance-state.test.ts | 2 +- packages/opencode/test/permission/next.test.ts | 2 +- packages/opencode/test/provider/gitlab-duo.test.ts | 1 + .../opencode/test/server/project-init-git.test.ts | 1 - packages/opencode/test/session/compaction.test.ts | 1 - packages/opencode/test/session/prompt-effect.test.ts | 1 - .../opencode/test/session/snapshot-tool-race.test.ts | 1 - packages/opencode/test/share/share-next.test.ts | 1 - packages/opencode/test/tool/question.test.ts | 1 - packages/slack/src/index.ts | 2 +- packages/ui/src/components/message-part.tsx | 1 - .../ui/src/components/timeline-playground.stories.tsx | 2 +- packages/ui/src/pierre/commented-lines.ts | 2 +- sdks/vscode/src/extension.ts | 2 +- 80 files changed, 82 insertions(+), 106 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index 0875f38326..c366084ee7 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -4,7 +4,13 @@ // Effect uses `function*` with Effect.gen/Effect.fnUntraced that don't always yield "require-yield": "off", // SolidJS uses `let ref: T | undefined` for JSX ref bindings assigned at runtime - "no-unassigned-vars": "off" + "no-unassigned-vars": "off", + // SolidJS tracks reactive deps by reading properties inside createEffect + "no-unused-expressions": "off", + // Intentional control char matching (ANSI escapes, null byte sanitization) + "no-control-regex": "off", + // SST and plugin tools require triple-slash references + "triple-slash-reference": "off" }, "ignorePatterns": ["**/node_modules", "**/dist", "**/.build", "**/.sst", "**/*.d.ts"] } diff --git a/github/index.ts b/github/index.ts index 6bfa964623..be8e5aafcd 100644 --- a/github/index.ts +++ b/github/index.ts @@ -281,7 +281,7 @@ async function assertOpencodeConnected() { }) connected = true break - } catch (e) {} + } catch {} await sleep(300) } while (retry++ < 30) @@ -561,7 +561,7 @@ async function subscribeSessionEvents() { if (evt.properties.info.id !== session.id) continue session = evt.properties.info } - } catch (e) { + } catch { // Ignore parse errors } } @@ -576,7 +576,7 @@ async function subscribeSessionEvents() { async function summarize(response: string) { try { return await chat(`Summarize the following in less than 40 characters:\n\n${response}`) - } catch (e) { + } catch { if (isScheduleEvent()) { return "Scheduled task changes" } diff --git a/infra/enterprise.ts b/infra/enterprise.ts index 22b4c6f44e..38f0c3c8fd 100644 --- a/infra/enterprise.ts +++ b/infra/enterprise.ts @@ -1,5 +1,5 @@ import { SECRET } from "./secret" -import { domain, shortDomain } from "./stage" +import { shortDomain } from "./stage" const storage = new sst.cloudflare.Bucket("EnterpriseStorage") diff --git a/packages/app/src/addons/serialize.ts b/packages/app/src/addons/serialize.ts index 4cab55b3f2..3823fb443a 100644 --- a/packages/app/src/addons/serialize.ts +++ b/packages/app/src/addons/serialize.ts @@ -258,8 +258,8 @@ class StringSerializeHandler extends BaseSerializeHandler { } protected _beforeSerialize(rows: number, start: number, _end: number): void { - this._allRows = new Array(rows) - this._allRowSeparators = new Array(rows) + this._allRows = Array.from({ length: rows }) + this._allRowSeparators = Array.from({ length: rows }) this._rowIndex = 0 this._currentRow = "" diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index e65b575ac5..7acfdfc374 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -8,7 +8,7 @@ import { Spinner } from "@opencode-ai/ui/spinner" import { showToast } from "@opencode-ai/ui/toast" import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" import { getFilename } from "@opencode-ai/shared/util/path" -import { createEffect, createMemo, For, onCleanup, Show } from "solid-js" +import { createEffect, createMemo, For, Show } from "solid-js" import { createStore } from "solid-js/store" import { Portal } from "solid-js/web" import { useCommand } from "@/context/command" diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx index 0a41f31196..a90178abdd 100644 --- a/packages/app/src/components/titlebar.tsx +++ b/packages/app/src/components/titlebar.tsx @@ -1,4 +1,4 @@ -import { createEffect, createMemo, onCleanup, Show, untrack } from "solid-js" +import { createEffect, createMemo, Show, untrack } from "solid-js" import { createStore } from "solid-js/store" import { useLocation, useNavigate, useParams } from "@solidjs/router" import { IconButton } from "@opencode-ai/ui/icon-button" diff --git a/packages/app/src/context/global-sync/queue.ts b/packages/app/src/context/global-sync/queue.ts index c3468583b9..5c228dac04 100644 --- a/packages/app/src/context/global-sync/queue.ts +++ b/packages/app/src/context/global-sync/queue.ts @@ -63,6 +63,7 @@ export function createRefreshQueue(input: QueueInput) { } } finally { running = false + // oxlint-disable-next-line no-unsafe-finally -- intentional: early return skips schedule() when paused if (input.paused()) return if (root || queued.size) schedule() } diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 62d5cba615..3ba2659a3b 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -704,7 +704,7 @@ export default function Layout(props: ParentProps) { createEffect(() => { const active = new Set(visibleSessionDirs()) - for (const directory of [...prefetchedByDir.keys()]) { + for (const directory of prefetchedByDir.keys()) { if (active.has(directory)) continue prefetchedByDir.delete(directory) } diff --git a/packages/app/src/pages/session/review-tab.tsx b/packages/app/src/pages/session/review-tab.tsx index 71dfe375e0..5719fce318 100644 --- a/packages/app/src/pages/session/review-tab.tsx +++ b/packages/app/src/pages/session/review-tab.tsx @@ -1,4 +1,4 @@ -import { createEffect, createSignal, onCleanup, type JSX } from "solid-js" +import { createEffect, onCleanup, type JSX } from "solid-js" import { makeEventListener } from "@solid-primitives/event-listener" import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" import { SessionReview } from "@opencode-ai/ui/session-review" diff --git a/packages/console/app/src/component/email-signup.tsx b/packages/console/app/src/component/email-signup.tsx index bd33e92006..caedaf0f2e 100644 --- a/packages/console/app/src/component/email-signup.tsx +++ b/packages/console/app/src/component/email-signup.tsx @@ -1,5 +1,4 @@ import { action, useSubmission } from "@solidjs/router" -import dock from "../asset/lander/dock.png" import { Resource } from "@opencode-ai/console-resource" import { Show } from "solid-js" import { useI18n } from "~/context/i18n" diff --git a/packages/console/app/src/component/header.tsx b/packages/console/app/src/component/header.tsx index 1e129d5908..cc45ed534f 100644 --- a/packages/console/app/src/component/header.tsx +++ b/packages/console/app/src/component/header.tsx @@ -47,7 +47,7 @@ export function Header(props: { zen?: boolean; go?: boolean; hideGetStarted?: bo notation: "compact", compactDisplay: "short", maximumFractionDigits: 0, - }).format(githubData()?.stars!) + }).format(githubData()?.stars) : config.github.starsFormatted.compact, ) diff --git a/packages/console/app/src/context/auth.session.ts b/packages/console/app/src/context/auth.session.ts index e69de29bb2..336ce12bb9 100644 --- a/packages/console/app/src/context/auth.session.ts +++ b/packages/console/app/src/context/auth.session.ts @@ -0,0 +1 @@ +export {} diff --git a/packages/console/app/src/routes/bench/[id].tsx b/packages/console/app/src/routes/bench/[id].tsx index dd96bcbbce..c6d10826b3 100644 --- a/packages/console/app/src/routes/bench/[id].tsx +++ b/packages/console/app/src/routes/bench/[id].tsx @@ -1,7 +1,7 @@ import { Title } from "@solidjs/meta" import { createAsync, query, useParams } from "@solidjs/router" import { createSignal, For, Show } from "solid-js" -import { Database, desc, eq } from "@opencode-ai/console-core/drizzle/index.js" +import { Database, eq } from "@opencode-ai/console-core/drizzle/index.js" import { BenchmarkTable } from "@opencode-ai/console-core/schema/benchmark.sql.js" import { useI18n } from "~/context/i18n" diff --git a/packages/console/app/src/routes/go/index.tsx b/packages/console/app/src/routes/go/index.tsx index 0ac85a9570..82b3caf664 100644 --- a/packages/console/app/src/routes/go/index.tsx +++ b/packages/console/app/src/routes/go/index.tsx @@ -1,5 +1,5 @@ import "./index.css" -import { createAsync, query, redirect } from "@solidjs/router" +import { createAsync, query } from "@solidjs/router" import { Title, Meta } from "@solidjs/meta" import { For, createMemo, createSignal, onCleanup, onMount } from "solid-js" //import { HttpHeader } from "@solidjs/start" diff --git a/packages/console/app/src/routes/workspace-picker.tsx b/packages/console/app/src/routes/workspace-picker.tsx index ffec2f3bee..8778abefd1 100644 --- a/packages/console/app/src/routes/workspace-picker.tsx +++ b/packages/console/app/src/routes/workspace-picker.tsx @@ -1,5 +1,5 @@ import { query, useParams, action, createAsync, redirect, useSubmission } from "@solidjs/router" -import { For, Show, createEffect } from "solid-js" +import { For, createEffect } from "solid-js" import { createStore } from "solid-js/store" import { withActor } from "~/context/auth.withActor" import { Actor } from "@opencode-ai/console-core/actor.js" diff --git a/packages/console/app/src/routes/zen/index.tsx b/packages/console/app/src/routes/zen/index.tsx index 62e8f5d379..6285a0bd8a 100644 --- a/packages/console/app/src/routes/zen/index.tsx +++ b/packages/console/app/src/routes/zen/index.tsx @@ -1,5 +1,5 @@ import "./index.css" -import { createAsync, query, redirect } from "@solidjs/router" +import { createAsync, query } from "@solidjs/router" import { Title, Meta } from "@solidjs/meta" //import { HttpHeader } from "@solidjs/start" import zenLogoLight from "../../asset/zen-ornate-light.svg" diff --git a/packages/console/app/src/routes/zen/util/handler.ts b/packages/console/app/src/routes/zen/util/handler.ts index 358d8736c4..d1c5985a81 100644 --- a/packages/console/app/src/routes/zen/util/handler.ts +++ b/packages/console/app/src/routes/zen/util/handler.ts @@ -345,7 +345,7 @@ export async function handler( logger.metric({ "error.cause2": JSON.stringify(error.cause), }) - } catch (e) {} + } catch {} } // Note: both top level "type" and "error.type" fields are used by the @ai-sdk/anthropic client to render the error message. diff --git a/packages/console/app/src/routes/zen/util/provider/anthropic.ts b/packages/console/app/src/routes/zen/util/provider/anthropic.ts index b63be8688a..0f6f11da78 100644 --- a/packages/console/app/src/routes/zen/util/provider/anthropic.ts +++ b/packages/console/app/src/routes/zen/util/provider/anthropic.ts @@ -153,7 +153,7 @@ export const anthropicHelper: ProviderHelper = ({ reqModel, providerModel }) => let json try { json = JSON.parse(data.slice(6)) - } catch (e) { + } catch { return } diff --git a/packages/console/app/src/routes/zen/util/provider/google.ts b/packages/console/app/src/routes/zen/util/provider/google.ts index f6f7d6e19b..ef7937c358 100644 --- a/packages/console/app/src/routes/zen/util/provider/google.ts +++ b/packages/console/app/src/routes/zen/util/provider/google.ts @@ -48,7 +48,7 @@ export const googleHelper: ProviderHelper = ({ providerModel }) => ({ let json try { json = JSON.parse(chunk.slice(6)) as { usageMetadata?: Usage } - } catch (e) { + } catch { return } diff --git a/packages/console/app/src/routes/zen/util/provider/openai-compatible.ts b/packages/console/app/src/routes/zen/util/provider/openai-compatible.ts index cf9ee287c4..e05f0d6c0b 100644 --- a/packages/console/app/src/routes/zen/util/provider/openai-compatible.ts +++ b/packages/console/app/src/routes/zen/util/provider/openai-compatible.ts @@ -49,7 +49,7 @@ export const oaCompatHelper: ProviderHelper = ({ adjustCacheUsage, safetyIdentif let json try { json = JSON.parse(chunk.slice(6)) as { usage?: Usage } - } catch (e) { + } catch { return } @@ -289,7 +289,7 @@ export function fromOaCompatibleResponse(resp: any): CommonResponse { index: 0, message: { role: "assistant" as const, - ...(content.length > 0 && content.some((c) => c.type === "text") + ...(content.some((c) => c.type === "text") ? { content: content .filter((c) => c.type === "text") @@ -297,7 +297,7 @@ export function fromOaCompatibleResponse(resp: any): CommonResponse { .join(""), } : {}), - ...(content.length > 0 && content.some((c) => c.type === "tool_use") + ...(content.some((c) => c.type === "tool_use") ? { tool_calls: content .filter((c) => c.type === "tool_use") diff --git a/packages/console/app/src/routes/zen/util/provider/openai.ts b/packages/console/app/src/routes/zen/util/provider/openai.ts index 3c5831a9af..bee1e01ec0 100644 --- a/packages/console/app/src/routes/zen/util/provider/openai.ts +++ b/packages/console/app/src/routes/zen/util/provider/openai.ts @@ -36,7 +36,7 @@ export const openaiHelper: ProviderHelper = ({ workspaceID }) => ({ let json try { json = JSON.parse(data.slice(6)) as { response?: { usage?: Usage } } - } catch (e) { + } catch { return } diff --git a/packages/console/core/script/black-cancel-waitlist.ts b/packages/console/core/script/black-cancel-waitlist.ts index ab2aa16d5d..7c3584e009 100644 --- a/packages/console/core/script/black-cancel-waitlist.ts +++ b/packages/console/core/script/black-cancel-waitlist.ts @@ -1,7 +1,5 @@ -import { subscribe } from "diagnostics_channel" -import { Billing } from "../src/billing.js" -import { and, Database, eq } from "../src/drizzle/index.js" -import { BillingTable, PaymentTable, SubscriptionTable } from "../src/schema/billing.sql.js" +import { Database, eq } from "../src/drizzle/index.js" +import { BillingTable } from "../src/schema/billing.sql.js" const workspaceID = process.argv[2] diff --git a/packages/console/core/script/black-gift.ts b/packages/console/core/script/black-gift.ts index c666a1ab66..e57ec9775f 100644 --- a/packages/console/core/script/black-gift.ts +++ b/packages/console/core/script/black-gift.ts @@ -1,12 +1,10 @@ import { Billing } from "../src/billing.js" -import { and, Database, eq, isNull, sql } from "../src/drizzle/index.js" +import { and, Database, eq, isNull } from "../src/drizzle/index.js" import { UserTable } from "../src/schema/user.sql.js" -import { BillingTable, PaymentTable, SubscriptionTable } from "../src/schema/billing.sql.js" +import { BillingTable, SubscriptionTable } from "../src/schema/billing.sql.js" import { Identifier } from "../src/identifier.js" -import { centsToMicroCents } from "../src/util/price.js" import { AuthTable } from "../src/schema/auth.sql.js" import { BlackData } from "../src/black.js" -import { Actor } from "../src/actor.js" const plan = "200" const couponID = "JAIr0Pe1" diff --git a/packages/console/core/script/black-onboard-waitlist.ts b/packages/console/core/script/black-onboard-waitlist.ts index 96d0f8f912..9e7d9e935d 100644 --- a/packages/console/core/script/black-onboard-waitlist.ts +++ b/packages/console/core/script/black-onboard-waitlist.ts @@ -1,7 +1,5 @@ -import { subscribe } from "diagnostics_channel" -import { Billing } from "../src/billing.js" -import { and, Database, eq } from "../src/drizzle/index.js" -import { BillingTable, PaymentTable, SubscriptionTable } from "../src/schema/billing.sql.js" +import { Database, eq } from "../src/drizzle/index.js" +import { BillingTable } from "../src/schema/billing.sql.js" const workspaceID = process.argv[2] diff --git a/packages/console/core/script/black-select-workspaces.ts b/packages/console/core/script/black-select-workspaces.ts index 63bfab8875..0772bd2129 100644 --- a/packages/console/core/script/black-select-workspaces.ts +++ b/packages/console/core/script/black-select-workspaces.ts @@ -1,4 +1,4 @@ -import { Database, eq, and, sql, inArray, isNull, count } from "../src/drizzle/index.js" +import { Database, eq, and, sql, inArray, isNull } from "../src/drizzle/index.js" import { BillingTable, BlackPlans } from "../src/schema/billing.sql.js" import { UserTable } from "../src/schema/user.sql.js" import { AuthTable } from "../src/schema/auth.sql.js" diff --git a/packages/console/core/src/util/env.cloudflare.ts b/packages/console/core/src/util/env.cloudflare.ts index e69de29bb2..336ce12bb9 100644 --- a/packages/console/core/src/util/env.cloudflare.ts +++ b/packages/console/core/src/util/env.cloudflare.ts @@ -0,0 +1 @@ +export {} diff --git a/packages/console/core/src/util/log.ts b/packages/console/core/src/util/log.ts index 4f2d25c136..ef3ad85c6b 100644 --- a/packages/console/core/src/util/log.ts +++ b/packages/console/core/src/util/log.ts @@ -48,7 +48,7 @@ export namespace Log { function use() { try { return ctx.use() - } catch (e) { + } catch { return { tags: {} } } } diff --git a/packages/enterprise/test/core/share.test.ts b/packages/enterprise/test/core/share.test.ts index 34f3b17a3f..2877f8e0e0 100644 --- a/packages/enterprise/test/core/share.test.ts +++ b/packages/enterprise/test/core/share.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test, afterAll } from "bun:test" +import { describe, expect, test } from "bun:test" import { Share } from "../../src/core/share" import { Storage } from "../../src/core/storage" import { Identifier } from "@opencode-ai/shared/util/identifier" diff --git a/packages/opencode/script/publish.ts b/packages/opencode/script/publish.ts index fbc1c83ba6..9c4b8f187d 100755 --- a/packages/opencode/script/publish.ts +++ b/packages/opencode/script/publish.ts @@ -107,7 +107,7 @@ if (!Script.preview) { await $`cd ./dist/aur-${pkg} && git commit -m "Update to v${Script.version}"` await $`cd ./dist/aur-${pkg} && git push` break - } catch (e) { + } catch { continue } } diff --git a/packages/opencode/src/cli/cmd/debug/lsp.ts b/packages/opencode/src/cli/cmd/debug/lsp.ts index 5f0a1807d8..18f67b3917 100644 --- a/packages/opencode/src/cli/cmd/debug/lsp.ts +++ b/packages/opencode/src/cli/cmd/debug/lsp.ts @@ -5,7 +5,6 @@ import { bootstrap } from "../../bootstrap" import { cmd } from "../cmd" import { Log } from "../../../util/log" import { EOL } from "os" -import { setTimeout as sleep } from "node:timers/promises" export const LSPCommand = cmd({ command: "lsp", diff --git a/packages/opencode/src/cli/cmd/export.ts b/packages/opencode/src/cli/cmd/export.ts index 9a1a51adc4..06b361c6d5 100644 --- a/packages/opencode/src/cli/cmd/export.ts +++ b/packages/opencode/src/cli/cmd/export.ts @@ -297,7 +297,7 @@ export const ExportCommand = cmd({ process.stdout.write(JSON.stringify(args.sanitize ? sanitize(exportData) : exportData, null, 2)) process.stdout.write(EOL) - } catch (error) { + } catch { UI.error(`Session not found: ${sessionID!}`) process.exit(1) } diff --git a/packages/opencode/src/cli/cmd/github.ts b/packages/opencode/src/cli/cmd/github.ts index 074d9e5185..b6781d0852 100644 --- a/packages/opencode/src/cli/cmd/github.ts +++ b/packages/opencode/src/cli/cmd/github.ts @@ -362,7 +362,7 @@ export const GithubInstallCommand = cmd({ retries++ await sleep(1000) - } while (true) + } while (true) // oxlint-disable-line no-constant-condition s.stop("Installed GitHub app") @@ -931,7 +931,7 @@ export const GithubRunCommand = cmd({ async function summarize(response: string) { try { return await chat(`Summarize the following in less than 40 characters:\n\n${response}`) - } catch (e) { + } catch { const title = issueEvent ? issueEvent.issue.title : (payload as PullRequestReviewCommentEvent).pull_request.title diff --git a/packages/opencode/src/cli/cmd/serve.ts b/packages/opencode/src/cli/cmd/serve.ts index 73e7a18a70..d5eee75dd1 100644 --- a/packages/opencode/src/cli/cmd/serve.ts +++ b/packages/opencode/src/cli/cmd/serve.ts @@ -2,9 +2,6 @@ import { Server } from "../../server/server" import { cmd } from "./cmd" import { withNetworkOptions, resolveNetworkOptions } from "../network" import { Flag } from "../../flag/flag" -import { Workspace } from "../../control-plane/workspace" -import { Project } from "../../project/project" -import { Installation } from "../../installation" export const ServeCommand = cmd({ command: "serve", diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index acf007197b..4c6c74ff3d 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -23,7 +23,7 @@ import { DialogProvider, useDialog } from "@tui/ui/dialog" import { DialogProvider as DialogProviderList } from "@tui/component/dialog-provider" import { ErrorComponent } from "@tui/component/error-component" import { PluginRouteMissing } from "@tui/component/plugin-route-missing" -import { ProjectProvider, useProject } from "@tui/context/project" +import { ProjectProvider } from "@tui/context/project" import { useEvent } from "@tui/context/event" import { SDKProvider, useSDK } from "@tui/context/sdk" import { StartupLoading } from "@tui/component/startup-loading" @@ -115,6 +115,7 @@ export function tui(input: { events?: EventSource }) { // promise to prevent immediate exit + // oxlint-disable-next-line no-async-promise-executor -- intentional: async executor used for sequential setup before resolve return new Promise(async (resolve) => { const unguard = win32InstallCtrlCGuard() win32DisableProcessedInput() diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-theme-list.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-theme-list.tsx index f4072c9785..6cf3539ad9 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-theme-list.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-theme-list.tsx @@ -1,7 +1,7 @@ import { DialogSelect, type DialogSelectRef } from "../ui/dialog-select" import { useTheme } from "../context/theme" import { useDialog } from "../ui/dialog" -import { onCleanup, onMount } from "solid-js" +import { onCleanup } from "solid-js" export function DialogThemeList() { const theme = useTheme() diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index d0f5b481cb..87440d0e24 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -1,4 +1,4 @@ -import { BoxRenderable, TextareaRenderable, MouseEvent, PasteEvent, decodePasteBytes, t, dim, fg } from "@opentui/core" +import { BoxRenderable, TextareaRenderable, MouseEvent, PasteEvent, decodePasteBytes } from "@opentui/core" import { createEffect, createMemo, onMount, createSignal, onCleanup, on, Show, Switch, Match } from "solid-js" import "opentui-spinner/solid" import path from "path" diff --git a/packages/opencode/src/cli/cmd/tui/event.ts b/packages/opencode/src/cli/cmd/tui/event.ts index b2e4b92c55..fa164d53e8 100644 --- a/packages/opencode/src/cli/cmd/tui/event.ts +++ b/packages/opencode/src/cli/cmd/tui/event.ts @@ -1,5 +1,4 @@ import { BusEvent } from "@/bus/bus-event" -import { Bus } from "@/bus" import { SessionID } from "@/session/schema" import z from "zod" diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 2b95cd5ae4..f9fd5a9b9c 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -863,7 +863,7 @@ export function Session() { ) await Clipboard.copy(transcript) toast.show({ message: "Session transcript copied to clipboard!", variant: "success" }) - } catch (error) { + } catch { toast.show({ message: "Failed to copy session transcript", variant: "error" }) } dialog.clear() @@ -925,7 +925,7 @@ export function Session() { toast.show({ message: `Session exported to ${filename}`, variant: "success" }) } - } catch (error) { + } catch { toast.show({ message: "Failed to export session", variant: "error" }) } dialog.clear() @@ -1010,7 +1010,7 @@ export function Session() { ), } }) - } catch (error) { + } catch { return [] } }) diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx index 109b5f2f11..b6c937f411 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx @@ -1,6 +1,6 @@ import { InputRenderable, RGBA, ScrollBoxRenderable, TextAttributes } from "@opentui/core" import { useTheme, selectedForeground } from "@tui/context/theme" -import { entries, filter, flatMap, groupBy, pipe, take } from "remeda" +import { entries, filter, flatMap, groupBy, pipe } from "remeda" import { batch, createEffect, createMemo, For, Show, type JSX, on } from "solid-js" import { createStore } from "solid-js/store" import { useKeyboard, useTerminalDimensions } from "@opentui/solid" diff --git a/packages/opencode/src/control-plane/workspace-context.ts b/packages/opencode/src/control-plane/workspace-context.ts index 541657b88c..273adbb24a 100644 --- a/packages/opencode/src/control-plane/workspace-context.ts +++ b/packages/opencode/src/control-plane/workspace-context.ts @@ -19,7 +19,7 @@ export const WorkspaceContext = { get workspaceID() { try { return context.use().workspaceID - } catch (err) { + } catch { return undefined } }, diff --git a/packages/opencode/src/file/ignore.ts b/packages/opencode/src/file/ignore.ts index a102e7d170..63f2f594eb 100644 --- a/packages/opencode/src/file/ignore.ts +++ b/packages/opencode/src/file/ignore.ts @@ -1,4 +1,3 @@ -import { sep } from "node:path" import { Glob } from "@opencode-ai/shared/util/glob" export namespace FileIgnore { diff --git a/packages/opencode/src/file/watcher.ts b/packages/opencode/src/file/watcher.ts index 74966fd47a..4dcec5094c 100644 --- a/packages/opencode/src/file/watcher.ts +++ b/packages/opencode/src/file/watcher.ts @@ -1,4 +1,4 @@ -import { Cause, Effect, Layer, Scope, Context } from "effect" +import { Cause, Effect, Layer, Context } from "effect" // @ts-ignore import { createWrapper } from "@parcel/watcher/wrapper" import type ParcelWatcher from "@parcel/watcher" diff --git a/packages/opencode/src/format/index.ts b/packages/opencode/src/format/index.ts index 595bb7a608..d65ed2944e 100644 --- a/packages/opencode/src/format/index.ts +++ b/packages/opencode/src/format/index.ts @@ -6,7 +6,6 @@ import path from "path" import { mergeDeep } from "remeda" import z from "zod" import { Config } from "../config" -import { Instance } from "../project/instance" import { Log } from "../util/log" import * as Formatter from "./formatter" diff --git a/packages/opencode/src/global/index.ts b/packages/opencode/src/global/index.ts index 32d5153213..df46397816 100644 --- a/packages/opencode/src/global/index.ts +++ b/packages/opencode/src/global/index.ts @@ -53,6 +53,6 @@ if (version !== CACHE_VERSION) { }), ), ) - } catch (e) {} + } catch {} await Filesystem.write(path.join(Global.Path.cache, "version"), CACHE_VERSION) } diff --git a/packages/opencode/src/ide/index.ts b/packages/opencode/src/ide/index.ts index 46efea2cce..24ba53f82e 100644 --- a/packages/opencode/src/ide/index.ts +++ b/packages/opencode/src/ide/index.ts @@ -1,5 +1,4 @@ import { BusEvent } from "@/bus/bus-event" -import { Bus } from "@/bus" import z from "zod" import { NamedError } from "@opencode-ai/shared/util/error" import { Log } from "../util/log" diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index 9ffef7a425..f4554ae3e6 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -826,7 +826,7 @@ export namespace LSPServer { if (cargoTomlContent.includes("[workspace]")) { return currentDir } - } catch (err) { + } catch { // File doesn't exist or can't be read, continue searching up } diff --git a/packages/opencode/src/patch/index.ts b/packages/opencode/src/patch/index.ts index b87ad55528..f003606c4d 100644 --- a/packages/opencode/src/patch/index.ts +++ b/packages/opencode/src/patch/index.ts @@ -630,7 +630,7 @@ export namespace Patch { type: "delete", content, }) - } catch (error) { + } catch { return { type: MaybeApplyPatchVerified.CorrectnessError, error: new Error(`Failed to read file for deletion: ${deletePath}`), diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts index 71d321080a..0100485492 100644 --- a/packages/opencode/src/permission/index.ts +++ b/packages/opencode/src/permission/index.ts @@ -3,7 +3,6 @@ import { BusEvent } from "@/bus/bus-event" import { Config } from "@/config" import { InstanceState } from "@/effect/instance-state" import { ProjectID } from "@/project/schema" -import { Instance } from "@/project/instance" import { MessageID, SessionID } from "@/session/schema" import { PermissionTable } from "@/session/session.sql" import { Database, eq } from "@/storage/db" diff --git a/packages/opencode/src/plugin/codex.ts b/packages/opencode/src/plugin/codex.ts index 1e127fae54..ea356d55d2 100644 --- a/packages/opencode/src/plugin/codex.ts +++ b/packages/opencode/src/plugin/codex.ts @@ -1,10 +1,8 @@ import type { Hooks, PluginInput } from "@opencode-ai/plugin" import { Log } from "../util/log" import { Installation } from "../installation" -import { Auth, OAUTH_DUMMY_KEY } from "../auth" +import { OAUTH_DUMMY_KEY } from "../auth" import os from "os" -import { ProviderTransform } from "@/provider/transform" -import { ModelID, ProviderID } from "@/provider/schema" import { setTimeout as sleep } from "node:timers/promises" import { createServer } from "http" diff --git a/packages/opencode/src/provider/sdk/copilot/responses/openai-responses-language-model.ts b/packages/opencode/src/provider/sdk/copilot/responses/openai-responses-language-model.ts index 4606af7a15..92c8fd857b 100644 --- a/packages/opencode/src/provider/sdk/copilot/responses/openai-responses-language-model.ts +++ b/packages/opencode/src/provider/sdk/copilot/responses/openai-responses-language-model.ts @@ -793,6 +793,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 { fetch: this.config.fetch, }) + // oxlint-disable-next-line no-this-alias -- needed for closure scope inside generator const self = this let finishReason: { diff --git a/packages/opencode/src/server/instance/config.ts b/packages/opencode/src/server/instance/config.ts index 11845c69c9..68a6b50764 100644 --- a/packages/opencode/src/server/instance/config.ts +++ b/packages/opencode/src/server/instance/config.ts @@ -7,7 +7,6 @@ import { mapValues } from "remeda" import { errors } from "../error" import { lazy } from "../../util/lazy" import { AppRuntime } from "../../effect/app-runtime" -import { Effect } from "effect" import { jsonRequest } from "./trace" export const ConfigRoutes = lazy(() => diff --git a/packages/opencode/src/server/instance/event.ts b/packages/opencode/src/server/instance/event.ts index 5d631d954e..f13ed035e0 100644 --- a/packages/opencode/src/server/instance/event.ts +++ b/packages/opencode/src/server/instance/event.ts @@ -4,7 +4,6 @@ import { describeRoute, resolver } from "hono-openapi" import { streamSSE } from "hono/streaming" import { Log } from "@/util/log" import { BusEvent } from "@/bus/bus-event" -import { SyncEvent } from "@/sync" import { Bus } from "@/bus" import { AsyncQueue } from "../../util/queue" diff --git a/packages/opencode/src/server/instance/pty.ts b/packages/opencode/src/server/instance/pty.ts index 576cbe5de6..3cb8dbfe2e 100644 --- a/packages/opencode/src/server/instance/pty.ts +++ b/packages/opencode/src/server/instance/pty.ts @@ -1,4 +1,4 @@ -import { Hono, type MiddlewareHandler } from "hono" +import { Hono } from "hono" import { describeRoute, validator, resolver } from "hono-openapi" import type { UpgradeWebSocket } from "hono/ws" import { Effect } from "effect" diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index 810b949743..03f9723112 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -2,7 +2,6 @@ import { BusEvent } from "@/bus/bus-event" import { Bus } from "@/bus" import { Session } from "." import { SessionID, MessageID, PartID } from "./schema" -import { Instance } from "../project/instance" import { Provider } from "../provider/provider" import { MessageV2 } from "./message-v2" import z from "zod" diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index d91b1427b0..0f8cd41b30 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -249,6 +249,7 @@ export namespace SessionProcessor { case "reasoning-end": if (!(value.id in ctx.reasoningMap)) return + // oxlint-disable-next-line no-self-assign -- reactivity trigger ctx.reasoningMap[value.id].text = ctx.reasoningMap[value.id].text ctx.reasoningMap[value.id].time = { ...ctx.reasoningMap[value.id].time, end: Date.now() } if (value.providerMetadata) ctx.reasoningMap[value.id].metadata = value.providerMetadata @@ -431,6 +432,7 @@ export namespace SessionProcessor { case "text-end": if (!ctx.currentText) return + // oxlint-disable-next-line no-self-assign -- reactivity trigger ctx.currentText.text = ctx.currentText.text ctx.currentText.text = (yield* plugin.trigger( "experimental.text.complete", diff --git a/packages/opencode/src/session/projectors.ts b/packages/opencode/src/session/projectors.ts index 460f0a41c5..a1b2e401d0 100644 --- a/packages/opencode/src/session/projectors.ts +++ b/packages/opencode/src/session/projectors.ts @@ -1,11 +1,9 @@ -import { NotFoundError, eq, and, sql } from "../storage/db" +import { NotFoundError, eq, and } from "../storage/db" import { SyncEvent } from "@/sync" import { Session } from "./index" import { MessageV2 } from "./message-v2" -import { SessionTable, MessageTable, PartTable, SessionEntryTable } from "./session.sql" +import { SessionTable, MessageTable, PartTable } from "./session.sql" import { Log } from "../util/log" -import { DateTime } from "effect" -import { SessionEntry } from "@/v2/session-entry" const log = Log.create({ service: "session.projector" }) diff --git a/packages/opencode/src/session/revert.ts b/packages/opencode/src/session/revert.ts index a4a7a27d6d..7a7f847ad1 100644 --- a/packages/opencode/src/session/revert.ts +++ b/packages/opencode/src/session/revert.ts @@ -10,7 +10,6 @@ import { MessageV2 } from "./message-v2" import { SessionID, MessageID, PartID } from "./schema" import { SessionRunState } from "./run-state" import { SessionSummary } from "./summary" -import { SessionStatus } from "./status" export namespace SessionRevert { const log = Log.create({ service: "session.revert" }) diff --git a/packages/opencode/src/storage/json-migration.ts b/packages/opencode/src/storage/json-migration.ts index 89d27b9a7b..c13a005ca6 100644 --- a/packages/opencode/src/storage/json-migration.ts +++ b/packages/opencode/src/storage/json-migration.ts @@ -77,11 +77,13 @@ export namespace JsonMigration { async function read(files: string[], start: number, end: number) { const count = end - start + // oxlint-disable-next-line unicorn/no-new-array -- pre-allocated for index-based batch fill const tasks = new Array(count) for (let i = 0; i < count; i++) { tasks[i] = Filesystem.readJson(files[start + i]) } const results = await Promise.allSettled(tasks) + // oxlint-disable-next-line unicorn/no-new-array -- pre-allocated for index-based batch fill const items = new Array(count) for (let i = 0; i < results.length; i++) { const result = results[i] @@ -243,6 +245,7 @@ export namespace JsonMigration { for (let i = 0; i < allMessageFiles.length; i += batchSize) { const end = Math.min(i + batchSize, allMessageFiles.length) const batch = await read(allMessageFiles, i, end) + // oxlint-disable-next-line unicorn/no-new-array -- pre-allocated for index-based batch fill const values = new Array(batch.length) let count = 0 for (let j = 0; j < batch.length; j++) { @@ -273,6 +276,7 @@ export namespace JsonMigration { for (let i = 0; i < partFiles.length; i += batchSize) { const end = Math.min(i + batchSize, partFiles.length) const batch = await read(partFiles, i, end) + // oxlint-disable-next-line unicorn/no-new-array -- pre-allocated for index-based batch fill const values = new Array(batch.length) let count = 0 for (let j = 0; j < batch.length; j++) { diff --git a/packages/opencode/src/sync/index.ts b/packages/opencode/src/sync/index.ts index ce598dae67..e89d57e181 100644 --- a/packages/opencode/src/sync/index.ts +++ b/packages/opencode/src/sync/index.ts @@ -1,6 +1,5 @@ import z from "zod" import type { ZodObject } from "zod" -import { EventEmitter } from "events" import { Database, eq } from "@/storage/db" import { GlobalBus } from "@/bus/global" import { Bus as ProjectBus } from "@/bus" diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index 5c82463945..2303618a0b 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -416,7 +416,7 @@ export const WhitespaceNormalizedReplacer: Replacer = function* (content, find) if (match) { yield match[0] } - } catch (e) { + } catch { // Invalid regex pattern, skip } } diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index bbb07caa40..8f7104e80d 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -8,7 +8,6 @@ import { Agent } from "../agent/agent" import type { SessionPrompt } from "../session/prompt" import { Config } from "../config" import { Effect } from "effect" -import { Log } from "@/util/log" export interface TaskPromptOps { cancel(sessionID: SessionID): void diff --git a/packages/opencode/src/tool/webfetch.ts b/packages/opencode/src/tool/webfetch.ts index 9339038b0f..14d5465846 100644 --- a/packages/opencode/src/tool/webfetch.ts +++ b/packages/opencode/src/tool/webfetch.ts @@ -1,6 +1,6 @@ import z from "zod" import { Effect } from "effect" -import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { HttpClient, HttpClientRequest } from "effect/unstable/http" import { Tool } from "./tool" import TurndownService from "turndown" import DESCRIPTION from "./webfetch.txt" diff --git a/packages/opencode/src/util/lazy.ts b/packages/opencode/src/util/lazy.ts index 55643dc6a7..86967e11a0 100644 --- a/packages/opencode/src/util/lazy.ts +++ b/packages/opencode/src/util/lazy.ts @@ -4,14 +4,9 @@ export function lazy(fn: () => T) { const result = (): T => { if (loaded) return value as T - try { - value = fn() - loaded = true - return value as T - } catch (e) { - // Don't mark as loaded if initialization failed - throw e - } + value = fn() + loaded = true + return value as T } result.reset = () => { diff --git a/packages/opencode/src/v2/session.ts b/packages/opencode/src/v2/session.ts index b7191a4c9b..97df0a2207 100644 --- a/packages/opencode/src/v2/session.ts +++ b/packages/opencode/src/v2/session.ts @@ -1,8 +1,6 @@ import { Context, Layer, Schema, Effect } from "effect" import { SessionEntry } from "./session-entry" import { Struct } from "effect" -import { Identifier } from "@/id/id" -import { withStatics } from "@/util/schema" import { Session } from "@/session" import { SessionID } from "@/session/schema" diff --git a/packages/opencode/test/cli/tui/plugin-lifecycle.test.ts b/packages/opencode/test/cli/tui/plugin-lifecycle.test.ts index 9c868a4c99..b22180ef31 100644 --- a/packages/opencode/test/cli/tui/plugin-lifecycle.test.ts +++ b/packages/opencode/test/cli/tui/plugin-lifecycle.test.ts @@ -5,7 +5,6 @@ import { pathToFileURL } from "url" import { tmpdir } from "../../fixture/fixture" import { createTuiPluginApi } from "../../fixture/tui-plugin" import { mockTuiRuntime } from "../../fixture/tui-runtime" -import { TuiConfig } from "../../../src/config/tui" const { TuiPluginRuntime } = await import("../../../src/cli/cmd/tui/plugin/runtime") diff --git a/packages/opencode/test/effect/cross-spawn-spawner.test.ts b/packages/opencode/test/effect/cross-spawn-spawner.test.ts index 2cc5092029..5990635aa2 100644 --- a/packages/opencode/test/effect/cross-spawn-spawner.test.ts +++ b/packages/opencode/test/effect/cross-spawn-spawner.test.ts @@ -1,8 +1,7 @@ -import { NodeFileSystem, NodePath } from "@effect/platform-node" import { describe, expect } from "bun:test" import fs from "node:fs/promises" import path from "node:path" -import { Effect, Exit, Layer, Stream } from "effect" +import { Effect, Exit, Stream } from "effect" import type * as PlatformError from "effect/PlatformError" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" diff --git a/packages/opencode/test/effect/instance-state.test.ts b/packages/opencode/test/effect/instance-state.test.ts index 813ca344a9..ca74c915be 100644 --- a/packages/opencode/test/effect/instance-state.test.ts +++ b/packages/opencode/test/effect/instance-state.test.ts @@ -1,5 +1,5 @@ import { afterEach, expect, test } from "bun:test" -import { Cause, Deferred, Duration, Effect, Exit, Fiber, Layer, ManagedRuntime, Context } from "effect" +import { Deferred, Duration, Effect, Exit, Fiber, Layer, ManagedRuntime, Context } from "effect" import { InstanceState } from "../../src/effect/instance-state" import { InstanceRef } from "../../src/effect/instance-ref" import { Instance } from "../../src/project/instance" diff --git a/packages/opencode/test/permission/next.test.ts b/packages/opencode/test/permission/next.test.ts index 9e3007f6dc..805c230f3e 100644 --- a/packages/opencode/test/permission/next.test.ts +++ b/packages/opencode/test/permission/next.test.ts @@ -6,7 +6,7 @@ import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" import { Permission } from "../../src/permission" import { PermissionID } from "../../src/permission/schema" import { Instance } from "../../src/project/instance" -import { provideInstance, provideTmpdirInstance, tmpdir, tmpdirScoped } from "../fixture/fixture" +import { provideInstance, provideTmpdirInstance, tmpdirScoped } from "../fixture/fixture" import { testEffect } from "../lib/effect" import { MessageID, SessionID } from "../../src/session/schema" diff --git a/packages/opencode/test/provider/gitlab-duo.test.ts b/packages/opencode/test/provider/gitlab-duo.test.ts index 9b5441fe22..a80ecf5aee 100644 --- a/packages/opencode/test/provider/gitlab-duo.test.ts +++ b/packages/opencode/test/provider/gitlab-duo.test.ts @@ -1,3 +1,4 @@ +export {} // TODO: UNCOMMENT WHEN GITLAB SUPPORT IS COMPLETED // // diff --git a/packages/opencode/test/server/project-init-git.test.ts b/packages/opencode/test/server/project-init-git.test.ts index 25d7066434..406b3d6d89 100644 --- a/packages/opencode/test/server/project-init-git.test.ts +++ b/packages/opencode/test/server/project-init-git.test.ts @@ -3,7 +3,6 @@ import { Effect } from "effect" import path from "path" import { GlobalBus } from "../../src/bus/global" import { Snapshot } from "../../src/snapshot" -import { InstanceBootstrap } from "../../src/project/bootstrap" import { Instance } from "../../src/project/instance" import { Server } from "../../src/server/server" import { Filesystem } from "../../src/util/filesystem" diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index 1174cdf6a1..aaf34348b9 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/packages/opencode/test/session/compaction.test.ts @@ -2,7 +2,6 @@ import { afterEach, describe, expect, mock, test } from "bun:test" import { APICallError } from "ai" import { Cause, Effect, Exit, Layer, ManagedRuntime } from "effect" import * as Stream from "effect/Stream" -import path from "path" import z from "zod" import { Bus } from "../../src/bus" import { Config } from "../../src/config" diff --git a/packages/opencode/test/session/prompt-effect.test.ts b/packages/opencode/test/session/prompt-effect.test.ts index ec1a87e969..3963c815da 100644 --- a/packages/opencode/test/session/prompt-effect.test.ts +++ b/packages/opencode/test/session/prompt-effect.test.ts @@ -3,7 +3,6 @@ import { FetchHttpClient } from "effect/unstable/http" import { expect } from "bun:test" import { Cause, Effect, Exit, Fiber, Layer } from "effect" import path from "path" -import z from "zod" import { Agent as AgentSvc } from "../../src/agent/agent" import { Bus } from "../../src/bus" import { Command } from "../../src/command" diff --git a/packages/opencode/test/session/snapshot-tool-race.test.ts b/packages/opencode/test/session/snapshot-tool-race.test.ts index a0ea47c89c..e32919aeda 100644 --- a/packages/opencode/test/session/snapshot-tool-race.test.ts +++ b/packages/opencode/test/session/snapshot-tool-race.test.ts @@ -49,7 +49,6 @@ import { Instruction } from "../../src/session/instruction" import { SessionProcessor } from "../../src/session/processor" import { SessionRunState } from "../../src/session/run-state" import { SessionStatus } from "../../src/session/status" -import { Shell } from "../../src/shell/shell" import { Snapshot } from "../../src/snapshot" import { ToolRegistry } from "../../src/tool/registry" import { Truncate } from "../../src/tool/truncate" diff --git a/packages/opencode/test/share/share-next.test.ts b/packages/opencode/test/share/share-next.test.ts index 135d44db09..7475411953 100644 --- a/packages/opencode/test/share/share-next.test.ts +++ b/packages/opencode/test/share/share-next.test.ts @@ -13,7 +13,6 @@ import { Provider } from "../../src/provider/provider" import { Session } from "../../src/session" import type { SessionID } from "../../src/session/schema" import { ShareNext } from "../../src/share/share-next" -import { Storage } from "../../src/storage/storage" import { SessionShareTable } from "../../src/share/share.sql" import { Database, eq } from "../../src/storage/db" import { provideTmpdirInstance } from "../fixture/fixture" diff --git a/packages/opencode/test/tool/question.test.ts b/packages/opencode/test/tool/question.test.ts index eb69f1d966..629e5d2d28 100644 --- a/packages/opencode/test/tool/question.test.ts +++ b/packages/opencode/test/tool/question.test.ts @@ -1,6 +1,5 @@ import { describe, expect } from "bun:test" import { Effect, Fiber, Layer } from "effect" -import { Tool } from "../../src/tool/tool" import { QuestionTool } from "../../src/tool/question" import { Question } from "../../src/question" import { SessionID, MessageID } from "../../src/session/schema" diff --git a/packages/slack/src/index.ts b/packages/slack/src/index.ts index d07e3dfb41..123710aa46 100644 --- a/packages/slack/src/index.ts +++ b/packages/slack/src/index.ts @@ -95,7 +95,7 @@ app.message(async ({ message, say }) => { const shareResult = await client.session.share({ path: { id: createResult.data.id } }) if (!shareResult.error && shareResult.data) { - const sessionUrl = shareResult.data.share?.url! + const sessionUrl = shareResult.data.share?.url console.log("🔗 Session shared:", sessionUrl) await app.client.chat.postMessage({ channel, thread_ts: thread, text: sessionUrl }) } diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 48444cd017..81e6a52a26 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -37,7 +37,6 @@ import { type UiI18n, useI18n } from "../context/i18n" import { BasicTool, GenericTool } from "./basic-tool" import { Accordion } from "./accordion" import { StickyAccordionHeader } from "./sticky-accordion-header" -import { Card } from "./card" import { Collapsible } from "./collapsible" import { FileIcon } from "./file-icon" import { Icon } from "./icon" diff --git a/packages/ui/src/components/timeline-playground.stories.tsx b/packages/ui/src/components/timeline-playground.stories.tsx index 98cdf85001..282592ff63 100644 --- a/packages/ui/src/components/timeline-playground.stories.tsx +++ b/packages/ui/src/components/timeline-playground.stories.tsx @@ -1,5 +1,5 @@ // @ts-nocheck -import { createSignal, createMemo, createEffect, on, For, Show, Index, batch } from "solid-js" +import { createSignal, createMemo, createEffect, on, For, Show, batch } from "solid-js" import { createStore, produce } from "solid-js/store" import type { Message, diff --git a/packages/ui/src/pierre/commented-lines.ts b/packages/ui/src/pierre/commented-lines.ts index d2fa648663..e970b7841b 100644 --- a/packages/ui/src/pierre/commented-lines.ts +++ b/packages/ui/src/pierre/commented-lines.ts @@ -1,5 +1,5 @@ import { type SelectedLineRange } from "@pierre/diffs" -import { diffLineIndex, diffRowIndex, findDiffSide } from "./diff-selection" +import { diffLineIndex, diffRowIndex } from "./diff-selection" export type CommentSide = "additions" | "deletions" diff --git a/sdks/vscode/src/extension.ts b/sdks/vscode/src/extension.ts index 105ab0293a..772da9cc2b 100644 --- a/sdks/vscode/src/extension.ts +++ b/sdks/vscode/src/extension.ts @@ -78,7 +78,7 @@ export function activate(context: vscode.ExtensionContext) { await fetch(`http://localhost:${port}/app`) connected = true break - } catch (e) {} + } catch {} tries-- } while (tries > 0) From 1d81335ab5c7e4f3a4c0652c9c7d59240028fe9f Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 21:44:46 -0400 Subject: [PATCH 171/300] feat: unwrap Provider namespace + improved automation script (#22690) --- packages/opencode/script/unwrap-namespace.ts | 193 +- packages/opencode/src/acp/agent.ts | 2 +- packages/opencode/src/agent/agent.ts | 2 +- packages/opencode/src/cli/cmd/agent.ts | 2 +- packages/opencode/src/cli/cmd/debug/agent.ts | 2 +- packages/opencode/src/cli/cmd/github.ts | 2 +- packages/opencode/src/cli/cmd/models.ts | 2 +- packages/opencode/src/cli/cmd/run.ts | 2 +- packages/opencode/src/cli/cmd/tui/app.tsx | 2 +- .../src/cli/cmd/tui/context/local.tsx | 2 +- packages/opencode/src/cli/error.ts | 2 +- packages/opencode/src/effect/app-runtime.ts | 2 +- packages/opencode/src/provider/index.ts | 1 + packages/opencode/src/provider/provider.ts | 3040 ++++++++--------- packages/opencode/src/provider/transform.ts | 2 +- .../opencode/src/server/instance/config.ts | 2 +- .../opencode/src/server/instance/provider.ts | 2 +- packages/opencode/src/server/middleware.ts | 2 +- packages/opencode/src/session/compaction.ts | 2 +- packages/opencode/src/session/index.ts | 2 +- packages/opencode/src/session/llm.ts | 2 +- packages/opencode/src/session/message-v2.ts | 2 +- packages/opencode/src/session/overflow.ts | 2 +- packages/opencode/src/session/processor.ts | 2 +- packages/opencode/src/session/prompt.ts | 2 +- packages/opencode/src/session/system.ts | 2 +- packages/opencode/src/share/share-next.ts | 2 +- packages/opencode/src/tool/plan.ts | 2 +- packages/opencode/src/tool/registry.ts | 2 +- packages/opencode/test/fake/provider.ts | 2 +- .../test/provider/amazon-bedrock.test.ts | 2 +- .../opencode/test/provider/gitlab-duo.test.ts | 2 +- .../opencode/test/provider/provider.test.ts | 2 +- .../opencode/test/session/compaction.test.ts | 2 +- packages/opencode/test/session/llm.test.ts | 2 +- .../opencode/test/session/message-v2.test.ts | 2 +- .../test/session/processor-effect.test.ts | 2 +- .../test/session/prompt-effect.test.ts | 4 +- .../test/session/snapshot-tool-race.test.ts | 2 +- .../opencode/test/share/share-next.test.ts | 2 +- 40 files changed, 1711 insertions(+), 1599 deletions(-) create mode 100644 packages/opencode/src/provider/index.ts diff --git a/packages/opencode/script/unwrap-namespace.ts b/packages/opencode/script/unwrap-namespace.ts index 65ce498be8..bdb49a7fcf 100644 --- a/packages/opencode/script/unwrap-namespace.ts +++ b/packages/opencode/script/unwrap-namespace.ts @@ -10,11 +10,11 @@ * 1. Reads the file and finds the `export namespace Foo { ... }` block * (uses ast-grep for accurate AST-based boundary detection) * 2. Removes the namespace wrapper and dedents the body - * 3. If the file is index.ts, renames it to .ts - * 4. Creates/updates index.ts with `export * as Foo from "./"` - * 5. Prints the import rewrite commands to run across the codebase - * - * Does NOT auto-rewrite imports — prints the commands so you can review them. + * 3. Fixes self-references (e.g. Config.PermissionAction → PermissionAction) + * 4. If the file is index.ts, renames it to .ts + * 5. Creates/updates index.ts with `export * as Foo from "./"` + * 6. Rewrites import paths across src/, test/, and script/ + * 7. Fixes sibling imports within the same directory * * Requires: ast-grep (`brew install ast-grep` or `cargo install ast-grep`) */ @@ -90,22 +90,107 @@ const after = lines.slice(closeLine + 1) const dedented = body.map((line) => { if (line === "") return "" if (line.startsWith(" ")) return line.slice(2) - return line // don't touch lines that aren't indented (shouldn't happen) + return line }) -const newContent = [...before, ...dedented, ...after].join("\n") +let newContent = [...before, ...dedented, ...after].join("\n") + +// --- Fix self-references --- +// After unwrapping, references like `Config.PermissionAction` inside the same file +// need to become just `PermissionAction`. Only fix code positions, not strings. +const exportedNames = new Set() +const exportRegex = /export\s+(?:const|function|class|interface|type|enum|abstract\s+class)\s+(\w+)/g +for (const line of dedented) { + for (const m of line.matchAll(exportRegex)) exportedNames.add(m[1]) +} +const reExportRegex = /export\s*\{\s*([^}]+)\}/g +for (const line of dedented) { + for (const m of line.matchAll(reExportRegex)) { + for (const name of m[1].split(",")) { + const trimmed = name + .trim() + .split(/\s+as\s+/) + .pop()! + .trim() + if (trimmed) exportedNames.add(trimmed) + } + } +} + +let selfRefCount = 0 +if (exportedNames.size > 0) { + const fixedLines = newContent.split("\n").map((line) => { + // Split line into string-literal and code segments to avoid replacing inside strings + const segments: Array<{ text: string; isString: boolean }> = [] + let i = 0 + let current = "" + let inString: string | null = null + + while (i < line.length) { + const ch = line[i] + if (inString) { + current += ch + if (ch === "\\" && i + 1 < line.length) { + current += line[i + 1] + i += 2 + continue + } + if (ch === inString) { + segments.push({ text: current, isString: true }) + current = "" + inString = null + } + i++ + continue + } + if (ch === '"' || ch === "'" || ch === "`") { + if (current) segments.push({ text: current, isString: false }) + current = ch + inString = ch + i++ + continue + } + if (ch === "/" && i + 1 < line.length && line[i + 1] === "/") { + current += line.slice(i) + segments.push({ text: current, isString: true }) + current = "" + i = line.length + continue + } + current += ch + i++ + } + if (current) segments.push({ text: current, isString: !!inString }) + + return segments + .map((seg) => { + if (seg.isString) return seg.text + let result = seg.text + for (const name of exportedNames) { + const pattern = `${nsName}.${name}` + while (result.includes(pattern)) { + const idx = result.indexOf(pattern) + const charBefore = idx > 0 ? result[idx - 1] : " " + const charAfter = idx + pattern.length < result.length ? result[idx + pattern.length] : " " + if (/\w/.test(charBefore) || /\w/.test(charAfter)) break + result = result.slice(0, idx) + name + result.slice(idx + pattern.length) + selfRefCount++ + } + } + return result + }) + .join("") + }) + newContent = fixedLines.join("\n") +} // Figure out file naming const dir = path.dirname(absPath) const basename = path.basename(absPath, ".ts") const isIndex = basename === "index" - -// The implementation file name (lowercase namespace name if currently index.ts) const implName = isIndex ? nsName.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase() : basename const implFile = path.join(dir, `${implName}.ts`) const indexFile = path.join(dir, "index.ts") - -// The barrel line const barrelLine = `export * as ${nsName} from "./${implName}"\n` console.log("") @@ -114,6 +199,7 @@ if (isIndex) { } else { console.log(`Plan: rewrite ${basename}.ts in place, create index.ts barrel`) } +if (selfRefCount > 0) console.log(`Fixed ${selfRefCount} self-reference(s) (${nsName}.X → X)`) console.log("") if (dryRun) { @@ -128,19 +214,23 @@ if (dryRun) { console.log("") console.log(`=== index.ts ===`) console.log(` ${barrelLine.trim()}`) + console.log("") + if (!isIndex) { + const relDir = path.relative(path.resolve("src"), dir) + console.log(`=== Import rewrites (would apply) ===`) + console.log(` ${relDir}/${basename}" → ${relDir}" across src/, test/, script/`) + } else { + console.log("No import rewrites needed (was index.ts)") + } } else { - // Write the implementation file if (isIndex) { - // Rename: write new content to implFile, then overwrite index.ts with barrel fs.writeFileSync(implFile, newContent) fs.writeFileSync(indexFile, barrelLine) console.log(`Wrote ${implName}.ts (${newContent.split("\n").length} lines)`) console.log(`Wrote index.ts (barrel)`) } else { - // Rewrite in place, create index.ts fs.writeFileSync(absPath, newContent) if (fs.existsSync(indexFile)) { - // Append to existing barrel const existing = fs.readFileSync(indexFile, "utf-8") if (!existing.includes(`export * as ${nsName}`)) { fs.appendFileSync(indexFile, barrelLine) @@ -154,37 +244,60 @@ if (dryRun) { } console.log(`Rewrote ${basename}.ts (${newContent.split("\n").length} lines)`) } -} -// Print the import rewrite guidance -const relDir = path.relative(path.resolve("src"), dir) + // --- Rewrite import paths across src/, test/, script/ --- + const relDir = path.relative(path.resolve("src"), dir) + if (!isIndex) { + const oldTail = `${relDir}/${basename}` + const searchDirs = ["src", "test", "script"].filter((d) => fs.existsSync(d)) + const rgResult = Bun.spawnSync(["rg", "-l", `from.*${oldTail}"`, ...searchDirs], { + stdout: "pipe", + stderr: "pipe", + }) + const filesToRewrite = rgResult.stdout + .toString() + .trim() + .split("\n") + .filter((f) => f.length > 0) -console.log("") -console.log("=== Import rewrites ===") -console.log("") + if (filesToRewrite.length > 0) { + console.log(`\nRewriting imports in ${filesToRewrite.length} file(s)...`) + for (const file of filesToRewrite) { + const content = fs.readFileSync(file, "utf-8") + fs.writeFileSync(file, content.replaceAll(`${oldTail}"`, `${relDir}"`)) + } + console.log(` Done: ${oldTail}" → ${relDir}"`) + } else { + console.log("\nNo import rewrites needed") + } + } else { + console.log("\nNo import rewrites needed (was index.ts)") + } -if (!isIndex) { - // Non-index files: imports like "../provider/provider" need to become "../provider" - const oldTail = `${relDir}/${basename}` + // --- Fix sibling imports within the same directory --- + const siblingFiles = fs.readdirSync(dir).filter((f) => { + if (!f.endsWith(".ts")) return false + if (f === "index.ts" || f === `${implName}.ts`) return false + return true + }) - console.log(`# Find all imports to rewrite:`) - console.log(`rg 'from.*${oldTail}' src/ --files-with-matches`) - console.log("") - - // Auto-rewrite with sed (safe: only rewrites the import path, not other occurrences) - console.log("# Auto-rewrite (review diff afterward):") - console.log(`rg -l 'from.*${oldTail}' src/ | xargs sed -i '' 's|${oldTail}"|${relDir}"|g'`) - console.log("") - console.log("# What changes:") - console.log(`# import { ${nsName} } from ".../${oldTail}"`) - console.log(`# import { ${nsName} } from ".../${relDir}"`) -} else { - console.log("# File was index.ts — import paths already resolve correctly.") - console.log("# No import rewrites needed!") + let siblingFixCount = 0 + for (const sibFile of siblingFiles) { + const sibPath = path.join(dir, sibFile) + const content = fs.readFileSync(sibPath, "utf-8") + const pattern = new RegExp(`from\\s+["']\\./${basename}["']`, "g") + if (pattern.test(content)) { + fs.writeFileSync(sibPath, content.replace(pattern, `from "."`)) + siblingFixCount++ + } + } + if (siblingFixCount > 0) { + console.log(`Fixed ${siblingFixCount} sibling import(s) in ${path.basename(dir)}/ (./${basename} → .)`) + } } console.log("") console.log("=== Verify ===") console.log("") -console.log("bun typecheck # from packages/opencode") -console.log("bun run test # run tests") +console.log("bunx --bun tsgo --noEmit # typecheck") +console.log("bun run test # run tests") diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index c065c64ffc..5f0bcdc24b 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -37,7 +37,7 @@ import { Filesystem } from "../util/filesystem" import { Hash } from "@opencode-ai/shared/util/hash" import { ACPSessionManager } from "./session" import type { ACPConfig } from "./types" -import { Provider } from "../provider/provider" +import { Provider } from "../provider" import { ModelID, ProviderID } from "../provider/schema" import { Agent as AgentModule } from "../agent/agent" import { AppRuntime } from "@/effect/app-runtime" diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 5887ee28e3..8e6bfe5e9b 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -1,6 +1,6 @@ import { Config } from "../config" import z from "zod" -import { Provider } from "../provider/provider" +import { Provider } from "../provider" import { ModelID, ProviderID } from "../provider/schema" import { generateObject, streamObject, type ModelMessage } from "ai" import { Instance } from "../project/instance" diff --git a/packages/opencode/src/cli/cmd/agent.ts b/packages/opencode/src/cli/cmd/agent.ts index b001389461..0e93946a23 100644 --- a/packages/opencode/src/cli/cmd/agent.ts +++ b/packages/opencode/src/cli/cmd/agent.ts @@ -4,7 +4,7 @@ import { AppRuntime } from "@/effect/app-runtime" import { UI } from "../ui" import { Global } from "../../global" import { Agent } from "../../agent/agent" -import { Provider } from "../../provider/provider" +import { Provider } from "../../provider" import path from "path" import fs from "fs/promises" import { Filesystem } from "../../util/filesystem" diff --git a/packages/opencode/src/cli/cmd/debug/agent.ts b/packages/opencode/src/cli/cmd/debug/agent.ts index ea45cde664..6c7ad39c1a 100644 --- a/packages/opencode/src/cli/cmd/debug/agent.ts +++ b/packages/opencode/src/cli/cmd/debug/agent.ts @@ -2,7 +2,7 @@ import { EOL } from "os" import { basename } from "path" import { Effect } from "effect" import { Agent } from "../../../agent/agent" -import { Provider } from "../../../provider/provider" +import { Provider } from "../../../provider" import { Session } from "../../../session" import type { MessageV2 } from "../../../session/message-v2" import { MessageID, PartID } from "../../../session/schema" diff --git a/packages/opencode/src/cli/cmd/github.ts b/packages/opencode/src/cli/cmd/github.ts index b6781d0852..191aa2dfdf 100644 --- a/packages/opencode/src/cli/cmd/github.ts +++ b/packages/opencode/src/cli/cmd/github.ts @@ -25,7 +25,7 @@ import { SessionShare } from "@/share/session" import { Session } from "../../session" import type { SessionID } from "../../session/schema" import { MessageID, PartID } from "../../session/schema" -import { Provider } from "../../provider/provider" +import { Provider } from "../../provider" import { Bus } from "../../bus" import { MessageV2 } from "../../session/message-v2" import { SessionPrompt } from "@/session/prompt" diff --git a/packages/opencode/src/cli/cmd/models.ts b/packages/opencode/src/cli/cmd/models.ts index ad9300da2e..af5ca2f957 100644 --- a/packages/opencode/src/cli/cmd/models.ts +++ b/packages/opencode/src/cli/cmd/models.ts @@ -1,6 +1,6 @@ import type { Argv } from "yargs" import { Instance } from "../../project/instance" -import { Provider } from "../../provider/provider" +import { Provider } from "../../provider" import { ProviderID } from "../../provider/schema" import { ModelsDev } from "../../provider/models" import { cmd } from "./cmd" diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 2d3574c683..e94ba5d119 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -9,7 +9,7 @@ import { EOL } from "os" import { Filesystem } from "../../util/filesystem" import { createOpencodeClient, type OpencodeClient, type ToolPart } from "@opencode-ai/sdk/v2" import { Server } from "../../server/server" -import { Provider } from "../../provider/provider" +import { Provider } from "../../provider" import { Agent } from "../../agent/agent" import { Permission } from "../../permission" import { Tool } from "../../tool/tool" diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 4c6c74ff3d..3d5350cb69 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -52,7 +52,7 @@ import { ExitProvider, useExit } from "./context/exit" import { Session as SessionApi } from "@/session" import { TuiEvent } from "./event" import { KVProvider, useKV } from "./context/kv" -import { Provider } from "@/provider/provider" +import { Provider } from "@/provider" import { ArgsProvider, useArgs, type Args } from "./context/args" import open from "open" import { PromptRefProvider, usePromptRef } from "./context/prompt" diff --git a/packages/opencode/src/cli/cmd/tui/context/local.tsx b/packages/opencode/src/cli/cmd/tui/context/local.tsx index ec3931b209..29f95141c9 100644 --- a/packages/opencode/src/cli/cmd/tui/context/local.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/local.tsx @@ -8,7 +8,7 @@ import { Global } from "@/global" import { iife } from "@/util/iife" import { createSimpleContext } from "./helper" import { useToast } from "../ui/toast" -import { Provider } from "@/provider/provider" +import { Provider } from "@/provider" import { useArgs } from "./args" import { useSDK } from "./sdk" import { RGBA } from "@opentui/core" diff --git a/packages/opencode/src/cli/error.ts b/packages/opencode/src/cli/error.ts index 1277f5046c..6ba110d34f 100644 --- a/packages/opencode/src/cli/error.ts +++ b/packages/opencode/src/cli/error.ts @@ -3,7 +3,7 @@ import { ConfigMarkdown } from "@/config/markdown" import { errorFormat } from "@/util/error" import { Config } from "../config" import { MCP } from "../mcp" -import { Provider } from "../provider/provider" +import { Provider } from "../provider" import { UI } from "./ui" export function FormatError(input: unknown) { diff --git a/packages/opencode/src/effect/app-runtime.ts b/packages/opencode/src/effect/app-runtime.ts index 54139eb777..f9f811e711 100644 --- a/packages/opencode/src/effect/app-runtime.ts +++ b/packages/opencode/src/effect/app-runtime.ts @@ -15,7 +15,7 @@ import { FileWatcher } from "@/file/watcher" import { Storage } from "@/storage/storage" import { Snapshot } from "@/snapshot" import { Plugin } from "@/plugin" -import { Provider } from "@/provider/provider" +import { Provider } from "@/provider" import { ProviderAuth } from "@/provider/auth" import { Agent } from "@/agent/agent" import { Skill } from "@/skill" diff --git a/packages/opencode/src/provider/index.ts b/packages/opencode/src/provider/index.ts new file mode 100644 index 0000000000..3c0174548d --- /dev/null +++ b/packages/opencode/src/provider/index.ts @@ -0,0 +1 @@ +export * as Provider from "./provider" diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 1dd6027db9..36a5a68e99 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -59,1651 +59,1649 @@ import { ProviderTransform } from "./transform" import { Installation } from "../installation" import { ModelID, ProviderID } from "./schema" -export namespace Provider { - const log = Log.create({ service: "provider" }) +const log = Log.create({ service: "provider" }) - function shouldUseCopilotResponsesApi(modelID: string): boolean { - const match = /^gpt-(\d+)/.exec(modelID) - if (!match) return false - return Number(match[1]) >= 5 && !modelID.startsWith("gpt-5-mini") - } +function shouldUseCopilotResponsesApi(modelID: string): boolean { + const match = /^gpt-(\d+)/.exec(modelID) + if (!match) return false + return Number(match[1]) >= 5 && !modelID.startsWith("gpt-5-mini") +} - function wrapSSE(res: Response, ms: number, ctl: AbortController) { - if (typeof ms !== "number" || ms <= 0) return res - if (!res.body) return res - if (!res.headers.get("content-type")?.includes("text/event-stream")) return res +function wrapSSE(res: Response, ms: number, ctl: AbortController) { + if (typeof ms !== "number" || ms <= 0) return res + if (!res.body) return res + if (!res.headers.get("content-type")?.includes("text/event-stream")) return res - const reader = res.body.getReader() - const body = new ReadableStream({ - async pull(ctrl) { - const part = await new Promise>>((resolve, reject) => { - const id = setTimeout(() => { - const err = new Error("SSE read timed out") - ctl.abort(err) - void reader.cancel(err) + const reader = res.body.getReader() + const body = new ReadableStream({ + async pull(ctrl) { + const part = await new Promise>>((resolve, reject) => { + const id = setTimeout(() => { + const err = new Error("SSE read timed out") + ctl.abort(err) + void reader.cancel(err) + reject(err) + }, ms) + + reader.read().then( + (part) => { + clearTimeout(id) + resolve(part) + }, + (err) => { + clearTimeout(id) reject(err) - }, ms) - - reader.read().then( - (part) => { - clearTimeout(id) - resolve(part) - }, - (err) => { - clearTimeout(id) - reject(err) - }, - ) - }) - - if (part.done) { - ctrl.close() - return - } - - ctrl.enqueue(part.value) - }, - async cancel(reason) { - ctl.abort(reason) - await reader.cancel(reason) - }, - }) - - return new Response(body, { - headers: new Headers(res.headers), - status: res.status, - statusText: res.statusText, - }) - } - - type BundledSDK = { - languageModel(modelId: string): LanguageModelV3 - } - - const BUNDLED_PROVIDERS: Record BundledSDK> = { - "@ai-sdk/amazon-bedrock": createAmazonBedrock, - "@ai-sdk/anthropic": createAnthropic, - "@ai-sdk/azure": createAzure, - "@ai-sdk/google": createGoogleGenerativeAI, - "@ai-sdk/google-vertex": createVertex, - "@ai-sdk/google-vertex/anthropic": createVertexAnthropic, - "@ai-sdk/openai": createOpenAI, - "@ai-sdk/openai-compatible": createOpenAICompatible, - "@openrouter/ai-sdk-provider": createOpenRouter, - "@ai-sdk/xai": createXai, - "@ai-sdk/mistral": createMistral, - "@ai-sdk/groq": createGroq, - "@ai-sdk/deepinfra": createDeepInfra, - "@ai-sdk/cerebras": createCerebras, - "@ai-sdk/cohere": createCohere, - "@ai-sdk/gateway": createGateway, - "@ai-sdk/togetherai": createTogetherAI, - "@ai-sdk/perplexity": createPerplexity, - "@ai-sdk/vercel": createVercel, - "@ai-sdk/alibaba": createAlibaba, - "gitlab-ai-provider": createGitLab, - "@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible, - "venice-ai-sdk-provider": createVenice, - } - - type CustomModelLoader = (sdk: any, modelID: string, options?: Record) => Promise - type CustomVarsLoader = (options: Record) => Record - type CustomDiscoverModels = () => Promise> - type CustomLoader = (provider: Info) => Effect.Effect<{ - autoload: boolean - getModel?: CustomModelLoader - vars?: CustomVarsLoader - options?: Record - discoverModels?: CustomDiscoverModels - }> - - type CustomDep = { - auth: (id: string) => Effect.Effect - config: () => Effect.Effect - env: () => Effect.Effect> - get: (key: string) => Effect.Effect - } - - function useLanguageModel(sdk: any) { - return sdk.responses === undefined && sdk.chat === undefined - } - - function custom(dep: CustomDep): Record { - return { - anthropic: () => - Effect.succeed({ - autoload: false, - options: { - headers: { - "anthropic-beta": "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14", - }, }, - }), - opencode: Effect.fnUntraced(function* (input: Info) { - const env = yield* dep.env() - const hasKey = iife(() => { - if (input.env.some((item) => env[item])) return true - return false - }) - const ok = - hasKey || - Boolean(yield* dep.auth(input.id)) || - Boolean((yield* dep.config()).provider?.["opencode"]?.options?.apiKey) - - if (!ok) { - for (const [key, value] of Object.entries(input.models)) { - if (value.cost.input === 0) continue - delete input.models[key] - } - } - - return { - autoload: Object.keys(input.models).length > 0, - options: ok ? {} : { apiKey: "public" }, - } - }), - openai: () => - Effect.succeed({ - autoload: false, - async getModel(sdk: any, modelID: string, _options?: Record) { - return sdk.responses(modelID) - }, - options: {}, - }), - xai: () => - Effect.succeed({ - autoload: false, - async getModel(sdk: any, modelID: string, _options?: Record) { - return sdk.responses(modelID) - }, - options: {}, - }), - "github-copilot": () => - Effect.succeed({ - autoload: false, - async getModel(sdk: any, modelID: string, _options?: Record) { - if (useLanguageModel(sdk)) return sdk.languageModel(modelID) - return shouldUseCopilotResponsesApi(modelID) ? sdk.responses(modelID) : sdk.chat(modelID) - }, - options: {}, - }), - azure: Effect.fnUntraced(function* (provider: Info) { - const env = yield* dep.env() - const resource = iife(() => { - const name = provider.options?.resourceName - if (typeof name === "string" && name.trim() !== "") return name - return env["AZURE_RESOURCE_NAME"] - }) - - return { - autoload: false, - async getModel(sdk: any, modelID: string, options?: Record) { - if (useLanguageModel(sdk)) return sdk.languageModel(modelID) - if (options?.["useCompletionUrls"]) { - return sdk.chat(modelID) - } else { - return sdk.responses(modelID) - } - }, - options: {}, - vars(_options) { - return { - ...(resource && { AZURE_RESOURCE_NAME: resource }), - } - }, - } - }), - "azure-cognitive-services": Effect.fnUntraced(function* () { - const resourceName = yield* dep.get("AZURE_COGNITIVE_SERVICES_RESOURCE_NAME") - return { - autoload: false, - async getModel(sdk: any, modelID: string, options?: Record) { - if (useLanguageModel(sdk)) return sdk.languageModel(modelID) - if (options?.["useCompletionUrls"]) { - return sdk.chat(modelID) - } else { - return sdk.responses(modelID) - } - }, - options: { - baseURL: resourceName ? `https://${resourceName}.cognitiveservices.azure.com/openai` : undefined, - }, - } - }), - "amazon-bedrock": Effect.fnUntraced(function* () { - const providerConfig = (yield* dep.config()).provider?.["amazon-bedrock"] - const auth = yield* dep.auth("amazon-bedrock") - const env = yield* dep.env() - - // Region precedence: 1) config file, 2) env var, 3) default - const configRegion = providerConfig?.options?.region - const envRegion = env["AWS_REGION"] - const defaultRegion = configRegion ?? envRegion ?? "us-east-1" - - // Profile: config file takes precedence over env var - const configProfile = providerConfig?.options?.profile - const envProfile = env["AWS_PROFILE"] - const profile = configProfile ?? envProfile - - const awsAccessKeyId = env["AWS_ACCESS_KEY_ID"] - - // TODO: Using process.env directly because Env.set only updates a process.env shallow copy, - // until the scope of the Env API is clarified (test only or runtime?) - const awsBearerToken = iife(() => { - const envToken = process.env.AWS_BEARER_TOKEN_BEDROCK - if (envToken) return envToken - if (auth?.type === "api") { - process.env.AWS_BEARER_TOKEN_BEDROCK = auth.key - return auth.key - } - return undefined - }) - - const awsWebIdentityTokenFile = env["AWS_WEB_IDENTITY_TOKEN_FILE"] - - const containerCreds = Boolean( - process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI || process.env.AWS_CONTAINER_CREDENTIALS_FULL_URI, ) + }) - if (!profile && !awsAccessKeyId && !awsBearerToken && !awsWebIdentityTokenFile && !containerCreds) - return { autoload: false } + if (part.done) { + ctrl.close() + return + } - const providerOptions: AmazonBedrockProviderSettings = { - region: defaultRegion, + ctrl.enqueue(part.value) + }, + async cancel(reason) { + ctl.abort(reason) + await reader.cancel(reason) + }, + }) + + return new Response(body, { + headers: new Headers(res.headers), + status: res.status, + statusText: res.statusText, + }) +} + +type BundledSDK = { + languageModel(modelId: string): LanguageModelV3 +} + +const BUNDLED_PROVIDERS: Record BundledSDK> = { + "@ai-sdk/amazon-bedrock": createAmazonBedrock, + "@ai-sdk/anthropic": createAnthropic, + "@ai-sdk/azure": createAzure, + "@ai-sdk/google": createGoogleGenerativeAI, + "@ai-sdk/google-vertex": createVertex, + "@ai-sdk/google-vertex/anthropic": createVertexAnthropic, + "@ai-sdk/openai": createOpenAI, + "@ai-sdk/openai-compatible": createOpenAICompatible, + "@openrouter/ai-sdk-provider": createOpenRouter, + "@ai-sdk/xai": createXai, + "@ai-sdk/mistral": createMistral, + "@ai-sdk/groq": createGroq, + "@ai-sdk/deepinfra": createDeepInfra, + "@ai-sdk/cerebras": createCerebras, + "@ai-sdk/cohere": createCohere, + "@ai-sdk/gateway": createGateway, + "@ai-sdk/togetherai": createTogetherAI, + "@ai-sdk/perplexity": createPerplexity, + "@ai-sdk/vercel": createVercel, + "@ai-sdk/alibaba": createAlibaba, + "gitlab-ai-provider": createGitLab, + "@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible, + "venice-ai-sdk-provider": createVenice, +} + +type CustomModelLoader = (sdk: any, modelID: string, options?: Record) => Promise +type CustomVarsLoader = (options: Record) => Record +type CustomDiscoverModels = () => Promise> +type CustomLoader = (provider: Info) => Effect.Effect<{ + autoload: boolean + getModel?: CustomModelLoader + vars?: CustomVarsLoader + options?: Record + discoverModels?: CustomDiscoverModels +}> + +type CustomDep = { + auth: (id: string) => Effect.Effect + config: () => Effect.Effect + env: () => Effect.Effect> + get: (key: string) => Effect.Effect +} + +function useLanguageModel(sdk: any) { + return sdk.responses === undefined && sdk.chat === undefined +} + +function custom(dep: CustomDep): Record { + return { + anthropic: () => + Effect.succeed({ + autoload: false, + options: { + headers: { + "anthropic-beta": "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14", + }, + }, + }), + opencode: Effect.fnUntraced(function* (input: Info) { + const env = yield* dep.env() + const hasKey = iife(() => { + if (input.env.some((item) => env[item])) return true + return false + }) + const ok = + hasKey || + Boolean(yield* dep.auth(input.id)) || + Boolean((yield* dep.config()).provider?.["opencode"]?.options?.apiKey) + + if (!ok) { + for (const [key, value] of Object.entries(input.models)) { + if (value.cost.input === 0) continue + delete input.models[key] } + } - // Only use credential chain if no bearer token exists - // Bearer token takes precedence over credential chain (profiles, access keys, IAM roles, web identity tokens) - if (!awsBearerToken) { - // Build credential provider options (only pass profile if specified) - const credentialProviderOptions = profile ? { profile } : {} + return { + autoload: Object.keys(input.models).length > 0, + options: ok ? {} : { apiKey: "public" }, + } + }), + openai: () => + Effect.succeed({ + autoload: false, + async getModel(sdk: any, modelID: string, _options?: Record) { + return sdk.responses(modelID) + }, + options: {}, + }), + xai: () => + Effect.succeed({ + autoload: false, + async getModel(sdk: any, modelID: string, _options?: Record) { + return sdk.responses(modelID) + }, + options: {}, + }), + "github-copilot": () => + Effect.succeed({ + autoload: false, + async getModel(sdk: any, modelID: string, _options?: Record) { + if (useLanguageModel(sdk)) return sdk.languageModel(modelID) + return shouldUseCopilotResponsesApi(modelID) ? sdk.responses(modelID) : sdk.chat(modelID) + }, + options: {}, + }), + azure: Effect.fnUntraced(function* (provider: Info) { + const env = yield* dep.env() + const resource = iife(() => { + const name = provider.options?.resourceName + if (typeof name === "string" && name.trim() !== "") return name + return env["AZURE_RESOURCE_NAME"] + }) - providerOptions.credentialProvider = fromNodeProviderChain(credentialProviderOptions) + return { + autoload: false, + async getModel(sdk: any, modelID: string, options?: Record) { + if (useLanguageModel(sdk)) return sdk.languageModel(modelID) + if (options?.["useCompletionUrls"]) { + return sdk.chat(modelID) + } else { + return sdk.responses(modelID) + } + }, + options: {}, + vars(_options) { + return { + ...(resource && { AZURE_RESOURCE_NAME: resource }), + } + }, + } + }), + "azure-cognitive-services": Effect.fnUntraced(function* () { + const resourceName = yield* dep.get("AZURE_COGNITIVE_SERVICES_RESOURCE_NAME") + return { + autoload: false, + async getModel(sdk: any, modelID: string, options?: Record) { + if (useLanguageModel(sdk)) return sdk.languageModel(modelID) + if (options?.["useCompletionUrls"]) { + return sdk.chat(modelID) + } else { + return sdk.responses(modelID) + } + }, + options: { + baseURL: resourceName ? `https://${resourceName}.cognitiveservices.azure.com/openai` : undefined, + }, + } + }), + "amazon-bedrock": Effect.fnUntraced(function* () { + const providerConfig = (yield* dep.config()).provider?.["amazon-bedrock"] + const auth = yield* dep.auth("amazon-bedrock") + const env = yield* dep.env() + + // Region precedence: 1) config file, 2) env var, 3) default + const configRegion = providerConfig?.options?.region + const envRegion = env["AWS_REGION"] + const defaultRegion = configRegion ?? envRegion ?? "us-east-1" + + // Profile: config file takes precedence over env var + const configProfile = providerConfig?.options?.profile + const envProfile = env["AWS_PROFILE"] + const profile = configProfile ?? envProfile + + const awsAccessKeyId = env["AWS_ACCESS_KEY_ID"] + + // TODO: Using process.env directly because Env.set only updates a process.env shallow copy, + // until the scope of the Env API is clarified (test only or runtime?) + const awsBearerToken = iife(() => { + const envToken = process.env.AWS_BEARER_TOKEN_BEDROCK + if (envToken) return envToken + if (auth?.type === "api") { + process.env.AWS_BEARER_TOKEN_BEDROCK = auth.key + return auth.key } + return undefined + }) - // Add custom endpoint if specified (endpoint takes precedence over baseURL) - const endpoint = providerConfig?.options?.endpoint ?? providerConfig?.options?.baseURL - if (endpoint) { - providerOptions.baseURL = endpoint - } + const awsWebIdentityTokenFile = env["AWS_WEB_IDENTITY_TOKEN_FILE"] - return { - autoload: true, - options: providerOptions, - async getModel(sdk: any, modelID: string, options?: Record) { - // Skip region prefixing if model already has a cross-region inference profile prefix - // Models from models.dev may already include prefixes like us., eu., global., etc. - const crossRegionPrefixes = ["global.", "us.", "eu.", "jp.", "apac.", "au."] - if (crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix))) { - return sdk.languageModel(modelID) - } + const containerCreds = Boolean( + process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI || process.env.AWS_CONTAINER_CREDENTIALS_FULL_URI, + ) - // Region resolution precedence (highest to lowest): - // 1. options.region from opencode.json provider config - // 2. defaultRegion from AWS_REGION environment variable - // 3. Default "us-east-1" (baked into defaultRegion) - const region = options?.region ?? defaultRegion + if (!profile && !awsAccessKeyId && !awsBearerToken && !awsWebIdentityTokenFile && !containerCreds) + return { autoload: false } - let regionPrefix = region.split("-")[0] + const providerOptions: AmazonBedrockProviderSettings = { + region: defaultRegion, + } - switch (regionPrefix) { - case "us": { - const modelRequiresPrefix = [ - "nova-micro", - "nova-lite", - "nova-pro", - "nova-premier", - "nova-2", - "claude", - "deepseek", - ].some((m) => modelID.includes(m)) - const isGovCloud = region.startsWith("us-gov") - if (modelRequiresPrefix && !isGovCloud) { - modelID = `${regionPrefix}.${modelID}` - } - break + // Only use credential chain if no bearer token exists + // Bearer token takes precedence over credential chain (profiles, access keys, IAM roles, web identity tokens) + if (!awsBearerToken) { + // Build credential provider options (only pass profile if specified) + const credentialProviderOptions = profile ? { profile } : {} + + providerOptions.credentialProvider = fromNodeProviderChain(credentialProviderOptions) + } + + // Add custom endpoint if specified (endpoint takes precedence over baseURL) + const endpoint = providerConfig?.options?.endpoint ?? providerConfig?.options?.baseURL + if (endpoint) { + providerOptions.baseURL = endpoint + } + + return { + autoload: true, + options: providerOptions, + async getModel(sdk: any, modelID: string, options?: Record) { + // Skip region prefixing if model already has a cross-region inference profile prefix + // Models from models.dev may already include prefixes like us., eu., global., etc. + const crossRegionPrefixes = ["global.", "us.", "eu.", "jp.", "apac.", "au."] + if (crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix))) { + return sdk.languageModel(modelID) + } + + // Region resolution precedence (highest to lowest): + // 1. options.region from opencode.json provider config + // 2. defaultRegion from AWS_REGION environment variable + // 3. Default "us-east-1" (baked into defaultRegion) + const region = options?.region ?? defaultRegion + + let regionPrefix = region.split("-")[0] + + switch (regionPrefix) { + case "us": { + const modelRequiresPrefix = [ + "nova-micro", + "nova-lite", + "nova-pro", + "nova-premier", + "nova-2", + "claude", + "deepseek", + ].some((m) => modelID.includes(m)) + const isGovCloud = region.startsWith("us-gov") + if (modelRequiresPrefix && !isGovCloud) { + modelID = `${regionPrefix}.${modelID}` } - case "eu": { - const regionRequiresPrefix = [ - "eu-west-1", - "eu-west-2", - "eu-west-3", - "eu-north-1", - "eu-central-1", - "eu-south-1", - "eu-south-2", - ].some((r) => region.includes(r)) - const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "llama3", "pixtral"].some((m) => + break + } + case "eu": { + const regionRequiresPrefix = [ + "eu-west-1", + "eu-west-2", + "eu-west-3", + "eu-north-1", + "eu-central-1", + "eu-south-1", + "eu-south-2", + ].some((r) => region.includes(r)) + const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "llama3", "pixtral"].some((m) => + modelID.includes(m), + ) + if (regionRequiresPrefix && modelRequiresPrefix) { + modelID = `${regionPrefix}.${modelID}` + } + break + } + case "ap": { + const isAustraliaRegion = ["ap-southeast-2", "ap-southeast-4"].includes(region) + const isTokyoRegion = region === "ap-northeast-1" + if ( + isAustraliaRegion && + ["anthropic.claude-sonnet-4-5", "anthropic.claude-haiku"].some((m) => modelID.includes(m)) + ) { + regionPrefix = "au" + modelID = `${regionPrefix}.${modelID}` + } else if (isTokyoRegion) { + // Tokyo region uses jp. prefix for cross-region inference + const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "nova-pro"].some((m) => modelID.includes(m), ) - if (regionRequiresPrefix && modelRequiresPrefix) { + if (modelRequiresPrefix) { + regionPrefix = "jp" modelID = `${regionPrefix}.${modelID}` } - break - } - case "ap": { - const isAustraliaRegion = ["ap-southeast-2", "ap-southeast-4"].includes(region) - const isTokyoRegion = region === "ap-northeast-1" - if ( - isAustraliaRegion && - ["anthropic.claude-sonnet-4-5", "anthropic.claude-haiku"].some((m) => modelID.includes(m)) - ) { - regionPrefix = "au" + } else { + // Other APAC regions use apac. prefix + const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "nova-pro"].some((m) => + modelID.includes(m), + ) + if (modelRequiresPrefix) { + regionPrefix = "apac" modelID = `${regionPrefix}.${modelID}` - } else if (isTokyoRegion) { - // Tokyo region uses jp. prefix for cross-region inference - const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "nova-pro"].some((m) => - modelID.includes(m), - ) - if (modelRequiresPrefix) { - regionPrefix = "jp" - modelID = `${regionPrefix}.${modelID}` - } - } else { - // Other APAC regions use apac. prefix - const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "nova-pro"].some((m) => - modelID.includes(m), - ) - if (modelRequiresPrefix) { - regionPrefix = "apac" - modelID = `${regionPrefix}.${modelID}` - } } - break } + break } - - return sdk.languageModel(modelID) - }, - } - }), - openrouter: () => - Effect.succeed({ - autoload: false, - options: { - headers: { - "HTTP-Referer": "https://opencode.ai/", - "X-Title": "opencode", - }, - }, - }), - vercel: () => - Effect.succeed({ - autoload: false, - options: { - headers: { - "http-referer": "https://opencode.ai/", - "x-title": "opencode", - }, - }, - }), - "google-vertex": Effect.fnUntraced(function* (provider: Info) { - const env = yield* dep.env() - const project = - provider.options?.project ?? env["GOOGLE_CLOUD_PROJECT"] ?? env["GCP_PROJECT"] ?? env["GCLOUD_PROJECT"] - - const location = String( - provider.options?.location ?? - env["GOOGLE_VERTEX_LOCATION"] ?? - env["GOOGLE_CLOUD_LOCATION"] ?? - env["VERTEX_LOCATION"] ?? - "us-central1", - ) - - const autoload = Boolean(project) - if (!autoload) return { autoload: false } - return { - autoload: true, - vars(_options: Record) { - const endpoint = - location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com` - return { - ...(project && { GOOGLE_VERTEX_PROJECT: project }), - GOOGLE_VERTEX_LOCATION: location, - GOOGLE_VERTEX_ENDPOINT: endpoint, - } - }, - options: { - project, - location, - fetch: async (input: RequestInfo | URL, init?: RequestInit) => { - const auth = new GoogleAuth() - const client = await auth.getApplicationDefault() - const token = await client.credential.getAccessToken() - - const headers = new Headers(init?.headers) - headers.set("Authorization", `Bearer ${token.token}`) - - return fetch(input, { ...init, headers }) - }, - }, - async getModel(sdk: any, modelID: string) { - const id = String(modelID).trim() - return sdk.languageModel(id) - }, - } - }), - "google-vertex-anthropic": Effect.fnUntraced(function* () { - const env = yield* dep.env() - const project = env["GOOGLE_CLOUD_PROJECT"] ?? env["GCP_PROJECT"] ?? env["GCLOUD_PROJECT"] - const location = env["GOOGLE_CLOUD_LOCATION"] ?? env["VERTEX_LOCATION"] ?? "global" - const autoload = Boolean(project) - if (!autoload) return { autoload: false } - return { - autoload: true, - options: { - project, - location, - }, - async getModel(sdk: any, modelID) { - const id = String(modelID).trim() - return sdk.languageModel(id) - }, - } - }), - "sap-ai-core": Effect.fnUntraced(function* () { - const auth = yield* dep.auth("sap-ai-core") - // TODO: Using process.env directly because Env.set only updates a shallow copy (not process.env), - // until the scope of the Env API is clarified (test only or runtime?) - const envServiceKey = iife(() => { - const envAICoreServiceKey = process.env.AICORE_SERVICE_KEY - if (envAICoreServiceKey) return envAICoreServiceKey - if (auth?.type === "api") { - process.env.AICORE_SERVICE_KEY = auth.key - return auth.key } - return undefined - }) - const deploymentId = process.env.AICORE_DEPLOYMENT_ID - const resourceGroup = process.env.AICORE_RESOURCE_GROUP - return { - autoload: !!envServiceKey, - options: envServiceKey ? { deploymentId, resourceGroup } : {}, - async getModel(sdk: any, modelID: string) { - return sdk(modelID) + return sdk.languageModel(modelID) + }, + } + }), + openrouter: () => + Effect.succeed({ + autoload: false, + options: { + headers: { + "HTTP-Referer": "https://opencode.ai/", + "X-Title": "opencode", }, - } + }, }), - zenmux: () => - Effect.succeed({ - autoload: false, - options: { - headers: { - "HTTP-Referer": "https://opencode.ai/", - "X-Title": "opencode", - }, + vercel: () => + Effect.succeed({ + autoload: false, + options: { + headers: { + "http-referer": "https://opencode.ai/", + "x-title": "opencode", }, - }), - gitlab: Effect.fnUntraced(function* (input: Info) { - const instanceUrl = (yield* dep.get("GITLAB_INSTANCE_URL")) || "https://gitlab.com" + }, + }), + "google-vertex": Effect.fnUntraced(function* (provider: Info) { + const env = yield* dep.env() + const project = + provider.options?.project ?? env["GOOGLE_CLOUD_PROJECT"] ?? env["GCP_PROJECT"] ?? env["GCLOUD_PROJECT"] - const auth = yield* dep.auth(input.id) - const apiKey = yield* Effect.sync(() => { - if (auth?.type === "oauth") return auth.access - if (auth?.type === "api") return auth.key - return undefined - }) - const token = apiKey ?? (yield* dep.get("GITLAB_TOKEN")) + const location = String( + provider.options?.location ?? + env["GOOGLE_VERTEX_LOCATION"] ?? + env["GOOGLE_CLOUD_LOCATION"] ?? + env["VERTEX_LOCATION"] ?? + "us-central1", + ) - const providerConfig = (yield* dep.config()).provider?.["gitlab"] + const autoload = Boolean(project) + if (!autoload) return { autoload: false } + return { + autoload: true, + vars(_options: Record) { + const endpoint = + location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com` + return { + ...(project && { GOOGLE_VERTEX_PROJECT: project }), + GOOGLE_VERTEX_LOCATION: location, + GOOGLE_VERTEX_ENDPOINT: endpoint, + } + }, + options: { + project, + location, + fetch: async (input: RequestInfo | URL, init?: RequestInit) => { + const auth = new GoogleAuth() + const client = await auth.getApplicationDefault() + const token = await client.credential.getAccessToken() - const aiGatewayHeaders = { - "User-Agent": `opencode/${Installation.VERSION} gitlab-ai-provider/${GITLAB_PROVIDER_VERSION} (${os.platform()} ${os.release()}; ${os.arch()})`, - "anthropic-beta": "context-1m-2025-08-07", - ...providerConfig?.options?.aiGatewayHeaders, + const headers = new Headers(init?.headers) + headers.set("Authorization", `Bearer ${token.token}`) + + return fetch(input, { ...init, headers }) + }, + }, + async getModel(sdk: any, modelID: string) { + const id = String(modelID).trim() + return sdk.languageModel(id) + }, + } + }), + "google-vertex-anthropic": Effect.fnUntraced(function* () { + const env = yield* dep.env() + const project = env["GOOGLE_CLOUD_PROJECT"] ?? env["GCP_PROJECT"] ?? env["GCLOUD_PROJECT"] + const location = env["GOOGLE_CLOUD_LOCATION"] ?? env["VERTEX_LOCATION"] ?? "global" + const autoload = Boolean(project) + if (!autoload) return { autoload: false } + return { + autoload: true, + options: { + project, + location, + }, + async getModel(sdk: any, modelID) { + const id = String(modelID).trim() + return sdk.languageModel(id) + }, + } + }), + "sap-ai-core": Effect.fnUntraced(function* () { + const auth = yield* dep.auth("sap-ai-core") + // TODO: Using process.env directly because Env.set only updates a shallow copy (not process.env), + // until the scope of the Env API is clarified (test only or runtime?) + const envServiceKey = iife(() => { + const envAICoreServiceKey = process.env.AICORE_SERVICE_KEY + if (envAICoreServiceKey) return envAICoreServiceKey + if (auth?.type === "api") { + process.env.AICORE_SERVICE_KEY = auth.key + return auth.key } + return undefined + }) + const deploymentId = process.env.AICORE_DEPLOYMENT_ID + const resourceGroup = process.env.AICORE_RESOURCE_GROUP - const featureFlags = { - duo_agent_platform_agentic_chat: true, - duo_agent_platform: true, - ...providerConfig?.options?.featureFlags, - } + return { + autoload: !!envServiceKey, + options: envServiceKey ? { deploymentId, resourceGroup } : {}, + async getModel(sdk: any, modelID: string) { + return sdk(modelID) + }, + } + }), + zenmux: () => + Effect.succeed({ + autoload: false, + options: { + headers: { + "HTTP-Referer": "https://opencode.ai/", + "X-Title": "opencode", + }, + }, + }), + gitlab: Effect.fnUntraced(function* (input: Info) { + const instanceUrl = (yield* dep.get("GITLAB_INSTANCE_URL")) || "https://gitlab.com" - return { - autoload: !!token, - options: { - instanceUrl, - apiKey: token, + const auth = yield* dep.auth(input.id) + const apiKey = yield* Effect.sync(() => { + if (auth?.type === "oauth") return auth.access + if (auth?.type === "api") return auth.key + return undefined + }) + const token = apiKey ?? (yield* dep.get("GITLAB_TOKEN")) + + const providerConfig = (yield* dep.config()).provider?.["gitlab"] + + const aiGatewayHeaders = { + "User-Agent": `opencode/${Installation.VERSION} gitlab-ai-provider/${GITLAB_PROVIDER_VERSION} (${os.platform()} ${os.release()}; ${os.arch()})`, + "anthropic-beta": "context-1m-2025-08-07", + ...providerConfig?.options?.aiGatewayHeaders, + } + + const featureFlags = { + duo_agent_platform_agentic_chat: true, + duo_agent_platform: true, + ...providerConfig?.options?.featureFlags, + } + + return { + autoload: !!token, + options: { + instanceUrl, + apiKey: token, + aiGatewayHeaders, + featureFlags, + }, + async getModel(sdk: ReturnType, modelID: string, options?: Record) { + if (modelID.startsWith("duo-workflow-")) { + const workflowRef = options?.workflowRef as string | undefined + // Use the static mapping if it exists, otherwise use duo-workflow with selectedModelRef + const sdkModelID = isWorkflowModel(modelID) ? modelID : "duo-workflow" + const model = sdk.workflowChat(sdkModelID, { + featureFlags, + workflowDefinition: options?.workflowDefinition as string | undefined, + }) + if (workflowRef) { + model.selectedModelRef = workflowRef + } + return model + } + return sdk.agenticChat(modelID, { aiGatewayHeaders, featureFlags, - }, - async getModel(sdk: ReturnType, modelID: string, options?: Record) { - if (modelID.startsWith("duo-workflow-")) { - const workflowRef = options?.workflowRef as string | undefined - // Use the static mapping if it exists, otherwise use duo-workflow with selectedModelRef - const sdkModelID = isWorkflowModel(modelID) ? modelID : "duo-workflow" - const model = sdk.workflowChat(sdkModelID, { - featureFlags, - workflowDefinition: options?.workflowDefinition as string | undefined, + }) + }, + async discoverModels(): Promise> { + if (!apiKey) { + log.info("gitlab model discovery skipped: no apiKey") + return {} + } + + try { + const token = apiKey + const getHeaders = (): Record => + auth?.type === "api" ? { "PRIVATE-TOKEN": token } : { Authorization: `Bearer ${token}` } + + log.info("gitlab model discovery starting", { instanceUrl }) + const result = await discoverWorkflowModels( + { instanceUrl, getHeaders }, + { workingDirectory: Instance.directory }, + ) + + if (!result.models.length) { + log.info("gitlab model discovery skipped: no models found", { + project: result.project + ? { + id: result.project.id, + path: result.project.pathWithNamespace, + } + : null, }) - if (workflowRef) { - model.selectedModelRef = workflowRef - } - return model - } - return sdk.agenticChat(modelID, { - aiGatewayHeaders, - featureFlags, - }) - }, - async discoverModels(): Promise> { - if (!apiKey) { - log.info("gitlab model discovery skipped: no apiKey") return {} } - try { - const token = apiKey - const getHeaders = (): Record => - auth?.type === "api" ? { "PRIVATE-TOKEN": token } : { Authorization: `Bearer ${token}` } - - log.info("gitlab model discovery starting", { instanceUrl }) - const result = await discoverWorkflowModels( - { instanceUrl, getHeaders }, - { workingDirectory: Instance.directory }, - ) - - if (!result.models.length) { - log.info("gitlab model discovery skipped: no models found", { - project: result.project - ? { - id: result.project.id, - path: result.project.pathWithNamespace, - } - : null, - }) - return {} - } - - const models: Record = {} - for (const m of result.models) { - if (!input.models[m.id]) { - models[m.id] = { - id: ModelID.make(m.id), - providerID: ProviderID.make("gitlab"), - name: `Agent Platform (${m.name})`, - family: "", - api: { - id: m.id, - url: instanceUrl, - npm: "gitlab-ai-provider", + const models: Record = {} + for (const m of result.models) { + if (!input.models[m.id]) { + models[m.id] = { + id: ModelID.make(m.id), + providerID: ProviderID.make("gitlab"), + name: `Agent Platform (${m.name})`, + family: "", + api: { + id: m.id, + url: instanceUrl, + npm: "gitlab-ai-provider", + }, + status: "active", + headers: {}, + options: { workflowRef: m.ref }, + cost: { input: 0, output: 0, cache: { read: 0, write: 0 } }, + limit: { context: m.context, output: m.output }, + capabilities: { + temperature: false, + reasoning: true, + attachment: true, + toolcall: true, + input: { + text: true, + audio: false, + image: true, + video: false, + pdf: true, }, - status: "active", - headers: {}, - options: { workflowRef: m.ref }, - cost: { input: 0, output: 0, cache: { read: 0, write: 0 } }, - limit: { context: m.context, output: m.output }, - capabilities: { - temperature: false, - reasoning: true, - attachment: true, - toolcall: true, - input: { - text: true, - audio: false, - image: true, - video: false, - pdf: true, - }, - output: { - text: true, - audio: false, - image: false, - video: false, - pdf: false, - }, - interleaved: false, + output: { + text: true, + audio: false, + image: false, + video: false, + pdf: false, }, - release_date: "", - variants: {}, - } + interleaved: false, + }, + release_date: "", + variants: {}, } } - - log.info("gitlab model discovery complete", { - count: Object.keys(models).length, - models: Object.keys(models), - }) - return models - } catch (e) { - log.warn("gitlab model discovery failed", { error: e }) - return {} } - }, - } - }), - "cloudflare-workers-ai": Effect.fnUntraced(function* (input: Info) { - // When baseURL is already configured (e.g. corporate config routing through a proxy/gateway), - // skip the account ID check because the URL is already fully specified. - if (input.options?.baseURL) return { autoload: false } - const auth = yield* dep.auth(input.id) - const env = yield* dep.env() - const accountId = env["CLOUDFLARE_ACCOUNT_ID"] || (auth?.type === "api" ? auth.metadata?.accountId : undefined) - if (!accountId) - return { - autoload: false, - async getModel() { - throw new Error( - "CLOUDFLARE_ACCOUNT_ID is missing. Set it with: export CLOUDFLARE_ACCOUNT_ID=", - ) - }, + log.info("gitlab model discovery complete", { + count: Object.keys(models).length, + models: Object.keys(models), + }) + return models + } catch (e) { + log.warn("gitlab model discovery failed", { error: e }) + return {} } + }, + } + }), + "cloudflare-workers-ai": Effect.fnUntraced(function* (input: Info) { + // When baseURL is already configured (e.g. corporate config routing through a proxy/gateway), + // skip the account ID check because the URL is already fully specified. + if (input.options?.baseURL) return { autoload: false } - const apiKey = yield* Effect.gen(function* () { - const envToken = env["CLOUDFLARE_API_KEY"] - if (envToken) return envToken - if (auth?.type === "api") return auth.key - return undefined - }) - + const auth = yield* dep.auth(input.id) + const env = yield* dep.env() + const accountId = env["CLOUDFLARE_ACCOUNT_ID"] || (auth?.type === "api" ? auth.metadata?.accountId : undefined) + if (!accountId) return { - autoload: !!apiKey, - options: { - apiKey, - headers: { - "User-Agent": `opencode/${Installation.VERSION} cloudflare-workers-ai (${os.platform()} ${os.release()}; ${os.arch()})`, - }, - }, - async getModel(sdk: any, modelID: string) { - return sdk.languageModel(modelID) - }, - vars(_options) { - return { - CLOUDFLARE_ACCOUNT_ID: accountId, - } + autoload: false, + async getModel() { + throw new Error( + "CLOUDFLARE_ACCOUNT_ID is missing. Set it with: export CLOUDFLARE_ACCOUNT_ID=", + ) }, } - }), - "cloudflare-ai-gateway": Effect.fnUntraced(function* (input: Info) { - // When baseURL is already configured (e.g. corporate config), skip the ID checks. - if (input.options?.baseURL) return { autoload: false } - const auth = yield* dep.auth(input.id) - const env = yield* dep.env() - const accountId = env["CLOUDFLARE_ACCOUNT_ID"] || (auth?.type === "api" ? auth.metadata?.accountId : undefined) - const gateway = env["CLOUDFLARE_GATEWAY_ID"] || (auth?.type === "api" ? auth.metadata?.gatewayId : undefined) + const apiKey = yield* Effect.gen(function* () { + const envToken = env["CLOUDFLARE_API_KEY"] + if (envToken) return envToken + if (auth?.type === "api") return auth.key + return undefined + }) - if (!accountId || !gateway) { - const missing = [ - !accountId ? "CLOUDFLARE_ACCOUNT_ID" : undefined, - !gateway ? "CLOUDFLARE_GATEWAY_ID" : undefined, - ].filter((x): x is string => Boolean(x)) - return { - autoload: false, - async getModel() { - throw new Error( - `${missing.join(" and ")} missing. Set with: ${missing.map((x) => `export ${x}=`).join(" && ")}`, - ) - }, - } - } - - // Get API token from env or auth - required for authenticated gateways - const apiToken = yield* Effect.gen(function* () { - const envToken = env["CLOUDFLARE_API_TOKEN"] || env["CF_AIG_TOKEN"] - if (envToken) return envToken - if (auth?.type === "api") return auth.key - return undefined - }) - - if (!apiToken) { - throw new Error( - "CLOUDFLARE_API_TOKEN (or CF_AIG_TOKEN) is required for Cloudflare AI Gateway. " + - "Set it via environment variable or run `opencode auth cloudflare-ai-gateway`.", - ) - } - - // Use official ai-gateway-provider package (v2.x for AI SDK v5 compatibility) - const { createAiGateway } = yield* Effect.promise(() => import("ai-gateway-provider")) - const { createUnified } = yield* Effect.promise(() => import("ai-gateway-provider/providers/unified")) - - const metadata = iife(() => { - if (input.options?.metadata) return input.options.metadata - try { - return JSON.parse(input.options?.headers?.["cf-aig-metadata"]) - } catch { - return undefined - } - }) - const opts = { - metadata, - cacheTtl: input.options?.cacheTtl, - cacheKey: input.options?.cacheKey, - skipCache: input.options?.skipCache, - collectLog: input.options?.collectLog, + return { + autoload: !!apiKey, + options: { + apiKey, headers: { - "User-Agent": `opencode/${Installation.VERSION} cloudflare-ai-gateway (${os.platform()} ${os.release()}; ${os.arch()})`, + "User-Agent": `opencode/${Installation.VERSION} cloudflare-workers-ai (${os.platform()} ${os.release()}; ${os.arch()})`, }, - } + }, + async getModel(sdk: any, modelID: string) { + return sdk.languageModel(modelID) + }, + vars(_options) { + return { + CLOUDFLARE_ACCOUNT_ID: accountId, + } + }, + } + }), + "cloudflare-ai-gateway": Effect.fnUntraced(function* (input: Info) { + // When baseURL is already configured (e.g. corporate config), skip the ID checks. + if (input.options?.baseURL) return { autoload: false } - const aigateway = createAiGateway({ - accountId, - gateway, - apiKey: apiToken, - ...(Object.values(opts).some((v) => v !== undefined) ? { options: opts } : {}), - }) - const unified = createUnified() + const auth = yield* dep.auth(input.id) + const env = yield* dep.env() + const accountId = env["CLOUDFLARE_ACCOUNT_ID"] || (auth?.type === "api" ? auth.metadata?.accountId : undefined) + const gateway = env["CLOUDFLARE_GATEWAY_ID"] || (auth?.type === "api" ? auth.metadata?.gatewayId : undefined) + if (!accountId || !gateway) { + const missing = [ + !accountId ? "CLOUDFLARE_ACCOUNT_ID" : undefined, + !gateway ? "CLOUDFLARE_GATEWAY_ID" : undefined, + ].filter((x): x is string => Boolean(x)) return { - autoload: true, - async getModel(_sdk: any, modelID: string, _options?: Record) { - // Model IDs use Unified API format: provider/model (e.g., "anthropic/claude-sonnet-4-5") - return aigateway(unified(modelID)) + autoload: false, + async getModel() { + throw new Error( + `${missing.join(" and ")} missing. Set with: ${missing.map((x) => `export ${x}=`).join(" && ")}`, + ) }, - options: {}, } - }), - cerebras: () => - Effect.succeed({ - autoload: false, - options: { - headers: { - "X-Cerebras-3rd-Party-Integration": "opencode", - }, - }, - }), - kilo: () => - Effect.succeed({ - autoload: false, - options: { - headers: { - "HTTP-Referer": "https://opencode.ai/", - "X-Title": "opencode", - }, - }, - }), - } - } + } - export const Model = z - .object({ - id: ModelID.zod, - providerID: ProviderID.zod, - api: z.object({ - id: z.string(), - url: z.string(), - npm: z.string(), + // Get API token from env or auth - required for authenticated gateways + const apiToken = yield* Effect.gen(function* () { + const envToken = env["CLOUDFLARE_API_TOKEN"] || env["CF_AIG_TOKEN"] + if (envToken) return envToken + if (auth?.type === "api") return auth.key + return undefined + }) + + if (!apiToken) { + throw new Error( + "CLOUDFLARE_API_TOKEN (or CF_AIG_TOKEN) is required for Cloudflare AI Gateway. " + + "Set it via environment variable or run `opencode auth cloudflare-ai-gateway`.", + ) + } + + // Use official ai-gateway-provider package (v2.x for AI SDK v5 compatibility) + const { createAiGateway } = yield* Effect.promise(() => import("ai-gateway-provider")) + const { createUnified } = yield* Effect.promise(() => import("ai-gateway-provider/providers/unified")) + + const metadata = iife(() => { + if (input.options?.metadata) return input.options.metadata + try { + return JSON.parse(input.options?.headers?.["cf-aig-metadata"]) + } catch { + return undefined + } + }) + const opts = { + metadata, + cacheTtl: input.options?.cacheTtl, + cacheKey: input.options?.cacheKey, + skipCache: input.options?.skipCache, + collectLog: input.options?.collectLog, + headers: { + "User-Agent": `opencode/${Installation.VERSION} cloudflare-ai-gateway (${os.platform()} ${os.release()}; ${os.arch()})`, + }, + } + + const aigateway = createAiGateway({ + accountId, + gateway, + apiKey: apiToken, + ...(Object.values(opts).some((v) => v !== undefined) ? { options: opts } : {}), + }) + const unified = createUnified() + + return { + autoload: true, + async getModel(_sdk: any, modelID: string, _options?: Record) { + // Model IDs use Unified API format: provider/model (e.g., "anthropic/claude-sonnet-4-5") + return aigateway(unified(modelID)) + }, + options: {}, + } + }), + cerebras: () => + Effect.succeed({ + autoload: false, + options: { + headers: { + "X-Cerebras-3rd-Party-Integration": "opencode", + }, + }, }), - name: z.string(), - family: z.string().optional(), - capabilities: z.object({ - temperature: z.boolean(), - reasoning: z.boolean(), - attachment: z.boolean(), - toolcall: z.boolean(), - input: z.object({ - text: z.boolean(), - audio: z.boolean(), - image: z.boolean(), - video: z.boolean(), - pdf: z.boolean(), + kilo: () => + Effect.succeed({ + autoload: false, + options: { + headers: { + "HTTP-Referer": "https://opencode.ai/", + "X-Title": "opencode", + }, + }, + }), + } +} + +export const Model = z + .object({ + id: ModelID.zod, + providerID: ProviderID.zod, + api: z.object({ + id: z.string(), + url: z.string(), + npm: z.string(), + }), + name: z.string(), + family: z.string().optional(), + capabilities: z.object({ + temperature: z.boolean(), + reasoning: z.boolean(), + attachment: z.boolean(), + toolcall: z.boolean(), + input: z.object({ + text: z.boolean(), + audio: z.boolean(), + image: z.boolean(), + video: z.boolean(), + pdf: z.boolean(), + }), + output: z.object({ + text: z.boolean(), + audio: z.boolean(), + image: z.boolean(), + video: z.boolean(), + pdf: z.boolean(), + }), + interleaved: z.union([ + z.boolean(), + z.object({ + field: z.enum(["reasoning_content", "reasoning_details"]), }), - output: z.object({ - text: z.boolean(), - audio: z.boolean(), - image: z.boolean(), - video: z.boolean(), - pdf: z.boolean(), - }), - interleaved: z.union([ - z.boolean(), - z.object({ - field: z.enum(["reasoning_content", "reasoning_details"]), + ]), + }), + cost: z.object({ + input: z.number(), + output: z.number(), + cache: z.object({ + read: z.number(), + write: z.number(), + }), + experimentalOver200K: z + .object({ + input: z.number(), + output: z.number(), + cache: z.object({ + read: z.number(), + write: z.number(), }), - ]), - }), - cost: z.object({ - input: z.number(), - output: z.number(), - cache: z.object({ - read: z.number(), - write: z.number(), - }), - experimentalOver200K: z - .object({ - input: z.number(), - output: z.number(), - cache: z.object({ - read: z.number(), - write: z.number(), - }), - }) - .optional(), - }), - limit: z.object({ - context: z.number(), - input: z.number().optional(), - output: z.number(), - }), - status: z.enum(["alpha", "beta", "deprecated", "active"]), - options: z.record(z.string(), z.any()), - headers: z.record(z.string(), z.string()), - release_date: z.string(), - variants: z.record(z.string(), z.record(z.string(), z.any())).optional(), - }) - .meta({ - ref: "Model", - }) - export type Model = z.infer + }) + .optional(), + }), + limit: z.object({ + context: z.number(), + input: z.number().optional(), + output: z.number(), + }), + status: z.enum(["alpha", "beta", "deprecated", "active"]), + options: z.record(z.string(), z.any()), + headers: z.record(z.string(), z.string()), + release_date: z.string(), + variants: z.record(z.string(), z.record(z.string(), z.any())).optional(), + }) + .meta({ + ref: "Model", + }) +export type Model = z.infer - export const Info = z - .object({ - id: ProviderID.zod, - name: z.string(), - source: z.enum(["env", "config", "custom", "api"]), - env: z.string().array(), - key: z.string().optional(), - options: z.record(z.string(), z.any()), - models: z.record(z.string(), Model), - }) - .meta({ - ref: "Provider", - }) - export type Info = z.infer +export const Info = z + .object({ + id: ProviderID.zod, + name: z.string(), + source: z.enum(["env", "config", "custom", "api"]), + env: z.string().array(), + key: z.string().optional(), + options: z.record(z.string(), z.any()), + models: z.record(z.string(), Model), + }) + .meta({ + ref: "Provider", + }) +export type Info = z.infer - export interface Interface { - readonly list: () => Effect.Effect> - readonly getProvider: (providerID: ProviderID) => Effect.Effect - readonly getModel: (providerID: ProviderID, modelID: ModelID) => Effect.Effect - readonly getLanguage: (model: Model) => Effect.Effect - readonly closest: ( - providerID: ProviderID, - query: string[], - ) => Effect.Effect<{ providerID: ProviderID; modelID: string } | undefined> - readonly getSmallModel: (providerID: ProviderID) => Effect.Effect - readonly defaultModel: () => Effect.Effect<{ providerID: ProviderID; modelID: ModelID }> +export interface Interface { + readonly list: () => Effect.Effect> + readonly getProvider: (providerID: ProviderID) => Effect.Effect + readonly getModel: (providerID: ProviderID, modelID: ModelID) => Effect.Effect + readonly getLanguage: (model: Model) => Effect.Effect + readonly closest: ( + providerID: ProviderID, + query: string[], + ) => Effect.Effect<{ providerID: ProviderID; modelID: string } | undefined> + readonly getSmallModel: (providerID: ProviderID) => Effect.Effect + readonly defaultModel: () => Effect.Effect<{ providerID: ProviderID; modelID: ModelID }> +} + +interface State { + models: Map + providers: Record + sdk: Map + modelLoaders: Record + varsLoaders: Record +} + +export class Service extends Context.Service()("@opencode/Provider") {} + +function cost(c: ModelsDev.Model["cost"]): Model["cost"] { + const result: Model["cost"] = { + input: c?.input ?? 0, + output: c?.output ?? 0, + cache: { + read: c?.cache_read ?? 0, + write: c?.cache_write ?? 0, + }, } - - interface State { - models: Map - providers: Record - sdk: Map - modelLoaders: Record - varsLoaders: Record - } - - export class Service extends Context.Service()("@opencode/Provider") {} - - function cost(c: ModelsDev.Model["cost"]): Model["cost"] { - const result: Model["cost"] = { - input: c?.input ?? 0, - output: c?.output ?? 0, + if (c?.context_over_200k) { + result.experimentalOver200K = { cache: { - read: c?.cache_read ?? 0, - write: c?.cache_write ?? 0, + read: c.context_over_200k.cache_read ?? 0, + write: c.context_over_200k.cache_write ?? 0, }, - } - if (c?.context_over_200k) { - result.experimentalOver200K = { - cache: { - read: c.context_over_200k.cache_read ?? 0, - write: c.context_over_200k.cache_write ?? 0, - }, - input: c.context_over_200k.input, - output: c.context_over_200k.output, - } - } - return result - } - - function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model { - const m: Model = { - id: ModelID.make(model.id), - providerID: ProviderID.make(provider.id), - name: model.name, - family: model.family, - api: { - id: model.id, - url: model.provider?.api ?? provider.api!, - npm: model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible", - }, - status: model.status ?? "active", - headers: {}, - options: {}, - cost: cost(model.cost), - limit: { - context: model.limit.context, - input: model.limit.input, - output: model.limit.output, - }, - capabilities: { - temperature: model.temperature, - reasoning: model.reasoning, - attachment: model.attachment, - toolcall: model.tool_call, - input: { - text: model.modalities?.input?.includes("text") ?? false, - audio: model.modalities?.input?.includes("audio") ?? false, - image: model.modalities?.input?.includes("image") ?? false, - video: model.modalities?.input?.includes("video") ?? false, - pdf: model.modalities?.input?.includes("pdf") ?? false, - }, - output: { - text: model.modalities?.output?.includes("text") ?? false, - audio: model.modalities?.output?.includes("audio") ?? false, - image: model.modalities?.output?.includes("image") ?? false, - video: model.modalities?.output?.includes("video") ?? false, - pdf: model.modalities?.output?.includes("pdf") ?? false, - }, - interleaved: model.interleaved ?? false, - }, - release_date: model.release_date, - variants: {}, - } - - m.variants = mapValues(ProviderTransform.variants(m), (v) => v) - - return m - } - - export function fromModelsDevProvider(provider: ModelsDev.Provider): Info { - const models: Record = {} - for (const [key, model] of Object.entries(provider.models)) { - models[key] = fromModelsDevModel(provider, model) - for (const [mode, opts] of Object.entries(model.experimental?.modes ?? {})) { - const id = `${model.id}-${mode}` - const m = fromModelsDevModel(provider, model) - m.id = ModelID.make(id) - m.name = `${model.name} ${mode[0].toUpperCase()}${mode.slice(1)}` - if (opts.cost) m.cost = mergeDeep(m.cost, cost(opts.cost)) - // convert body params to camelCase for ai sdk compatibility - if (opts.provider?.body) - m.options = Object.fromEntries( - Object.entries(opts.provider.body).map(([k, v]) => [k.replace(/_([a-z])/g, (_, c) => c.toUpperCase()), v]), - ) - if (opts.provider?.headers) m.headers = opts.provider.headers - models[id] = m - } - } - return { - id: ProviderID.make(provider.id), - source: "custom", - name: provider.name, - env: provider.env ?? [], - options: {}, - models, + input: c.context_over_200k.input, + output: c.context_over_200k.output, } } + return result +} - const layer: Layer.Layer< - Service, - never, - Config.Service | Auth.Service | Plugin.Service | AppFileSystem.Service | Env.Service - > = Layer.effect( - Service, - Effect.gen(function* () { - const fs = yield* AppFileSystem.Service - const config = yield* Config.Service - const auth = yield* Auth.Service - const env = yield* Env.Service - const plugin = yield* Plugin.Service +function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model { + const m: Model = { + id: ModelID.make(model.id), + providerID: ProviderID.make(provider.id), + name: model.name, + family: model.family, + api: { + id: model.id, + url: model.provider?.api ?? provider.api!, + npm: model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible", + }, + status: model.status ?? "active", + headers: {}, + options: {}, + cost: cost(model.cost), + limit: { + context: model.limit.context, + input: model.limit.input, + output: model.limit.output, + }, + capabilities: { + temperature: model.temperature, + reasoning: model.reasoning, + attachment: model.attachment, + toolcall: model.tool_call, + input: { + text: model.modalities?.input?.includes("text") ?? false, + audio: model.modalities?.input?.includes("audio") ?? false, + image: model.modalities?.input?.includes("image") ?? false, + video: model.modalities?.input?.includes("video") ?? false, + pdf: model.modalities?.input?.includes("pdf") ?? false, + }, + output: { + text: model.modalities?.output?.includes("text") ?? false, + audio: model.modalities?.output?.includes("audio") ?? false, + image: model.modalities?.output?.includes("image") ?? false, + video: model.modalities?.output?.includes("video") ?? false, + pdf: model.modalities?.output?.includes("pdf") ?? false, + }, + interleaved: model.interleaved ?? false, + }, + release_date: model.release_date, + variants: {}, + } - const state = yield* InstanceState.make(() => - Effect.gen(function* () { - using _ = log.time("state") - const bridge = yield* EffectBridge.make() - const cfg = yield* config.get() - const modelsDev = yield* Effect.promise(() => ModelsDev.get()) - const database = mapValues(modelsDev, fromModelsDevProvider) + m.variants = mapValues(ProviderTransform.variants(m), (v) => v) - const providers: Record = {} as Record - const languages = new Map() - const modelLoaders: { - [providerID: string]: CustomModelLoader - } = {} - const varsLoaders: { - [providerID: string]: CustomVarsLoader - } = {} - const sdk = new Map() - const discoveryLoaders: { - [providerID: string]: CustomDiscoverModels - } = {} - const dep = { - auth: (id: string) => auth.get(id).pipe(Effect.orDie), - config: () => config.get(), - env: () => env.all(), - get: (key: string) => env.get(key), - } + return m +} - log.info("init") +export function fromModelsDevProvider(provider: ModelsDev.Provider): Info { + const models: Record = {} + for (const [key, model] of Object.entries(provider.models)) { + models[key] = fromModelsDevModel(provider, model) + for (const [mode, opts] of Object.entries(model.experimental?.modes ?? {})) { + const id = `${model.id}-${mode}` + const m = fromModelsDevModel(provider, model) + m.id = ModelID.make(id) + m.name = `${model.name} ${mode[0].toUpperCase()}${mode.slice(1)}` + if (opts.cost) m.cost = mergeDeep(m.cost, cost(opts.cost)) + // convert body params to camelCase for ai sdk compatibility + if (opts.provider?.body) + m.options = Object.fromEntries( + Object.entries(opts.provider.body).map(([k, v]) => [k.replace(/_([a-z])/g, (_, c) => c.toUpperCase()), v]), + ) + if (opts.provider?.headers) m.headers = opts.provider.headers + models[id] = m + } + } + return { + id: ProviderID.make(provider.id), + source: "custom", + name: provider.name, + env: provider.env ?? [], + options: {}, + models, + } +} - function mergeProvider(providerID: ProviderID, provider: Partial) { - const existing = providers[providerID] - if (existing) { - // @ts-expect-error - providers[providerID] = mergeDeep(existing, provider) - return - } - const match = database[providerID] - if (!match) return +const layer: Layer.Layer< + Service, + never, + Config.Service | Auth.Service | Plugin.Service | AppFileSystem.Service | Env.Service +> = Layer.effect( + Service, + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + const config = yield* Config.Service + const auth = yield* Auth.Service + const env = yield* Env.Service + const plugin = yield* Plugin.Service + + const state = yield* InstanceState.make(() => + Effect.gen(function* () { + using _ = log.time("state") + const bridge = yield* EffectBridge.make() + const cfg = yield* config.get() + const modelsDev = yield* Effect.promise(() => ModelsDev.get()) + const database = mapValues(modelsDev, fromModelsDevProvider) + + const providers: Record = {} as Record + const languages = new Map() + const modelLoaders: { + [providerID: string]: CustomModelLoader + } = {} + const varsLoaders: { + [providerID: string]: CustomVarsLoader + } = {} + const sdk = new Map() + const discoveryLoaders: { + [providerID: string]: CustomDiscoverModels + } = {} + const dep = { + auth: (id: string) => auth.get(id).pipe(Effect.orDie), + config: () => config.get(), + env: () => env.all(), + get: (key: string) => env.get(key), + } + + log.info("init") + + function mergeProvider(providerID: ProviderID, provider: Partial) { + const existing = providers[providerID] + if (existing) { // @ts-expect-error - providers[providerID] = mergeDeep(match, provider) + providers[providerID] = mergeDeep(existing, provider) + return + } + const match = database[providerID] + if (!match) return + // @ts-expect-error + providers[providerID] = mergeDeep(match, provider) + } + + // load plugins first so config() hook runs before reading cfg.provider + const plugins = yield* plugin.list() + + // now read config providers - includes any modifications from plugin config() hook + const configProviders = Object.entries(cfg.provider ?? {}) + const disabled = new Set(cfg.disabled_providers ?? []) + const enabled = cfg.enabled_providers ? new Set(cfg.enabled_providers) : null + + function isProviderAllowed(providerID: ProviderID): boolean { + if (enabled && !enabled.has(providerID)) return false + if (disabled.has(providerID)) return false + return true + } + + // extend database from config + for (const [providerID, provider] of configProviders) { + const existing = database[providerID] + const parsed: Info = { + id: ProviderID.make(providerID), + name: provider.name ?? existing?.name ?? providerID, + env: provider.env ?? existing?.env ?? [], + options: mergeDeep(existing?.options ?? {}, provider.options ?? {}), + source: "config", + models: existing?.models ?? {}, } - // load plugins first so config() hook runs before reading cfg.provider - const plugins = yield* plugin.list() - - // now read config providers - includes any modifications from plugin config() hook - const configProviders = Object.entries(cfg.provider ?? {}) - const disabled = new Set(cfg.disabled_providers ?? []) - const enabled = cfg.enabled_providers ? new Set(cfg.enabled_providers) : null - - function isProviderAllowed(providerID: ProviderID): boolean { - if (enabled && !enabled.has(providerID)) return false - if (disabled.has(providerID)) return false - return true - } - - // extend database from config - for (const [providerID, provider] of configProviders) { - const existing = database[providerID] - const parsed: Info = { - id: ProviderID.make(providerID), - name: provider.name ?? existing?.name ?? providerID, - env: provider.env ?? existing?.env ?? [], - options: mergeDeep(existing?.options ?? {}, provider.options ?? {}), - source: "config", - models: existing?.models ?? {}, + for (const [modelID, model] of Object.entries(provider.models ?? {})) { + const existingModel = parsed.models[model.id ?? modelID] + const name = iife(() => { + if (model.name) return model.name + if (model.id && model.id !== modelID) return modelID + return existingModel?.name ?? modelID + }) + const parsedModel: Model = { + id: ModelID.make(modelID), + api: { + id: model.id ?? existingModel?.api.id ?? modelID, + npm: + model.provider?.npm ?? + provider.npm ?? + existingModel?.api.npm ?? + modelsDev[providerID]?.npm ?? + "@ai-sdk/openai-compatible", + url: model.provider?.api ?? provider?.api ?? existingModel?.api.url ?? modelsDev[providerID]?.api, + }, + status: model.status ?? existingModel?.status ?? "active", + name, + providerID: ProviderID.make(providerID), + capabilities: { + temperature: model.temperature ?? existingModel?.capabilities.temperature ?? false, + reasoning: model.reasoning ?? existingModel?.capabilities.reasoning ?? false, + attachment: model.attachment ?? existingModel?.capabilities.attachment ?? false, + toolcall: model.tool_call ?? existingModel?.capabilities.toolcall ?? true, + input: { + text: model.modalities?.input?.includes("text") ?? existingModel?.capabilities.input.text ?? true, + audio: + model.modalities?.input?.includes("audio") ?? existingModel?.capabilities.input.audio ?? false, + image: + model.modalities?.input?.includes("image") ?? existingModel?.capabilities.input.image ?? false, + video: + model.modalities?.input?.includes("video") ?? existingModel?.capabilities.input.video ?? false, + pdf: model.modalities?.input?.includes("pdf") ?? existingModel?.capabilities.input.pdf ?? false, + }, + output: { + text: model.modalities?.output?.includes("text") ?? existingModel?.capabilities.output.text ?? true, + audio: + model.modalities?.output?.includes("audio") ?? existingModel?.capabilities.output.audio ?? false, + image: + model.modalities?.output?.includes("image") ?? existingModel?.capabilities.output.image ?? false, + video: + model.modalities?.output?.includes("video") ?? existingModel?.capabilities.output.video ?? false, + pdf: model.modalities?.output?.includes("pdf") ?? existingModel?.capabilities.output.pdf ?? false, + }, + interleaved: model.interleaved ?? false, + }, + cost: { + input: model?.cost?.input ?? existingModel?.cost?.input ?? 0, + output: model?.cost?.output ?? existingModel?.cost?.output ?? 0, + cache: { + read: model?.cost?.cache_read ?? existingModel?.cost?.cache.read ?? 0, + write: model?.cost?.cache_write ?? existingModel?.cost?.cache.write ?? 0, + }, + }, + options: mergeDeep(existingModel?.options ?? {}, model.options ?? {}), + limit: { + context: model.limit?.context ?? existingModel?.limit?.context ?? 0, + input: model.limit?.input ?? existingModel?.limit?.input, + output: model.limit?.output ?? existingModel?.limit?.output ?? 0, + }, + headers: mergeDeep(existingModel?.headers ?? {}, model.headers ?? {}), + family: model.family ?? existingModel?.family ?? "", + release_date: model.release_date ?? existingModel?.release_date ?? "", + variants: {}, } + const merged = mergeDeep(ProviderTransform.variants(parsedModel), model.variants ?? {}) + parsedModel.variants = mapValues( + pickBy(merged, (v) => !v.disabled), + (v) => omit(v, ["disabled"]), + ) + parsed.models[modelID] = parsedModel + } + database[providerID] = parsed + } - for (const [modelID, model] of Object.entries(provider.models ?? {})) { - const existingModel = parsed.models[model.id ?? modelID] - const name = iife(() => { - if (model.name) return model.name - if (model.id && model.id !== modelID) return modelID - return existingModel?.name ?? modelID - }) - const parsedModel: Model = { - id: ModelID.make(modelID), - api: { - id: model.id ?? existingModel?.api.id ?? modelID, - npm: - model.provider?.npm ?? - provider.npm ?? - existingModel?.api.npm ?? - modelsDev[providerID]?.npm ?? - "@ai-sdk/openai-compatible", - url: model.provider?.api ?? provider?.api ?? existingModel?.api.url ?? modelsDev[providerID]?.api, - }, - status: model.status ?? existingModel?.status ?? "active", - name, - providerID: ProviderID.make(providerID), - capabilities: { - temperature: model.temperature ?? existingModel?.capabilities.temperature ?? false, - reasoning: model.reasoning ?? existingModel?.capabilities.reasoning ?? false, - attachment: model.attachment ?? existingModel?.capabilities.attachment ?? false, - toolcall: model.tool_call ?? existingModel?.capabilities.toolcall ?? true, - input: { - text: model.modalities?.input?.includes("text") ?? existingModel?.capabilities.input.text ?? true, - audio: - model.modalities?.input?.includes("audio") ?? existingModel?.capabilities.input.audio ?? false, - image: - model.modalities?.input?.includes("image") ?? existingModel?.capabilities.input.image ?? false, - video: - model.modalities?.input?.includes("video") ?? existingModel?.capabilities.input.video ?? false, - pdf: model.modalities?.input?.includes("pdf") ?? existingModel?.capabilities.input.pdf ?? false, - }, - output: { - text: model.modalities?.output?.includes("text") ?? existingModel?.capabilities.output.text ?? true, - audio: - model.modalities?.output?.includes("audio") ?? existingModel?.capabilities.output.audio ?? false, - image: - model.modalities?.output?.includes("image") ?? existingModel?.capabilities.output.image ?? false, - video: - model.modalities?.output?.includes("video") ?? existingModel?.capabilities.output.video ?? false, - pdf: model.modalities?.output?.includes("pdf") ?? existingModel?.capabilities.output.pdf ?? false, - }, - interleaved: model.interleaved ?? false, - }, - cost: { - input: model?.cost?.input ?? existingModel?.cost?.input ?? 0, - output: model?.cost?.output ?? existingModel?.cost?.output ?? 0, - cache: { - read: model?.cost?.cache_read ?? existingModel?.cost?.cache.read ?? 0, - write: model?.cost?.cache_write ?? existingModel?.cost?.cache.write ?? 0, - }, - }, - options: mergeDeep(existingModel?.options ?? {}, model.options ?? {}), - limit: { - context: model.limit?.context ?? existingModel?.limit?.context ?? 0, - input: model.limit?.input ?? existingModel?.limit?.input, - output: model.limit?.output ?? existingModel?.limit?.output ?? 0, - }, - headers: mergeDeep(existingModel?.headers ?? {}, model.headers ?? {}), - family: model.family ?? existingModel?.family ?? "", - release_date: model.release_date ?? existingModel?.release_date ?? "", - variants: {}, + // load env + const envs = yield* env.all() + for (const [id, provider] of Object.entries(database)) { + const providerID = ProviderID.make(id) + if (disabled.has(providerID)) continue + const apiKey = provider.env.map((item) => envs[item]).find(Boolean) + if (!apiKey) continue + mergeProvider(providerID, { + source: "env", + key: provider.env.length === 1 ? apiKey : undefined, + }) + } + + // load apikeys + const auths = yield* auth.all().pipe(Effect.orDie) + for (const [id, provider] of Object.entries(auths)) { + const providerID = ProviderID.make(id) + if (disabled.has(providerID)) continue + if (provider.type === "api") { + mergeProvider(providerID, { + source: "api", + key: provider.key, + }) + } + } + + // plugin auth loader - database now has entries for config providers + for (const plugin of plugins) { + if (!plugin.auth) continue + const providerID = ProviderID.make(plugin.auth.provider) + if (disabled.has(providerID)) continue + + const stored = yield* auth.get(providerID).pipe(Effect.orDie) + if (!stored) continue + if (!plugin.auth.loader) continue + + const options = yield* Effect.promise(() => + plugin.auth!.loader!( + () => bridge.promise(auth.get(providerID).pipe(Effect.orDie)) as any, + database[plugin.auth!.provider], + ), + ) + const opts = options ?? {} + const patch: Partial = providers[providerID] ? { options: opts } : { source: "custom", options: opts } + mergeProvider(providerID, patch) + } + + for (const [id, fn] of Object.entries(custom(dep))) { + const providerID = ProviderID.make(id) + if (disabled.has(providerID)) continue + const data = database[providerID] + if (!data) { + log.error("Provider does not exist in model list " + providerID) + continue + } + const result = yield* fn(data) + if (result && (result.autoload || providers[providerID])) { + if (result.getModel) modelLoaders[providerID] = result.getModel + if (result.vars) varsLoaders[providerID] = result.vars + if (result.discoverModels) discoveryLoaders[providerID] = result.discoverModels + const opts = result.options ?? {} + const patch: Partial = providers[providerID] + ? { options: opts } + : { source: "custom", options: opts } + mergeProvider(providerID, patch) + } + } + + // load config - re-apply with updated data + for (const [id, provider] of configProviders) { + const providerID = ProviderID.make(id) + const partial: Partial = { source: "config" } + if (provider.env) partial.env = provider.env + if (provider.name) partial.name = provider.name + if (provider.options) partial.options = provider.options + mergeProvider(providerID, partial) + } + + const gitlab = ProviderID.make("gitlab") + if (discoveryLoaders[gitlab] && providers[gitlab] && isProviderAllowed(gitlab)) { + yield* Effect.promise(async () => { + try { + const discovered = await discoveryLoaders[gitlab]() + for (const [modelID, model] of Object.entries(discovered)) { + if (!providers[gitlab].models[modelID]) { + providers[gitlab].models[modelID] = model + } } - const merged = mergeDeep(ProviderTransform.variants(parsedModel), model.variants ?? {}) - parsedModel.variants = mapValues( + } catch (e) { + log.warn("state discovery error", { id: "gitlab", error: e }) + } + }) + } + + for (const hook of plugins) { + const p = hook.provider + const models = p?.models + if (!p || !models) continue + + const providerID = ProviderID.make(p.id) + if (disabled.has(providerID)) continue + + const provider = providers[providerID] + if (!provider) continue + const pluginAuth = yield* auth.get(providerID).pipe(Effect.orDie) + + provider.models = yield* Effect.promise(async () => { + const next = await models(provider, { auth: pluginAuth }) + return Object.fromEntries( + Object.entries(next).map(([id, model]) => [ + id, + { + ...model, + id: ModelID.make(id), + providerID, + }, + ]), + ) + }) + } + + for (const [id, provider] of Object.entries(providers)) { + const providerID = ProviderID.make(id) + if (!isProviderAllowed(providerID)) { + delete providers[providerID] + continue + } + + const configProvider = cfg.provider?.[providerID] + + for (const [modelID, model] of Object.entries(provider.models)) { + model.api.id = model.api.id ?? model.id ?? modelID + if ( + modelID === "gpt-5-chat-latest" || + (providerID === ProviderID.openrouter && modelID === "openai/gpt-5-chat") + ) + delete provider.models[modelID] + if (model.status === "alpha" && !Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS) delete provider.models[modelID] + if (model.status === "deprecated") delete provider.models[modelID] + if ( + (configProvider?.blacklist && configProvider.blacklist.includes(modelID)) || + (configProvider?.whitelist && !configProvider.whitelist.includes(modelID)) + ) + delete provider.models[modelID] + + model.variants = mapValues(ProviderTransform.variants(model), (v) => v) + + const configVariants = configProvider?.models?.[modelID]?.variants + if (configVariants && model.variants) { + const merged = mergeDeep(model.variants, configVariants) + model.variants = mapValues( pickBy(merged, (v) => !v.disabled), (v) => omit(v, ["disabled"]), ) - parsed.models[modelID] = parsedModel - } - database[providerID] = parsed - } - - // load env - const envs = yield* env.all() - for (const [id, provider] of Object.entries(database)) { - const providerID = ProviderID.make(id) - if (disabled.has(providerID)) continue - const apiKey = provider.env.map((item) => envs[item]).find(Boolean) - if (!apiKey) continue - mergeProvider(providerID, { - source: "env", - key: provider.env.length === 1 ? apiKey : undefined, - }) - } - - // load apikeys - const auths = yield* auth.all().pipe(Effect.orDie) - for (const [id, provider] of Object.entries(auths)) { - const providerID = ProviderID.make(id) - if (disabled.has(providerID)) continue - if (provider.type === "api") { - mergeProvider(providerID, { - source: "api", - key: provider.key, - }) } } - // plugin auth loader - database now has entries for config providers - for (const plugin of plugins) { - if (!plugin.auth) continue - const providerID = ProviderID.make(plugin.auth.provider) - if (disabled.has(providerID)) continue - - const stored = yield* auth.get(providerID).pipe(Effect.orDie) - if (!stored) continue - if (!plugin.auth.loader) continue - - const options = yield* Effect.promise(() => - plugin.auth!.loader!( - () => bridge.promise(auth.get(providerID).pipe(Effect.orDie)) as any, - database[plugin.auth!.provider], - ), - ) - const opts = options ?? {} - const patch: Partial = providers[providerID] ? { options: opts } : { source: "custom", options: opts } - mergeProvider(providerID, patch) + if (Object.keys(provider.models).length === 0) { + delete providers[providerID] + continue } - for (const [id, fn] of Object.entries(custom(dep))) { - const providerID = ProviderID.make(id) - if (disabled.has(providerID)) continue - const data = database[providerID] - if (!data) { - log.error("Provider does not exist in model list " + providerID) - continue - } - const result = yield* fn(data) - if (result && (result.autoload || providers[providerID])) { - if (result.getModel) modelLoaders[providerID] = result.getModel - if (result.vars) varsLoaders[providerID] = result.vars - if (result.discoverModels) discoveryLoaders[providerID] = result.discoverModels - const opts = result.options ?? {} - const patch: Partial = providers[providerID] - ? { options: opts } - : { source: "custom", options: opts } - mergeProvider(providerID, patch) + log.info("found", { providerID }) + } + + return { + models: languages, + providers, + sdk, + modelLoaders, + varsLoaders, + } + }), + ) + + const list = Effect.fn("Provider.list")(() => InstanceState.use(state, (s) => s.providers)) + + async function resolveSDK(model: Model, s: State, envs: Record) { + try { + using _ = log.time("getSDK", { + providerID: model.providerID, + }) + const provider = s.providers[model.providerID] + const options = { ...provider.options } + + if (model.providerID === "google-vertex" && !model.api.npm.includes("@ai-sdk/openai-compatible")) { + delete options.fetch + } + + if (model.api.npm.includes("@ai-sdk/openai-compatible") && options["includeUsage"] !== false) { + options["includeUsage"] = true + } + + const baseURL = iife(() => { + let url = + typeof options["baseURL"] === "string" && options["baseURL"] !== "" ? options["baseURL"] : model.api.url + if (!url) return + + const loader = s.varsLoaders[model.providerID] + if (loader) { + const vars = loader(options) + for (const [key, value] of Object.entries(vars)) { + const field = "${" + key + "}" + url = url.replaceAll(field, value) } } - // load config - re-apply with updated data - for (const [id, provider] of configProviders) { - const providerID = ProviderID.make(id) - const partial: Partial = { source: "config" } - if (provider.env) partial.env = provider.env - if (provider.name) partial.name = provider.name - if (provider.options) partial.options = provider.options - mergeProvider(providerID, partial) + url = url.replace(/\$\{([^}]+)\}/g, (item, key) => { + const val = envs[String(key)] + return val ?? item + }) + return url + }) + + if (baseURL !== undefined) options["baseURL"] = baseURL + if (options["apiKey"] === undefined && provider.key) options["apiKey"] = provider.key + if (model.headers) + options["headers"] = { + ...options["headers"], + ...model.headers, } - const gitlab = ProviderID.make("gitlab") - if (discoveryLoaders[gitlab] && providers[gitlab] && isProviderAllowed(gitlab)) { - yield* Effect.promise(async () => { - try { - const discovered = await discoveryLoaders[gitlab]() - for (const [modelID, model] of Object.entries(discovered)) { - if (!providers[gitlab].models[modelID]) { - providers[gitlab].models[modelID] = model - } - } - } catch (e) { - log.warn("state discovery error", { id: "gitlab", error: e }) - } - }) - } - - for (const hook of plugins) { - const p = hook.provider - const models = p?.models - if (!p || !models) continue - - const providerID = ProviderID.make(p.id) - if (disabled.has(providerID)) continue - - const provider = providers[providerID] - if (!provider) continue - const pluginAuth = yield* auth.get(providerID).pipe(Effect.orDie) - - provider.models = yield* Effect.promise(async () => { - const next = await models(provider, { auth: pluginAuth }) - return Object.fromEntries( - Object.entries(next).map(([id, model]) => [ - id, - { - ...model, - id: ModelID.make(id), - providerID, - }, - ]), - ) - }) - } - - for (const [id, provider] of Object.entries(providers)) { - const providerID = ProviderID.make(id) - if (!isProviderAllowed(providerID)) { - delete providers[providerID] - continue - } - - const configProvider = cfg.provider?.[providerID] - - for (const [modelID, model] of Object.entries(provider.models)) { - model.api.id = model.api.id ?? model.id ?? modelID - if ( - modelID === "gpt-5-chat-latest" || - (providerID === ProviderID.openrouter && modelID === "openai/gpt-5-chat") - ) - delete provider.models[modelID] - if (model.status === "alpha" && !Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS) delete provider.models[modelID] - if (model.status === "deprecated") delete provider.models[modelID] - if ( - (configProvider?.blacklist && configProvider.blacklist.includes(modelID)) || - (configProvider?.whitelist && !configProvider.whitelist.includes(modelID)) - ) - delete provider.models[modelID] - - model.variants = mapValues(ProviderTransform.variants(model), (v) => v) - - const configVariants = configProvider?.models?.[modelID]?.variants - if (configVariants && model.variants) { - const merged = mergeDeep(model.variants, configVariants) - model.variants = mapValues( - pickBy(merged, (v) => !v.disabled), - (v) => omit(v, ["disabled"]), - ) - } - } - - if (Object.keys(provider.models).length === 0) { - delete providers[providerID] - continue - } - - log.info("found", { providerID }) - } - - return { - models: languages, - providers, - sdk, - modelLoaders, - varsLoaders, - } - }), - ) - - const list = Effect.fn("Provider.list")(() => InstanceState.use(state, (s) => s.providers)) - - async function resolveSDK(model: Model, s: State, envs: Record) { - try { - using _ = log.time("getSDK", { + const key = Hash.fast( + JSON.stringify({ providerID: model.providerID, - }) - const provider = s.providers[model.providerID] - const options = { ...provider.options } + npm: model.api.npm, + options, + }), + ) + const existing = s.sdk.get(key) + if (existing) return existing - if (model.providerID === "google-vertex" && !model.api.npm.includes("@ai-sdk/openai-compatible")) { - delete options.fetch - } + const customFetch = options["fetch"] + const chunkTimeout = options["chunkTimeout"] + delete options["chunkTimeout"] - if (model.api.npm.includes("@ai-sdk/openai-compatible") && options["includeUsage"] !== false) { - options["includeUsage"] = true - } + options["fetch"] = async (input: any, init?: BunFetchRequestInit) => { + const fetchFn = customFetch ?? fetch + const opts = init ?? {} + const chunkAbortCtl = + typeof chunkTimeout === "number" && chunkTimeout > 0 ? new AbortController() : undefined + const signals: AbortSignal[] = [] - const baseURL = iife(() => { - let url = - typeof options["baseURL"] === "string" && options["baseURL"] !== "" ? options["baseURL"] : model.api.url - if (!url) return + if (opts.signal) signals.push(opts.signal) + if (chunkAbortCtl) signals.push(chunkAbortCtl.signal) + if (options["timeout"] !== undefined && options["timeout"] !== null && options["timeout"] !== false) + signals.push(AbortSignal.timeout(options["timeout"])) - const loader = s.varsLoaders[model.providerID] - if (loader) { - const vars = loader(options) - for (const [key, value] of Object.entries(vars)) { - const field = "${" + key + "}" - url = url.replaceAll(field, value) - } - } + const combined = signals.length === 0 ? null : signals.length === 1 ? signals[0] : AbortSignal.any(signals) + if (combined) opts.signal = combined - url = url.replace(/\$\{([^}]+)\}/g, (item, key) => { - const val = envs[String(key)] - return val ?? item - }) - return url - }) - - if (baseURL !== undefined) options["baseURL"] = baseURL - if (options["apiKey"] === undefined && provider.key) options["apiKey"] = provider.key - if (model.headers) - options["headers"] = { - ...options["headers"], - ...model.headers, - } - - const key = Hash.fast( - JSON.stringify({ - providerID: model.providerID, - npm: model.api.npm, - options, - }), - ) - const existing = s.sdk.get(key) - if (existing) return existing - - const customFetch = options["fetch"] - const chunkTimeout = options["chunkTimeout"] - delete options["chunkTimeout"] - - options["fetch"] = async (input: any, init?: BunFetchRequestInit) => { - const fetchFn = customFetch ?? fetch - const opts = init ?? {} - const chunkAbortCtl = - typeof chunkTimeout === "number" && chunkTimeout > 0 ? new AbortController() : undefined - const signals: AbortSignal[] = [] - - if (opts.signal) signals.push(opts.signal) - if (chunkAbortCtl) signals.push(chunkAbortCtl.signal) - if (options["timeout"] !== undefined && options["timeout"] !== null && options["timeout"] !== false) - signals.push(AbortSignal.timeout(options["timeout"])) - - const combined = signals.length === 0 ? null : signals.length === 1 ? signals[0] : AbortSignal.any(signals) - if (combined) opts.signal = combined - - // Strip openai itemId metadata following what codex does - if (model.api.npm === "@ai-sdk/openai" && opts.body && opts.method === "POST") { - const body = JSON.parse(opts.body as string) - const isAzure = model.providerID.includes("azure") - const keepIds = isAzure && body.store === true - if (!keepIds && Array.isArray(body.input)) { - for (const item of body.input) { - if ("id" in item) { - delete item.id - } + // Strip openai itemId metadata following what codex does + if (model.api.npm === "@ai-sdk/openai" && opts.body && opts.method === "POST") { + const body = JSON.parse(opts.body as string) + const isAzure = model.providerID.includes("azure") + const keepIds = isAzure && body.store === true + if (!keepIds && Array.isArray(body.input)) { + for (const item of body.input) { + if ("id" in item) { + delete item.id } - opts.body = JSON.stringify(body) } + opts.body = JSON.stringify(body) } - - const res = await fetchFn(input, { - ...opts, - // @ts-ignore see here: https://github.com/oven-sh/bun/issues/16682 - timeout: false, - }) - - if (!chunkAbortCtl) return res - return wrapSSE(res, chunkTimeout, chunkAbortCtl) } - const bundledFn = BUNDLED_PROVIDERS[model.api.npm] - if (bundledFn) { - log.info("using bundled provider", { - providerID: model.providerID, - pkg: model.api.npm, - }) - const loaded = bundledFn({ - name: model.providerID, - ...options, - }) - s.sdk.set(key, loaded) - return loaded as SDK - } + const res = await fetchFn(input, { + ...opts, + // @ts-ignore see here: https://github.com/oven-sh/bun/issues/16682 + timeout: false, + }) - let installedPath: string - if (!model.api.npm.startsWith("file://")) { - const item = await Npm.add(model.api.npm) - if (!item.entrypoint) throw new Error(`Package ${model.api.npm} has no import entrypoint`) - installedPath = item.entrypoint - } else { - log.info("loading local provider", { pkg: model.api.npm }) - installedPath = model.api.npm - } + if (!chunkAbortCtl) return res + return wrapSSE(res, chunkTimeout, chunkAbortCtl) + } - const mod = await import(installedPath) - - const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!] - const loaded = fn({ + const bundledFn = BUNDLED_PROVIDERS[model.api.npm] + if (bundledFn) { + log.info("using bundled provider", { + providerID: model.providerID, + pkg: model.api.npm, + }) + const loaded = bundledFn({ name: model.providerID, ...options, }) s.sdk.set(key, loaded) return loaded as SDK + } + + let installedPath: string + if (!model.api.npm.startsWith("file://")) { + const item = await Npm.add(model.api.npm) + if (!item.entrypoint) throw new Error(`Package ${model.api.npm} has no import entrypoint`) + installedPath = item.entrypoint + } else { + log.info("loading local provider", { pkg: model.api.npm }) + installedPath = model.api.npm + } + + const mod = await import(installedPath) + + const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!] + const loaded = fn({ + name: model.providerID, + ...options, + }) + s.sdk.set(key, loaded) + return loaded as SDK + } catch (e) { + throw new InitError({ providerID: model.providerID }, { cause: e }) + } + } + + const getProvider = Effect.fn("Provider.getProvider")((providerID: ProviderID) => + InstanceState.use(state, (s) => s.providers[providerID]), + ) + + const getModel = Effect.fn("Provider.getModel")(function* (providerID: ProviderID, modelID: ModelID) { + const s = yield* InstanceState.get(state) + const provider = s.providers[providerID] + if (!provider) { + const available = Object.keys(s.providers) + const matches = fuzzysort.go(providerID, available, { limit: 3, threshold: -10000 }) + throw new ModelNotFoundError({ providerID, modelID, suggestions: matches.map((m) => m.target) }) + } + + const info = provider.models[modelID] + if (!info) { + const available = Object.keys(provider.models) + const matches = fuzzysort.go(modelID, available, { limit: 3, threshold: -10000 }) + throw new ModelNotFoundError({ providerID, modelID, suggestions: matches.map((m) => m.target) }) + } + return info + }) + + const getLanguage = Effect.fn("Provider.getLanguage")(function* (model: Model) { + const s = yield* InstanceState.get(state) + const envs = yield* env.all() + const key = `${model.providerID}/${model.id}` + if (s.models.has(key)) return s.models.get(key)! + + return yield* Effect.promise(async () => { + const provider = s.providers[model.providerID] + const sdk = await resolveSDK(model, s, envs) + + try { + const language = s.modelLoaders[model.providerID] + ? await s.modelLoaders[model.providerID](sdk, model.api.id, { + ...provider.options, + ...model.options, + }) + : sdk.languageModel(model.api.id) + s.models.set(key, language) + return language } catch (e) { - throw new InitError({ providerID: model.providerID }, { cause: e }) + if (e instanceof NoSuchModelError) + throw new ModelNotFoundError( + { + modelID: model.id, + providerID: model.providerID, + }, + { cause: e }, + ) + throw e + } + }) + }) + + const closest = Effect.fn("Provider.closest")(function* (providerID: ProviderID, query: string[]) { + const s = yield* InstanceState.get(state) + const provider = s.providers[providerID] + if (!provider) return undefined + for (const item of query) { + for (const modelID of Object.keys(provider.models)) { + if (modelID.includes(item)) return { providerID, modelID } + } + } + return undefined + }) + + const getSmallModel = Effect.fn("Provider.getSmallModel")(function* (providerID: ProviderID) { + const cfg = yield* config.get() + + if (cfg.small_model) { + const parsed = parseModel(cfg.small_model) + return yield* getModel(parsed.providerID, parsed.modelID) + } + + const s = yield* InstanceState.get(state) + const provider = s.providers[providerID] + if (!provider) return undefined + + let priority = [ + "claude-haiku-4-5", + "claude-haiku-4.5", + "3-5-haiku", + "3.5-haiku", + "gemini-3-flash", + "gemini-2.5-flash", + "gpt-5-nano", + ] + if (providerID.startsWith("opencode")) { + priority = ["gpt-5-nano"] + } + if (providerID.startsWith("github-copilot")) { + priority = ["gpt-5-mini", "claude-haiku-4.5", ...priority] + } + for (const item of priority) { + if (providerID === ProviderID.amazonBedrock) { + const crossRegionPrefixes = ["global.", "us.", "eu."] + const candidates = Object.keys(provider.models).filter((m) => m.includes(item)) + + const globalMatch = candidates.find((m) => m.startsWith("global.")) + if (globalMatch) return yield* getModel(providerID, ModelID.make(globalMatch)) + + const region = provider.options?.region + if (region) { + const regionPrefix = region.split("-")[0] + if (regionPrefix === "us" || regionPrefix === "eu") { + const regionalMatch = candidates.find((m) => m.startsWith(`${regionPrefix}.`)) + if (regionalMatch) return yield* getModel(providerID, ModelID.make(regionalMatch)) + } + } + + const unprefixed = candidates.find((m) => !crossRegionPrefixes.some((p) => m.startsWith(p))) + if (unprefixed) return yield* getModel(providerID, ModelID.make(unprefixed)) + } else { + for (const model of Object.keys(provider.models)) { + if (model.includes(item)) return yield* getModel(providerID, ModelID.make(model)) + } } } - const getProvider = Effect.fn("Provider.getProvider")((providerID: ProviderID) => - InstanceState.use(state, (s) => s.providers[providerID]), + return undefined + }) + + const defaultModel = Effect.fn("Provider.defaultModel")(function* () { + const cfg = yield* config.get() + if (cfg.model) return parseModel(cfg.model) + + const s = yield* InstanceState.get(state) + const recent = yield* fs.readJson(path.join(Global.Path.state, "model.json")).pipe( + Effect.map((x): { providerID: ProviderID; modelID: ModelID }[] => { + if (!isRecord(x) || !Array.isArray(x.recent)) return [] + return x.recent.flatMap((item) => { + if (!isRecord(item)) return [] + if (typeof item.providerID !== "string") return [] + if (typeof item.modelID !== "string") return [] + return [{ providerID: ProviderID.make(item.providerID), modelID: ModelID.make(item.modelID) }] + }) + }), + Effect.catch(() => Effect.succeed([] as { providerID: ProviderID; modelID: ModelID }[])), ) + for (const entry of recent) { + const provider = s.providers[entry.providerID] + if (!provider) continue + if (!provider.models[entry.modelID]) continue + return { providerID: entry.providerID, modelID: entry.modelID } + } - const getModel = Effect.fn("Provider.getModel")(function* (providerID: ProviderID, modelID: ModelID) { - const s = yield* InstanceState.get(state) - const provider = s.providers[providerID] - if (!provider) { - const available = Object.keys(s.providers) - const matches = fuzzysort.go(providerID, available, { limit: 3, threshold: -10000 }) - throw new ModelNotFoundError({ providerID, modelID, suggestions: matches.map((m) => m.target) }) - } + const provider = Object.values(s.providers).find( + (p) => !cfg.provider || Object.keys(cfg.provider).includes(p.id), + ) + if (!provider) throw new Error("no providers found") + const [model] = sort(Object.values(provider.models)) + if (!model) throw new Error("no models found") + return { + providerID: provider.id, + modelID: model.id, + } + }) - const info = provider.models[modelID] - if (!info) { - const available = Object.keys(provider.models) - const matches = fuzzysort.go(modelID, available, { limit: 3, threshold: -10000 }) - throw new ModelNotFoundError({ providerID, modelID, suggestions: matches.map((m) => m.target) }) - } - return info - }) + return Service.of({ list, getProvider, getModel, getLanguage, closest, getSmallModel, defaultModel }) + }), +) - const getLanguage = Effect.fn("Provider.getLanguage")(function* (model: Model) { - const s = yield* InstanceState.get(state) - const envs = yield* env.all() - const key = `${model.providerID}/${model.id}` - if (s.models.has(key)) return s.models.get(key)! +export const defaultLayer = Layer.suspend(() => + layer.pipe( + Layer.provide(AppFileSystem.defaultLayer), + Layer.provide(Env.defaultLayer), + Layer.provide(Config.defaultLayer), + Layer.provide(Auth.defaultLayer), + Layer.provide(Plugin.defaultLayer), + ), +) - return yield* Effect.promise(async () => { - const provider = s.providers[model.providerID] - const sdk = await resolveSDK(model, s, envs) - - try { - const language = s.modelLoaders[model.providerID] - ? await s.modelLoaders[model.providerID](sdk, model.api.id, { - ...provider.options, - ...model.options, - }) - : sdk.languageModel(model.api.id) - s.models.set(key, language) - return language - } catch (e) { - if (e instanceof NoSuchModelError) - throw new ModelNotFoundError( - { - modelID: model.id, - providerID: model.providerID, - }, - { cause: e }, - ) - throw e - } - }) - }) - - const closest = Effect.fn("Provider.closest")(function* (providerID: ProviderID, query: string[]) { - const s = yield* InstanceState.get(state) - const provider = s.providers[providerID] - if (!provider) return undefined - for (const item of query) { - for (const modelID of Object.keys(provider.models)) { - if (modelID.includes(item)) return { providerID, modelID } - } - } - return undefined - }) - - const getSmallModel = Effect.fn("Provider.getSmallModel")(function* (providerID: ProviderID) { - const cfg = yield* config.get() - - if (cfg.small_model) { - const parsed = parseModel(cfg.small_model) - return yield* getModel(parsed.providerID, parsed.modelID) - } - - const s = yield* InstanceState.get(state) - const provider = s.providers[providerID] - if (!provider) return undefined - - let priority = [ - "claude-haiku-4-5", - "claude-haiku-4.5", - "3-5-haiku", - "3.5-haiku", - "gemini-3-flash", - "gemini-2.5-flash", - "gpt-5-nano", - ] - if (providerID.startsWith("opencode")) { - priority = ["gpt-5-nano"] - } - if (providerID.startsWith("github-copilot")) { - priority = ["gpt-5-mini", "claude-haiku-4.5", ...priority] - } - for (const item of priority) { - if (providerID === ProviderID.amazonBedrock) { - const crossRegionPrefixes = ["global.", "us.", "eu."] - const candidates = Object.keys(provider.models).filter((m) => m.includes(item)) - - const globalMatch = candidates.find((m) => m.startsWith("global.")) - if (globalMatch) return yield* getModel(providerID, ModelID.make(globalMatch)) - - const region = provider.options?.region - if (region) { - const regionPrefix = region.split("-")[0] - if (regionPrefix === "us" || regionPrefix === "eu") { - const regionalMatch = candidates.find((m) => m.startsWith(`${regionPrefix}.`)) - if (regionalMatch) return yield* getModel(providerID, ModelID.make(regionalMatch)) - } - } - - const unprefixed = candidates.find((m) => !crossRegionPrefixes.some((p) => m.startsWith(p))) - if (unprefixed) return yield* getModel(providerID, ModelID.make(unprefixed)) - } else { - for (const model of Object.keys(provider.models)) { - if (model.includes(item)) return yield* getModel(providerID, ModelID.make(model)) - } - } - } - - return undefined - }) - - const defaultModel = Effect.fn("Provider.defaultModel")(function* () { - const cfg = yield* config.get() - if (cfg.model) return parseModel(cfg.model) - - const s = yield* InstanceState.get(state) - const recent = yield* fs.readJson(path.join(Global.Path.state, "model.json")).pipe( - Effect.map((x): { providerID: ProviderID; modelID: ModelID }[] => { - if (!isRecord(x) || !Array.isArray(x.recent)) return [] - return x.recent.flatMap((item) => { - if (!isRecord(item)) return [] - if (typeof item.providerID !== "string") return [] - if (typeof item.modelID !== "string") return [] - return [{ providerID: ProviderID.make(item.providerID), modelID: ModelID.make(item.modelID) }] - }) - }), - Effect.catch(() => Effect.succeed([] as { providerID: ProviderID; modelID: ModelID }[])), - ) - for (const entry of recent) { - const provider = s.providers[entry.providerID] - if (!provider) continue - if (!provider.models[entry.modelID]) continue - return { providerID: entry.providerID, modelID: entry.modelID } - } - - const provider = Object.values(s.providers).find( - (p) => !cfg.provider || Object.keys(cfg.provider).includes(p.id), - ) - if (!provider) throw new Error("no providers found") - const [model] = sort(Object.values(provider.models)) - if (!model) throw new Error("no models found") - return { - providerID: provider.id, - modelID: model.id, - } - }) - - return Service.of({ list, getProvider, getModel, getLanguage, closest, getSmallModel, defaultModel }) - }), - ) - - export const defaultLayer = Layer.suspend(() => - layer.pipe( - Layer.provide(AppFileSystem.defaultLayer), - Layer.provide(Env.defaultLayer), - Layer.provide(Config.defaultLayer), - Layer.provide(Auth.defaultLayer), - Layer.provide(Plugin.defaultLayer), - ), - ) - - const priority = ["gpt-5", "claude-sonnet-4", "big-pickle", "gemini-3-pro"] - export function sort(models: T[]) { - return sortBy( - models, - [(model) => priority.findIndex((filter) => model.id.includes(filter)), "desc"], - [(model) => (model.id.includes("latest") ? 0 : 1), "asc"], - [(model) => model.id, "desc"], - ) - } - - export function parseModel(model: string) { - const [providerID, ...rest] = model.split("/") - return { - providerID: ProviderID.make(providerID), - modelID: ModelID.make(rest.join("/")), - } - } - - export const ModelNotFoundError = NamedError.create( - "ProviderModelNotFoundError", - z.object({ - providerID: ProviderID.zod, - modelID: ModelID.zod, - suggestions: z.array(z.string()).optional(), - }), - ) - - export const InitError = NamedError.create( - "ProviderInitError", - z.object({ - providerID: ProviderID.zod, - }), +const priority = ["gpt-5", "claude-sonnet-4", "big-pickle", "gemini-3-pro"] +export function sort(models: T[]) { + return sortBy( + models, + [(model) => priority.findIndex((filter) => model.id.includes(filter)), "desc"], + [(model) => (model.id.includes("latest") ? 0 : 1), "asc"], + [(model) => model.id, "desc"], ) } + +export function parseModel(model: string) { + const [providerID, ...rest] = model.split("/") + return { + providerID: ProviderID.make(providerID), + modelID: ModelID.make(rest.join("/")), + } +} + +export const ModelNotFoundError = NamedError.create( + "ProviderModelNotFoundError", + z.object({ + providerID: ProviderID.zod, + modelID: ModelID.zod, + suggestions: z.array(z.string()).optional(), + }), +) + +export const InitError = NamedError.create( + "ProviderInitError", + z.object({ + providerID: ProviderID.zod, + }), +) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 61561ec969..3138f8e293 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -2,7 +2,7 @@ import type { ModelMessage } from "ai" import { mergeDeep, unique } from "remeda" import type { JSONSchema7 } from "@ai-sdk/provider" import type { JSONSchema } from "zod/v4/core" -import type { Provider } from "./provider" +import type { Provider } from "." import type { ModelsDev } from "./models" import { iife } from "@/util/iife" import { Flag } from "@/flag/flag" diff --git a/packages/opencode/src/server/instance/config.ts b/packages/opencode/src/server/instance/config.ts index 68a6b50764..e3291a8c36 100644 --- a/packages/opencode/src/server/instance/config.ts +++ b/packages/opencode/src/server/instance/config.ts @@ -2,7 +2,7 @@ import { Hono } from "hono" import { describeRoute, validator, resolver } from "hono-openapi" import z from "zod" import { Config } from "../../config" -import { Provider } from "../../provider/provider" +import { Provider } from "../../provider" import { mapValues } from "remeda" import { errors } from "../error" import { lazy } from "../../util/lazy" diff --git a/packages/opencode/src/server/instance/provider.ts b/packages/opencode/src/server/instance/provider.ts index b9e39d4eff..8018dfbea4 100644 --- a/packages/opencode/src/server/instance/provider.ts +++ b/packages/opencode/src/server/instance/provider.ts @@ -2,7 +2,7 @@ import { Hono } from "hono" import { describeRoute, validator, resolver } from "hono-openapi" import z from "zod" import { Config } from "../../config" -import { Provider } from "../../provider/provider" +import { Provider } from "../../provider" import { ModelsDev } from "../../provider/models" import { ProviderAuth } from "../../provider/auth" import { ProviderID } from "../../provider/schema" diff --git a/packages/opencode/src/server/middleware.ts b/packages/opencode/src/server/middleware.ts index 6e91651866..880c432c7c 100644 --- a/packages/opencode/src/server/middleware.ts +++ b/packages/opencode/src/server/middleware.ts @@ -1,4 +1,4 @@ -import { Provider } from "../provider/provider" +import { Provider } from "../provider" import { NamedError } from "@opencode-ai/shared/util/error" import { NotFoundError } from "../storage/db" import { Session } from "../session" diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index 03f9723112..644a76752d 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -2,7 +2,7 @@ import { BusEvent } from "@/bus/bus-event" import { Bus } from "@/bus" import { Session } from "." import { SessionID, MessageID, PartID } from "./schema" -import { Provider } from "../provider/provider" +import { Provider } from "../provider" import { MessageV2 } from "./message-v2" import z from "zod" import { Token } from "../util/token" diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 49d8359497..585b9a135d 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -24,7 +24,7 @@ import { ProjectID } from "../project/schema" import { WorkspaceID } from "../control-plane/schema" import { SessionID, MessageID, PartID } from "./schema" -import type { Provider } from "@/provider/provider" +import type { Provider } from "@/provider" import { Permission } from "@/permission" import { Global } from "@/global" import { Effect, Layer, Option, Context } from "effect" diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index 2efe4a4054..3db1c99d6b 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -1,4 +1,4 @@ -import { Provider } from "@/provider/provider" +import { Provider } from "@/provider" import { Log } from "@/util/log" import { Context, Effect, Layer, Record } from "effect" import * as Stream from "effect/Stream" diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 8c82d4d73f..2a501167a5 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -12,7 +12,7 @@ import { ProviderError } from "@/provider/error" import { iife } from "@/util/iife" import { errorMessage } from "@/util/error" import type { SystemError } from "bun" -import type { Provider } from "@/provider/provider" +import type { Provider } from "@/provider" import { ModelID, ProviderID } from "@/provider/schema" import { Effect } from "effect" import { EffectLogger } from "@/effect/logger" diff --git a/packages/opencode/src/session/overflow.ts b/packages/opencode/src/session/overflow.ts index c4c6d09279..10f4bccda3 100644 --- a/packages/opencode/src/session/overflow.ts +++ b/packages/opencode/src/session/overflow.ts @@ -1,5 +1,5 @@ import type { Config } from "@/config" -import type { Provider } from "@/provider/provider" +import type { Provider } from "@/provider" import { ProviderTransform } from "@/provider/transform" import type { MessageV2 } from "./message-v2" diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 0f8cd41b30..1ae70c3c6e 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -15,7 +15,7 @@ import type { SessionID } from "./schema" import { SessionRetry } from "./retry" import { SessionStatus } from "./status" import { SessionSummary } from "./summary" -import type { Provider } from "@/provider/provider" +import type { Provider } from "@/provider" import { Question } from "@/question" import { errorMessage } from "@/util/error" import { Log } from "@/util/log" diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index f2a160e268..4e10fdf2d6 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -7,7 +7,7 @@ import { Log } from "../util/log" import { SessionRevert } from "./revert" import { Session } from "." import { Agent } from "../agent/agent" -import { Provider } from "../provider/provider" +import { Provider } from "../provider" import { ModelID, ProviderID } from "../provider/schema" import { type Tool as AITool, tool, jsonSchema, type ToolExecutionOptions, asSchema } from "ai" import { SessionCompaction } from "./compaction" diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts index 2a001ba9b1..952ff5b04b 100644 --- a/packages/opencode/src/session/system.ts +++ b/packages/opencode/src/session/system.ts @@ -11,7 +11,7 @@ import PROMPT_KIMI from "./prompt/kimi.txt" import PROMPT_CODEX from "./prompt/codex.txt" import PROMPT_TRINITY from "./prompt/trinity.txt" -import type { Provider } from "@/provider/provider" +import type { Provider } from "@/provider" import type { Agent } from "@/agent/agent" import { Permission } from "@/permission" import { Skill } from "@/skill" diff --git a/packages/opencode/src/share/share-next.ts b/packages/opencode/src/share/share-next.ts index 667e0720c4..c764c20b99 100644 --- a/packages/opencode/src/share/share-next.ts +++ b/packages/opencode/src/share/share-next.ts @@ -4,7 +4,7 @@ import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } fr import { Account } from "@/account" import { Bus } from "@/bus" import { InstanceState } from "@/effect/instance-state" -import { Provider } from "@/provider/provider" +import { Provider } from "@/provider" import { ModelID, ProviderID } from "@/provider/schema" import { Session } from "@/session" import { MessageV2 } from "@/session/message-v2" diff --git a/packages/opencode/src/tool/plan.ts b/packages/opencode/src/tool/plan.ts index 1613821fe0..cc52c2abde 100644 --- a/packages/opencode/src/tool/plan.ts +++ b/packages/opencode/src/tool/plan.ts @@ -5,7 +5,7 @@ import { Tool } from "./tool" import { Question } from "../question" import { Session } from "../session" import { MessageV2 } from "../session/message-v2" -import { Provider } from "../provider/provider" +import { Provider } from "../provider" import { Instance } from "../project/instance" import { type SessionID, MessageID, PartID } from "../session/schema" import EXIT_DESCRIPTION from "./plan-exit.txt" diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 2e9971ad71..ef55758a57 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -17,7 +17,7 @@ import { Config } from "../config" import { type ToolContext as PluginToolContext, type ToolDefinition } from "@opencode-ai/plugin" import z from "zod" import { Plugin } from "../plugin" -import { Provider } from "../provider/provider" +import { Provider } from "../provider" import { ProviderID, type ModelID } from "../provider/schema" import { WebSearchTool } from "./websearch" import { CodeSearchTool } from "./codesearch" diff --git a/packages/opencode/test/fake/provider.ts b/packages/opencode/test/fake/provider.ts index b6f72f53db..bfb185a4b1 100644 --- a/packages/opencode/test/fake/provider.ts +++ b/packages/opencode/test/fake/provider.ts @@ -1,5 +1,5 @@ import { Effect, Layer } from "effect" -import { Provider } from "../../src/provider/provider" +import { Provider } from "../../src/provider" import { ModelID, ProviderID } from "../../src/provider/schema" export namespace ProviderTest { diff --git a/packages/opencode/test/provider/amazon-bedrock.test.ts b/packages/opencode/test/provider/amazon-bedrock.test.ts index 6783ff5889..6809e4d17e 100644 --- a/packages/opencode/test/provider/amazon-bedrock.test.ts +++ b/packages/opencode/test/provider/amazon-bedrock.test.ts @@ -5,7 +5,7 @@ import { unlink } from "fs/promises" import { ProviderID } from "../../src/provider/schema" import { tmpdir } from "../fixture/fixture" import { Instance } from "../../src/project/instance" -import { Provider } from "../../src/provider/provider" +import { Provider } from "../../src/provider" import { Env } from "../../src/env" import { Global } from "../../src/global" import { Filesystem } from "../../src/util/filesystem" diff --git a/packages/opencode/test/provider/gitlab-duo.test.ts b/packages/opencode/test/provider/gitlab-duo.test.ts index a80ecf5aee..907a32d61d 100644 --- a/packages/opencode/test/provider/gitlab-duo.test.ts +++ b/packages/opencode/test/provider/gitlab-duo.test.ts @@ -9,7 +9,7 @@ export {} // import { ProviderID, ModelID } from "../../src/provider/schema" // import { tmpdir } from "../fixture/fixture" // import { Instance } from "../../src/project/instance" -// import { Provider } from "../../src/provider/provider" +// import { Provider } from "../../src/provider" // import { Env } from "../../src/env" // import { Global } from "../../src/global" // import { GitLabWorkflowLanguageModel } from "gitlab-ai-provider" diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index dafa9dd822..a6a93e8091 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -7,7 +7,7 @@ import { Global } from "../../src/global" import { Instance } from "../../src/project/instance" import { Plugin } from "../../src/plugin/index" import { ModelsDev } from "../../src/provider/models" -import { Provider } from "../../src/provider/provider" +import { Provider } from "../../src/provider" import { ProviderID, ModelID } from "../../src/provider/schema" import { Filesystem } from "../../src/util/filesystem" import { Env } from "../../src/env" diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index aaf34348b9..d658f48bd8 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/packages/opencode/test/session/compaction.test.ts @@ -20,7 +20,7 @@ import { MessageID, PartID, SessionID } from "../../src/session/schema" import { SessionStatus } from "../../src/session/status" import { SessionSummary } from "../../src/session/summary" import { ModelID, ProviderID } from "../../src/provider/schema" -import type { Provider } from "../../src/provider/provider" +import type { Provider } from "../../src/provider" import * as SessionProcessorModule from "../../src/session/processor" import { Snapshot } from "../../src/snapshot" import { ProviderTest } from "../fake/provider" diff --git a/packages/opencode/test/session/llm.test.ts b/packages/opencode/test/session/llm.test.ts index a7fde90f01..e908545d4a 100644 --- a/packages/opencode/test/session/llm.test.ts +++ b/packages/opencode/test/session/llm.test.ts @@ -6,7 +6,7 @@ import z from "zod" import { makeRuntime } from "../../src/effect/run-service" import { LLM } from "../../src/session/llm" import { Instance } from "../../src/project/instance" -import { Provider } from "../../src/provider/provider" +import { Provider } from "../../src/provider" import { ProviderTransform } from "../../src/provider/transform" import { ModelsDev } from "../../src/provider/models" import { ProviderID, ModelID } from "../../src/provider/schema" diff --git a/packages/opencode/test/session/message-v2.test.ts b/packages/opencode/test/session/message-v2.test.ts index 64a5d3e4b2..6d4e994a87 100644 --- a/packages/opencode/test/session/message-v2.test.ts +++ b/packages/opencode/test/session/message-v2.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test" import { APICallError } from "ai" import { MessageV2 } from "../../src/session/message-v2" -import type { Provider } from "../../src/provider/provider" +import type { Provider } from "../../src/provider" import { ModelID, ProviderID } from "../../src/provider/schema" import { SessionID, MessageID, PartID } from "../../src/session/schema" import { Question } from "../../src/question" diff --git a/packages/opencode/test/session/processor-effect.test.ts b/packages/opencode/test/session/processor-effect.test.ts index 10945be188..982399d6d1 100644 --- a/packages/opencode/test/session/processor-effect.test.ts +++ b/packages/opencode/test/session/processor-effect.test.ts @@ -8,7 +8,7 @@ import { Bus } from "../../src/bus" import { Config } from "../../src/config" import { Permission } from "../../src/permission" import { Plugin } from "../../src/plugin" -import { Provider } from "../../src/provider/provider" +import { Provider } from "../../src/provider" import { ModelID, ProviderID } from "../../src/provider/schema" import { Session } from "../../src/session" import { LLM } from "../../src/session/llm" diff --git a/packages/opencode/test/session/prompt-effect.test.ts b/packages/opencode/test/session/prompt-effect.test.ts index 3963c815da..91297aed1d 100644 --- a/packages/opencode/test/session/prompt-effect.test.ts +++ b/packages/opencode/test/session/prompt-effect.test.ts @@ -12,9 +12,9 @@ import { LSP } from "../../src/lsp" import { MCP } from "../../src/mcp" import { Permission } from "../../src/permission" import { Plugin } from "../../src/plugin" -import { Provider as ProviderSvc } from "../../src/provider/provider" +import { Provider as ProviderSvc } from "../../src/provider" import { Env } from "../../src/env" -import type { Provider } from "../../src/provider/provider" +import type { Provider } from "../../src/provider" import { ModelID, ProviderID } from "../../src/provider/schema" import { Question } from "../../src/question" import { Todo } from "../../src/session/todo" diff --git a/packages/opencode/test/session/snapshot-tool-race.test.ts b/packages/opencode/test/session/snapshot-tool-race.test.ts index e32919aeda..3681b14f7a 100644 --- a/packages/opencode/test/session/snapshot-tool-race.test.ts +++ b/packages/opencode/test/session/snapshot-tool-race.test.ts @@ -38,7 +38,7 @@ import { LSP } from "../../src/lsp" import { MCP } from "../../src/mcp" import { Permission } from "../../src/permission" import { Plugin } from "../../src/plugin" -import { Provider as ProviderSvc } from "../../src/provider/provider" +import { Provider as ProviderSvc } from "../../src/provider" import { Env } from "../../src/env" import { Question } from "../../src/question" import { Skill } from "../../src/skill" diff --git a/packages/opencode/test/share/share-next.test.ts b/packages/opencode/test/share/share-next.test.ts index 7475411953..8150e03623 100644 --- a/packages/opencode/test/share/share-next.test.ts +++ b/packages/opencode/test/share/share-next.test.ts @@ -9,7 +9,7 @@ import { AccountRepo } from "../../src/account/repo" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" import { Bus } from "../../src/bus" import { Config } from "../../src/config" -import { Provider } from "../../src/provider/provider" +import { Provider } from "../../src/provider" import { Session } from "../../src/session" import type { SessionID } from "../../src/session/schema" import { ShareNext } from "../../src/share/share-next" From 7baf998752f0cd1df0ff818d1cd5587e27f8d721 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Thu, 16 Apr 2026 01:45:44 +0000 Subject: [PATCH 172/300] chore: generate --- packages/opencode/src/provider/provider.ts | 23 +++++++--------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 36a5a68e99..fed4d93583 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -454,8 +454,7 @@ function custom(dep: CustomDep): Record { return { autoload: true, vars(_options: Record) { - const endpoint = - location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com` + const endpoint = location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com` return { ...(project && { GOOGLE_VERTEX_PROJECT: project }), GOOGLE_VERTEX_LOCATION: location, @@ -1136,12 +1135,9 @@ const layer: Layer.Layer< toolcall: model.tool_call ?? existingModel?.capabilities.toolcall ?? true, input: { text: model.modalities?.input?.includes("text") ?? existingModel?.capabilities.input.text ?? true, - audio: - model.modalities?.input?.includes("audio") ?? existingModel?.capabilities.input.audio ?? false, - image: - model.modalities?.input?.includes("image") ?? existingModel?.capabilities.input.image ?? false, - video: - model.modalities?.input?.includes("video") ?? existingModel?.capabilities.input.video ?? false, + audio: model.modalities?.input?.includes("audio") ?? existingModel?.capabilities.input.audio ?? false, + image: model.modalities?.input?.includes("image") ?? existingModel?.capabilities.input.image ?? false, + video: model.modalities?.input?.includes("video") ?? existingModel?.capabilities.input.video ?? false, pdf: model.modalities?.input?.includes("pdf") ?? existingModel?.capabilities.input.pdf ?? false, }, output: { @@ -1246,9 +1242,7 @@ const layer: Layer.Layer< if (result.vars) varsLoaders[providerID] = result.vars if (result.discoverModels) discoveryLoaders[providerID] = result.discoverModels const opts = result.options ?? {} - const patch: Partial = providers[providerID] - ? { options: opts } - : { source: "custom", options: opts } + const patch: Partial = providers[providerID] ? { options: opts } : { source: "custom", options: opts } mergeProvider(providerID, patch) } } @@ -1424,8 +1418,7 @@ const layer: Layer.Layer< options["fetch"] = async (input: any, init?: BunFetchRequestInit) => { const fetchFn = customFetch ?? fetch const opts = init ?? {} - const chunkAbortCtl = - typeof chunkTimeout === "number" && chunkTimeout > 0 ? new AbortController() : undefined + const chunkAbortCtl = typeof chunkTimeout === "number" && chunkTimeout > 0 ? new AbortController() : undefined const signals: AbortSignal[] = [] if (opts.signal) signals.push(opts.signal) @@ -1646,9 +1639,7 @@ const layer: Layer.Layer< return { providerID: entry.providerID, modelID: entry.modelID } } - const provider = Object.values(s.providers).find( - (p) => !cfg.provider || Object.keys(cfg.provider).includes(p.id), - ) + const provider = Object.values(s.providers).find((p) => !cfg.provider || Object.keys(cfg.provider).includes(p.id)) if (!provider) throw new Error("no providers found") const [model] = sort(Object.values(provider.models)) if (!model) throw new Error("no models found") From 66257663509bc12bb208c4c65f73c45206106aae Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 21:56:02 -0400 Subject: [PATCH 173/300] feat: unwrap MCP namespace to flat exports + barrel (#22693) --- packages/opencode/src/mcp/index.ts | 931 +---------------------------- packages/opencode/src/mcp/mcp.ts | 928 ++++++++++++++++++++++++++++ 2 files changed, 929 insertions(+), 930 deletions(-) create mode 100644 packages/opencode/src/mcp/mcp.ts diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index cbaa2c24b3..c42b9eb5c1 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -1,930 +1 @@ -import { dynamicTool, type Tool, jsonSchema, type JSONSchema7 } from "ai" -import { Client } from "@modelcontextprotocol/sdk/client/index.js" -import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" -import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js" -import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js" -import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js" -import { - CallToolResultSchema, - type Tool as MCPToolDef, - ToolListChangedNotificationSchema, -} from "@modelcontextprotocol/sdk/types.js" -import { Config } from "../config" -import { Log } from "../util/log" -import { NamedError } from "@opencode-ai/shared/util/error" -import z from "zod/v4" -import { Instance } from "../project/instance" -import { Installation } from "../installation" -import { withTimeout } from "@/util/timeout" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" -import { McpOAuthProvider } from "./oauth-provider" -import { McpOAuthCallback } from "./oauth-callback" -import { McpAuth } from "./auth" -import { BusEvent } from "../bus/bus-event" -import { Bus } from "@/bus" -import { TuiEvent } from "@/cli/cmd/tui/event" -import open from "open" -import { Effect, Exit, Layer, Option, Context, Stream } from "effect" -import { EffectBridge } from "@/effect/bridge" -import { InstanceState } from "@/effect/instance-state" -import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" -import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" - -export namespace MCP { - const log = Log.create({ service: "mcp" }) - const DEFAULT_TIMEOUT = 30_000 - - export const Resource = z - .object({ - name: z.string(), - uri: z.string(), - description: z.string().optional(), - mimeType: z.string().optional(), - client: z.string(), - }) - .meta({ ref: "McpResource" }) - export type Resource = z.infer - - export const ToolsChanged = BusEvent.define( - "mcp.tools.changed", - z.object({ - server: z.string(), - }), - ) - - export const BrowserOpenFailed = BusEvent.define( - "mcp.browser.open.failed", - z.object({ - mcpName: z.string(), - url: z.string(), - }), - ) - - export const Failed = NamedError.create( - "MCPFailed", - z.object({ - name: z.string(), - }), - ) - - type MCPClient = Client - - export const Status = z - .discriminatedUnion("status", [ - z - .object({ - status: z.literal("connected"), - }) - .meta({ - ref: "MCPStatusConnected", - }), - z - .object({ - status: z.literal("disabled"), - }) - .meta({ - ref: "MCPStatusDisabled", - }), - z - .object({ - status: z.literal("failed"), - error: z.string(), - }) - .meta({ - ref: "MCPStatusFailed", - }), - z - .object({ - status: z.literal("needs_auth"), - }) - .meta({ - ref: "MCPStatusNeedsAuth", - }), - z - .object({ - status: z.literal("needs_client_registration"), - error: z.string(), - }) - .meta({ - ref: "MCPStatusNeedsClientRegistration", - }), - ]) - .meta({ - ref: "MCPStatus", - }) - export type Status = z.infer - - // Store transports for OAuth servers to allow finishing auth - type TransportWithAuth = StreamableHTTPClientTransport | SSEClientTransport - const pendingOAuthTransports = new Map() - - // Prompt cache types - type PromptInfo = Awaited>["prompts"][number] - type ResourceInfo = Awaited>["resources"][number] - type McpEntry = NonNullable[string] - - function isMcpConfigured(entry: McpEntry): entry is Config.Mcp { - return typeof entry === "object" && entry !== null && "type" in entry - } - - const sanitize = (s: string) => s.replace(/[^a-zA-Z0-9_-]/g, "_") - - // Convert MCP tool definition to AI SDK Tool type - function convertMcpTool(mcpTool: MCPToolDef, client: MCPClient, timeout?: number): Tool { - const inputSchema = mcpTool.inputSchema - - // Spread first, then override type to ensure it's always "object" - const schema: JSONSchema7 = { - ...(inputSchema as JSONSchema7), - type: "object", - properties: (inputSchema.properties ?? {}) as JSONSchema7["properties"], - additionalProperties: false, - } - - return dynamicTool({ - description: mcpTool.description ?? "", - inputSchema: jsonSchema(schema), - execute: async (args: unknown) => { - return client.callTool( - { - name: mcpTool.name, - arguments: (args || {}) as Record, - }, - CallToolResultSchema, - { - resetTimeoutOnProgress: true, - timeout, - }, - ) - }, - }) - } - - function defs(key: string, client: MCPClient, timeout?: number) { - return Effect.tryPromise({ - try: () => withTimeout(client.listTools(), timeout ?? DEFAULT_TIMEOUT), - catch: (err) => (err instanceof Error ? err : new Error(String(err))), - }).pipe( - Effect.map((result) => result.tools), - Effect.catch((err) => { - log.error("failed to get tools from client", { key, error: err }) - return Effect.succeed(undefined) - }), - ) - } - - function fetchFromClient( - clientName: string, - client: Client, - listFn: (c: Client) => Promise, - label: string, - ) { - return Effect.tryPromise({ - try: () => listFn(client), - catch: (e: any) => { - log.error(`failed to get ${label}`, { clientName, error: e.message }) - return e - }, - }).pipe( - Effect.map((items) => { - const out: Record = {} - const sanitizedClient = sanitize(clientName) - for (const item of items) { - out[sanitizedClient + ":" + sanitize(item.name)] = { ...item, client: clientName } - } - return out - }), - Effect.orElseSucceed(() => undefined), - ) - } - - interface CreateResult { - mcpClient?: MCPClient - status: Status - defs?: MCPToolDef[] - } - - interface AuthResult { - authorizationUrl: string - oauthState: string - client?: MCPClient - } - - // --- Effect Service --- - - interface State { - status: Record - clients: Record - defs: Record - } - - export interface Interface { - readonly status: () => Effect.Effect> - readonly clients: () => Effect.Effect> - readonly tools: () => Effect.Effect> - readonly prompts: () => Effect.Effect> - readonly resources: () => Effect.Effect> - readonly add: (name: string, mcp: Config.Mcp) => Effect.Effect<{ status: Record | Status }> - readonly connect: (name: string) => Effect.Effect - readonly disconnect: (name: string) => Effect.Effect - readonly getPrompt: ( - clientName: string, - name: string, - args?: Record, - ) => Effect.Effect> | undefined> - readonly readResource: ( - clientName: string, - resourceUri: string, - ) => Effect.Effect> | undefined> - readonly startAuth: (mcpName: string) => Effect.Effect<{ authorizationUrl: string; oauthState: string }> - readonly authenticate: (mcpName: string) => Effect.Effect - readonly finishAuth: (mcpName: string, authorizationCode: string) => Effect.Effect - readonly removeAuth: (mcpName: string) => Effect.Effect - readonly supportsOAuth: (mcpName: string) => Effect.Effect - readonly hasStoredTokens: (mcpName: string) => Effect.Effect - readonly getAuthStatus: (mcpName: string) => Effect.Effect - } - - export class Service extends Context.Service()("@opencode/MCP") {} - - export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const spawner = yield* ChildProcessSpawner.ChildProcessSpawner - const auth = yield* McpAuth.Service - const bus = yield* Bus.Service - - type Transport = StdioClientTransport | StreamableHTTPClientTransport | SSEClientTransport - - /** - * Connect a client via the given transport with resource safety: - * on failure the transport is closed; on success the caller owns it. - */ - const connectTransport = (transport: Transport, timeout: number) => - Effect.acquireUseRelease( - Effect.succeed(transport), - (t) => - Effect.tryPromise({ - try: () => { - const client = new Client({ name: "opencode", version: Installation.VERSION }) - return withTimeout(client.connect(t), timeout).then(() => client) - }, - catch: (e) => (e instanceof Error ? e : new Error(String(e))), - }), - (t, exit) => (Exit.isFailure(exit) ? Effect.tryPromise(() => t.close()).pipe(Effect.ignore) : Effect.void), - ) - - const DISABLED_RESULT: CreateResult = { status: { status: "disabled" } } - - const connectRemote = Effect.fn("MCP.connectRemote")(function* ( - key: string, - mcp: Config.Mcp & { type: "remote" }, - ) { - const oauthDisabled = mcp.oauth === false - const oauthConfig = typeof mcp.oauth === "object" ? mcp.oauth : undefined - let authProvider: McpOAuthProvider | undefined - - if (!oauthDisabled) { - authProvider = new McpOAuthProvider( - key, - mcp.url, - { - clientId: oauthConfig?.clientId, - clientSecret: oauthConfig?.clientSecret, - scope: oauthConfig?.scope, - redirectUri: oauthConfig?.redirectUri, - }, - { - onRedirect: async (url) => { - log.info("oauth redirect requested", { key, url: url.toString() }) - }, - }, - auth, - ) - } - - const transports: Array<{ name: string; transport: TransportWithAuth }> = [ - { - name: "StreamableHTTP", - transport: new StreamableHTTPClientTransport(new URL(mcp.url), { - authProvider, - requestInit: mcp.headers ? { headers: mcp.headers } : undefined, - }), - }, - { - name: "SSE", - transport: new SSEClientTransport(new URL(mcp.url), { - authProvider, - requestInit: mcp.headers ? { headers: mcp.headers } : undefined, - }), - }, - ] - - const connectTimeout = mcp.timeout ?? DEFAULT_TIMEOUT - let lastStatus: Status | undefined - - for (const { name, transport } of transports) { - const result = yield* connectTransport(transport, connectTimeout).pipe( - Effect.map((client) => ({ client, transportName: name })), - Effect.catch((error) => { - const lastError = error instanceof Error ? error : new Error(String(error)) - const isAuthError = - error instanceof UnauthorizedError || (authProvider && lastError.message.includes("OAuth")) - - if (isAuthError) { - log.info("mcp server requires authentication", { key, transport: name }) - - if (lastError.message.includes("registration") || lastError.message.includes("client_id")) { - lastStatus = { - status: "needs_client_registration" as const, - error: "Server does not support dynamic client registration. Please provide clientId in config.", - } - return bus - .publish(TuiEvent.ToastShow, { - title: "MCP Authentication Required", - message: `Server "${key}" requires a pre-registered client ID. Add clientId to your config.`, - variant: "warning", - duration: 8000, - }) - .pipe(Effect.ignore, Effect.as(undefined)) - } else { - pendingOAuthTransports.set(key, transport) - lastStatus = { status: "needs_auth" as const } - return bus - .publish(TuiEvent.ToastShow, { - title: "MCP Authentication Required", - message: `Server "${key}" requires authentication. Run: opencode mcp auth ${key}`, - variant: "warning", - duration: 8000, - }) - .pipe(Effect.ignore, Effect.as(undefined)) - } - } - - log.debug("transport connection failed", { - key, - transport: name, - url: mcp.url, - error: lastError.message, - }) - lastStatus = { status: "failed" as const, error: lastError.message } - return Effect.succeed(undefined) - }), - ) - if (result) { - log.info("connected", { key, transport: result.transportName }) - return { client: result.client as MCPClient | undefined, status: { status: "connected" } as Status } - } - // If this was an auth error, stop trying other transports - if (lastStatus?.status === "needs_auth" || lastStatus?.status === "needs_client_registration") break - } - - return { - client: undefined as MCPClient | undefined, - status: (lastStatus ?? { status: "failed", error: "Unknown error" }) as Status, - } - }) - - const connectLocal = Effect.fn("MCP.connectLocal")(function* (key: string, mcp: Config.Mcp & { type: "local" }) { - const [cmd, ...args] = mcp.command - const cwd = Instance.directory - const transport = new StdioClientTransport({ - stderr: "pipe", - command: cmd, - args, - cwd, - env: { - ...process.env, - ...(cmd === "opencode" ? { BUN_BE_BUN: "1" } : {}), - ...mcp.environment, - }, - }) - transport.stderr?.on("data", (chunk: Buffer) => { - log.info(`mcp stderr: ${chunk.toString()}`, { key }) - }) - - const connectTimeout = mcp.timeout ?? DEFAULT_TIMEOUT - return yield* connectTransport(transport, connectTimeout).pipe( - Effect.map((client): { client: MCPClient | undefined; status: Status } => ({ - client, - status: { status: "connected" }, - })), - Effect.catch((error): Effect.Effect<{ client: MCPClient | undefined; status: Status }> => { - const msg = error instanceof Error ? error.message : String(error) - log.error("local mcp startup failed", { key, command: mcp.command, cwd, error: msg }) - return Effect.succeed({ client: undefined, status: { status: "failed", error: msg } }) - }), - ) - }) - - const create = Effect.fn("MCP.create")(function* (key: string, mcp: Config.Mcp) { - if (mcp.enabled === false) { - log.info("mcp server disabled", { key }) - return DISABLED_RESULT - } - - log.info("found", { key, type: mcp.type }) - - const { client: mcpClient, status } = - mcp.type === "remote" - ? yield* connectRemote(key, mcp as Config.Mcp & { type: "remote" }) - : yield* connectLocal(key, mcp as Config.Mcp & { type: "local" }) - - if (!mcpClient) { - return { status } satisfies CreateResult - } - - const listed = yield* defs(key, mcpClient, mcp.timeout) - if (!listed) { - yield* Effect.tryPromise(() => mcpClient.close()).pipe(Effect.ignore) - return { status: { status: "failed", error: "Failed to get tools" } } satisfies CreateResult - } - - log.info("create() successfully created client", { key, toolCount: listed.length }) - return { mcpClient, status, defs: listed } satisfies CreateResult - }) - const cfgSvc = yield* Config.Service - - const descendants = Effect.fnUntraced( - function* (pid: number) { - if (process.platform === "win32") return [] as number[] - const pids: number[] = [] - const queue = [pid] - while (queue.length > 0) { - const current = queue.shift()! - const handle = yield* spawner.spawn( - ChildProcess.make("pgrep", ["-P", String(current)], { stdin: "ignore" }), - ) - const text = yield* Stream.mkString(Stream.decodeText(handle.stdout)) - yield* handle.exitCode - for (const tok of text.split("\n")) { - const cpid = parseInt(tok, 10) - if (!isNaN(cpid) && !pids.includes(cpid)) { - pids.push(cpid) - queue.push(cpid) - } - } - } - return pids - }, - Effect.scoped, - Effect.catch(() => Effect.succeed([] as number[])), - ) - - function watch(s: State, name: string, client: MCPClient, bridge: EffectBridge.Shape, timeout?: number) { - client.setNotificationHandler(ToolListChangedNotificationSchema, async () => { - log.info("tools list changed notification received", { server: name }) - if (s.clients[name] !== client || s.status[name]?.status !== "connected") return - - const listed = await bridge.promise(defs(name, client, timeout)) - if (!listed) return - if (s.clients[name] !== client || s.status[name]?.status !== "connected") return - - s.defs[name] = listed - await bridge.promise(bus.publish(ToolsChanged, { server: name }).pipe(Effect.ignore)) - }) - } - - const state = yield* InstanceState.make( - Effect.fn("MCP.state")(function* () { - const cfg = yield* cfgSvc.get() - const bridge = yield* EffectBridge.make() - const config = cfg.mcp ?? {} - const s: State = { - status: {}, - clients: {}, - defs: {}, - } - - yield* Effect.forEach( - Object.entries(config), - ([key, mcp]) => - Effect.gen(function* () { - if (!isMcpConfigured(mcp)) { - log.error("Ignoring MCP config entry without type", { key }) - return - } - - if (mcp.enabled === false) { - s.status[key] = { status: "disabled" } - return - } - - const result = yield* create(key, mcp).pipe(Effect.catch(() => Effect.void)) - if (!result) return - - s.status[key] = result.status - if (result.mcpClient) { - s.clients[key] = result.mcpClient - s.defs[key] = result.defs! - watch(s, key, result.mcpClient, bridge, mcp.timeout) - } - }), - { concurrency: "unbounded" }, - ) - - yield* Effect.addFinalizer(() => - Effect.gen(function* () { - yield* Effect.forEach( - Object.values(s.clients), - (client) => - Effect.gen(function* () { - const pid = (client.transport as any)?.pid - if (typeof pid === "number") { - const pids = yield* descendants(pid) - for (const dpid of pids) { - try { - process.kill(dpid, "SIGTERM") - } catch {} - } - } - yield* Effect.tryPromise(() => client.close()).pipe(Effect.ignore) - }), - { concurrency: "unbounded" }, - ) - pendingOAuthTransports.clear() - }), - ) - - return s - }), - ) - - function closeClient(s: State, name: string) { - const client = s.clients[name] - delete s.defs[name] - if (!client) return Effect.void - return Effect.tryPromise(() => client.close()).pipe(Effect.ignore) - } - - const storeClient = Effect.fnUntraced(function* ( - s: State, - name: string, - client: MCPClient, - listed: MCPToolDef[], - timeout?: number, - ) { - const bridge = yield* EffectBridge.make() - yield* closeClient(s, name) - s.status[name] = { status: "connected" } - s.clients[name] = client - s.defs[name] = listed - watch(s, name, client, bridge, timeout) - return s.status[name] - }) - - const status = Effect.fn("MCP.status")(function* () { - const s = yield* InstanceState.get(state) - - const cfg = yield* cfgSvc.get() - const config = cfg.mcp ?? {} - const result: Record = {} - - for (const [key, mcp] of Object.entries(config)) { - if (!isMcpConfigured(mcp)) continue - result[key] = s.status[key] ?? { status: "disabled" } - } - - return result - }) - - const clients = Effect.fn("MCP.clients")(function* () { - const s = yield* InstanceState.get(state) - return s.clients - }) - - const createAndStore = Effect.fn("MCP.createAndStore")(function* (name: string, mcp: Config.Mcp) { - const s = yield* InstanceState.get(state) - const result = yield* create(name, mcp) - - s.status[name] = result.status - if (!result.mcpClient) { - yield* closeClient(s, name) - delete s.clients[name] - return result.status - } - - return yield* storeClient(s, name, result.mcpClient, result.defs!, mcp.timeout) - }) - - const add = Effect.fn("MCP.add")(function* (name: string, mcp: Config.Mcp) { - yield* createAndStore(name, mcp) - const s = yield* InstanceState.get(state) - return { status: s.status } - }) - - const connect = Effect.fn("MCP.connect")(function* (name: string) { - const mcp = yield* getMcpConfig(name) - if (!mcp) { - log.error("MCP config not found or invalid", { name }) - return - } - yield* createAndStore(name, { ...mcp, enabled: true }) - }) - - const disconnect = Effect.fn("MCP.disconnect")(function* (name: string) { - const s = yield* InstanceState.get(state) - yield* closeClient(s, name) - delete s.clients[name] - s.status[name] = { status: "disabled" } - }) - - const tools = Effect.fn("MCP.tools")(function* () { - const result: Record = {} - const s = yield* InstanceState.get(state) - - const cfg = yield* cfgSvc.get() - const config = cfg.mcp ?? {} - const defaultTimeout = cfg.experimental?.mcp_timeout - - const connectedClients = Object.entries(s.clients).filter( - ([clientName]) => s.status[clientName]?.status === "connected", - ) - - yield* Effect.forEach( - connectedClients, - ([clientName, client]) => - Effect.gen(function* () { - const mcpConfig = config[clientName] - const entry = mcpConfig && isMcpConfigured(mcpConfig) ? mcpConfig : undefined - - const listed = s.defs[clientName] - if (!listed) { - log.warn("missing cached tools for connected server", { clientName }) - return - } - - const timeout = entry?.timeout ?? defaultTimeout - for (const mcpTool of listed) { - result[sanitize(clientName) + "_" + sanitize(mcpTool.name)] = convertMcpTool(mcpTool, client, timeout) - } - }), - { concurrency: "unbounded" }, - ) - return result - }) - - function collectFromConnected( - s: State, - listFn: (c: Client) => Promise, - label: string, - ) { - return Effect.forEach( - Object.entries(s.clients).filter(([name]) => s.status[name]?.status === "connected"), - ([clientName, client]) => - fetchFromClient(clientName, client, listFn, label).pipe(Effect.map((items) => Object.entries(items ?? {}))), - { concurrency: "unbounded" }, - ).pipe(Effect.map((results) => Object.fromEntries(results.flat()))) - } - - const prompts = Effect.fn("MCP.prompts")(function* () { - const s = yield* InstanceState.get(state) - return yield* collectFromConnected(s, (c) => c.listPrompts().then((r) => r.prompts), "prompts") - }) - - const resources = Effect.fn("MCP.resources")(function* () { - const s = yield* InstanceState.get(state) - return yield* collectFromConnected(s, (c) => c.listResources().then((r) => r.resources), "resources") - }) - - const withClient = Effect.fnUntraced(function* ( - clientName: string, - fn: (client: MCPClient) => Promise, - label: string, - meta?: Record, - ) { - const s = yield* InstanceState.get(state) - const client = s.clients[clientName] - if (!client) { - log.warn(`client not found for ${label}`, { clientName }) - return undefined - } - return yield* Effect.tryPromise({ - try: () => fn(client), - catch: (e: any) => { - log.error(`failed to ${label}`, { clientName, ...meta, error: e?.message }) - return e - }, - }).pipe(Effect.orElseSucceed(() => undefined)) - }) - - const getPrompt = Effect.fn("MCP.getPrompt")(function* ( - clientName: string, - name: string, - args?: Record, - ) { - return yield* withClient(clientName, (client) => client.getPrompt({ name, arguments: args }), "getPrompt", { - promptName: name, - }) - }) - - const readResource = Effect.fn("MCP.readResource")(function* (clientName: string, resourceUri: string) { - return yield* withClient(clientName, (client) => client.readResource({ uri: resourceUri }), "readResource", { - resourceUri, - }) - }) - - const getMcpConfig = Effect.fnUntraced(function* (mcpName: string) { - const cfg = yield* cfgSvc.get() - const mcpConfig = cfg.mcp?.[mcpName] - if (!mcpConfig || !isMcpConfigured(mcpConfig)) return undefined - return mcpConfig - }) - - const startAuth = Effect.fn("MCP.startAuth")(function* (mcpName: string) { - const mcpConfig = yield* getMcpConfig(mcpName) - if (!mcpConfig) throw new Error(`MCP server ${mcpName} not found or disabled`) - if (mcpConfig.type !== "remote") throw new Error(`MCP server ${mcpName} is not a remote server`) - if (mcpConfig.oauth === false) throw new Error(`MCP server ${mcpName} has OAuth explicitly disabled`) - - // OAuth config is optional - if not provided, we'll use auto-discovery - const oauthConfig = typeof mcpConfig.oauth === "object" ? mcpConfig.oauth : undefined - - // Start the callback server with custom redirectUri if configured - yield* Effect.promise(() => McpOAuthCallback.ensureRunning(oauthConfig?.redirectUri)) - - const oauthState = Array.from(crypto.getRandomValues(new Uint8Array(32))) - .map((b) => b.toString(16).padStart(2, "0")) - .join("") - yield* auth.updateOAuthState(mcpName, oauthState) - let capturedUrl: URL | undefined - const authProvider = new McpOAuthProvider( - mcpName, - mcpConfig.url, - { - clientId: oauthConfig?.clientId, - clientSecret: oauthConfig?.clientSecret, - scope: oauthConfig?.scope, - redirectUri: oauthConfig?.redirectUri, - }, - { - onRedirect: async (url) => { - capturedUrl = url - }, - }, - auth, - ) - - const transport = new StreamableHTTPClientTransport(new URL(mcpConfig.url), { authProvider }) - - return yield* Effect.tryPromise({ - try: () => { - const client = new Client({ name: "opencode", version: Installation.VERSION }) - return client - .connect(transport) - .then(() => ({ authorizationUrl: "", oauthState, client }) satisfies AuthResult) - }, - catch: (error) => error, - }).pipe( - Effect.catch((error) => { - if (error instanceof UnauthorizedError && capturedUrl) { - pendingOAuthTransports.set(mcpName, transport) - return Effect.succeed({ authorizationUrl: capturedUrl.toString(), oauthState } satisfies AuthResult) - } - return Effect.die(error) - }), - ) - }) - - const authenticate = Effect.fn("MCP.authenticate")(function* (mcpName: string) { - const result = yield* startAuth(mcpName) - if (!result.authorizationUrl) { - const client = "client" in result ? result.client : undefined - const mcpConfig = yield* getMcpConfig(mcpName) - if (!mcpConfig) { - yield* Effect.tryPromise(() => client?.close() ?? Promise.resolve()).pipe(Effect.ignore) - return { status: "failed", error: "MCP config not found after auth" } as Status - } - - const listed = client ? yield* defs(mcpName, client, mcpConfig.timeout) : undefined - if (!client || !listed) { - yield* Effect.tryPromise(() => client?.close() ?? Promise.resolve()).pipe(Effect.ignore) - return { status: "failed", error: "Failed to get tools" } as Status - } - - const s = yield* InstanceState.get(state) - yield* auth.clearOAuthState(mcpName) - return yield* storeClient(s, mcpName, client, listed, mcpConfig.timeout) - } - - log.info("opening browser for oauth", { mcpName, url: result.authorizationUrl, state: result.oauthState }) - - const callbackPromise = McpOAuthCallback.waitForCallback(result.oauthState, mcpName) - - yield* Effect.tryPromise(() => open(result.authorizationUrl)).pipe( - Effect.flatMap((subprocess) => - Effect.callback((resume) => { - const timer = setTimeout(() => resume(Effect.void), 500) - subprocess.on("error", (err) => { - clearTimeout(timer) - resume(Effect.fail(err)) - }) - subprocess.on("exit", (code) => { - if (code !== null && code !== 0) { - clearTimeout(timer) - resume(Effect.fail(new Error(`Browser open failed with exit code ${code}`))) - } - }) - }), - ), - Effect.catch(() => { - log.warn("failed to open browser, user must open URL manually", { mcpName }) - return bus.publish(BrowserOpenFailed, { mcpName, url: result.authorizationUrl }).pipe(Effect.ignore) - }), - ) - - const code = yield* Effect.promise(() => callbackPromise) - - const storedState = yield* auth.getOAuthState(mcpName) - if (storedState !== result.oauthState) { - yield* auth.clearOAuthState(mcpName) - throw new Error("OAuth state mismatch - potential CSRF attack") - } - yield* auth.clearOAuthState(mcpName) - return yield* finishAuth(mcpName, code) - }) - - const finishAuth = Effect.fn("MCP.finishAuth")(function* (mcpName: string, authorizationCode: string) { - const transport = pendingOAuthTransports.get(mcpName) - if (!transport) throw new Error(`No pending OAuth flow for MCP server: ${mcpName}`) - - const result = yield* Effect.tryPromise({ - try: () => transport.finishAuth(authorizationCode).then(() => true as const), - catch: (error) => { - log.error("failed to finish oauth", { mcpName, error }) - return error - }, - }).pipe(Effect.option) - - if (Option.isNone(result)) { - return { status: "failed", error: "OAuth completion failed" } as Status - } - - yield* auth.clearCodeVerifier(mcpName) - pendingOAuthTransports.delete(mcpName) - - const mcpConfig = yield* getMcpConfig(mcpName) - if (!mcpConfig) return { status: "failed", error: "MCP config not found after auth" } as Status - - return yield* createAndStore(mcpName, mcpConfig) - }) - - const removeAuth = Effect.fn("MCP.removeAuth")(function* (mcpName: string) { - yield* auth.remove(mcpName) - McpOAuthCallback.cancelPending(mcpName) - pendingOAuthTransports.delete(mcpName) - log.info("removed oauth credentials", { mcpName }) - }) - - const supportsOAuth = Effect.fn("MCP.supportsOAuth")(function* (mcpName: string) { - const mcpConfig = yield* getMcpConfig(mcpName) - if (!mcpConfig) return false - return mcpConfig.type === "remote" && mcpConfig.oauth !== false - }) - - const hasStoredTokens = Effect.fn("MCP.hasStoredTokens")(function* (mcpName: string) { - const entry = yield* auth.get(mcpName) - return !!entry?.tokens - }) - - const getAuthStatus = Effect.fn("MCP.getAuthStatus")(function* (mcpName: string) { - const entry = yield* auth.get(mcpName) - if (!entry?.tokens) return "not_authenticated" as AuthStatus - const expired = yield* auth.isTokenExpired(mcpName) - return (expired ? "expired" : "authenticated") as AuthStatus - }) - - return Service.of({ - status, - clients, - tools, - prompts, - resources, - add, - connect, - disconnect, - getPrompt, - readResource, - startAuth, - authenticate, - finishAuth, - removeAuth, - supportsOAuth, - hasStoredTokens, - getAuthStatus, - }) - }), - ) - - export type AuthStatus = "authenticated" | "expired" | "not_authenticated" - - // --- Per-service runtime --- - - export const defaultLayer = layer.pipe( - Layer.provide(McpAuth.layer), - Layer.provide(Bus.layer), - Layer.provide(Config.defaultLayer), - Layer.provide(CrossSpawnSpawner.defaultLayer), - Layer.provide(AppFileSystem.defaultLayer), - ) -} +export * as MCP from "./mcp" diff --git a/packages/opencode/src/mcp/mcp.ts b/packages/opencode/src/mcp/mcp.ts new file mode 100644 index 0000000000..1e3288682e --- /dev/null +++ b/packages/opencode/src/mcp/mcp.ts @@ -0,0 +1,928 @@ +import { dynamicTool, type Tool, jsonSchema, type JSONSchema7 } from "ai" +import { Client } from "@modelcontextprotocol/sdk/client/index.js" +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" +import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js" +import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js" +import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js" +import { + CallToolResultSchema, + type Tool as MCPToolDef, + ToolListChangedNotificationSchema, +} from "@modelcontextprotocol/sdk/types.js" +import { Config } from "../config" +import { Log } from "../util/log" +import { NamedError } from "@opencode-ai/shared/util/error" +import z from "zod/v4" +import { Instance } from "../project/instance" +import { Installation } from "../installation" +import { withTimeout } from "@/util/timeout" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { McpOAuthProvider } from "./oauth-provider" +import { McpOAuthCallback } from "./oauth-callback" +import { McpAuth } from "./auth" +import { BusEvent } from "../bus/bus-event" +import { Bus } from "@/bus" +import { TuiEvent } from "@/cli/cmd/tui/event" +import open from "open" +import { Effect, Exit, Layer, Option, Context, Stream } from "effect" +import { EffectBridge } from "@/effect/bridge" +import { InstanceState } from "@/effect/instance-state" +import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" +import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" + +const log = Log.create({ service: "mcp" }) +const DEFAULT_TIMEOUT = 30_000 + +export const Resource = z + .object({ + name: z.string(), + uri: z.string(), + description: z.string().optional(), + mimeType: z.string().optional(), + client: z.string(), + }) + .meta({ ref: "McpResource" }) +export type Resource = z.infer + +export const ToolsChanged = BusEvent.define( + "mcp.tools.changed", + z.object({ + server: z.string(), + }), +) + +export const BrowserOpenFailed = BusEvent.define( + "mcp.browser.open.failed", + z.object({ + mcpName: z.string(), + url: z.string(), + }), +) + +export const Failed = NamedError.create( + "MCPFailed", + z.object({ + name: z.string(), + }), +) + +type MCPClient = Client + +export const Status = z + .discriminatedUnion("status", [ + z + .object({ + status: z.literal("connected"), + }) + .meta({ + ref: "MCPStatusConnected", + }), + z + .object({ + status: z.literal("disabled"), + }) + .meta({ + ref: "MCPStatusDisabled", + }), + z + .object({ + status: z.literal("failed"), + error: z.string(), + }) + .meta({ + ref: "MCPStatusFailed", + }), + z + .object({ + status: z.literal("needs_auth"), + }) + .meta({ + ref: "MCPStatusNeedsAuth", + }), + z + .object({ + status: z.literal("needs_client_registration"), + error: z.string(), + }) + .meta({ + ref: "MCPStatusNeedsClientRegistration", + }), + ]) + .meta({ + ref: "MCPStatus", + }) +export type Status = z.infer + +// Store transports for OAuth servers to allow finishing auth +type TransportWithAuth = StreamableHTTPClientTransport | SSEClientTransport +const pendingOAuthTransports = new Map() + +// Prompt cache types +type PromptInfo = Awaited>["prompts"][number] +type ResourceInfo = Awaited>["resources"][number] +type McpEntry = NonNullable[string] + +function isMcpConfigured(entry: McpEntry): entry is Config.Mcp { + return typeof entry === "object" && entry !== null && "type" in entry +} + +const sanitize = (s: string) => s.replace(/[^a-zA-Z0-9_-]/g, "_") + +// Convert MCP tool definition to AI SDK Tool type +function convertMcpTool(mcpTool: MCPToolDef, client: MCPClient, timeout?: number): Tool { + const inputSchema = mcpTool.inputSchema + + // Spread first, then override type to ensure it's always "object" + const schema: JSONSchema7 = { + ...(inputSchema as JSONSchema7), + type: "object", + properties: (inputSchema.properties ?? {}) as JSONSchema7["properties"], + additionalProperties: false, + } + + return dynamicTool({ + description: mcpTool.description ?? "", + inputSchema: jsonSchema(schema), + execute: async (args: unknown) => { + return client.callTool( + { + name: mcpTool.name, + arguments: (args || {}) as Record, + }, + CallToolResultSchema, + { + resetTimeoutOnProgress: true, + timeout, + }, + ) + }, + }) +} + +function defs(key: string, client: MCPClient, timeout?: number) { + return Effect.tryPromise({ + try: () => withTimeout(client.listTools(), timeout ?? DEFAULT_TIMEOUT), + catch: (err) => (err instanceof Error ? err : new Error(String(err))), + }).pipe( + Effect.map((result) => result.tools), + Effect.catch((err) => { + log.error("failed to get tools from client", { key, error: err }) + return Effect.succeed(undefined) + }), + ) +} + +function fetchFromClient( + clientName: string, + client: Client, + listFn: (c: Client) => Promise, + label: string, +) { + return Effect.tryPromise({ + try: () => listFn(client), + catch: (e: any) => { + log.error(`failed to get ${label}`, { clientName, error: e.message }) + return e + }, + }).pipe( + Effect.map((items) => { + const out: Record = {} + const sanitizedClient = sanitize(clientName) + for (const item of items) { + out[sanitizedClient + ":" + sanitize(item.name)] = { ...item, client: clientName } + } + return out + }), + Effect.orElseSucceed(() => undefined), + ) +} + +interface CreateResult { + mcpClient?: MCPClient + status: Status + defs?: MCPToolDef[] +} + +interface AuthResult { + authorizationUrl: string + oauthState: string + client?: MCPClient +} + +// --- Effect Service --- + +interface State { + status: Record + clients: Record + defs: Record +} + +export interface Interface { + readonly status: () => Effect.Effect> + readonly clients: () => Effect.Effect> + readonly tools: () => Effect.Effect> + readonly prompts: () => Effect.Effect> + readonly resources: () => Effect.Effect> + readonly add: (name: string, mcp: Config.Mcp) => Effect.Effect<{ status: Record | Status }> + readonly connect: (name: string) => Effect.Effect + readonly disconnect: (name: string) => Effect.Effect + readonly getPrompt: ( + clientName: string, + name: string, + args?: Record, + ) => Effect.Effect> | undefined> + readonly readResource: ( + clientName: string, + resourceUri: string, + ) => Effect.Effect> | undefined> + readonly startAuth: (mcpName: string) => Effect.Effect<{ authorizationUrl: string; oauthState: string }> + readonly authenticate: (mcpName: string) => Effect.Effect + readonly finishAuth: (mcpName: string, authorizationCode: string) => Effect.Effect + readonly removeAuth: (mcpName: string) => Effect.Effect + readonly supportsOAuth: (mcpName: string) => Effect.Effect + readonly hasStoredTokens: (mcpName: string) => Effect.Effect + readonly getAuthStatus: (mcpName: string) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/MCP") {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner + const auth = yield* McpAuth.Service + const bus = yield* Bus.Service + + type Transport = StdioClientTransport | StreamableHTTPClientTransport | SSEClientTransport + + /** + * Connect a client via the given transport with resource safety: + * on failure the transport is closed; on success the caller owns it. + */ + const connectTransport = (transport: Transport, timeout: number) => + Effect.acquireUseRelease( + Effect.succeed(transport), + (t) => + Effect.tryPromise({ + try: () => { + const client = new Client({ name: "opencode", version: Installation.VERSION }) + return withTimeout(client.connect(t), timeout).then(() => client) + }, + catch: (e) => (e instanceof Error ? e : new Error(String(e))), + }), + (t, exit) => (Exit.isFailure(exit) ? Effect.tryPromise(() => t.close()).pipe(Effect.ignore) : Effect.void), + ) + + const DISABLED_RESULT: CreateResult = { status: { status: "disabled" } } + + const connectRemote = Effect.fn("MCP.connectRemote")(function* ( + key: string, + mcp: Config.Mcp & { type: "remote" }, + ) { + const oauthDisabled = mcp.oauth === false + const oauthConfig = typeof mcp.oauth === "object" ? mcp.oauth : undefined + let authProvider: McpOAuthProvider | undefined + + if (!oauthDisabled) { + authProvider = new McpOAuthProvider( + key, + mcp.url, + { + clientId: oauthConfig?.clientId, + clientSecret: oauthConfig?.clientSecret, + scope: oauthConfig?.scope, + redirectUri: oauthConfig?.redirectUri, + }, + { + onRedirect: async (url) => { + log.info("oauth redirect requested", { key, url: url.toString() }) + }, + }, + auth, + ) + } + + const transports: Array<{ name: string; transport: TransportWithAuth }> = [ + { + name: "StreamableHTTP", + transport: new StreamableHTTPClientTransport(new URL(mcp.url), { + authProvider, + requestInit: mcp.headers ? { headers: mcp.headers } : undefined, + }), + }, + { + name: "SSE", + transport: new SSEClientTransport(new URL(mcp.url), { + authProvider, + requestInit: mcp.headers ? { headers: mcp.headers } : undefined, + }), + }, + ] + + const connectTimeout = mcp.timeout ?? DEFAULT_TIMEOUT + let lastStatus: Status | undefined + + for (const { name, transport } of transports) { + const result = yield* connectTransport(transport, connectTimeout).pipe( + Effect.map((client) => ({ client, transportName: name })), + Effect.catch((error) => { + const lastError = error instanceof Error ? error : new Error(String(error)) + const isAuthError = + error instanceof UnauthorizedError || (authProvider && lastError.message.includes("OAuth")) + + if (isAuthError) { + log.info("mcp server requires authentication", { key, transport: name }) + + if (lastError.message.includes("registration") || lastError.message.includes("client_id")) { + lastStatus = { + status: "needs_client_registration" as const, + error: "Server does not support dynamic client registration. Please provide clientId in config.", + } + return bus + .publish(TuiEvent.ToastShow, { + title: "MCP Authentication Required", + message: `Server "${key}" requires a pre-registered client ID. Add clientId to your config.`, + variant: "warning", + duration: 8000, + }) + .pipe(Effect.ignore, Effect.as(undefined)) + } else { + pendingOAuthTransports.set(key, transport) + lastStatus = { status: "needs_auth" as const } + return bus + .publish(TuiEvent.ToastShow, { + title: "MCP Authentication Required", + message: `Server "${key}" requires authentication. Run: opencode mcp auth ${key}`, + variant: "warning", + duration: 8000, + }) + .pipe(Effect.ignore, Effect.as(undefined)) + } + } + + log.debug("transport connection failed", { + key, + transport: name, + url: mcp.url, + error: lastError.message, + }) + lastStatus = { status: "failed" as const, error: lastError.message } + return Effect.succeed(undefined) + }), + ) + if (result) { + log.info("connected", { key, transport: result.transportName }) + return { client: result.client as MCPClient | undefined, status: { status: "connected" } as Status } + } + // If this was an auth error, stop trying other transports + if (lastStatus?.status === "needs_auth" || lastStatus?.status === "needs_client_registration") break + } + + return { + client: undefined as MCPClient | undefined, + status: (lastStatus ?? { status: "failed", error: "Unknown error" }) as Status, + } + }) + + const connectLocal = Effect.fn("MCP.connectLocal")(function* (key: string, mcp: Config.Mcp & { type: "local" }) { + const [cmd, ...args] = mcp.command + const cwd = Instance.directory + const transport = new StdioClientTransport({ + stderr: "pipe", + command: cmd, + args, + cwd, + env: { + ...process.env, + ...(cmd === "opencode" ? { BUN_BE_BUN: "1" } : {}), + ...mcp.environment, + }, + }) + transport.stderr?.on("data", (chunk: Buffer) => { + log.info(`mcp stderr: ${chunk.toString()}`, { key }) + }) + + const connectTimeout = mcp.timeout ?? DEFAULT_TIMEOUT + return yield* connectTransport(transport, connectTimeout).pipe( + Effect.map((client): { client: MCPClient | undefined; status: Status } => ({ + client, + status: { status: "connected" }, + })), + Effect.catch((error): Effect.Effect<{ client: MCPClient | undefined; status: Status }> => { + const msg = error instanceof Error ? error.message : String(error) + log.error("local mcp startup failed", { key, command: mcp.command, cwd, error: msg }) + return Effect.succeed({ client: undefined, status: { status: "failed", error: msg } }) + }), + ) + }) + + const create = Effect.fn("MCP.create")(function* (key: string, mcp: Config.Mcp) { + if (mcp.enabled === false) { + log.info("mcp server disabled", { key }) + return DISABLED_RESULT + } + + log.info("found", { key, type: mcp.type }) + + const { client: mcpClient, status } = + mcp.type === "remote" + ? yield* connectRemote(key, mcp as Config.Mcp & { type: "remote" }) + : yield* connectLocal(key, mcp as Config.Mcp & { type: "local" }) + + if (!mcpClient) { + return { status } satisfies CreateResult + } + + const listed = yield* defs(key, mcpClient, mcp.timeout) + if (!listed) { + yield* Effect.tryPromise(() => mcpClient.close()).pipe(Effect.ignore) + return { status: { status: "failed", error: "Failed to get tools" } } satisfies CreateResult + } + + log.info("create() successfully created client", { key, toolCount: listed.length }) + return { mcpClient, status, defs: listed } satisfies CreateResult + }) + const cfgSvc = yield* Config.Service + + const descendants = Effect.fnUntraced( + function* (pid: number) { + if (process.platform === "win32") return [] as number[] + const pids: number[] = [] + const queue = [pid] + while (queue.length > 0) { + const current = queue.shift()! + const handle = yield* spawner.spawn( + ChildProcess.make("pgrep", ["-P", String(current)], { stdin: "ignore" }), + ) + const text = yield* Stream.mkString(Stream.decodeText(handle.stdout)) + yield* handle.exitCode + for (const tok of text.split("\n")) { + const cpid = parseInt(tok, 10) + if (!isNaN(cpid) && !pids.includes(cpid)) { + pids.push(cpid) + queue.push(cpid) + } + } + } + return pids + }, + Effect.scoped, + Effect.catch(() => Effect.succeed([] as number[])), + ) + + function watch(s: State, name: string, client: MCPClient, bridge: EffectBridge.Shape, timeout?: number) { + client.setNotificationHandler(ToolListChangedNotificationSchema, async () => { + log.info("tools list changed notification received", { server: name }) + if (s.clients[name] !== client || s.status[name]?.status !== "connected") return + + const listed = await bridge.promise(defs(name, client, timeout)) + if (!listed) return + if (s.clients[name] !== client || s.status[name]?.status !== "connected") return + + s.defs[name] = listed + await bridge.promise(bus.publish(ToolsChanged, { server: name }).pipe(Effect.ignore)) + }) + } + + const state = yield* InstanceState.make( + Effect.fn("MCP.state")(function* () { + const cfg = yield* cfgSvc.get() + const bridge = yield* EffectBridge.make() + const config = cfg.mcp ?? {} + const s: State = { + status: {}, + clients: {}, + defs: {}, + } + + yield* Effect.forEach( + Object.entries(config), + ([key, mcp]) => + Effect.gen(function* () { + if (!isMcpConfigured(mcp)) { + log.error("Ignoring MCP config entry without type", { key }) + return + } + + if (mcp.enabled === false) { + s.status[key] = { status: "disabled" } + return + } + + const result = yield* create(key, mcp).pipe(Effect.catch(() => Effect.void)) + if (!result) return + + s.status[key] = result.status + if (result.mcpClient) { + s.clients[key] = result.mcpClient + s.defs[key] = result.defs! + watch(s, key, result.mcpClient, bridge, mcp.timeout) + } + }), + { concurrency: "unbounded" }, + ) + + yield* Effect.addFinalizer(() => + Effect.gen(function* () { + yield* Effect.forEach( + Object.values(s.clients), + (client) => + Effect.gen(function* () { + const pid = (client.transport as any)?.pid + if (typeof pid === "number") { + const pids = yield* descendants(pid) + for (const dpid of pids) { + try { + process.kill(dpid, "SIGTERM") + } catch {} + } + } + yield* Effect.tryPromise(() => client.close()).pipe(Effect.ignore) + }), + { concurrency: "unbounded" }, + ) + pendingOAuthTransports.clear() + }), + ) + + return s + }), + ) + + function closeClient(s: State, name: string) { + const client = s.clients[name] + delete s.defs[name] + if (!client) return Effect.void + return Effect.tryPromise(() => client.close()).pipe(Effect.ignore) + } + + const storeClient = Effect.fnUntraced(function* ( + s: State, + name: string, + client: MCPClient, + listed: MCPToolDef[], + timeout?: number, + ) { + const bridge = yield* EffectBridge.make() + yield* closeClient(s, name) + s.status[name] = { status: "connected" } + s.clients[name] = client + s.defs[name] = listed + watch(s, name, client, bridge, timeout) + return s.status[name] + }) + + const status = Effect.fn("MCP.status")(function* () { + const s = yield* InstanceState.get(state) + + const cfg = yield* cfgSvc.get() + const config = cfg.mcp ?? {} + const result: Record = {} + + for (const [key, mcp] of Object.entries(config)) { + if (!isMcpConfigured(mcp)) continue + result[key] = s.status[key] ?? { status: "disabled" } + } + + return result + }) + + const clients = Effect.fn("MCP.clients")(function* () { + const s = yield* InstanceState.get(state) + return s.clients + }) + + const createAndStore = Effect.fn("MCP.createAndStore")(function* (name: string, mcp: Config.Mcp) { + const s = yield* InstanceState.get(state) + const result = yield* create(name, mcp) + + s.status[name] = result.status + if (!result.mcpClient) { + yield* closeClient(s, name) + delete s.clients[name] + return result.status + } + + return yield* storeClient(s, name, result.mcpClient, result.defs!, mcp.timeout) + }) + + const add = Effect.fn("MCP.add")(function* (name: string, mcp: Config.Mcp) { + yield* createAndStore(name, mcp) + const s = yield* InstanceState.get(state) + return { status: s.status } + }) + + const connect = Effect.fn("MCP.connect")(function* (name: string) { + const mcp = yield* getMcpConfig(name) + if (!mcp) { + log.error("MCP config not found or invalid", { name }) + return + } + yield* createAndStore(name, { ...mcp, enabled: true }) + }) + + const disconnect = Effect.fn("MCP.disconnect")(function* (name: string) { + const s = yield* InstanceState.get(state) + yield* closeClient(s, name) + delete s.clients[name] + s.status[name] = { status: "disabled" } + }) + + const tools = Effect.fn("MCP.tools")(function* () { + const result: Record = {} + const s = yield* InstanceState.get(state) + + const cfg = yield* cfgSvc.get() + const config = cfg.mcp ?? {} + const defaultTimeout = cfg.experimental?.mcp_timeout + + const connectedClients = Object.entries(s.clients).filter( + ([clientName]) => s.status[clientName]?.status === "connected", + ) + + yield* Effect.forEach( + connectedClients, + ([clientName, client]) => + Effect.gen(function* () { + const mcpConfig = config[clientName] + const entry = mcpConfig && isMcpConfigured(mcpConfig) ? mcpConfig : undefined + + const listed = s.defs[clientName] + if (!listed) { + log.warn("missing cached tools for connected server", { clientName }) + return + } + + const timeout = entry?.timeout ?? defaultTimeout + for (const mcpTool of listed) { + result[sanitize(clientName) + "_" + sanitize(mcpTool.name)] = convertMcpTool(mcpTool, client, timeout) + } + }), + { concurrency: "unbounded" }, + ) + return result + }) + + function collectFromConnected( + s: State, + listFn: (c: Client) => Promise, + label: string, + ) { + return Effect.forEach( + Object.entries(s.clients).filter(([name]) => s.status[name]?.status === "connected"), + ([clientName, client]) => + fetchFromClient(clientName, client, listFn, label).pipe(Effect.map((items) => Object.entries(items ?? {}))), + { concurrency: "unbounded" }, + ).pipe(Effect.map((results) => Object.fromEntries(results.flat()))) + } + + const prompts = Effect.fn("MCP.prompts")(function* () { + const s = yield* InstanceState.get(state) + return yield* collectFromConnected(s, (c) => c.listPrompts().then((r) => r.prompts), "prompts") + }) + + const resources = Effect.fn("MCP.resources")(function* () { + const s = yield* InstanceState.get(state) + return yield* collectFromConnected(s, (c) => c.listResources().then((r) => r.resources), "resources") + }) + + const withClient = Effect.fnUntraced(function* ( + clientName: string, + fn: (client: MCPClient) => Promise, + label: string, + meta?: Record, + ) { + const s = yield* InstanceState.get(state) + const client = s.clients[clientName] + if (!client) { + log.warn(`client not found for ${label}`, { clientName }) + return undefined + } + return yield* Effect.tryPromise({ + try: () => fn(client), + catch: (e: any) => { + log.error(`failed to ${label}`, { clientName, ...meta, error: e?.message }) + return e + }, + }).pipe(Effect.orElseSucceed(() => undefined)) + }) + + const getPrompt = Effect.fn("MCP.getPrompt")(function* ( + clientName: string, + name: string, + args?: Record, + ) { + return yield* withClient(clientName, (client) => client.getPrompt({ name, arguments: args }), "getPrompt", { + promptName: name, + }) + }) + + const readResource = Effect.fn("MCP.readResource")(function* (clientName: string, resourceUri: string) { + return yield* withClient(clientName, (client) => client.readResource({ uri: resourceUri }), "readResource", { + resourceUri, + }) + }) + + const getMcpConfig = Effect.fnUntraced(function* (mcpName: string) { + const cfg = yield* cfgSvc.get() + const mcpConfig = cfg.mcp?.[mcpName] + if (!mcpConfig || !isMcpConfigured(mcpConfig)) return undefined + return mcpConfig + }) + + const startAuth = Effect.fn("MCP.startAuth")(function* (mcpName: string) { + const mcpConfig = yield* getMcpConfig(mcpName) + if (!mcpConfig) throw new Error(`MCP server ${mcpName} not found or disabled`) + if (mcpConfig.type !== "remote") throw new Error(`MCP server ${mcpName} is not a remote server`) + if (mcpConfig.oauth === false) throw new Error(`MCP server ${mcpName} has OAuth explicitly disabled`) + + // OAuth config is optional - if not provided, we'll use auto-discovery + const oauthConfig = typeof mcpConfig.oauth === "object" ? mcpConfig.oauth : undefined + + // Start the callback server with custom redirectUri if configured + yield* Effect.promise(() => McpOAuthCallback.ensureRunning(oauthConfig?.redirectUri)) + + const oauthState = Array.from(crypto.getRandomValues(new Uint8Array(32))) + .map((b) => b.toString(16).padStart(2, "0")) + .join("") + yield* auth.updateOAuthState(mcpName, oauthState) + let capturedUrl: URL | undefined + const authProvider = new McpOAuthProvider( + mcpName, + mcpConfig.url, + { + clientId: oauthConfig?.clientId, + clientSecret: oauthConfig?.clientSecret, + scope: oauthConfig?.scope, + redirectUri: oauthConfig?.redirectUri, + }, + { + onRedirect: async (url) => { + capturedUrl = url + }, + }, + auth, + ) + + const transport = new StreamableHTTPClientTransport(new URL(mcpConfig.url), { authProvider }) + + return yield* Effect.tryPromise({ + try: () => { + const client = new Client({ name: "opencode", version: Installation.VERSION }) + return client + .connect(transport) + .then(() => ({ authorizationUrl: "", oauthState, client }) satisfies AuthResult) + }, + catch: (error) => error, + }).pipe( + Effect.catch((error) => { + if (error instanceof UnauthorizedError && capturedUrl) { + pendingOAuthTransports.set(mcpName, transport) + return Effect.succeed({ authorizationUrl: capturedUrl.toString(), oauthState } satisfies AuthResult) + } + return Effect.die(error) + }), + ) + }) + + const authenticate = Effect.fn("MCP.authenticate")(function* (mcpName: string) { + const result = yield* startAuth(mcpName) + if (!result.authorizationUrl) { + const client = "client" in result ? result.client : undefined + const mcpConfig = yield* getMcpConfig(mcpName) + if (!mcpConfig) { + yield* Effect.tryPromise(() => client?.close() ?? Promise.resolve()).pipe(Effect.ignore) + return { status: "failed", error: "MCP config not found after auth" } as Status + } + + const listed = client ? yield* defs(mcpName, client, mcpConfig.timeout) : undefined + if (!client || !listed) { + yield* Effect.tryPromise(() => client?.close() ?? Promise.resolve()).pipe(Effect.ignore) + return { status: "failed", error: "Failed to get tools" } as Status + } + + const s = yield* InstanceState.get(state) + yield* auth.clearOAuthState(mcpName) + return yield* storeClient(s, mcpName, client, listed, mcpConfig.timeout) + } + + log.info("opening browser for oauth", { mcpName, url: result.authorizationUrl, state: result.oauthState }) + + const callbackPromise = McpOAuthCallback.waitForCallback(result.oauthState, mcpName) + + yield* Effect.tryPromise(() => open(result.authorizationUrl)).pipe( + Effect.flatMap((subprocess) => + Effect.callback((resume) => { + const timer = setTimeout(() => resume(Effect.void), 500) + subprocess.on("error", (err) => { + clearTimeout(timer) + resume(Effect.fail(err)) + }) + subprocess.on("exit", (code) => { + if (code !== null && code !== 0) { + clearTimeout(timer) + resume(Effect.fail(new Error(`Browser open failed with exit code ${code}`))) + } + }) + }), + ), + Effect.catch(() => { + log.warn("failed to open browser, user must open URL manually", { mcpName }) + return bus.publish(BrowserOpenFailed, { mcpName, url: result.authorizationUrl }).pipe(Effect.ignore) + }), + ) + + const code = yield* Effect.promise(() => callbackPromise) + + const storedState = yield* auth.getOAuthState(mcpName) + if (storedState !== result.oauthState) { + yield* auth.clearOAuthState(mcpName) + throw new Error("OAuth state mismatch - potential CSRF attack") + } + yield* auth.clearOAuthState(mcpName) + return yield* finishAuth(mcpName, code) + }) + + const finishAuth = Effect.fn("MCP.finishAuth")(function* (mcpName: string, authorizationCode: string) { + const transport = pendingOAuthTransports.get(mcpName) + if (!transport) throw new Error(`No pending OAuth flow for MCP server: ${mcpName}`) + + const result = yield* Effect.tryPromise({ + try: () => transport.finishAuth(authorizationCode).then(() => true as const), + catch: (error) => { + log.error("failed to finish oauth", { mcpName, error }) + return error + }, + }).pipe(Effect.option) + + if (Option.isNone(result)) { + return { status: "failed", error: "OAuth completion failed" } as Status + } + + yield* auth.clearCodeVerifier(mcpName) + pendingOAuthTransports.delete(mcpName) + + const mcpConfig = yield* getMcpConfig(mcpName) + if (!mcpConfig) return { status: "failed", error: "MCP config not found after auth" } as Status + + return yield* createAndStore(mcpName, mcpConfig) + }) + + const removeAuth = Effect.fn("MCP.removeAuth")(function* (mcpName: string) { + yield* auth.remove(mcpName) + McpOAuthCallback.cancelPending(mcpName) + pendingOAuthTransports.delete(mcpName) + log.info("removed oauth credentials", { mcpName }) + }) + + const supportsOAuth = Effect.fn("MCP.supportsOAuth")(function* (mcpName: string) { + const mcpConfig = yield* getMcpConfig(mcpName) + if (!mcpConfig) return false + return mcpConfig.type === "remote" && mcpConfig.oauth !== false + }) + + const hasStoredTokens = Effect.fn("MCP.hasStoredTokens")(function* (mcpName: string) { + const entry = yield* auth.get(mcpName) + return !!entry?.tokens + }) + + const getAuthStatus = Effect.fn("MCP.getAuthStatus")(function* (mcpName: string) { + const entry = yield* auth.get(mcpName) + if (!entry?.tokens) return "not_authenticated" as AuthStatus + const expired = yield* auth.isTokenExpired(mcpName) + return (expired ? "expired" : "authenticated") as AuthStatus + }) + + return Service.of({ + status, + clients, + tools, + prompts, + resources, + add, + connect, + disconnect, + getPrompt, + readResource, + startAuth, + authenticate, + finishAuth, + removeAuth, + supportsOAuth, + hasStoredTokens, + getAuthStatus, + }) + }), +) + +export type AuthStatus = "authenticated" | "expired" | "not_authenticated" + +// --- Per-service runtime --- + +export const defaultLayer = layer.pipe( + Layer.provide(McpAuth.layer), + Layer.provide(Bus.layer), + Layer.provide(Config.defaultLayer), + Layer.provide(CrossSpawnSpawner.defaultLayer), + Layer.provide(AppFileSystem.defaultLayer), +) From d6b14e24678db678163c281257322c5a9bf0e6fa Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 21:56:23 -0400 Subject: [PATCH 174/300] fix: prefix 32 unused parameters with underscore (#22694) --- packages/console/app/src/component/icon.tsx | 4 ++-- .../console/app/src/routes/auth/status.ts | 2 +- .../console/app/src/routes/debug/index.ts | 2 +- .../zen/util/provider/openai-compatible.ts | 2 +- .../console/app/src/routes/zen/v1/models.ts | 2 +- .../app/src/routes/zen/v1/models/[model].ts | 4 ++-- packages/function/src/api.ts | 4 ++-- packages/opencode/src/agent/agent.ts | 2 +- .../src/cli/cmd/tui/component/dialog-mcp.tsx | 2 +- .../src/cli/cmd/tui/ui/dialog-confirm.tsx | 2 +- packages/opencode/src/config/config.ts | 2 +- packages/opencode/src/provider/schema.ts | 2 +- packages/opencode/src/provider/transform.ts | 2 +- packages/opencode/src/v2/session.ts | 4 ++-- .../test/session/prompt-effect.test.ts | 20 +++++++++---------- packages/opencode/test/session/retry.test.ts | 2 +- packages/plugin/src/example.ts | 2 +- 17 files changed, 30 insertions(+), 30 deletions(-) diff --git a/packages/console/app/src/component/icon.tsx b/packages/console/app/src/component/icon.tsx index 4627c00845..da2e87ef4c 100644 --- a/packages/console/app/src/component/icon.tsx +++ b/packages/console/app/src/component/icon.tsx @@ -1,6 +1,6 @@ import { JSX } from "solid-js" -export function IconZen(props: JSX.SvgSVGAttributes) { +export function IconZen(_props: JSX.SvgSVGAttributes) { return ( @@ -13,7 +13,7 @@ export function IconZen(props: JSX.SvgSVGAttributes) { ) } -export function IconGo(props: JSX.SvgSVGAttributes) { +export function IconGo(_props: JSX.SvgSVGAttributes) { return ( diff --git a/packages/console/app/src/routes/auth/status.ts b/packages/console/app/src/routes/auth/status.ts index 215cae698f..ed522d7404 100644 --- a/packages/console/app/src/routes/auth/status.ts +++ b/packages/console/app/src/routes/auth/status.ts @@ -1,7 +1,7 @@ import { APIEvent } from "@solidjs/start" import { useAuthSession } from "~/context/auth" -export async function GET(input: APIEvent) { +export async function GET(_input: APIEvent) { const session = await useAuthSession() return Response.json(session.data) } diff --git a/packages/console/app/src/routes/debug/index.ts b/packages/console/app/src/routes/debug/index.ts index 2bdd269e78..4bfb633944 100644 --- a/packages/console/app/src/routes/debug/index.ts +++ b/packages/console/app/src/routes/debug/index.ts @@ -3,7 +3,7 @@ import { json } from "@solidjs/router" import { Database } from "@opencode-ai/console-core/drizzle/index.js" import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js" -export async function GET(evt: APIEvent) { +export async function GET(_evt: APIEvent) { return json({ data: await Database.use(async (tx) => { const result = await tx.$count(UserTable) diff --git a/packages/console/app/src/routes/zen/util/provider/openai-compatible.ts b/packages/console/app/src/routes/zen/util/provider/openai-compatible.ts index e05f0d6c0b..97b0abc64f 100644 --- a/packages/console/app/src/routes/zen/util/provider/openai-compatible.ts +++ b/packages/console/app/src/routes/zen/util/provider/openai-compatible.ts @@ -30,7 +30,7 @@ export const oaCompatHelper: ProviderHelper = ({ adjustCacheUsage, safetyIdentif headers.set("authorization", `Bearer ${apiKey}`) headers.set("x-session-affinity", headers.get("x-opencode-session") ?? "") }, - modifyBody: (body: Record, workspaceID?: string) => { + modifyBody: (body: Record, _workspaceID?: string) => { return { ...body, ...(body.stream ? { stream_options: { include_usage: true } } : {}), diff --git a/packages/console/app/src/routes/zen/v1/models.ts b/packages/console/app/src/routes/zen/v1/models.ts index d2592d20b0..6b4a878fc7 100644 --- a/packages/console/app/src/routes/zen/v1/models.ts +++ b/packages/console/app/src/routes/zen/v1/models.ts @@ -5,7 +5,7 @@ import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.j import { ModelTable } from "@opencode-ai/console-core/schema/model.sql.js" import { ZenData } from "@opencode-ai/console-core/model.js" -export async function OPTIONS(input: APIEvent) { +export async function OPTIONS(_input: APIEvent) { return new Response(null, { status: 200, headers: { diff --git a/packages/console/app/src/routes/zen/v1/models/[model].ts b/packages/console/app/src/routes/zen/v1/models/[model].ts index a4edd5861a..bc1168eb0c 100644 --- a/packages/console/app/src/routes/zen/v1/models/[model].ts +++ b/packages/console/app/src/routes/zen/v1/models/[model].ts @@ -6,8 +6,8 @@ export function POST(input: APIEvent) { format: "google", modelList: "full", parseApiKey: (headers: Headers) => headers.get("x-goog-api-key") ?? undefined, - parseModel: (url: string, body: any) => url.split("/").pop()?.split(":")?.[0] ?? "", - parseIsStream: (url: string, body: any) => + parseModel: (url: string, _body: any) => url.split("/").pop()?.split(":")?.[0] ?? "", + parseIsStream: (url: string, _body: any) => // ie. url: https://opencode.ai/zen/v1/models/gemini-3-pro:streamGenerateContent?alt=sse' url.split("/").pop()?.split(":")?.[1]?.startsWith("streamGenerateContent") ?? false, }) diff --git a/packages/function/src/api.ts b/packages/function/src/api.ts index 54b93ad715..d6565b2870 100644 --- a/packages/function/src/api.ts +++ b/packages/function/src/api.ts @@ -49,9 +49,9 @@ export class SyncServer extends DurableObject { }) } - async webSocketMessage(ws, message) {} + async webSocketMessage(_ws, _message) {} - async webSocketClose(ws, code, reason, wasClean) { + async webSocketClose(ws, code, _reason, _wasClean) { ws.close(code, "Durable Object is closing WebSocket") } diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 8e6bfe5e9b..8d11a93b39 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -80,7 +80,7 @@ export namespace Agent { const provider = yield* Provider.Service const state = yield* InstanceState.make( - Effect.fn("Agent.state")(function* (ctx) { + Effect.fn("Agent.state")(function* (_ctx) { const cfg = yield* config.get() const skillDirs = yield* skill.dirs() const whitelistedDirs = [Truncate.GLOB, ...skillDirs.map((dir) => path.join(dir, "*"))] diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-mcp.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-mcp.tsx index 9cfa30d4df..173c5ff60c 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-mcp.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-mcp.tsx @@ -78,7 +78,7 @@ export function DialogMcp() { title="MCPs" options={options()} keybind={keybinds()} - onSelect={(option) => { + onSelect={(_option) => { // Don't close on select, only on escape }} /> diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx index ef75764a29..48adddaedc 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx @@ -54,7 +54,7 @@ export function DialogConfirm(props: DialogConfirmProps) { paddingLeft={1} paddingRight={1} backgroundColor={key === store.active ? theme.primary : undefined} - onMouseUp={(evt) => { + onMouseUp={(_evt) => { if (key === "confirm") props.onConfirm?.() if (key === "cancel") props.onCancel?.() dialog.clear() diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index f35e8c83df..63e41f4455 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -510,7 +510,7 @@ export const Agent = z permission: Permission.optional(), }) .catchall(z.any()) - .transform((agent, ctx) => { + .transform((agent, _ctx) => { const knownKeys = new Set([ "name", "model", diff --git a/packages/opencode/src/provider/schema.ts b/packages/opencode/src/provider/schema.ts index 4490ca2898..702616018a 100644 --- a/packages/opencode/src/provider/schema.ts +++ b/packages/opencode/src/provider/schema.ts @@ -30,7 +30,7 @@ const modelIdSchema = Schema.String.pipe(Schema.brand("ModelID")) export type ModelID = typeof modelIdSchema.Type export const ModelID = modelIdSchema.pipe( - withStatics((schema: typeof modelIdSchema) => ({ + withStatics((_schema: typeof modelIdSchema) => ({ zod: z.string().pipe(z.custom()), })), ) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 3138f8e293..7b83c245f4 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -49,7 +49,7 @@ export namespace ProviderTransform { function normalizeMessages( msgs: ModelMessage[], model: Provider.Model, - options: Record, + _options: Record, ): ModelMessage[] { // Anthropic rejects messages with empty content - filter out empty string messages // and remove empty text/reasoning parts from array content diff --git a/packages/opencode/src/v2/session.ts b/packages/opencode/src/v2/session.ts index 97df0a2207..ce1b39031f 100644 --- a/packages/opencode/src/v2/session.ts +++ b/packages/opencode/src/v2/session.ts @@ -40,11 +40,11 @@ export namespace SessionV2 { Effect.gen(function* () { const session = yield* Session.Service - const create: Interface["create"] = Effect.fn("Session.create")(function* (input) { + const create: Interface["create"] = Effect.fn("Session.create")(function* (_input) { throw new Error("Not implemented") }) - const prompt: Interface["prompt"] = Effect.fn("Session.prompt")(function* (input) { + const prompt: Interface["prompt"] = Effect.fn("Session.prompt")(function* (_input) { throw new Error("Not implemented") }) diff --git a/packages/opencode/test/session/prompt-effect.test.ts b/packages/opencode/test/session/prompt-effect.test.ts index 91297aed1d..7a118cb050 100644 --- a/packages/opencode/test/session/prompt-effect.test.ts +++ b/packages/opencode/test/session/prompt-effect.test.ts @@ -786,7 +786,7 @@ it.live( const { task } = yield* registry.named() const original = task.execute task.execute = (_args, ctx) => - Effect.callback((resume) => { + Effect.callback((_resume) => { ready.resolve() ctx.abort.addEventListener("abort", () => aborted.resolve(), { once: true }) return Effect.sync(() => aborted.resolve()) @@ -856,7 +856,7 @@ it.live( it.live("concurrent loop callers get same result", () => provideTmpdirInstance( - (dir) => + (_dir) => Effect.gen(function* () { const { prompt, run, chat } = yield* boot() yield* seed(chat.id, { finish: "stop" }) @@ -997,7 +997,7 @@ it.live( it.live("assertNotBusy succeeds when idle", () => provideTmpdirInstance( - (dir) => + (_dir) => Effect.gen(function* () { const run = yield* SessionRunState.Service const sessions = yield* Session.Service @@ -1042,7 +1042,7 @@ it.live( unix("shell captures stdout and stderr in completed tool output", () => provideTmpdirInstance( - (dir) => + (_dir) => Effect.gen(function* () { const { prompt, run, chat } = yield* boot() const result = yield* prompt.shell({ @@ -1117,7 +1117,7 @@ unix("shell lists files from the project directory", () => unix("shell captures stderr from a failing command", () => provideTmpdirInstance( - (dir) => + (_dir) => Effect.gen(function* () { const { prompt, run, chat } = yield* boot() const result = yield* prompt.shell({ @@ -1143,7 +1143,7 @@ unix( () => withSh(() => provideTmpdirInstance( - (dir) => + (_dir) => Effect.gen(function* () { const { prompt, chat } = yield* boot() @@ -1255,7 +1255,7 @@ unix( () => withSh(() => provideTmpdirInstance( - (dir) => + (_dir) => Effect.gen(function* () { const { prompt, run, chat } = yield* boot() @@ -1292,7 +1292,7 @@ unix( () => withSh(() => provideTmpdirInstance( - (dir) => + (_dir) => Effect.gen(function* () { const { prompt, chat } = yield* boot() @@ -1374,7 +1374,7 @@ unix( "cancel interrupts loop queued behind shell", () => provideTmpdirInstance( - (dir) => + (_dir) => Effect.gen(function* () { const { prompt, chat } = yield* boot() @@ -1403,7 +1403,7 @@ unix( () => withSh(() => provideTmpdirInstance( - (dir) => + (_dir) => Effect.gen(function* () { const { prompt, chat } = yield* boot() diff --git a/packages/opencode/test/session/retry.test.ts b/packages/opencode/test/session/retry.test.ts index 2d01a8f354..ade2647869 100644 --- a/packages/opencode/test/session/retry.test.ts +++ b/packages/opencode/test/session/retry.test.ts @@ -239,7 +239,7 @@ describe("session.message-v2.fromError", () => { using server = Bun.serve({ port: 0, idleTimeout: 8, - async fetch(req) { + async fetch(_req) { return new Response( new ReadableStream({ async pull(controller) { diff --git a/packages/plugin/src/example.ts b/packages/plugin/src/example.ts index 1cf042fe96..9d7e178a96 100644 --- a/packages/plugin/src/example.ts +++ b/packages/plugin/src/example.ts @@ -1,7 +1,7 @@ import { Plugin } from "./index.js" import { tool } from "./tool.js" -export const ExamplePlugin: Plugin = async (ctx) => { +export const ExamplePlugin: Plugin = async (_ctx) => { return { tool: { mytool: tool({ From 70aeebf2dfe59009ba7254facaa81b8baf73c3cf Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Thu, 16 Apr 2026 01:57:23 +0000 Subject: [PATCH 175/300] chore: generate --- packages/opencode/src/mcp/mcp.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/opencode/src/mcp/mcp.ts b/packages/opencode/src/mcp/mcp.ts index 1e3288682e..947f29c05b 100644 --- a/packages/opencode/src/mcp/mcp.ts +++ b/packages/opencode/src/mcp/mcp.ts @@ -275,10 +275,7 @@ export const layer = Layer.effect( const DISABLED_RESULT: CreateResult = { status: { status: "disabled" } } - const connectRemote = Effect.fn("MCP.connectRemote")(function* ( - key: string, - mcp: Config.Mcp & { type: "remote" }, - ) { + const connectRemote = Effect.fn("MCP.connectRemote")(function* (key: string, mcp: Config.Mcp & { type: "remote" }) { const oauthDisabled = mcp.oauth === false const oauthConfig = typeof mcp.oauth === "object" ? mcp.oauth : undefined let authProvider: McpOAuthProvider | undefined @@ -451,9 +448,7 @@ export const layer = Layer.effect( const queue = [pid] while (queue.length > 0) { const current = queue.shift()! - const handle = yield* spawner.spawn( - ChildProcess.make("pgrep", ["-P", String(current)], { stdin: "ignore" }), - ) + const handle = yield* spawner.spawn(ChildProcess.make("pgrep", ["-P", String(current)], { stdin: "ignore" })) const text = yield* Stream.mkString(Stream.decodeText(handle.stdout)) yield* handle.exitCode for (const tok of text.split("\n")) { From 34213d444681a8953c5693bd01dd754c4e79a30b Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 22:01:02 -0400 Subject: [PATCH 176/300] fix: delete 9 dead functions with zero callers (#22697) --- .../app/src/context/global-sync/bootstrap.ts | 16 ------------- packages/function/src/api.ts | 14 ----------- packages/opencode/script/postinstall.mjs | 12 ---------- .../src/server/instance/httpapi/server.ts | 4 ---- packages/opencode/test/lib/llm-server.ts | 24 ------------------- .../opencode/test/session/compaction.test.ts | 19 --------------- packages/ui/src/components/session-diff.ts | 14 ----------- .../timeline-playground.stories.tsx | 4 ---- 8 files changed, 107 deletions(-) diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts index ad987efa6e..2f9147498e 100644 --- a/packages/app/src/context/global-sync/bootstrap.ts +++ b/packages/app/src/context/global-sync/bootstrap.ts @@ -65,22 +65,6 @@ function runAll(list: Array<() => Promise>) { return Promise.allSettled(list.map((item) => item())) } -function showErrors(input: { - errors: unknown[] - title: string - translate: (key: string, vars?: Record) => string - formatMoreCount: (count: number) => string -}) { - if (input.errors.length === 0) return - const message = formatServerError(input.errors[0], input.translate) - const more = input.errors.length > 1 ? input.formatMoreCount(input.errors.length - 1) : "" - showToast({ - variant: "error", - title: input.title, - description: message + more, - }) -} - export async function bootstrapGlobal(input: { globalSDK: OpencodeClient requestFailedTitle: string diff --git a/packages/function/src/api.ts b/packages/function/src/api.ts index d6565b2870..4d8b295ec7 100644 --- a/packages/function/src/api.ts +++ b/packages/function/src/api.ts @@ -12,20 +12,6 @@ type Env = { WEB_DOMAIN: string } -async function getFeishuTenantToken(): Promise { - const response = await fetch("https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - app_id: Resource.FEISHU_APP_ID.value, - app_secret: Resource.FEISHU_APP_SECRET.value, - }), - }) - const data = (await response.json()) as { tenant_access_token?: string } - if (!data.tenant_access_token) throw new Error("Failed to get Feishu tenant token") - return data.tenant_access_token -} - export class SyncServer extends DurableObject { constructor(ctx: DurableObjectState, env: Env) { super(ctx, env) diff --git a/packages/opencode/script/postinstall.mjs b/packages/opencode/script/postinstall.mjs index 98f23e16fb..2b990251ce 100644 --- a/packages/opencode/script/postinstall.mjs +++ b/packages/opencode/script/postinstall.mjs @@ -85,18 +85,6 @@ function prepareBinDirectory(binaryName) { return { binDir, targetPath } } -function symlinkBinary(sourcePath, binaryName) { - const { targetPath } = prepareBinDirectory(binaryName) - - fs.symlinkSync(sourcePath, targetPath) - console.log(`opencode binary symlinked: ${targetPath} -> ${sourcePath}`) - - // Verify the file exists after operation - if (!fs.existsSync(targetPath)) { - throw new Error(`Failed to symlink binary to ${targetPath}`) - } -} - async function main() { try { if (os.platform() === "win32") { diff --git a/packages/opencode/src/server/instance/httpapi/server.ts b/packages/opencode/src/server/instance/httpapi/server.ts index 363e93a240..54c3c57ff5 100644 --- a/packages/opencode/src/server/instance/httpapi/server.ts +++ b/packages/opencode/src/server/instance/httpapi/server.ts @@ -26,10 +26,6 @@ const Headers = Schema.Struct({ }) export namespace ExperimentalHttpApiServer { - function text(input: string, status: number, headers?: Record) { - return HttpServerResponse.text(input, { status, headers }) - } - function decode(input: string) { try { return decodeURIComponent(input) diff --git a/packages/opencode/test/lib/llm-server.ts b/packages/opencode/test/lib/llm-server.ts index 2e2a2ea895..1f873a9fbb 100644 --- a/packages/opencode/test/lib/llm-server.ts +++ b/packages/opencode/test/lib/llm-server.ts @@ -596,35 +596,11 @@ function hit(url: string, body: unknown) { } satisfies Hit } -/** Auto-acknowledging tool-result follow-ups avoids requiring tests to queue two responses per tool call. */ -function isToolResultFollowUp(body: unknown): boolean { - if (!body || typeof body !== "object") return false - // OpenAI chat format: last message has role "tool" - if ("messages" in body && Array.isArray(body.messages)) { - const last = body.messages[body.messages.length - 1] - return last?.role === "tool" - } - // Responses API: input contains function_call_output - if ("input" in body && Array.isArray(body.input)) { - return body.input.some((item: Record) => item?.type === "function_call_output") - } - return false -} - function isTitleRequest(body: unknown): boolean { if (!body || typeof body !== "object") return false return JSON.stringify(body).includes("Generate a title for this conversation") } -function requestSummary(body: unknown): string { - if (!body || typeof body !== "object") return "empty body" - if ("messages" in body && Array.isArray(body.messages)) { - const roles = body.messages.map((m: Record) => m.role).join(",") - return `messages=[${roles}]` - } - return `keys=[${Object.keys(body).join(",")}]` -} - namespace TestLLMServer { export interface Service { readonly url: string diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index d658f48bd8..7711d31931 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/packages/opencode/test/session/compaction.test.ts @@ -143,25 +143,6 @@ async function assistant(sessionID: SessionID, parentID: MessageID, root: string return msg } -async function tool(sessionID: SessionID, messageID: MessageID, tool: string, output: string) { - return svc.updatePart({ - id: PartID.ascending(), - messageID, - sessionID, - type: "tool", - callID: crypto.randomUUID(), - tool, - state: { - status: "completed", - input: {}, - output, - title: "done", - metadata: {}, - time: { start: Date.now(), end: Date.now() }, - }, - }) -} - function fake( input: Parameters[0], result: "continue" | "compact", diff --git a/packages/ui/src/components/session-diff.ts b/packages/ui/src/components/session-diff.ts index d791c7fc10..a5fbdbc5c0 100644 --- a/packages/ui/src/components/session-diff.ts +++ b/packages/ui/src/components/session-diff.ts @@ -25,20 +25,6 @@ export type ViewDiff = { const cache = new Map() -function empty(file: string, key: string) { - return { - name: file, - type: "change", - hunks: [], - splitLineCount: 0, - unifiedLineCount: 0, - isPartial: true, - deletionLines: [], - additionLines: [], - cacheKey: key, - } satisfies FileDiffMetadata -} - function patch(diff: ReviewDiff) { if (typeof diff.patch === "string") { const [patch] = parsePatch(diff.patch) diff --git a/packages/ui/src/components/timeline-playground.stories.tsx b/packages/ui/src/components/timeline-playground.stories.tsx index 282592ff63..fa3e7ff798 100644 --- a/packages/ui/src/components/timeline-playground.stories.tsx +++ b/packages/ui/src/components/timeline-playground.stories.tsx @@ -555,10 +555,6 @@ function toolPart(sample: (typeof TOOL_SAMPLES)[keyof typeof TOOL_SAMPLES], stat } as ToolPart } -function compactionPart(): CompactionPart { - return { id: uid(), type: "compaction", auto: true } as CompactionPart -} - // --------------------------------------------------------------------------- // CSS Controls definition // --------------------------------------------------------------------------- From cce05c16658a39d091f658bdb53dcce1e88c66d0 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 22:01:53 -0400 Subject: [PATCH 177/300] fix: clean up 49 unused variables, catch params, and stale imports (#22695) --- infra/enterprise.ts | 2 +- packages/app/src/addons/serialize.test.ts | 4 +-- packages/app/src/app.tsx | 7 +---- packages/app/src/pages/session.tsx | 3 -- packages/app/src/pages/session/file-tabs.tsx | 6 ---- .../console/app/script/generate-sitemap.ts | 1 - packages/console/app/src/routes/index.tsx | 2 -- packages/console/app/src/routes/user-menu.tsx | 2 +- packages/function/src/api.ts | 2 +- packages/opencode/src/bus/bus.ts | 1 - .../cmd/tui/component/prompt/autocomplete.tsx | 2 +- .../src/cli/cmd/tui/routes/session/index.tsx | 4 +-- .../cli/cmd/tui/routes/session/permission.tsx | 2 +- .../tui/routes/session/subagent-footer.tsx | 2 +- packages/opencode/src/cli/network.ts | 2 -- packages/opencode/src/command/index.ts | 3 -- packages/opencode/src/config/config.ts | 2 +- .../opencode/src/control-plane/workspace.ts | 4 +-- packages/opencode/src/mcp/oauth-callback.ts | 2 +- packages/opencode/src/server/fence.ts | 2 +- .../src/server/instance/middleware.ts | 2 -- .../opencode/src/server/instance/provider.ts | 3 -- packages/opencode/src/session/prompt.ts | 2 +- packages/opencode/src/tool/registry.ts | 2 +- packages/opencode/test/file/index.test.ts | 2 +- .../test/fixture/lsp/fake-lsp-server.js | 2 -- .../opencode/test/provider/transform.test.ts | 2 -- .../opencode/test/server/session-list.test.ts | 2 +- packages/opencode/test/session/llm.test.ts | 1 - .../test/session/messages-pagination.test.ts | 8 +++--- packages/sdk/js/src/v2/data.ts | 2 +- .../shared/test/filesystem/filesystem.test.ts | 4 +-- .../test/fixture/effect-flock-worker.ts | 1 - packages/slack/src/index.ts | 2 +- script/publish.ts | 28 ------------------- script/stats.ts | 2 +- sdks/vscode/src/extension.ts | 6 ++-- 37 files changed, 32 insertions(+), 94 deletions(-) diff --git a/infra/enterprise.ts b/infra/enterprise.ts index 38f0c3c8fd..dc336a6843 100644 --- a/infra/enterprise.ts +++ b/infra/enterprise.ts @@ -3,7 +3,7 @@ import { shortDomain } from "./stage" const storage = new sst.cloudflare.Bucket("EnterpriseStorage") -const teams = new sst.cloudflare.x.SolidStart("Teams", { +new sst.cloudflare.x.SolidStart("Teams", { domain: shortDomain, path: "packages/enterprise", buildCommand: "bun run build:cloudflare", diff --git a/packages/app/src/addons/serialize.test.ts b/packages/app/src/addons/serialize.test.ts index 7f6780557d..6828e60f84 100644 --- a/packages/app/src/addons/serialize.test.ts +++ b/packages/app/src/addons/serialize.test.ts @@ -180,8 +180,8 @@ describe("SerializeAddon", () => { await writeAndWait(term, input) const origLine = term.buffer.active.getLine(0) - const origFg = origLine!.getCell(0)!.getFgColor() - const origBg = origLine!.getCell(0)!.getBgColor() + const _origFg = origLine!.getCell(0)!.getFgColor() + const _origBg = origLine!.getCell(0)!.getBgColor() expect(origLine!.getCell(0)!.isBold()).toBe(1) const serialized = addon.serialize({ range: { start: 0, end: 0 } }) diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index 35fd36cca3..9983548ba0 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -10,7 +10,7 @@ import { ThemeProvider } from "@opencode-ai/ui/theme/context" import { MetaProvider } from "@solidjs/meta" import { type BaseRouterProps, Navigate, Route, Router } from "@solidjs/router" import { QueryClient, QueryClientProvider } from "@tanstack/solid-query" -import { type Duration, Effect } from "effect" +import { Effect } from "effect" import { type Component, createMemo, @@ -156,11 +156,6 @@ export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) { ) } -const effectMinDuration = - (duration: Duration.Input) => - (e: Effect.Effect) => - Effect.all([e, Effect.sleep(duration)], { concurrency: "unbounded" }).pipe(Effect.map((v) => v[0])) - function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) { const server = useServer() const checkServerHealth = useCheckServerHealth() diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index e328e3f0cc..32df997f7f 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -433,7 +433,6 @@ export default function Page() { const isChildSession = createMemo(() => !!info()?.parentID) const diffs = createMemo(() => (params.id ? list(sync.data.session_diff[params.id]) : [])) const sessionCount = createMemo(() => Math.max(info()?.summary?.files ?? 0, diffs().length)) - const hasSessionReview = createMemo(() => sessionCount() > 0) const canReview = createMemo(() => !!sync.project) const reviewTab = createMemo(() => isDesktop()) const tabState = createSessionTabs({ @@ -443,8 +442,6 @@ export default function Page() { review: reviewTab, hasReview: canReview, }) - const contextOpen = tabState.contextOpen - const openedTabs = tabState.openedTabs const activeTab = tabState.activeTab const activeFileTab = tabState.activeFileTab const revertMessageID = createMemo(() => info()?.revert?.messageID) diff --git a/packages/app/src/pages/session/file-tabs.tsx b/packages/app/src/pages/session/file-tabs.tsx index a64dff64e2..37bffcd2fa 100644 --- a/packages/app/src/pages/session/file-tabs.tsx +++ b/packages/app/src/pages/session/file-tabs.tsx @@ -378,12 +378,6 @@ export function FileTabContent(props: { tab: string }) { requestAnimationFrame(() => comments.clearFocus()) }) - const cancelCommenting = () => { - const p = path() - if (p) file.setSelectedLines(p, null) - setNote("commenting", null) - } - let prev = { loaded: false, ready: false, diff --git a/packages/console/app/script/generate-sitemap.ts b/packages/console/app/script/generate-sitemap.ts index 89bca6bac5..9fd3ba0f0f 100755 --- a/packages/console/app/script/generate-sitemap.ts +++ b/packages/console/app/script/generate-sitemap.ts @@ -8,7 +8,6 @@ import { LOCALES, route } from "../src/lib/language.js" const __dirname = dirname(fileURLToPath(import.meta.url)) const BASE_URL = config.baseUrl const PUBLIC_DIR = join(__dirname, "../public") -const ROUTES_DIR = join(__dirname, "../src/routes") const DOCS_DIR = join(__dirname, "../../../web/src/content/docs") interface SitemapEntry { diff --git a/packages/console/app/src/routes/index.tsx b/packages/console/app/src/routes/index.tsx index e47134d2b9..b5b12a84bd 100644 --- a/packages/console/app/src/routes/index.tsx +++ b/packages/console/app/src/routes/index.tsx @@ -31,8 +31,6 @@ export default function Home() { const i18n = useI18n() const language = useLanguage() const githubData = createAsync(() => github()) - const release = createMemo(() => githubData()?.release) - const handleCopyClick = (event: Event) => { const button = event.currentTarget as HTMLButtonElement const text = button.textContent diff --git a/packages/console/app/src/routes/user-menu.tsx b/packages/console/app/src/routes/user-menu.tsx index fa1c1f60bb..7b305d8ead 100644 --- a/packages/console/app/src/routes/user-menu.tsx +++ b/packages/console/app/src/routes/user-menu.tsx @@ -6,7 +6,7 @@ import { useI18n } from "~/context/i18n" import { useLanguage } from "~/context/language" import "./user-menu.css" -const logout = action(async () => { +const _logout = action(async () => { "use server" const auth = await useAuthSession() const event = getRequestEvent() diff --git a/packages/function/src/api.ts b/packages/function/src/api.ts index 4d8b295ec7..68b2d450bb 100644 --- a/packages/function/src/api.ts +++ b/packages/function/src/api.ts @@ -181,7 +181,7 @@ export default new Hono<{ Bindings: Env }>() let info const messages: Record = {} data.forEach((d) => { - const [root, type, ...splits] = d.key.split("/") + const [root, type] = d.key.split("/") if (root !== "session") return if (type === "info") { info = d.content diff --git a/packages/opencode/src/bus/bus.ts b/packages/opencode/src/bus/bus.ts index c5e31e6c20..fe9169171c 100644 --- a/packages/opencode/src/bus/bus.ts +++ b/packages/opencode/src/bus/bus.ts @@ -4,7 +4,6 @@ import { EffectBridge } from "@/effect/bridge" import { Log } from "../util/log" import { BusEvent } from "./bus-event" import { GlobalBus } from "./global" -import { WorkspaceContext } from "@/control-plane/workspace-context" import { InstanceState } from "@/effect/instance-state" import { makeRuntime } from "@/effect/run-service" diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index 2118fe98e1..7ca73310bc 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -111,7 +111,7 @@ export function Autocomplete(props: { const position = createMemo(() => { if (!store.visible) return { x: 0, y: 0, width: 0 } - const dims = dimensions() + dimensions() positionTick() const anchor = props.anchor() const parent = anchor.parent diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index f9fd5a9b9c..9f0dfa6038 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -157,10 +157,10 @@ export function Session() { const [showThinking, setShowThinking] = kv.signal("thinking_visibility", true) const [timestamps, setTimestamps] = kv.signal<"hide" | "show">("timestamps", "hide") const [showDetails, setShowDetails] = kv.signal("tool_details_visibility", true) - const [showAssistantMetadata, setShowAssistantMetadata] = kv.signal("assistant_metadata_visibility", true) + const [showAssistantMetadata, _setShowAssistantMetadata] = kv.signal("assistant_metadata_visibility", true) const [showScrollbar, setShowScrollbar] = kv.signal("scrollbar_visible", false) const [diffWrapMode] = kv.signal<"word" | "none">("diff_wrap_mode", "word") - const [animationsEnabled, setAnimationsEnabled] = kv.signal("animations_enabled", true) + const [_animationsEnabled, _setAnimationsEnabled] = kv.signal("animations_enabled", true) const [showGenericToolOutput, setShowGenericToolOutput] = kv.signal("generic_tool_output_visibility", false) const wide = createMemo(() => dimensions().width > 120) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx index e0b5002b61..ad824fe48f 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx @@ -599,7 +599,7 @@ function Prompt>(props: { }) const hint = createMemo(() => (store.expanded ? "minimize" : "fullscreen")) - const renderer = useRenderer() + useRenderer() const content = () => ( (null) - const dimensions = useTerminalDimensions() + useTerminalDimensions() return ( diff --git a/packages/opencode/src/cli/network.ts b/packages/opencode/src/cli/network.ts index 6321c056d0..ea281aafb9 100644 --- a/packages/opencode/src/cli/network.ts +++ b/packages/opencode/src/cli/network.ts @@ -43,8 +43,6 @@ export async function resolveNetworkOptions(args: NetworkOptions) { const hostnameExplicitlySet = process.argv.includes("--hostname") const mdnsExplicitlySet = process.argv.includes("--mdns") const mdnsDomainExplicitlySet = process.argv.includes("--mdns-domain") - const corsExplicitlySet = process.argv.includes("--cors") - const mdns = mdnsExplicitlySet ? args.mdns : (config?.server?.mdns ?? args.mdns) const mdnsDomain = mdnsDomainExplicitlySet ? args["mdns-domain"] : (config?.server?.mdnsDomain ?? args["mdns-domain"]) const port = portExplicitlySet ? args.port : (config?.server?.port ?? args.port) diff --git a/packages/opencode/src/command/index.ts b/packages/opencode/src/command/index.ts index 28fb37f272..539ae0dac6 100644 --- a/packages/opencode/src/command/index.ts +++ b/packages/opencode/src/command/index.ts @@ -8,13 +8,10 @@ import z from "zod" import { Config } from "../config" import { MCP } from "../mcp" import { Skill } from "../skill" -import { Log } from "../util/log" import PROMPT_INITIALIZE from "./template/initialize.txt" import PROMPT_REVIEW from "./template/review.txt" export namespace Command { - const log = Log.create({ service: "command" }) - type State = { commands: Record } diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 63e41f4455..58d9343ad9 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -1095,7 +1095,7 @@ function patchJsonc(input: string, patch: unknown, path: string[] = []): string } function writable(info: Info) { - const { plugin_origins, ...next } = info + const { plugin_origins: _plugin_origins, ...next } = info return next } diff --git a/packages/opencode/src/control-plane/workspace.ts b/packages/opencode/src/control-plane/workspace.ts index 67583107fc..4fef4f9321 100644 --- a/packages/opencode/src/control-plane/workspace.ts +++ b/packages/opencode/src/control-plane/workspace.ts @@ -328,7 +328,7 @@ export namespace Workspace { try { const adaptor = await getAdaptor(info.projectID, row.type) await adaptor.remove(info) - } catch (err) { + } catch { log.error("adaptor not available when removing workspace", { type: row.type }) } Database.use((db) => db.delete(WorkspaceTable).where(eq(WorkspaceTable.id, id)).run()) @@ -404,7 +404,7 @@ export namespace Workspace { return synced(state) }, }) - } catch (error) { + } catch { if (signal?.aborted) throw signal.reason ?? new Error("Request aborted") throw new Error(`Timed out waiting for sync fence: ${JSON.stringify(state)}`) } diff --git a/packages/opencode/src/mcp/oauth-callback.ts b/packages/opencode/src/mcp/oauth-callback.ts index b5b6a7a6eb..6babccd779 100644 --- a/packages/opencode/src/mcp/oauth-callback.ts +++ b/packages/opencode/src/mcp/oauth-callback.ts @@ -218,7 +218,7 @@ export namespace McpOAuthCallback { log.info("oauth callback server stopped") } - for (const [name, pending] of pendingAuths) { + for (const [_name, pending] of pendingAuths) { clearTimeout(pending.timeout) pending.reject(new Error("OAuth callback server stopped")) } diff --git a/packages/opencode/src/server/fence.ts b/packages/opencode/src/server/fence.ts index bb41bd7a43..b6dbde0081 100644 --- a/packages/opencode/src/server/fence.ts +++ b/packages/opencode/src/server/fence.ts @@ -40,7 +40,7 @@ export function parse(headers: Headers) { try { data = JSON.parse(raw) - } catch (err) { + } catch { return } diff --git a/packages/opencode/src/server/instance/middleware.ts b/packages/opencode/src/server/instance/middleware.ts index 0e29daa9ee..5fd1fc25e8 100644 --- a/packages/opencode/src/server/instance/middleware.ts +++ b/packages/opencode/src/server/instance/middleware.ts @@ -16,8 +16,6 @@ import { AppFileSystem } from "@opencode-ai/shared/filesystem" type Rule = { method?: string; path: string; exact?: boolean; action: "local" | "forward" } -const OPENCODE_WORKSPACE = process.env.OPENCODE_WORKSPACE - const RULES: Array = [ { path: "/session/status", action: "forward" }, { method: "GET", path: "/session", action: "local" }, diff --git a/packages/opencode/src/server/instance/provider.ts b/packages/opencode/src/server/instance/provider.ts index 8018dfbea4..bbde4c9552 100644 --- a/packages/opencode/src/server/instance/provider.ts +++ b/packages/opencode/src/server/instance/provider.ts @@ -10,11 +10,8 @@ import { AppRuntime } from "../../effect/app-runtime" import { mapValues } from "remeda" import { errors } from "../error" import { lazy } from "../../util/lazy" -import { Log } from "../../util/log" import { Effect } from "effect" -const log = Log.create({ service: "server" }) - export const ProviderRoutes = lazy(() => new Hono() .get( diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 4e10fdf2d6..b699676897 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1825,7 +1825,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the onSuccess: (output: unknown) => void }): AITool { // Remove $schema property if present (not needed for tool input) - const { $schema, ...toolSchema } = input.schema + const { $schema: _, ...toolSchema } = input.schema return tool({ id: "StructuredOutput" as any, diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index ef55758a57..b9870d194d 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -176,7 +176,7 @@ export namespace ToolRegistry { } } - const cfg = yield* config.get() + yield* config.get() const questionEnabled = ["app", "cli", "desktop"].includes(Flag.OPENCODE_CLIENT) || Flag.OPENCODE_ENABLE_QUESTION_TOOL diff --git a/packages/opencode/test/file/index.test.ts b/packages/opencode/test/file/index.test.ts index d8203ac12d..877e2ae0a3 100644 --- a/packages/opencode/test/file/index.test.ts +++ b/packages/opencode/test/file/index.test.ts @@ -276,7 +276,7 @@ describe("file/index Filesystem patterns", () => { test("returns empty array buffer on error for images", async () => { await using tmp = await tmpdir() - const filepath = path.join(tmp.path, "broken.png") + const _filepath = path.join(tmp.path, "broken.png") // Don't create the file await Instance.provide({ diff --git a/packages/opencode/test/fixture/lsp/fake-lsp-server.js b/packages/opencode/test/fixture/lsp/fake-lsp-server.js index 39e5788012..be62f96f38 100644 --- a/packages/opencode/test/fixture/lsp/fake-lsp-server.js +++ b/packages/opencode/test/fixture/lsp/fake-lsp-server.js @@ -1,8 +1,6 @@ // Simple JSON-RPC 2.0 LSP-like fake server over stdio // Implements a minimal LSP handshake and triggers a request upon notification -const net = require("net") - let nextId = 1 function encode(message) { diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 4952a126b3..0e0810d0e9 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -2,8 +2,6 @@ import { describe, expect, test } from "bun:test" import { ProviderTransform } from "../../src/provider/transform" import { ModelID, ProviderID } from "../../src/provider/schema" -const OUTPUT_TOKEN_MAX = 32000 - describe("ProviderTransform.options - setCacheKey", () => { const sessionID = "test-session-123" diff --git a/packages/opencode/test/server/session-list.test.ts b/packages/opencode/test/server/session-list.test.ts index 8c86dc2f06..75adb7f9f3 100644 --- a/packages/opencode/test/server/session-list.test.ts +++ b/packages/opencode/test/server/session-list.test.ts @@ -67,7 +67,7 @@ describe("session.list", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const session = await svc.create({ title: "new-session" }) + await svc.create({ title: "new-session" }) const futureStart = Date.now() + 86400000 const sessions = [...svc.list({ start: futureStart })] diff --git a/packages/opencode/test/session/llm.test.ts b/packages/opencode/test/session/llm.test.ts index e908545d4a..f25ecc356a 100644 --- a/packages/opencode/test/session/llm.test.ts +++ b/packages/opencode/test/session/llm.test.ts @@ -1181,7 +1181,6 @@ describe("session.llm.stream", () => { const providerID = "google" const modelID = "gemini-2.5-flash" const fixture = await loadFixture(providerID, modelID) - const provider = fixture.provider const model = fixture.model const pathSuffix = `/v1beta/models/${model.id}:streamGenerateContent` diff --git a/packages/opencode/test/session/messages-pagination.test.ts b/packages/opencode/test/session/messages-pagination.test.ts index 668918ec83..f728bd3646 100644 --- a/packages/opencode/test/session/messages-pagination.test.ts +++ b/packages/opencode/test/session/messages-pagination.test.ts @@ -724,7 +724,7 @@ describe("MessageV2.filterCompacted", () => { const u1 = await addUser(session.id, "hello") await addCompactionPart(session.id, u1) - const u2 = await addUser(session.id, "world") + await addUser(session.id, "world") const result = MessageV2.filterCompacted(MessageV2.stream(session.id)) expect(result).toHaveLength(2) @@ -748,7 +748,7 @@ describe("MessageV2.filterCompacted", () => { isRetryable: true, }).toObject() as MessageV2.Assistant["error"] await addAssistant(session.id, u1, { summary: true, finish: "end_turn", error }) - const u2 = await addUser(session.id, "retry") + await addUser(session.id, "retry") const result = MessageV2.filterCompacted(MessageV2.stream(session.id)) // Error assistant doesn't add to completed, so compaction boundary never triggers @@ -770,7 +770,7 @@ describe("MessageV2.filterCompacted", () => { // summary=true but no finish await addAssistant(session.id, u1, { summary: true }) - const u2 = await addUser(session.id, "next") + await addUser(session.id, "next") const result = MessageV2.filterCompacted(MessageV2.stream(session.id)) expect(result).toHaveLength(3) @@ -892,7 +892,7 @@ describe("MessageV2 consistency", () => { directory: root, fn: async () => { const session = await svc.create({}) - const ids = await fill(session.id, 4) + await fill(session.id, 4) const filtered = MessageV2.filterCompacted(MessageV2.stream(session.id)) const all = Array.from(MessageV2.stream(session.id)).reverse() diff --git a/packages/sdk/js/src/v2/data.ts b/packages/sdk/js/src/v2/data.ts index baae6f278d..776b168ad9 100644 --- a/packages/sdk/js/src/v2/data.ts +++ b/packages/sdk/js/src/v2/data.ts @@ -5,7 +5,7 @@ export const message = { info: UserMessage parts: Part[] } { - const { parts, ...rest } = input + const { parts: _parts, ...rest } = input const info: UserMessage = { ...rest, diff --git a/packages/shared/test/filesystem/filesystem.test.ts b/packages/shared/test/filesystem/filesystem.test.ts index ce990d3795..b49026bcba 100644 --- a/packages/shared/test/filesystem/filesystem.test.ts +++ b/packages/shared/test/filesystem/filesystem.test.ts @@ -290,7 +290,7 @@ describe("AppFileSystem", () => { it( "exists works", Effect.gen(function* () { - const fs = yield* AppFileSystem.Service + yield* AppFileSystem.Service const filesys = yield* FileSystem.FileSystem const tmp = yield* filesys.makeTempDirectoryScoped() const file = path.join(tmp, "exists.txt") @@ -304,7 +304,7 @@ describe("AppFileSystem", () => { it( "remove works", Effect.gen(function* () { - const fs = yield* AppFileSystem.Service + yield* AppFileSystem.Service const filesys = yield* FileSystem.FileSystem const tmp = yield* filesys.makeTempDirectoryScoped() const file = path.join(tmp, "delete-me.txt") diff --git a/packages/shared/test/fixture/effect-flock-worker.ts b/packages/shared/test/fixture/effect-flock-worker.ts index 7fd2e144a2..c9116c2d5c 100644 --- a/packages/shared/test/fixture/effect-flock-worker.ts +++ b/packages/shared/test/fixture/effect-flock-worker.ts @@ -1,5 +1,4 @@ import fs from "fs/promises" -import path from "path" import os from "os" import { Effect, Layer } from "effect" import { AppFileSystem } from "@opencode-ai/shared/filesystem" diff --git a/packages/slack/src/index.ts b/packages/slack/src/index.ts index 123710aa46..85d6851296 100644 --- a/packages/slack/src/index.ts +++ b/packages/slack/src/index.ts @@ -27,7 +27,7 @@ const sessions = new Map tags -- Highlights with the same source attribute get grouped together ---> - - - -` - console.log("=== publishing ===\n") const pkgjsons = await Array.fromAsync( diff --git a/script/stats.ts b/script/stats.ts index 318b590af7..9201e26e43 100755 --- a/script/stats.ts +++ b/script/stats.ts @@ -193,7 +193,7 @@ console.log("Fetching GitHub releases for anomalyco/opencode...\n") const releases = await fetchReleases() console.log(`\nFetched ${releases.length} releases total\n`) -const { total: githubTotal, stats } = calculate(releases) +const { total: githubTotal } = calculate(releases) console.log("Fetching npm all-time downloads for opencode-ai...\n") const npmDownloads = await fetchNpmDownloads("opencode-ai") diff --git a/sdks/vscode/src/extension.ts b/sdks/vscode/src/extension.ts index 772da9cc2b..693e7267e5 100644 --- a/sdks/vscode/src/extension.ts +++ b/sdks/vscode/src/extension.ts @@ -6,11 +6,11 @@ import * as vscode from "vscode" const TERMINAL_NAME = "opencode" export function activate(context: vscode.ExtensionContext) { - let openNewTerminalDisposable = vscode.commands.registerCommand("opencode.openNewTerminal", async () => { + const openNewTerminalDisposable = vscode.commands.registerCommand("opencode.openNewTerminal", async () => { await openTerminal() }) - let openTerminalDisposable = vscode.commands.registerCommand("opencode.openTerminal", async () => { + const openTerminalDisposable = vscode.commands.registerCommand("opencode.openTerminal", async () => { // An opencode terminal already exists => focus it const existingTerminal = vscode.window.terminals.find((t) => t.name === TERMINAL_NAME) if (existingTerminal) { @@ -40,7 +40,7 @@ export function activate(context: vscode.ExtensionContext) { } }) - context.subscriptions.push(openTerminalDisposable, addFilepathDisposable) + context.subscriptions.push(openNewTerminalDisposable, openTerminalDisposable, addFilepathDisposable) async function openTerminal() { // Create a new terminal in split screen From 5eae92684658c36a5026c9a36edcdf1163517022 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 22:07:42 -0400 Subject: [PATCH 178/300] add experimental provider auth HttpApi slice (#22389) --- bun.lock | 14 --- packages/opencode/package.json | 1 - packages/opencode/specs/effect/http-api.md | 13 +- packages/opencode/src/provider/auth.ts | 119 +++++++++--------- .../src/server/instance/httpapi/provider.ts | 46 +++++++ .../src/server/instance/httpapi/server.ts | 7 ++ .../opencode/src/server/instance/provider.ts | 4 +- packages/server/package.json | 30 ----- packages/server/src/api/index.ts | 2 - packages/server/src/api/question.ts | 37 ------ packages/server/src/definition/api.ts | 12 -- packages/server/src/definition/index.ts | 2 - packages/server/src/definition/question.ts | 94 -------------- packages/server/src/index.ts | 6 - packages/server/src/openapi.ts | 5 - packages/server/src/types.ts | 5 - packages/server/sst-env.d.ts | 10 -- packages/server/tsconfig.json | 15 --- 18 files changed, 122 insertions(+), 300 deletions(-) create mode 100644 packages/opencode/src/server/instance/httpapi/provider.ts delete mode 100644 packages/server/package.json delete mode 100644 packages/server/src/api/index.ts delete mode 100644 packages/server/src/api/question.ts delete mode 100644 packages/server/src/definition/api.ts delete mode 100644 packages/server/src/definition/index.ts delete mode 100644 packages/server/src/definition/question.ts delete mode 100644 packages/server/src/index.ts delete mode 100644 packages/server/src/openapi.ts delete mode 100644 packages/server/src/types.ts delete mode 100644 packages/server/sst-env.d.ts delete mode 100644 packages/server/tsconfig.json diff --git a/bun.lock b/bun.lock index 48243e652e..705181160a 100644 --- a/bun.lock +++ b/bun.lock @@ -358,7 +358,6 @@ "@opencode-ai/plugin": "workspace:*", "@opencode-ai/script": "workspace:*", "@opencode-ai/sdk": "workspace:*", - "@opencode-ai/server": "workspace:*", "@openrouter/ai-sdk-provider": "2.5.1", "@opentelemetry/api": "1.9.0", "@opentelemetry/context-async-hooks": "2.6.1", @@ -506,17 +505,6 @@ "typescript": "catalog:", }, }, - "packages/server": { - "name": "@opencode-ai/server", - "version": "1.4.6", - "dependencies": { - "effect": "catalog:", - }, - "devDependencies": { - "@typescript/native-preview": "catalog:", - "typescript": "catalog:", - }, - }, "packages/shared": { "name": "@opencode-ai/shared", "version": "1.4.6", @@ -1568,8 +1556,6 @@ "@opencode-ai/sdk": ["@opencode-ai/sdk@workspace:packages/sdk/js"], - "@opencode-ai/server": ["@opencode-ai/server@workspace:packages/server"], - "@opencode-ai/shared": ["@opencode-ai/shared@workspace:packages/shared"], "@opencode-ai/slack": ["@opencode-ai/slack@workspace:packages/slack"], diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 59be93d620..c0f82c1495 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -115,7 +115,6 @@ "@opencode-ai/plugin": "workspace:*", "@opencode-ai/script": "workspace:*", "@opencode-ai/sdk": "workspace:*", - "@opencode-ai/server": "workspace:*", "@openrouter/ai-sdk-provider": "2.5.1", "@opentelemetry/api": "1.9.0", "@opentelemetry/context-async-hooks": "2.6.1", diff --git a/packages/opencode/specs/effect/http-api.md b/packages/opencode/specs/effect/http-api.md index 1794927cce..bd1213bb6d 100644 --- a/packages/opencode/specs/effect/http-api.md +++ b/packages/opencode/specs/effect/http-api.md @@ -156,6 +156,14 @@ Ordering for a route-group migration: 3. move tagged route-facing errors to `Schema.TaggedErrorClass` where needed 4. switch existing Zod boundary validators to derived `.zod` 5. define the `HttpApi` contract from the canonical Effect schemas +6. regenerate the SDK (`./packages/sdk/js/script/build.ts`) and verify zero diff against `dev` + +SDK shape rule: + +- every schema migration must preserve the generated SDK output byte-for-byte +- `Schema.Class` emits a named `$ref` in OpenAPI via its identifier — use it only for types that already had `.meta({ ref })` in the old Zod schema +- inner / nested types that were anonymous in the old Zod schema should stay as `Schema.Struct` (not `Schema.Class`) to avoid introducing new named components in the OpenAPI spec +- if a diff appears in `packages/sdk/js/src/v2/gen/types.gen.ts`, the migration introduced an unintended API surface change — fix it before merging Temporary exception: @@ -195,8 +203,9 @@ Use the same sequence for each route group. 4. Define the `HttpApi` contract separately from the handlers. 5. Implement handlers by yielding the existing service from context. 6. Mount the new surface in parallel under an experimental prefix. -7. Add one end-to-end test and one OpenAPI-focused test. -8. Compare ergonomics before migrating the next endpoint. +7. Regenerate the SDK and verify zero diff against `dev` (see SDK shape rule above). +8. Add one end-to-end test and one OpenAPI-focused test. +9. Compare ergonomics before migrating the next endpoint. Rule of thumb: diff --git a/packages/opencode/src/provider/auth.ts b/packages/opencode/src/provider/auth.ts index c66ccffc12..0f2923a587 100644 --- a/packages/opencode/src/provider/auth.ts +++ b/packages/opencode/src/provider/auth.ts @@ -2,70 +2,62 @@ import type { AuthOAuthResult, Hooks } from "@opencode-ai/plugin" import { NamedError } from "@opencode-ai/shared/util/error" import { Auth } from "@/auth" import { InstanceState } from "@/effect/instance-state" +import { zod } from "@/util/effect-zod" +import { withStatics } from "@/util/schema" import { Plugin } from "../plugin" import { ProviderID } from "./schema" -import { Array as Arr, Effect, Layer, Record, Result, Context } from "effect" +import { Array as Arr, Effect, Layer, Record, Result, Context, Schema } from "effect" import z from "zod" export namespace ProviderAuth { - export const Method = z - .object({ - type: z.union([z.literal("oauth"), z.literal("api")]), - label: z.string(), - prompts: z - .array( - z.union([ - z.object({ - type: z.literal("text"), - key: z.string(), - message: z.string(), - placeholder: z.string().optional(), - when: z - .object({ - key: z.string(), - op: z.union([z.literal("eq"), z.literal("neq")]), - value: z.string(), - }) - .optional(), - }), - z.object({ - type: z.literal("select"), - key: z.string(), - message: z.string(), - options: z.array( - z.object({ - label: z.string(), - value: z.string(), - hint: z.string().optional(), - }), - ), - when: z - .object({ - key: z.string(), - op: z.union([z.literal("eq"), z.literal("neq")]), - value: z.string(), - }) - .optional(), - }), - ]), - ) - .optional(), - }) - .meta({ - ref: "ProviderAuthMethod", - }) - export type Method = z.infer + const When = Schema.Struct({ + key: Schema.String, + op: Schema.Literals(["eq", "neq"]), + value: Schema.String, + }) - export const Authorization = z - .object({ - url: z.string(), - method: z.union([z.literal("auto"), z.literal("code")]), - instructions: z.string(), - }) - .meta({ - ref: "ProviderAuthAuthorization", - }) - export type Authorization = z.infer + const TextPrompt = Schema.Struct({ + type: Schema.Literal("text"), + key: Schema.String, + message: Schema.String, + placeholder: Schema.optional(Schema.String), + when: Schema.optional(When), + }) + + const SelectOption = Schema.Struct({ + label: Schema.String, + value: Schema.String, + hint: Schema.optional(Schema.String), + }) + + const SelectPrompt = Schema.Struct({ + type: Schema.Literal("select"), + key: Schema.String, + message: Schema.String, + options: Schema.Array(SelectOption), + when: Schema.optional(When), + }) + + const Prompt = Schema.Union([TextPrompt, SelectPrompt]) + + export class Method extends Schema.Class("ProviderAuthMethod")({ + type: Schema.Literals(["oauth", "api"]), + label: Schema.String, + prompts: Schema.optional(Schema.Array(Prompt)), + }) { + static readonly zod = zod(this) + } + + export const Methods = Schema.Record(Schema.String, Schema.Array(Method)).pipe(withStatics((s) => ({ zod: zod(s) }))) + export type Methods = typeof Methods.Type + + export class Authorization extends Schema.Class("ProviderAuthAuthorization")({ + url: Schema.String, + method: Schema.Literals(["auto", "code"]), + instructions: Schema.String, + }) { + static readonly zod = zod(this) + } export const OauthMissing = NamedError.create("ProviderAuthOauthMissing", z.object({ providerID: ProviderID.zod })) @@ -94,7 +86,7 @@ export namespace ProviderAuth { type Hook = NonNullable export interface Interface { - readonly methods: () => Effect.Effect> + readonly methods: () => Effect.Effect readonly authorize: (input: { providerID: ProviderID method: number @@ -131,11 +123,12 @@ export namespace ProviderAuth { }), ) + const decode = Schema.decodeUnknownSync(Methods) const methods = Effect.fn("ProviderAuth.methods")(function* () { const hooks = (yield* InstanceState.get(state)).hooks - return Record.map(hooks, (item) => - item.methods.map( - (method): Method => ({ + return decode( + Record.map(hooks, (item) => + item.methods.map((method) => ({ type: method.type, label: method.label, prompts: method.prompts?.map((prompt) => { @@ -156,7 +149,7 @@ export namespace ProviderAuth { when: prompt.when, } }), - }), + })), ), ) }) diff --git a/packages/opencode/src/server/instance/httpapi/provider.ts b/packages/opencode/src/server/instance/httpapi/provider.ts new file mode 100644 index 0000000000..23e2d1ea73 --- /dev/null +++ b/packages/opencode/src/server/instance/httpapi/provider.ts @@ -0,0 +1,46 @@ +import { ProviderAuth } from "@/provider/auth" +import { Effect, Layer } from "effect" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" + +const root = "/experimental/httpapi/provider" + +export const ProviderApi = HttpApi.make("provider") + .add( + HttpApiGroup.make("provider") + .add( + HttpApiEndpoint.get("auth", `${root}/auth`, { + success: ProviderAuth.Methods, + }).annotateMerge( + OpenApi.annotations({ + identifier: "provider.auth", + summary: "Get provider auth methods", + description: "Retrieve available authentication methods for all AI providers.", + }), + ), + ) + .annotateMerge( + OpenApi.annotations({ + title: "provider", + description: "Experimental HttpApi provider routes.", + }), + ), + ) + .annotateMerge( + OpenApi.annotations({ + title: "opencode experimental HttpApi", + version: "0.0.1", + description: "Experimental HttpApi surface for selected instance routes.", + }), + ) + +export const ProviderLive = Layer.unwrap( + Effect.gen(function* () { + const svc = yield* ProviderAuth.Service + + const auth = Effect.fn("ProviderHttpApi.auth")(function* () { + return yield* svc.methods() + }) + + return HttpApiBuilder.group(ProviderApi, "provider", (handlers) => handlers.handle("auth", auth)) + }), +).pipe(Layer.provide(ProviderAuth.defaultLayer)) diff --git a/packages/opencode/src/server/instance/httpapi/server.ts b/packages/opencode/src/server/instance/httpapi/server.ts index 54c3c57ff5..9894343c56 100644 --- a/packages/opencode/src/server/instance/httpapi/server.ts +++ b/packages/opencode/src/server/instance/httpapi/server.ts @@ -10,8 +10,10 @@ import { InstanceBootstrap } from "@/project/bootstrap" import { Instance } from "@/project/instance" import { Filesystem } from "@/util/filesystem" import { Permission } from "@/permission" +import { ProviderAuth } from "@/provider/auth" import { Question } from "@/question" import { PermissionApi, PermissionLive } from "./permission" +import { ProviderApi, ProviderLive } from "./provider" import { QuestionApi, QuestionLive } from "./question" const Query = Schema.Struct({ @@ -108,6 +110,7 @@ export namespace ExperimentalHttpApiServer { const QuestionSecured = QuestionApi.middleware(Authorization) const PermissionSecured = PermissionApi.middleware(Authorization) + const ProviderSecured = ProviderApi.middleware(Authorization) export const routes = Layer.mergeAll( HttpApiBuilder.layer(QuestionSecured, { openapiPath: "/experimental/httpapi/question/doc" }).pipe( @@ -116,6 +119,9 @@ export namespace ExperimentalHttpApiServer { HttpApiBuilder.layer(PermissionSecured, { openapiPath: "/experimental/httpapi/permission/doc" }).pipe( Layer.provide(PermissionLive), ), + HttpApiBuilder.layer(ProviderSecured, { openapiPath: "/experimental/httpapi/provider/doc" }).pipe( + Layer.provide(ProviderLive), + ), ).pipe(Layer.provide(auth), Layer.provide(normalize), Layer.provide(instance)) export const layer = (opts: { hostname: string; port: number }) => @@ -127,5 +133,6 @@ export namespace ExperimentalHttpApiServer { Layer.provideMerge(NodeHttpServer.layerTest), Layer.provideMerge(Question.defaultLayer), Layer.provideMerge(Permission.defaultLayer), + Layer.provideMerge(ProviderAuth.defaultLayer), ) } diff --git a/packages/opencode/src/server/instance/provider.ts b/packages/opencode/src/server/instance/provider.ts index bbde4c9552..0057218f3b 100644 --- a/packages/opencode/src/server/instance/provider.ts +++ b/packages/opencode/src/server/instance/provider.ts @@ -82,7 +82,7 @@ export const ProviderRoutes = lazy(() => description: "Provider auth methods", content: { "application/json": { - schema: resolver(z.record(z.string(), z.array(ProviderAuth.Method))), + schema: resolver(ProviderAuth.Methods.zod), }, }, }, @@ -103,7 +103,7 @@ export const ProviderRoutes = lazy(() => description: "Authorization URL and method", content: { "application/json": { - schema: resolver(ProviderAuth.Authorization.optional()), + schema: resolver(ProviderAuth.Authorization.zod.optional()), }, }, }, diff --git a/packages/server/package.json b/packages/server/package.json deleted file mode 100644 index 9b8b31299d..0000000000 --- a/packages/server/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/package.json", - "name": "@opencode-ai/server", - "version": "1.4.6", - "type": "module", - "license": "MIT", - "exports": { - ".": "./src/index.ts", - "./openapi": "./src/openapi.ts", - "./definition": "./src/definition/index.ts", - "./definition/api": "./src/definition/api.ts", - "./definition/question": "./src/definition/question.ts", - "./api": "./src/api/index.ts", - "./api/question": "./src/api/question.ts" - }, - "files": [ - "dist" - ], - "scripts": { - "typecheck": "tsgo --noEmit", - "build": "tsc" - }, - "devDependencies": { - "@typescript/native-preview": "catalog:", - "typescript": "catalog:" - }, - "dependencies": { - "effect": "catalog:" - } -} diff --git a/packages/server/src/api/index.ts b/packages/server/src/api/index.ts deleted file mode 100644 index 375e3584b4..0000000000 --- a/packages/server/src/api/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { makeQuestionHandler } from "./question.js" -export type { QuestionOps } from "./question.js" diff --git a/packages/server/src/api/question.ts b/packages/server/src/api/question.ts deleted file mode 100644 index f72c37aa19..0000000000 --- a/packages/server/src/api/question.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Effect, Schema } from "effect" -import { HttpApiBuilder } from "effect/unstable/httpapi" -import { QuestionReply, QuestionRequest, questionApi } from "../definition/question.js" - -export interface QuestionOps { - readonly list: () => Effect.Effect, never, R> - readonly reply: (input: { - requestID: string - answers: Schema.Schema.Type["answers"] - }) => Effect.Effect -} - -export const makeQuestionHandler = (ops: QuestionOps) => - HttpApiBuilder.group( - questionApi, - "question", - Effect.fn("QuestionHttpApi.handlers")(function* (handlers) { - const decode = Schema.decodeUnknownSync(Schema.Array(QuestionRequest)) - - const list = Effect.fn("QuestionHttpApi.list")(function* () { - return decode(yield* ops.list()) - }) - - const reply = Effect.fn("QuestionHttpApi.reply")(function* (ctx: { - params: { requestID: string } - payload: Schema.Schema.Type - }) { - yield* ops.reply({ - requestID: ctx.params.requestID, - answers: ctx.payload.answers, - }) - return true - }) - - return handlers.handle("list", list).handle("reply", reply) - }), - ) diff --git a/packages/server/src/definition/api.ts b/packages/server/src/definition/api.ts deleted file mode 100644 index e2f70196da..0000000000 --- a/packages/server/src/definition/api.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { HttpApi, OpenApi } from "effect/unstable/httpapi" -import { questionApi } from "./question.js" - -export const api = HttpApi.make("opencode") - .addHttpApi(questionApi) - .annotateMerge( - OpenApi.annotations({ - title: "opencode experimental HttpApi", - version: "0.0.1", - description: "Experimental HttpApi surface for selected instance routes.", - }), - ) diff --git a/packages/server/src/definition/index.ts b/packages/server/src/definition/index.ts deleted file mode 100644 index e9a52dc930..0000000000 --- a/packages/server/src/definition/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { api } from "./api.js" -export { questionApi, QuestionReply, QuestionRequest } from "./question.js" diff --git a/packages/server/src/definition/question.ts b/packages/server/src/definition/question.ts deleted file mode 100644 index 0d161e013d..0000000000 --- a/packages/server/src/definition/question.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { Schema } from "effect" -import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" - -const root = "/experimental/httpapi/question" - -// Temporary transport-local schemas until canonical question schemas move into packages/core. -export const QuestionID = Schema.String.annotate({ identifier: "QuestionID" }) -export const SessionID = Schema.String.annotate({ identifier: "SessionID" }) -export const MessageID = Schema.String.annotate({ identifier: "MessageID" }) - -export class QuestionOption extends Schema.Class("QuestionOption")({ - label: Schema.String.annotate({ - description: "Display text (1-5 words, concise)", - }), - description: Schema.String.annotate({ - description: "Explanation of choice", - }), -}) {} - -const base = { - question: Schema.String.annotate({ - description: "Complete question", - }), - header: Schema.String.annotate({ - description: "Very short label (max 30 chars)", - }), - options: Schema.Array(QuestionOption).annotate({ - description: "Available choices", - }), - multiple: Schema.optional(Schema.Boolean).annotate({ - description: "Allow selecting multiple choices", - }), -} - -export class QuestionInfo extends Schema.Class("QuestionInfo")({ - ...base, - custom: Schema.optional(Schema.Boolean).annotate({ - description: "Allow typing a custom answer (default: true)", - }), -}) {} - -export class QuestionTool extends Schema.Class("QuestionTool")({ - messageID: MessageID, - callID: Schema.String, -}) {} - -export class QuestionRequest extends Schema.Class("QuestionRequest")({ - id: QuestionID, - sessionID: SessionID, - questions: Schema.Array(QuestionInfo).annotate({ - description: "Questions to ask", - }), - tool: Schema.optional(QuestionTool), -}) {} - -export const QuestionAnswer = Schema.Array(Schema.String).annotate({ identifier: "QuestionAnswer" }) - -export class QuestionReply extends Schema.Class("QuestionReply")({ - answers: Schema.Array(QuestionAnswer).annotate({ - description: "User answers in order of questions (each answer is an array of selected labels)", - }), -}) {} - -export const questionApi = HttpApi.make("question").add( - HttpApiGroup.make("question") - .add( - HttpApiEndpoint.get("list", root, { - success: Schema.Array(QuestionRequest), - }).annotateMerge( - OpenApi.annotations({ - identifier: "question.list", - summary: "List pending questions", - description: "Get all pending question requests across all sessions.", - }), - ), - HttpApiEndpoint.post("reply", `${root}/:requestID/reply`, { - params: { requestID: QuestionID }, - payload: QuestionReply, - success: Schema.Boolean, - }).annotateMerge( - OpenApi.annotations({ - identifier: "question.reply", - summary: "Reply to question request", - description: "Provide answers to a question request from the AI assistant.", - }), - ), - ) - .annotateMerge( - OpenApi.annotations({ - title: "question", - description: "Experimental HttpApi question routes.", - }), - ), -) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts deleted file mode 100644 index 67b82a0be5..0000000000 --- a/packages/server/src/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { openapi } from "./openapi.js" -export { makeQuestionHandler } from "./api/question.js" -export { api } from "./definition/api.js" -export { questionApi, QuestionReply, QuestionRequest } from "./definition/question.js" -export type { OpenApiSpec, ServerApi } from "./types.js" -export type { QuestionOps } from "./api/question.js" diff --git a/packages/server/src/openapi.ts b/packages/server/src/openapi.ts deleted file mode 100644 index dda870d2b6..0000000000 --- a/packages/server/src/openapi.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { OpenApi } from "effect/unstable/httpapi" -import { api } from "./definition/api.js" -import type { OpenApiSpec } from "./types.js" - -export const openapi = (): OpenApiSpec => OpenApi.fromApi(api) diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts deleted file mode 100644 index 9e89fe74c2..0000000000 --- a/packages/server/src/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { HttpApi, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" - -export type ServerApi = HttpApi.HttpApi - -export type OpenApiSpec = OpenApi.OpenAPISpec diff --git a/packages/server/sst-env.d.ts b/packages/server/sst-env.d.ts deleted file mode 100644 index 64441936d7..0000000000 --- a/packages/server/sst-env.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -/* biome-ignore-all lint: auto-generated */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json deleted file mode 100644 index eac2af3845..0000000000 --- a/packages/server/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig.json", - "compilerOptions": { - "target": "ES2022", - "rootDir": "src", - "outDir": "dist", - "module": "nodenext", - "declaration": true, - "moduleResolution": "nodenext", - "lib": ["es2022", "dom", "dom.iterable"], - "strict": true, - "skipLibCheck": true - }, - "include": ["src"] -} From 64cc4623b54a45c6d399110dbe4e147ef050dc8c Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Thu, 16 Apr 2026 02:08:47 +0000 Subject: [PATCH 179/300] chore: generate --- packages/sdk/openapi.json | 48 +++++++-------------------------------- 1 file changed, 8 insertions(+), 40 deletions(-) diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index c59e1ab910..f63d12490c 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -12699,16 +12699,8 @@ "type": "object", "properties": { "type": { - "anyOf": [ - { - "type": "string", - "const": "oauth" - }, - { - "type": "string", - "const": "api" - } - ] + "type": "string", + "enum": ["oauth", "api"] }, "label": { "type": "string" @@ -12740,16 +12732,8 @@ "type": "string" }, "op": { - "anyOf": [ - { - "type": "string", - "const": "eq" - }, - { - "type": "string", - "const": "neq" - } - ] + "type": "string", + "enum": ["eq", "neq"] }, "value": { "type": "string" @@ -12798,16 +12782,8 @@ "type": "string" }, "op": { - "anyOf": [ - { - "type": "string", - "const": "eq" - }, - { - "type": "string", - "const": "neq" - } - ] + "type": "string", + "enum": ["eq", "neq"] }, "value": { "type": "string" @@ -12831,16 +12807,8 @@ "type": "string" }, "method": { - "anyOf": [ - { - "type": "string", - "const": "auto" - }, - { - "type": "string", - "const": "code" - } - ] + "type": "string", + "enum": ["auto", "code"] }, "instructions": { "type": "string" From a1dbfb5967c564bd082c84a6fb8510208edde12f Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 22:13:33 -0400 Subject: [PATCH 180/300] feat: unwrap uaccount namespace to flat exports + barrel (#22698) --- packages/opencode/src/account/account.ts | 454 +++++++++++++++++++++++ packages/opencode/src/account/index.ts | 438 +--------------------- 2 files changed, 457 insertions(+), 435 deletions(-) create mode 100644 packages/opencode/src/account/account.ts diff --git a/packages/opencode/src/account/account.ts b/packages/opencode/src/account/account.ts new file mode 100644 index 0000000000..657c61b1e5 --- /dev/null +++ b/packages/opencode/src/account/account.ts @@ -0,0 +1,454 @@ +import { Cache, Clock, Duration, Effect, Layer, Option, Schema, SchemaGetter, Context } from "effect" +import { + FetchHttpClient, + HttpClient, + HttpClientError, + HttpClientRequest, + HttpClientResponse, +} from "effect/unstable/http" + +import { withTransientReadRetry } from "@/util/effect-http-client" +import { AccountRepo, type AccountRow } from "./repo" +import { normalizeServerUrl } from "./url" +import { + type AccountError, + AccessToken, + AccountID, + DeviceCode, + Info, + RefreshToken, + AccountServiceError, + AccountTransportError, + Login, + Org, + OrgID, + PollDenied, + PollError, + PollExpired, + PollPending, + type PollResult, + PollSlow, + PollSuccess, + UserCode, +} from "./schema" + +export { + AccountID, + type AccountError, + AccountRepoError, + AccountServiceError, + AccountTransportError, + AccessToken, + RefreshToken, + DeviceCode, + UserCode, + Info, + Org, + OrgID, + Login, + PollSuccess, + PollPending, + PollSlow, + PollExpired, + PollDenied, + PollError, + PollResult, +} from "./schema" + +export type AccountOrgs = { + account: Info + orgs: readonly Org[] +} + +export type ActiveOrg = { + account: Info + org: Org +} + +class RemoteConfig extends Schema.Class("RemoteConfig")({ + config: Schema.Record(Schema.String, Schema.Json), +}) {} + +const DurationFromSeconds = Schema.Number.pipe( + Schema.decodeTo(Schema.Duration, { + decode: SchemaGetter.transform((n) => Duration.seconds(n)), + encode: SchemaGetter.transform((d) => Duration.toSeconds(d)), + }), +) + +class TokenRefresh extends Schema.Class("TokenRefresh")({ + access_token: AccessToken, + refresh_token: RefreshToken, + expires_in: DurationFromSeconds, +}) {} + +class DeviceAuth extends Schema.Class("DeviceAuth")({ + device_code: DeviceCode, + user_code: UserCode, + verification_uri_complete: Schema.String, + expires_in: DurationFromSeconds, + interval: DurationFromSeconds, +}) {} + +class DeviceTokenSuccess extends Schema.Class("DeviceTokenSuccess")({ + access_token: AccessToken, + refresh_token: RefreshToken, + token_type: Schema.Literal("Bearer"), + expires_in: DurationFromSeconds, +}) {} + +class DeviceTokenError extends Schema.Class("DeviceTokenError")({ + error: Schema.String, + error_description: Schema.String, +}) { + toPollResult(): PollResult { + if (this.error === "authorization_pending") return new PollPending() + if (this.error === "slow_down") return new PollSlow() + if (this.error === "expired_token") return new PollExpired() + if (this.error === "access_denied") return new PollDenied() + return new PollError({ cause: this.error }) + } +} + +const DeviceToken = Schema.Union([DeviceTokenSuccess, DeviceTokenError]) + +class User extends Schema.Class("User")({ + id: AccountID, + email: Schema.String, +}) {} + +class ClientId extends Schema.Class("ClientId")({ client_id: Schema.String }) {} + +class DeviceTokenRequest extends Schema.Class("DeviceTokenRequest")({ + grant_type: Schema.String, + device_code: DeviceCode, + client_id: Schema.String, +}) {} + +class TokenRefreshRequest extends Schema.Class("TokenRefreshRequest")({ + grant_type: Schema.String, + refresh_token: RefreshToken, + client_id: Schema.String, +}) {} + +const clientId = "opencode-cli" +const eagerRefreshThreshold = Duration.minutes(5) +const eagerRefreshThresholdMs = Duration.toMillis(eagerRefreshThreshold) + +const isTokenFresh = (tokenExpiry: number | null, now: number) => + tokenExpiry != null && tokenExpiry > now + eagerRefreshThresholdMs + +const mapAccountServiceError = + (message = "Account service operation failed") => + (effect: Effect.Effect): Effect.Effect => + effect.pipe(Effect.mapError((cause) => accountErrorFromCause(cause, message))) + +const accountErrorFromCause = (cause: unknown, message: string): AccountError => { + if (cause instanceof AccountServiceError || cause instanceof AccountTransportError) { + return cause + } + + if (HttpClientError.isHttpClientError(cause)) { + switch (cause.reason._tag) { + case "TransportError": { + return AccountTransportError.fromHttpClientError(cause.reason) + } + default: { + return new AccountServiceError({ message, cause }) + } + } + } + + return new AccountServiceError({ message, cause }) +} + +export interface Interface { + readonly active: () => Effect.Effect, AccountError> + readonly activeOrg: () => Effect.Effect, AccountError> + readonly list: () => Effect.Effect + readonly orgsByAccount: () => Effect.Effect + readonly remove: (accountID: AccountID) => Effect.Effect + readonly use: (accountID: AccountID, orgID: Option.Option) => Effect.Effect + readonly orgs: (accountID: AccountID) => Effect.Effect + readonly config: ( + accountID: AccountID, + orgID: OrgID, + ) => Effect.Effect>, AccountError> + readonly token: (accountID: AccountID) => Effect.Effect, AccountError> + readonly login: (url: string) => Effect.Effect + readonly poll: (input: Login) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/Account") {} + +export const layer: Layer.Layer = Layer.effect( + Service, + Effect.gen(function* () { + const repo = yield* AccountRepo + const http = yield* HttpClient.HttpClient + const httpRead = withTransientReadRetry(http) + const httpOk = HttpClient.filterStatusOk(http) + const httpReadOk = HttpClient.filterStatusOk(httpRead) + + const executeRead = (request: HttpClientRequest.HttpClientRequest) => + httpRead.execute(request).pipe(mapAccountServiceError("HTTP request failed")) + + const executeReadOk = (request: HttpClientRequest.HttpClientRequest) => + httpReadOk.execute(request).pipe(mapAccountServiceError("HTTP request failed")) + + const executeEffectOk = (request: Effect.Effect) => + request.pipe( + Effect.flatMap((req) => httpOk.execute(req)), + mapAccountServiceError("HTTP request failed"), + ) + + const executeEffect = (request: Effect.Effect) => + request.pipe( + Effect.flatMap((req) => http.execute(req)), + mapAccountServiceError("HTTP request failed"), + ) + + const refreshToken = Effect.fnUntraced(function* (row: AccountRow) { + const now = yield* Clock.currentTimeMillis + + const response = yield* executeEffectOk( + HttpClientRequest.post(`${row.url}/auth/device/token`).pipe( + HttpClientRequest.acceptJson, + HttpClientRequest.schemaBodyJson(TokenRefreshRequest)( + new TokenRefreshRequest({ + grant_type: "refresh_token", + refresh_token: row.refresh_token, + client_id: clientId, + }), + ), + ), + ) + + const parsed = yield* HttpClientResponse.schemaBodyJson(TokenRefresh)(response).pipe( + mapAccountServiceError("Failed to decode response"), + ) + + const expiry = Option.some(now + Duration.toMillis(parsed.expires_in)) + + yield* repo.persistToken({ + accountID: row.id, + accessToken: parsed.access_token, + refreshToken: parsed.refresh_token, + expiry, + }) + + return parsed.access_token + }) + + const refreshTokenCache = yield* Cache.make({ + capacity: Number.POSITIVE_INFINITY, + timeToLive: Duration.zero, + lookup: Effect.fnUntraced(function* (accountID) { + const maybeAccount = yield* repo.getRow(accountID) + if (Option.isNone(maybeAccount)) { + return yield* Effect.fail(new AccountServiceError({ message: "Account not found during token refresh" })) + } + + const account = maybeAccount.value + const now = yield* Clock.currentTimeMillis + if (isTokenFresh(account.token_expiry, now)) { + return account.access_token + } + + return yield* refreshToken(account) + }), + }) + + const resolveToken = Effect.fnUntraced(function* (row: AccountRow) { + const now = yield* Clock.currentTimeMillis + if (isTokenFresh(row.token_expiry, now)) { + return row.access_token + } + + return yield* Cache.get(refreshTokenCache, row.id) + }) + + const resolveAccess = Effect.fnUntraced(function* (accountID: AccountID) { + const maybeAccount = yield* repo.getRow(accountID) + if (Option.isNone(maybeAccount)) return Option.none() + + const account = maybeAccount.value + const accessToken = yield* resolveToken(account) + return Option.some({ account, accessToken }) + }) + + const fetchOrgs = Effect.fnUntraced(function* (url: string, accessToken: AccessToken) { + const response = yield* executeReadOk( + HttpClientRequest.get(`${url}/api/orgs`).pipe( + HttpClientRequest.acceptJson, + HttpClientRequest.bearerToken(accessToken), + ), + ) + + return yield* HttpClientResponse.schemaBodyJson(Schema.Array(Org))(response).pipe( + mapAccountServiceError("Failed to decode response"), + ) + }) + + const fetchUser = Effect.fnUntraced(function* (url: string, accessToken: AccessToken) { + const response = yield* executeReadOk( + HttpClientRequest.get(`${url}/api/user`).pipe( + HttpClientRequest.acceptJson, + HttpClientRequest.bearerToken(accessToken), + ), + ) + + return yield* HttpClientResponse.schemaBodyJson(User)(response).pipe( + mapAccountServiceError("Failed to decode response"), + ) + }) + + const token = Effect.fn("Account.token")((accountID: AccountID) => + resolveAccess(accountID).pipe(Effect.map(Option.map((r) => r.accessToken))), + ) + + const activeOrg = Effect.fn("Account.activeOrg")(function* () { + const activeAccount = yield* repo.active() + if (Option.isNone(activeAccount)) return Option.none() + + const account = activeAccount.value + if (!account.active_org_id) return Option.none() + + const accountOrgs = yield* orgs(account.id) + const org = accountOrgs.find((item) => item.id === account.active_org_id) + if (!org) return Option.none() + + return Option.some({ account, org }) + }) + + const orgsByAccount = Effect.fn("Account.orgsByAccount")(function* () { + const accounts = yield* repo.list() + return yield* Effect.forEach( + accounts, + (account) => + orgs(account.id).pipe( + Effect.catch(() => Effect.succeed([] as readonly Org[])), + Effect.map((orgs) => ({ account, orgs })), + ), + { concurrency: 3 }, + ) + }) + + const orgs = Effect.fn("Account.orgs")(function* (accountID: AccountID) { + const resolved = yield* resolveAccess(accountID) + if (Option.isNone(resolved)) return [] + + const { account, accessToken } = resolved.value + + return yield* fetchOrgs(account.url, accessToken) + }) + + const config = Effect.fn("Account.config")(function* (accountID: AccountID, orgID: OrgID) { + const resolved = yield* resolveAccess(accountID) + if (Option.isNone(resolved)) return Option.none() + + const { account, accessToken } = resolved.value + + const response = yield* executeRead( + HttpClientRequest.get(`${account.url}/api/config`).pipe( + HttpClientRequest.acceptJson, + HttpClientRequest.bearerToken(accessToken), + HttpClientRequest.setHeaders({ "x-org-id": orgID }), + ), + ) + + if (response.status === 404) return Option.none() + + const ok = yield* HttpClientResponse.filterStatusOk(response).pipe(mapAccountServiceError()) + + const parsed = yield* HttpClientResponse.schemaBodyJson(RemoteConfig)(ok).pipe( + mapAccountServiceError("Failed to decode response"), + ) + return Option.some(parsed.config) + }) + + const login = Effect.fn("Account.login")(function* (server: string) { + const normalizedServer = normalizeServerUrl(server) + const response = yield* executeEffectOk( + HttpClientRequest.post(`${normalizedServer}/auth/device/code`).pipe( + HttpClientRequest.acceptJson, + HttpClientRequest.schemaBodyJson(ClientId)(new ClientId({ client_id: clientId })), + ), + ) + + const parsed = yield* HttpClientResponse.schemaBodyJson(DeviceAuth)(response).pipe( + mapAccountServiceError("Failed to decode response"), + ) + return new Login({ + code: parsed.device_code, + user: parsed.user_code, + url: `${normalizedServer}${parsed.verification_uri_complete}`, + server: normalizedServer, + expiry: parsed.expires_in, + interval: parsed.interval, + }) + }) + + const poll = Effect.fn("Account.poll")(function* (input: Login) { + const response = yield* executeEffect( + HttpClientRequest.post(`${input.server}/auth/device/token`).pipe( + HttpClientRequest.acceptJson, + HttpClientRequest.schemaBodyJson(DeviceTokenRequest)( + new DeviceTokenRequest({ + grant_type: "urn:ietf:params:oauth:grant-type:device_code", + device_code: input.code, + client_id: clientId, + }), + ), + ), + ) + + const parsed = yield* HttpClientResponse.schemaBodyJson(DeviceToken)(response).pipe( + mapAccountServiceError("Failed to decode response"), + ) + + if (parsed instanceof DeviceTokenError) return parsed.toPollResult() + const accessToken = parsed.access_token + + const user = fetchUser(input.server, accessToken) + const orgs = fetchOrgs(input.server, accessToken) + + const [account, remoteOrgs] = yield* Effect.all([user, orgs], { concurrency: 2 }) + + // TODO: When there are multiple orgs, let the user choose + const firstOrgID = remoteOrgs.length > 0 ? Option.some(remoteOrgs[0].id) : Option.none() + + const now = yield* Clock.currentTimeMillis + const expiry = now + Duration.toMillis(parsed.expires_in) + const refreshToken = parsed.refresh_token + + yield* repo.persistAccount({ + id: account.id, + email: account.email, + url: input.server, + accessToken, + refreshToken, + expiry, + orgID: firstOrgID, + }) + + return new PollSuccess({ email: account.email }) + }) + + return Service.of({ + active: repo.active, + activeOrg, + list: repo.list, + orgsByAccount, + remove: repo.remove, + use: repo.use, + orgs, + config, + token, + login, + poll, + }) + }), +) + +export const defaultLayer = layer.pipe(Layer.provide(AccountRepo.layer), Layer.provide(FetchHttpClient.layer)) diff --git a/packages/opencode/src/account/index.ts b/packages/opencode/src/account/index.ts index 4c875caa6b..84152466a4 100644 --- a/packages/opencode/src/account/index.ts +++ b/packages/opencode/src/account/index.ts @@ -1,37 +1,4 @@ -import { Cache, Clock, Duration, Effect, Layer, Option, Schema, SchemaGetter, Context } from "effect" -import { - FetchHttpClient, - HttpClient, - HttpClientError, - HttpClientRequest, - HttpClientResponse, -} from "effect/unstable/http" - -import { withTransientReadRetry } from "@/util/effect-http-client" -import { AccountRepo, type AccountRow } from "./repo" -import { normalizeServerUrl } from "./url" -import { - type AccountError, - AccessToken, - AccountID, - DeviceCode, - Info, - RefreshToken, - AccountServiceError, - AccountTransportError, - Login, - Org, - OrgID, - PollDenied, - PollError, - PollExpired, - PollPending, - type PollResult, - PollSlow, - PollSuccess, - UserCode, -} from "./schema" - +export * as Account from "./account" export { AccountID, type AccountError, @@ -52,405 +19,6 @@ export { PollExpired, PollDenied, PollError, - PollResult, + type PollResult, } from "./schema" - -export type AccountOrgs = { - account: Info - orgs: readonly Org[] -} - -export type ActiveOrg = { - account: Info - org: Org -} - -class RemoteConfig extends Schema.Class("RemoteConfig")({ - config: Schema.Record(Schema.String, Schema.Json), -}) {} - -const DurationFromSeconds = Schema.Number.pipe( - Schema.decodeTo(Schema.Duration, { - decode: SchemaGetter.transform((n) => Duration.seconds(n)), - encode: SchemaGetter.transform((d) => Duration.toSeconds(d)), - }), -) - -class TokenRefresh extends Schema.Class("TokenRefresh")({ - access_token: AccessToken, - refresh_token: RefreshToken, - expires_in: DurationFromSeconds, -}) {} - -class DeviceAuth extends Schema.Class("DeviceAuth")({ - device_code: DeviceCode, - user_code: UserCode, - verification_uri_complete: Schema.String, - expires_in: DurationFromSeconds, - interval: DurationFromSeconds, -}) {} - -class DeviceTokenSuccess extends Schema.Class("DeviceTokenSuccess")({ - access_token: AccessToken, - refresh_token: RefreshToken, - token_type: Schema.Literal("Bearer"), - expires_in: DurationFromSeconds, -}) {} - -class DeviceTokenError extends Schema.Class("DeviceTokenError")({ - error: Schema.String, - error_description: Schema.String, -}) { - toPollResult(): PollResult { - if (this.error === "authorization_pending") return new PollPending() - if (this.error === "slow_down") return new PollSlow() - if (this.error === "expired_token") return new PollExpired() - if (this.error === "access_denied") return new PollDenied() - return new PollError({ cause: this.error }) - } -} - -const DeviceToken = Schema.Union([DeviceTokenSuccess, DeviceTokenError]) - -class User extends Schema.Class("User")({ - id: AccountID, - email: Schema.String, -}) {} - -class ClientId extends Schema.Class("ClientId")({ client_id: Schema.String }) {} - -class DeviceTokenRequest extends Schema.Class("DeviceTokenRequest")({ - grant_type: Schema.String, - device_code: DeviceCode, - client_id: Schema.String, -}) {} - -class TokenRefreshRequest extends Schema.Class("TokenRefreshRequest")({ - grant_type: Schema.String, - refresh_token: RefreshToken, - client_id: Schema.String, -}) {} - -const clientId = "opencode-cli" -const eagerRefreshThreshold = Duration.minutes(5) -const eagerRefreshThresholdMs = Duration.toMillis(eagerRefreshThreshold) - -const isTokenFresh = (tokenExpiry: number | null, now: number) => - tokenExpiry != null && tokenExpiry > now + eagerRefreshThresholdMs - -const mapAccountServiceError = - (message = "Account service operation failed") => - (effect: Effect.Effect): Effect.Effect => - effect.pipe(Effect.mapError((cause) => accountErrorFromCause(cause, message))) - -const accountErrorFromCause = (cause: unknown, message: string): AccountError => { - if (cause instanceof AccountServiceError || cause instanceof AccountTransportError) { - return cause - } - - if (HttpClientError.isHttpClientError(cause)) { - switch (cause.reason._tag) { - case "TransportError": { - return AccountTransportError.fromHttpClientError(cause.reason) - } - default: { - return new AccountServiceError({ message, cause }) - } - } - } - - return new AccountServiceError({ message, cause }) -} - -export namespace Account { - export interface Interface { - readonly active: () => Effect.Effect, AccountError> - readonly activeOrg: () => Effect.Effect, AccountError> - readonly list: () => Effect.Effect - readonly orgsByAccount: () => Effect.Effect - readonly remove: (accountID: AccountID) => Effect.Effect - readonly use: (accountID: AccountID, orgID: Option.Option) => Effect.Effect - readonly orgs: (accountID: AccountID) => Effect.Effect - readonly config: ( - accountID: AccountID, - orgID: OrgID, - ) => Effect.Effect>, AccountError> - readonly token: (accountID: AccountID) => Effect.Effect, AccountError> - readonly login: (url: string) => Effect.Effect - readonly poll: (input: Login) => Effect.Effect - } - - export class Service extends Context.Service()("@opencode/Account") {} - - export const layer: Layer.Layer = Layer.effect( - Service, - Effect.gen(function* () { - const repo = yield* AccountRepo - const http = yield* HttpClient.HttpClient - const httpRead = withTransientReadRetry(http) - const httpOk = HttpClient.filterStatusOk(http) - const httpReadOk = HttpClient.filterStatusOk(httpRead) - - const executeRead = (request: HttpClientRequest.HttpClientRequest) => - httpRead.execute(request).pipe(mapAccountServiceError("HTTP request failed")) - - const executeReadOk = (request: HttpClientRequest.HttpClientRequest) => - httpReadOk.execute(request).pipe(mapAccountServiceError("HTTP request failed")) - - const executeEffectOk = (request: Effect.Effect) => - request.pipe( - Effect.flatMap((req) => httpOk.execute(req)), - mapAccountServiceError("HTTP request failed"), - ) - - const executeEffect = (request: Effect.Effect) => - request.pipe( - Effect.flatMap((req) => http.execute(req)), - mapAccountServiceError("HTTP request failed"), - ) - - const refreshToken = Effect.fnUntraced(function* (row: AccountRow) { - const now = yield* Clock.currentTimeMillis - - const response = yield* executeEffectOk( - HttpClientRequest.post(`${row.url}/auth/device/token`).pipe( - HttpClientRequest.acceptJson, - HttpClientRequest.schemaBodyJson(TokenRefreshRequest)( - new TokenRefreshRequest({ - grant_type: "refresh_token", - refresh_token: row.refresh_token, - client_id: clientId, - }), - ), - ), - ) - - const parsed = yield* HttpClientResponse.schemaBodyJson(TokenRefresh)(response).pipe( - mapAccountServiceError("Failed to decode response"), - ) - - const expiry = Option.some(now + Duration.toMillis(parsed.expires_in)) - - yield* repo.persistToken({ - accountID: row.id, - accessToken: parsed.access_token, - refreshToken: parsed.refresh_token, - expiry, - }) - - return parsed.access_token - }) - - const refreshTokenCache = yield* Cache.make({ - capacity: Number.POSITIVE_INFINITY, - timeToLive: Duration.zero, - lookup: Effect.fnUntraced(function* (accountID) { - const maybeAccount = yield* repo.getRow(accountID) - if (Option.isNone(maybeAccount)) { - return yield* Effect.fail(new AccountServiceError({ message: "Account not found during token refresh" })) - } - - const account = maybeAccount.value - const now = yield* Clock.currentTimeMillis - if (isTokenFresh(account.token_expiry, now)) { - return account.access_token - } - - return yield* refreshToken(account) - }), - }) - - const resolveToken = Effect.fnUntraced(function* (row: AccountRow) { - const now = yield* Clock.currentTimeMillis - if (isTokenFresh(row.token_expiry, now)) { - return row.access_token - } - - return yield* Cache.get(refreshTokenCache, row.id) - }) - - const resolveAccess = Effect.fnUntraced(function* (accountID: AccountID) { - const maybeAccount = yield* repo.getRow(accountID) - if (Option.isNone(maybeAccount)) return Option.none() - - const account = maybeAccount.value - const accessToken = yield* resolveToken(account) - return Option.some({ account, accessToken }) - }) - - const fetchOrgs = Effect.fnUntraced(function* (url: string, accessToken: AccessToken) { - const response = yield* executeReadOk( - HttpClientRequest.get(`${url}/api/orgs`).pipe( - HttpClientRequest.acceptJson, - HttpClientRequest.bearerToken(accessToken), - ), - ) - - return yield* HttpClientResponse.schemaBodyJson(Schema.Array(Org))(response).pipe( - mapAccountServiceError("Failed to decode response"), - ) - }) - - const fetchUser = Effect.fnUntraced(function* (url: string, accessToken: AccessToken) { - const response = yield* executeReadOk( - HttpClientRequest.get(`${url}/api/user`).pipe( - HttpClientRequest.acceptJson, - HttpClientRequest.bearerToken(accessToken), - ), - ) - - return yield* HttpClientResponse.schemaBodyJson(User)(response).pipe( - mapAccountServiceError("Failed to decode response"), - ) - }) - - const token = Effect.fn("Account.token")((accountID: AccountID) => - resolveAccess(accountID).pipe(Effect.map(Option.map((r) => r.accessToken))), - ) - - const activeOrg = Effect.fn("Account.activeOrg")(function* () { - const activeAccount = yield* repo.active() - if (Option.isNone(activeAccount)) return Option.none() - - const account = activeAccount.value - if (!account.active_org_id) return Option.none() - - const accountOrgs = yield* orgs(account.id) - const org = accountOrgs.find((item) => item.id === account.active_org_id) - if (!org) return Option.none() - - return Option.some({ account, org }) - }) - - const orgsByAccount = Effect.fn("Account.orgsByAccount")(function* () { - const accounts = yield* repo.list() - return yield* Effect.forEach( - accounts, - (account) => - orgs(account.id).pipe( - Effect.catch(() => Effect.succeed([] as readonly Org[])), - Effect.map((orgs) => ({ account, orgs })), - ), - { concurrency: 3 }, - ) - }) - - const orgs = Effect.fn("Account.orgs")(function* (accountID: AccountID) { - const resolved = yield* resolveAccess(accountID) - if (Option.isNone(resolved)) return [] - - const { account, accessToken } = resolved.value - - return yield* fetchOrgs(account.url, accessToken) - }) - - const config = Effect.fn("Account.config")(function* (accountID: AccountID, orgID: OrgID) { - const resolved = yield* resolveAccess(accountID) - if (Option.isNone(resolved)) return Option.none() - - const { account, accessToken } = resolved.value - - const response = yield* executeRead( - HttpClientRequest.get(`${account.url}/api/config`).pipe( - HttpClientRequest.acceptJson, - HttpClientRequest.bearerToken(accessToken), - HttpClientRequest.setHeaders({ "x-org-id": orgID }), - ), - ) - - if (response.status === 404) return Option.none() - - const ok = yield* HttpClientResponse.filterStatusOk(response).pipe(mapAccountServiceError()) - - const parsed = yield* HttpClientResponse.schemaBodyJson(RemoteConfig)(ok).pipe( - mapAccountServiceError("Failed to decode response"), - ) - return Option.some(parsed.config) - }) - - const login = Effect.fn("Account.login")(function* (server: string) { - const normalizedServer = normalizeServerUrl(server) - const response = yield* executeEffectOk( - HttpClientRequest.post(`${normalizedServer}/auth/device/code`).pipe( - HttpClientRequest.acceptJson, - HttpClientRequest.schemaBodyJson(ClientId)(new ClientId({ client_id: clientId })), - ), - ) - - const parsed = yield* HttpClientResponse.schemaBodyJson(DeviceAuth)(response).pipe( - mapAccountServiceError("Failed to decode response"), - ) - return new Login({ - code: parsed.device_code, - user: parsed.user_code, - url: `${normalizedServer}${parsed.verification_uri_complete}`, - server: normalizedServer, - expiry: parsed.expires_in, - interval: parsed.interval, - }) - }) - - const poll = Effect.fn("Account.poll")(function* (input: Login) { - const response = yield* executeEffect( - HttpClientRequest.post(`${input.server}/auth/device/token`).pipe( - HttpClientRequest.acceptJson, - HttpClientRequest.schemaBodyJson(DeviceTokenRequest)( - new DeviceTokenRequest({ - grant_type: "urn:ietf:params:oauth:grant-type:device_code", - device_code: input.code, - client_id: clientId, - }), - ), - ), - ) - - const parsed = yield* HttpClientResponse.schemaBodyJson(DeviceToken)(response).pipe( - mapAccountServiceError("Failed to decode response"), - ) - - if (parsed instanceof DeviceTokenError) return parsed.toPollResult() - const accessToken = parsed.access_token - - const user = fetchUser(input.server, accessToken) - const orgs = fetchOrgs(input.server, accessToken) - - const [account, remoteOrgs] = yield* Effect.all([user, orgs], { concurrency: 2 }) - - // TODO: When there are multiple orgs, let the user choose - const firstOrgID = remoteOrgs.length > 0 ? Option.some(remoteOrgs[0].id) : Option.none() - - const now = yield* Clock.currentTimeMillis - const expiry = now + Duration.toMillis(parsed.expires_in) - const refreshToken = parsed.refresh_token - - yield* repo.persistAccount({ - id: account.id, - email: account.email, - url: input.server, - accessToken, - refreshToken, - expiry, - orgID: firstOrgID, - }) - - return new PollSuccess({ email: account.email }) - }) - - return Service.of({ - active: repo.active, - activeOrg, - list: repo.list, - orgsByAccount, - remove: repo.remove, - use: repo.use, - orgs, - config, - token, - login, - poll, - }) - }), - ) - - export const defaultLayer = layer.pipe(Layer.provide(AccountRepo.layer), Layer.provide(FetchHttpClient.layer)) -} +export type { AccountOrgs, ActiveOrg } from "./account" From 710c81984aa38618ca7106b9521100a9964ae51d Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 22:13:56 -0400 Subject: [PATCH 181/300] feat: unwrap uauth namespace to flat exports + barrel (#22699) --- packages/opencode/src/auth/auth.ts | 89 +++++++++++++++++++++++++++ packages/opencode/src/auth/index.ts | 93 +---------------------------- 2 files changed, 91 insertions(+), 91 deletions(-) create mode 100644 packages/opencode/src/auth/auth.ts diff --git a/packages/opencode/src/auth/auth.ts b/packages/opencode/src/auth/auth.ts new file mode 100644 index 0000000000..fb9d2b1495 --- /dev/null +++ b/packages/opencode/src/auth/auth.ts @@ -0,0 +1,89 @@ +import path from "path" +import { Effect, Layer, Record, Result, Schema, Context } from "effect" +import { zod } from "@/util/effect-zod" +import { Global } from "../global" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" + +export const OAUTH_DUMMY_KEY = "opencode-oauth-dummy-key" + +const file = path.join(Global.Path.data, "auth.json") + +const fail = (message: string) => (cause: unknown) => new AuthError({ message, cause }) + +export class Oauth extends Schema.Class("OAuth")({ + type: Schema.Literal("oauth"), + refresh: Schema.String, + access: Schema.String, + expires: Schema.Number, + accountId: Schema.optional(Schema.String), + enterpriseUrl: Schema.optional(Schema.String), +}) {} + +export class Api extends Schema.Class("ApiAuth")({ + type: Schema.Literal("api"), + key: Schema.String, + metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)), +}) {} + +export class WellKnown extends Schema.Class("WellKnownAuth")({ + type: Schema.Literal("wellknown"), + key: Schema.String, + token: Schema.String, +}) {} + +const _Info = Schema.Union([Oauth, Api, WellKnown]).annotate({ discriminator: "type", identifier: "Auth" }) +export const Info = Object.assign(_Info, { zod: zod(_Info) }) +export type Info = Schema.Schema.Type + +export class AuthError extends Schema.TaggedErrorClass()("AuthError", { + message: Schema.String, + cause: Schema.optional(Schema.Defect), +}) {} + +export interface Interface { + readonly get: (providerID: string) => Effect.Effect + readonly all: () => Effect.Effect, AuthError> + readonly set: (key: string, info: Info) => Effect.Effect + readonly remove: (key: string) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/Auth") {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const fsys = yield* AppFileSystem.Service + const decode = Schema.decodeUnknownOption(Info) + + const all = Effect.fn("Auth.all")(function* () { + const data = (yield* fsys.readJson(file).pipe(Effect.orElseSucceed(() => ({})))) as Record + return Record.filterMap(data, (value) => Result.fromOption(decode(value), () => undefined)) + }) + + const get = Effect.fn("Auth.get")(function* (providerID: string) { + return (yield* all())[providerID] + }) + + const set = Effect.fn("Auth.set")(function* (key: string, info: Info) { + const norm = key.replace(/\/+$/, "") + const data = yield* all() + if (norm !== key) delete data[key] + delete data[norm + "/"] + yield* fsys + .writeJson(file, { ...data, [norm]: info }, 0o600) + .pipe(Effect.mapError(fail("Failed to write auth data"))) + }) + + const remove = Effect.fn("Auth.remove")(function* (key: string) { + const norm = key.replace(/\/+$/, "") + const data = yield* all() + delete data[key] + delete data[norm] + yield* fsys.writeJson(file, data, 0o600).pipe(Effect.mapError(fail("Failed to write auth data"))) + }) + + return Service.of({ get, all, set, remove }) + }), +) + +export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer)) diff --git a/packages/opencode/src/auth/index.ts b/packages/opencode/src/auth/index.ts index b287ce551e..9174745fd8 100644 --- a/packages/opencode/src/auth/index.ts +++ b/packages/opencode/src/auth/index.ts @@ -1,91 +1,2 @@ -import path from "path" -import { Effect, Layer, Record, Result, Schema, Context } from "effect" -import { zod } from "@/util/effect-zod" -import { Global } from "../global" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" - -export const OAUTH_DUMMY_KEY = "opencode-oauth-dummy-key" - -const file = path.join(Global.Path.data, "auth.json") - -const fail = (message: string) => (cause: unknown) => new Auth.AuthError({ message, cause }) - -export namespace Auth { - export class Oauth extends Schema.Class("OAuth")({ - type: Schema.Literal("oauth"), - refresh: Schema.String, - access: Schema.String, - expires: Schema.Number, - accountId: Schema.optional(Schema.String), - enterpriseUrl: Schema.optional(Schema.String), - }) {} - - export class Api extends Schema.Class("ApiAuth")({ - type: Schema.Literal("api"), - key: Schema.String, - metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)), - }) {} - - export class WellKnown extends Schema.Class("WellKnownAuth")({ - type: Schema.Literal("wellknown"), - key: Schema.String, - token: Schema.String, - }) {} - - const _Info = Schema.Union([Oauth, Api, WellKnown]).annotate({ discriminator: "type", identifier: "Auth" }) - export const Info = Object.assign(_Info, { zod: zod(_Info) }) - export type Info = Schema.Schema.Type - - export class AuthError extends Schema.TaggedErrorClass()("AuthError", { - message: Schema.String, - cause: Schema.optional(Schema.Defect), - }) {} - - export interface Interface { - readonly get: (providerID: string) => Effect.Effect - readonly all: () => Effect.Effect, AuthError> - readonly set: (key: string, info: Info) => Effect.Effect - readonly remove: (key: string) => Effect.Effect - } - - export class Service extends Context.Service()("@opencode/Auth") {} - - export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const fsys = yield* AppFileSystem.Service - const decode = Schema.decodeUnknownOption(Info) - - const all = Effect.fn("Auth.all")(function* () { - const data = (yield* fsys.readJson(file).pipe(Effect.orElseSucceed(() => ({})))) as Record - return Record.filterMap(data, (value) => Result.fromOption(decode(value), () => undefined)) - }) - - const get = Effect.fn("Auth.get")(function* (providerID: string) { - return (yield* all())[providerID] - }) - - const set = Effect.fn("Auth.set")(function* (key: string, info: Info) { - const norm = key.replace(/\/+$/, "") - const data = yield* all() - if (norm !== key) delete data[key] - delete data[norm + "/"] - yield* fsys - .writeJson(file, { ...data, [norm]: info }, 0o600) - .pipe(Effect.mapError(fail("Failed to write auth data"))) - }) - - const remove = Effect.fn("Auth.remove")(function* (key: string) { - const norm = key.replace(/\/+$/, "") - const data = yield* all() - delete data[key] - delete data[norm] - yield* fsys.writeJson(file, data, 0o600).pipe(Effect.mapError(fail("Failed to write auth data"))) - }) - - return Service.of({ get, all, set, remove }) - }), - ) - - export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer)) -} +export * as Auth from "./auth" +export { OAUTH_DUMMY_KEY } from "./auth" From c6286d1bb94d208ba66a97d16445978bba7c5c6b Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 22:14:14 -0400 Subject: [PATCH 182/300] feat: unwrap uenv namespace to flat exports + barrel (#22701) --- packages/opencode/src/env/env.ts | 35 +++++++++++++++++++++++++++ packages/opencode/src/env/index.ts | 38 +----------------------------- 2 files changed, 36 insertions(+), 37 deletions(-) create mode 100644 packages/opencode/src/env/env.ts diff --git a/packages/opencode/src/env/env.ts b/packages/opencode/src/env/env.ts new file mode 100644 index 0000000000..0ffd5ebdc3 --- /dev/null +++ b/packages/opencode/src/env/env.ts @@ -0,0 +1,35 @@ +import { Context, Effect, Layer } from "effect" +import { InstanceState } from "@/effect/instance-state" + +type State = Record + +export interface Interface { + readonly get: (key: string) => Effect.Effect + readonly all: () => Effect.Effect + readonly set: (key: string, value: string) => Effect.Effect + readonly remove: (key: string) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/Env") {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const state = yield* InstanceState.make(Effect.fn("Env.state")(() => Effect.succeed({ ...process.env }))) + + const get = Effect.fn("Env.get")((key: string) => InstanceState.use(state, (env) => env[key])) + const all = Effect.fn("Env.all")(() => InstanceState.get(state)) + const set = Effect.fn("Env.set")(function* (key: string, value: string) { + const env = yield* InstanceState.get(state) + env[key] = value + }) + const remove = Effect.fn("Env.remove")(function* (key: string) { + const env = yield* InstanceState.get(state) + delete env[key] + }) + + return Service.of({ get, all, set, remove }) + }), +) + +export const defaultLayer = layer diff --git a/packages/opencode/src/env/index.ts b/packages/opencode/src/env/index.ts index b9efb68520..c589edbfdd 100644 --- a/packages/opencode/src/env/index.ts +++ b/packages/opencode/src/env/index.ts @@ -1,37 +1 @@ -import { Context, Effect, Layer } from "effect" -import { InstanceState } from "@/effect/instance-state" - -export namespace Env { - type State = Record - - export interface Interface { - readonly get: (key: string) => Effect.Effect - readonly all: () => Effect.Effect - readonly set: (key: string, value: string) => Effect.Effect - readonly remove: (key: string) => Effect.Effect - } - - export class Service extends Context.Service()("@opencode/Env") {} - - export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const state = yield* InstanceState.make(Effect.fn("Env.state")(() => Effect.succeed({ ...process.env }))) - - const get = Effect.fn("Env.get")((key: string) => InstanceState.use(state, (env) => env[key])) - const all = Effect.fn("Env.all")(() => InstanceState.get(state)) - const set = Effect.fn("Env.set")(function* (key: string, value: string) { - const env = yield* InstanceState.get(state) - env[key] = value - }) - const remove = Effect.fn("Env.remove")(function* (key: string) { - const env = yield* InstanceState.get(state) - delete env[key] - }) - - return Service.of({ get, all, set, remove }) - }), - ) - - export const defaultLayer = layer -} +export * as Env from "./env" From 426815a829fc56ec39121542ea2a741d93b162e8 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 22:14:18 -0400 Subject: [PATCH 183/300] feat: unwrap ucommand namespace to flat exports + barrel (#22700) --- packages/opencode/src/command/command.ts | 186 ++++++++++++++++++++++ packages/opencode/src/command/index.ts | 189 +---------------------- 2 files changed, 187 insertions(+), 188 deletions(-) create mode 100644 packages/opencode/src/command/command.ts diff --git a/packages/opencode/src/command/command.ts b/packages/opencode/src/command/command.ts new file mode 100644 index 0000000000..fe9005edb2 --- /dev/null +++ b/packages/opencode/src/command/command.ts @@ -0,0 +1,186 @@ +import { BusEvent } from "@/bus/bus-event" +import { InstanceState } from "@/effect/instance-state" +import { EffectBridge } from "@/effect/bridge" +import type { InstanceContext } from "@/project/instance" +import { SessionID, MessageID } from "@/session/schema" +import { Effect, Layer, Context } from "effect" +import z from "zod" +import { Config } from "../config" +import { MCP } from "../mcp" +import { Skill } from "../skill" +import PROMPT_INITIALIZE from "./template/initialize.txt" +import PROMPT_REVIEW from "./template/review.txt" + +type State = { + commands: Record +} + +export const Event = { + Executed: BusEvent.define( + "command.executed", + z.object({ + name: z.string(), + sessionID: SessionID.zod, + arguments: z.string(), + messageID: MessageID.zod, + }), + ), +} + +export const Info = z + .object({ + name: z.string(), + description: z.string().optional(), + agent: z.string().optional(), + model: z.string().optional(), + source: z.enum(["command", "mcp", "skill"]).optional(), + // workaround for zod not supporting async functions natively so we use getters + // https://zod.dev/v4/changelog?id=zfunction + template: z.promise(z.string()).or(z.string()), + subtask: z.boolean().optional(), + hints: z.array(z.string()), + }) + .meta({ + ref: "Command", + }) + +// for some reason zod is inferring `string` for z.promise(z.string()).or(z.string()) so we have to manually override it +export type Info = Omit, "template"> & { template: Promise | string } + +export function hints(template: string) { + const result: string[] = [] + const numbered = template.match(/\$\d+/g) + if (numbered) { + for (const match of [...new Set(numbered)].sort()) result.push(match) + } + if (template.includes("$ARGUMENTS")) result.push("$ARGUMENTS") + return result +} + +export const Default = { + INIT: "init", + REVIEW: "review", +} as const + +export interface Interface { + readonly get: (name: string) => Effect.Effect + readonly list: () => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/Command") {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const config = yield* Config.Service + const mcp = yield* MCP.Service + const skill = yield* Skill.Service + + const init = Effect.fn("Command.state")(function* (ctx: InstanceContext) { + const cfg = yield* config.get() + const bridge = yield* EffectBridge.make() + const commands: Record = {} + + commands[Default.INIT] = { + name: Default.INIT, + description: "guided AGENTS.md setup", + source: "command", + get template() { + return PROMPT_INITIALIZE.replace("${path}", ctx.worktree) + }, + hints: hints(PROMPT_INITIALIZE), + } + commands[Default.REVIEW] = { + name: Default.REVIEW, + description: "review changes [commit|branch|pr], defaults to uncommitted", + source: "command", + get template() { + return PROMPT_REVIEW.replace("${path}", ctx.worktree) + }, + subtask: true, + hints: hints(PROMPT_REVIEW), + } + + for (const [name, command] of Object.entries(cfg.command ?? {})) { + commands[name] = { + name, + agent: command.agent, + model: command.model, + description: command.description, + source: "command", + get template() { + return command.template + }, + subtask: command.subtask, + hints: hints(command.template), + } + } + + for (const [name, prompt] of Object.entries(yield* mcp.prompts())) { + commands[name] = { + name, + source: "mcp", + description: prompt.description, + get template() { + return bridge.promise( + mcp + .getPrompt( + prompt.client, + prompt.name, + prompt.arguments + ? Object.fromEntries(prompt.arguments.map((argument, i) => [argument.name, `$${i + 1}`])) + : {}, + ) + .pipe( + Effect.map( + (template) => + template?.messages + .map((message) => (message.content.type === "text" ? message.content.text : "")) + .join("\n") || "", + ), + ), + ) + }, + hints: prompt.arguments?.map((_, i) => `$${i + 1}`) ?? [], + } + } + + for (const item of yield* skill.all()) { + if (commands[item.name]) continue + commands[item.name] = { + name: item.name, + description: item.description, + source: "skill", + get template() { + return item.content + }, + hints: [], + } + } + + return { + commands, + } + }) + + const state = yield* InstanceState.make((ctx) => init(ctx)) + + const get = Effect.fn("Command.get")(function* (name: string) { + const s = yield* InstanceState.get(state) + return s.commands[name] + }) + + const list = Effect.fn("Command.list")(function* () { + const s = yield* InstanceState.get(state) + return Object.values(s.commands) + }) + + return Service.of({ get, list }) + }), +) + +export const defaultLayer = layer.pipe( + Layer.provide(Config.defaultLayer), + Layer.provide(MCP.defaultLayer), + Layer.provide(Skill.defaultLayer), +) diff --git a/packages/opencode/src/command/index.ts b/packages/opencode/src/command/index.ts index 539ae0dac6..2e530360c5 100644 --- a/packages/opencode/src/command/index.ts +++ b/packages/opencode/src/command/index.ts @@ -1,188 +1 @@ -import { BusEvent } from "@/bus/bus-event" -import { InstanceState } from "@/effect/instance-state" -import { EffectBridge } from "@/effect/bridge" -import type { InstanceContext } from "@/project/instance" -import { SessionID, MessageID } from "@/session/schema" -import { Effect, Layer, Context } from "effect" -import z from "zod" -import { Config } from "../config" -import { MCP } from "../mcp" -import { Skill } from "../skill" -import PROMPT_INITIALIZE from "./template/initialize.txt" -import PROMPT_REVIEW from "./template/review.txt" - -export namespace Command { - type State = { - commands: Record - } - - export const Event = { - Executed: BusEvent.define( - "command.executed", - z.object({ - name: z.string(), - sessionID: SessionID.zod, - arguments: z.string(), - messageID: MessageID.zod, - }), - ), - } - - export const Info = z - .object({ - name: z.string(), - description: z.string().optional(), - agent: z.string().optional(), - model: z.string().optional(), - source: z.enum(["command", "mcp", "skill"]).optional(), - // workaround for zod not supporting async functions natively so we use getters - // https://zod.dev/v4/changelog?id=zfunction - template: z.promise(z.string()).or(z.string()), - subtask: z.boolean().optional(), - hints: z.array(z.string()), - }) - .meta({ - ref: "Command", - }) - - // for some reason zod is inferring `string` for z.promise(z.string()).or(z.string()) so we have to manually override it - export type Info = Omit, "template"> & { template: Promise | string } - - export function hints(template: string) { - const result: string[] = [] - const numbered = template.match(/\$\d+/g) - if (numbered) { - for (const match of [...new Set(numbered)].sort()) result.push(match) - } - if (template.includes("$ARGUMENTS")) result.push("$ARGUMENTS") - return result - } - - export const Default = { - INIT: "init", - REVIEW: "review", - } as const - - export interface Interface { - readonly get: (name: string) => Effect.Effect - readonly list: () => Effect.Effect - } - - export class Service extends Context.Service()("@opencode/Command") {} - - export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const config = yield* Config.Service - const mcp = yield* MCP.Service - const skill = yield* Skill.Service - - const init = Effect.fn("Command.state")(function* (ctx: InstanceContext) { - const cfg = yield* config.get() - const bridge = yield* EffectBridge.make() - const commands: Record = {} - - commands[Default.INIT] = { - name: Default.INIT, - description: "guided AGENTS.md setup", - source: "command", - get template() { - return PROMPT_INITIALIZE.replace("${path}", ctx.worktree) - }, - hints: hints(PROMPT_INITIALIZE), - } - commands[Default.REVIEW] = { - name: Default.REVIEW, - description: "review changes [commit|branch|pr], defaults to uncommitted", - source: "command", - get template() { - return PROMPT_REVIEW.replace("${path}", ctx.worktree) - }, - subtask: true, - hints: hints(PROMPT_REVIEW), - } - - for (const [name, command] of Object.entries(cfg.command ?? {})) { - commands[name] = { - name, - agent: command.agent, - model: command.model, - description: command.description, - source: "command", - get template() { - return command.template - }, - subtask: command.subtask, - hints: hints(command.template), - } - } - - for (const [name, prompt] of Object.entries(yield* mcp.prompts())) { - commands[name] = { - name, - source: "mcp", - description: prompt.description, - get template() { - return bridge.promise( - mcp - .getPrompt( - prompt.client, - prompt.name, - prompt.arguments - ? Object.fromEntries(prompt.arguments.map((argument, i) => [argument.name, `$${i + 1}`])) - : {}, - ) - .pipe( - Effect.map( - (template) => - template?.messages - .map((message) => (message.content.type === "text" ? message.content.text : "")) - .join("\n") || "", - ), - ), - ) - }, - hints: prompt.arguments?.map((_, i) => `$${i + 1}`) ?? [], - } - } - - for (const item of yield* skill.all()) { - if (commands[item.name]) continue - commands[item.name] = { - name: item.name, - description: item.description, - source: "skill", - get template() { - return item.content - }, - hints: [], - } - } - - return { - commands, - } - }) - - const state = yield* InstanceState.make((ctx) => init(ctx)) - - const get = Effect.fn("Command.get")(function* (name: string) { - const s = yield* InstanceState.get(state) - return s.commands[name] - }) - - const list = Effect.fn("Command.list")(function* () { - const s = yield* InstanceState.get(state) - return Object.values(s.commands) - }) - - return Service.of({ get, list }) - }), - ) - - export const defaultLayer = layer.pipe( - Layer.provide(Config.defaultLayer), - Layer.provide(MCP.defaultLayer), - Layer.provide(Skill.defaultLayer), - ) -} +export * as Command from "./command" From 360d8dd940887ad2f01f57e3bd01d0e5a4d4b0c7 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 22:14:34 -0400 Subject: [PATCH 184/300] feat: unwrap uinstallation namespace to flat exports + barrel (#22707) --- packages/opencode/src/installation/index.ts | 341 +----------------- .../opencode/src/installation/installation.ts | 338 +++++++++++++++++ 2 files changed, 339 insertions(+), 340 deletions(-) create mode 100644 packages/opencode/src/installation/installation.ts diff --git a/packages/opencode/src/installation/index.ts b/packages/opencode/src/installation/index.ts index 29f9bf1be2..4e48fcd6a0 100644 --- a/packages/opencode/src/installation/index.ts +++ b/packages/opencode/src/installation/index.ts @@ -1,340 +1 @@ -import { Effect, Layer, Schema, Context, Stream } from "effect" -import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" -import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" -import { withTransientReadRetry } from "@/util/effect-http-client" -import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" -import path from "path" -import z from "zod" -import { BusEvent } from "@/bus/bus-event" -import { Flag } from "../flag/flag" -import { Log } from "../util/log" -import { CHANNEL as channel, VERSION as version } from "./meta" - -import semver from "semver" - -export namespace Installation { - const log = Log.create({ service: "installation" }) - - export type Method = "curl" | "npm" | "yarn" | "pnpm" | "bun" | "brew" | "scoop" | "choco" | "unknown" - - export type ReleaseType = "patch" | "minor" | "major" - - export const Event = { - Updated: BusEvent.define( - "installation.updated", - z.object({ - version: z.string(), - }), - ), - UpdateAvailable: BusEvent.define( - "installation.update-available", - z.object({ - version: z.string(), - }), - ), - } - - export function getReleaseType(current: string, latest: string): ReleaseType { - const currMajor = semver.major(current) - const currMinor = semver.minor(current) - const newMajor = semver.major(latest) - const newMinor = semver.minor(latest) - - if (newMajor > currMajor) return "major" - if (newMinor > currMinor) return "minor" - return "patch" - } - - export const Info = z - .object({ - version: z.string(), - latest: z.string(), - }) - .meta({ - ref: "InstallationInfo", - }) - export type Info = z.infer - - export const VERSION = version - export const CHANNEL = channel - export const USER_AGENT = `opencode/${CHANNEL}/${VERSION}/${Flag.OPENCODE_CLIENT}` - - export function isPreview() { - return CHANNEL !== "latest" - } - - export function isLocal() { - return CHANNEL === "local" - } - - export class UpgradeFailedError extends Schema.TaggedErrorClass()("UpgradeFailedError", { - stderr: Schema.String, - }) {} - - // Response schemas for external version APIs - const GitHubRelease = Schema.Struct({ tag_name: Schema.String }) - const NpmPackage = Schema.Struct({ version: Schema.String }) - const BrewFormula = Schema.Struct({ versions: Schema.Struct({ stable: Schema.String }) }) - const BrewInfoV2 = Schema.Struct({ - formulae: Schema.Array(Schema.Struct({ versions: Schema.Struct({ stable: Schema.String }) })), - }) - const ChocoPackage = Schema.Struct({ - d: Schema.Struct({ results: Schema.Array(Schema.Struct({ Version: Schema.String })) }), - }) - const ScoopManifest = NpmPackage - - export interface Interface { - readonly info: () => Effect.Effect - readonly method: () => Effect.Effect - readonly latest: (method?: Method) => Effect.Effect - readonly upgrade: (method: Method, target: string) => Effect.Effect - } - - export class Service extends Context.Service()("@opencode/Installation") {} - - export const layer: Layer.Layer = - Layer.effect( - Service, - Effect.gen(function* () { - const http = yield* HttpClient.HttpClient - const httpOk = HttpClient.filterStatusOk(withTransientReadRetry(http)) - const spawner = yield* ChildProcessSpawner.ChildProcessSpawner - - const text = Effect.fnUntraced( - function* (cmd: string[], opts?: { cwd?: string; env?: Record }) { - const proc = ChildProcess.make(cmd[0], cmd.slice(1), { - cwd: opts?.cwd, - env: opts?.env, - extendEnv: true, - }) - const handle = yield* spawner.spawn(proc) - const out = yield* Stream.mkString(Stream.decodeText(handle.stdout)) - yield* handle.exitCode - return out - }, - Effect.scoped, - Effect.catch(() => Effect.succeed("")), - ) - - const run = Effect.fnUntraced( - function* (cmd: string[], opts?: { cwd?: string; env?: Record }) { - const proc = ChildProcess.make(cmd[0], cmd.slice(1), { - cwd: opts?.cwd, - env: opts?.env, - extendEnv: true, - }) - const handle = yield* spawner.spawn(proc) - const [stdout, stderr] = yield* Effect.all( - [Stream.mkString(Stream.decodeText(handle.stdout)), Stream.mkString(Stream.decodeText(handle.stderr))], - { concurrency: 2 }, - ) - const code = yield* handle.exitCode - return { code, stdout, stderr } - }, - Effect.scoped, - Effect.catch(() => Effect.succeed({ code: ChildProcessSpawner.ExitCode(1), stdout: "", stderr: "" })), - ) - - const getBrewFormula = Effect.fnUntraced(function* () { - const tapFormula = yield* text(["brew", "list", "--formula", "anomalyco/tap/opencode"]) - if (tapFormula.includes("opencode")) return "anomalyco/tap/opencode" - const coreFormula = yield* text(["brew", "list", "--formula", "opencode"]) - if (coreFormula.includes("opencode")) return "opencode" - return "opencode" - }) - - const upgradeCurl = Effect.fnUntraced( - function* (target: string) { - const response = yield* httpOk.execute(HttpClientRequest.get("https://opencode.ai/install")) - const body = yield* response.text - const bodyBytes = new TextEncoder().encode(body) - const proc = ChildProcess.make("bash", [], { - stdin: Stream.make(bodyBytes), - env: { VERSION: target }, - extendEnv: true, - }) - const handle = yield* spawner.spawn(proc) - const [stdout, stderr] = yield* Effect.all( - [Stream.mkString(Stream.decodeText(handle.stdout)), Stream.mkString(Stream.decodeText(handle.stderr))], - { concurrency: 2 }, - ) - const code = yield* handle.exitCode - return { code, stdout, stderr } - }, - Effect.scoped, - Effect.orDie, - ) - - const methodImpl = Effect.fn("Installation.method")(function* () { - if (process.execPath.includes(path.join(".opencode", "bin"))) return "curl" as Method - if (process.execPath.includes(path.join(".local", "bin"))) return "curl" as Method - const exec = process.execPath.toLowerCase() - - const checks: Array<{ name: Method; command: () => Effect.Effect }> = [ - { name: "npm", command: () => text(["npm", "list", "-g", "--depth=0"]) }, - { name: "yarn", command: () => text(["yarn", "global", "list"]) }, - { name: "pnpm", command: () => text(["pnpm", "list", "-g", "--depth=0"]) }, - { name: "bun", command: () => text(["bun", "pm", "ls", "-g"]) }, - { name: "brew", command: () => text(["brew", "list", "--formula", "opencode"]) }, - { name: "scoop", command: () => text(["scoop", "list", "opencode"]) }, - { name: "choco", command: () => text(["choco", "list", "--limit-output", "opencode"]) }, - ] - - checks.sort((a, b) => { - const aMatches = exec.includes(a.name) - const bMatches = exec.includes(b.name) - if (aMatches && !bMatches) return -1 - if (!aMatches && bMatches) return 1 - return 0 - }) - - for (const check of checks) { - const output = yield* check.command() - const installedName = - check.name === "brew" || check.name === "choco" || check.name === "scoop" ? "opencode" : "opencode-ai" - if (output.includes(installedName)) { - return check.name - } - } - - return "unknown" as Method - }) - - const latestImpl = Effect.fn("Installation.latest")(function* (installMethod?: Method) { - const detectedMethod = installMethod || (yield* methodImpl()) - - if (detectedMethod === "brew") { - const formula = yield* getBrewFormula() - if (formula.includes("/")) { - const infoJson = yield* text(["brew", "info", "--json=v2", formula]) - const info = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(BrewInfoV2))(infoJson) - return info.formulae[0].versions.stable - } - const response = yield* httpOk.execute( - HttpClientRequest.get("https://formulae.brew.sh/api/formula/opencode.json").pipe( - HttpClientRequest.acceptJson, - ), - ) - const data = yield* HttpClientResponse.schemaBodyJson(BrewFormula)(response) - return data.versions.stable - } - - if (detectedMethod === "npm" || detectedMethod === "bun" || detectedMethod === "pnpm") { - const r = (yield* text(["npm", "config", "get", "registry"])).trim() - const reg = r || "https://registry.npmjs.org" - const registry = reg.endsWith("/") ? reg.slice(0, -1) : reg - const channel = CHANNEL - const response = yield* httpOk.execute( - HttpClientRequest.get(`${registry}/opencode-ai/${channel}`).pipe(HttpClientRequest.acceptJson), - ) - const data = yield* HttpClientResponse.schemaBodyJson(NpmPackage)(response) - return data.version - } - - if (detectedMethod === "choco") { - const response = yield* httpOk.execute( - HttpClientRequest.get( - "https://community.chocolatey.org/api/v2/Packages?$filter=Id%20eq%20%27opencode%27%20and%20IsLatestVersion&$select=Version", - ).pipe(HttpClientRequest.setHeaders({ Accept: "application/json;odata=verbose" })), - ) - const data = yield* HttpClientResponse.schemaBodyJson(ChocoPackage)(response) - return data.d.results[0].Version - } - - if (detectedMethod === "scoop") { - const response = yield* httpOk.execute( - HttpClientRequest.get( - "https://raw.githubusercontent.com/ScoopInstaller/Main/master/bucket/opencode.json", - ).pipe(HttpClientRequest.setHeaders({ Accept: "application/json" })), - ) - const data = yield* HttpClientResponse.schemaBodyJson(ScoopManifest)(response) - return data.version - } - - const response = yield* httpOk.execute( - HttpClientRequest.get("https://api.github.com/repos/anomalyco/opencode/releases/latest").pipe( - HttpClientRequest.acceptJson, - ), - ) - const data = yield* HttpClientResponse.schemaBodyJson(GitHubRelease)(response) - return data.tag_name.replace(/^v/, "") - }, Effect.orDie) - - const upgradeImpl = Effect.fn("Installation.upgrade")(function* (m: Method, target: string) { - let result: { code: ChildProcessSpawner.ExitCode; stdout: string; stderr: string } | undefined - switch (m) { - case "curl": - result = yield* upgradeCurl(target) - break - case "npm": - result = yield* run(["npm", "install", "-g", `opencode-ai@${target}`]) - break - case "pnpm": - result = yield* run(["pnpm", "install", "-g", `opencode-ai@${target}`]) - break - case "bun": - result = yield* run(["bun", "install", "-g", `opencode-ai@${target}`]) - break - case "brew": { - const formula = yield* getBrewFormula() - const env = { HOMEBREW_NO_AUTO_UPDATE: "1" } - if (formula.includes("/")) { - const tap = yield* run(["brew", "tap", "anomalyco/tap"], { env }) - if (tap.code !== 0) { - result = tap - break - } - const repo = yield* text(["brew", "--repo", "anomalyco/tap"]) - const dir = repo.trim() - if (dir) { - const pull = yield* run(["git", "pull", "--ff-only"], { cwd: dir, env }) - if (pull.code !== 0) { - result = pull - break - } - } - } - result = yield* run(["brew", "upgrade", formula], { env }) - break - } - case "choco": - result = yield* run(["choco", "upgrade", "opencode", `--version=${target}`, "-y"]) - break - case "scoop": - result = yield* run(["scoop", "install", `opencode@${target}`]) - break - default: - return yield* new UpgradeFailedError({ stderr: `Unknown method: ${m}` }) - } - if (!result || result.code !== 0) { - const stderr = m === "choco" ? "not running from an elevated command shell" : result?.stderr || "" - return yield* new UpgradeFailedError({ stderr }) - } - log.info("upgraded", { - method: m, - target, - stdout: result.stdout, - stderr: result.stderr, - }) - yield* text([process.execPath, "--version"]) - }) - - return Service.of({ - info: Effect.fn("Installation.info")(function* () { - return { - version: VERSION, - latest: yield* latestImpl(), - } - }), - method: methodImpl, - latest: latestImpl, - upgrade: upgradeImpl, - }) - }), - ) - - export const defaultLayer = layer.pipe( - Layer.provide(FetchHttpClient.layer), - Layer.provide(CrossSpawnSpawner.defaultLayer), - ) -} +export * as Installation from "./installation" diff --git a/packages/opencode/src/installation/installation.ts b/packages/opencode/src/installation/installation.ts new file mode 100644 index 0000000000..898af9269c --- /dev/null +++ b/packages/opencode/src/installation/installation.ts @@ -0,0 +1,338 @@ +import { Effect, Layer, Schema, Context, Stream } from "effect" +import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" +import { withTransientReadRetry } from "@/util/effect-http-client" +import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" +import path from "path" +import z from "zod" +import { BusEvent } from "@/bus/bus-event" +import { Flag } from "../flag/flag" +import { Log } from "../util/log" +import { CHANNEL as channel, VERSION as version } from "./meta" + +import semver from "semver" + +const log = Log.create({ service: "installation" }) + +export type Method = "curl" | "npm" | "yarn" | "pnpm" | "bun" | "brew" | "scoop" | "choco" | "unknown" + +export type ReleaseType = "patch" | "minor" | "major" + +export const Event = { + Updated: BusEvent.define( + "installation.updated", + z.object({ + version: z.string(), + }), + ), + UpdateAvailable: BusEvent.define( + "installation.update-available", + z.object({ + version: z.string(), + }), + ), +} + +export function getReleaseType(current: string, latest: string): ReleaseType { + const currMajor = semver.major(current) + const currMinor = semver.minor(current) + const newMajor = semver.major(latest) + const newMinor = semver.minor(latest) + + if (newMajor > currMajor) return "major" + if (newMinor > currMinor) return "minor" + return "patch" +} + +export const Info = z + .object({ + version: z.string(), + latest: z.string(), + }) + .meta({ + ref: "InstallationInfo", + }) +export type Info = z.infer + +export const VERSION = version +export const CHANNEL = channel +export const USER_AGENT = `opencode/${CHANNEL}/${VERSION}/${Flag.OPENCODE_CLIENT}` + +export function isPreview() { + return CHANNEL !== "latest" +} + +export function isLocal() { + return CHANNEL === "local" +} + +export class UpgradeFailedError extends Schema.TaggedErrorClass()("UpgradeFailedError", { + stderr: Schema.String, +}) {} + +// Response schemas for external version APIs +const GitHubRelease = Schema.Struct({ tag_name: Schema.String }) +const NpmPackage = Schema.Struct({ version: Schema.String }) +const BrewFormula = Schema.Struct({ versions: Schema.Struct({ stable: Schema.String }) }) +const BrewInfoV2 = Schema.Struct({ + formulae: Schema.Array(Schema.Struct({ versions: Schema.Struct({ stable: Schema.String }) })), +}) +const ChocoPackage = Schema.Struct({ + d: Schema.Struct({ results: Schema.Array(Schema.Struct({ Version: Schema.String })) }), +}) +const ScoopManifest = NpmPackage + +export interface Interface { + readonly info: () => Effect.Effect + readonly method: () => Effect.Effect + readonly latest: (method?: Method) => Effect.Effect + readonly upgrade: (method: Method, target: string) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/Installation") {} + +export const layer: Layer.Layer = + Layer.effect( + Service, + Effect.gen(function* () { + const http = yield* HttpClient.HttpClient + const httpOk = HttpClient.filterStatusOk(withTransientReadRetry(http)) + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner + + const text = Effect.fnUntraced( + function* (cmd: string[], opts?: { cwd?: string; env?: Record }) { + const proc = ChildProcess.make(cmd[0], cmd.slice(1), { + cwd: opts?.cwd, + env: opts?.env, + extendEnv: true, + }) + const handle = yield* spawner.spawn(proc) + const out = yield* Stream.mkString(Stream.decodeText(handle.stdout)) + yield* handle.exitCode + return out + }, + Effect.scoped, + Effect.catch(() => Effect.succeed("")), + ) + + const run = Effect.fnUntraced( + function* (cmd: string[], opts?: { cwd?: string; env?: Record }) { + const proc = ChildProcess.make(cmd[0], cmd.slice(1), { + cwd: opts?.cwd, + env: opts?.env, + extendEnv: true, + }) + const handle = yield* spawner.spawn(proc) + const [stdout, stderr] = yield* Effect.all( + [Stream.mkString(Stream.decodeText(handle.stdout)), Stream.mkString(Stream.decodeText(handle.stderr))], + { concurrency: 2 }, + ) + const code = yield* handle.exitCode + return { code, stdout, stderr } + }, + Effect.scoped, + Effect.catch(() => Effect.succeed({ code: ChildProcessSpawner.ExitCode(1), stdout: "", stderr: "" })), + ) + + const getBrewFormula = Effect.fnUntraced(function* () { + const tapFormula = yield* text(["brew", "list", "--formula", "anomalyco/tap/opencode"]) + if (tapFormula.includes("opencode")) return "anomalyco/tap/opencode" + const coreFormula = yield* text(["brew", "list", "--formula", "opencode"]) + if (coreFormula.includes("opencode")) return "opencode" + return "opencode" + }) + + const upgradeCurl = Effect.fnUntraced( + function* (target: string) { + const response = yield* httpOk.execute(HttpClientRequest.get("https://opencode.ai/install")) + const body = yield* response.text + const bodyBytes = new TextEncoder().encode(body) + const proc = ChildProcess.make("bash", [], { + stdin: Stream.make(bodyBytes), + env: { VERSION: target }, + extendEnv: true, + }) + const handle = yield* spawner.spawn(proc) + const [stdout, stderr] = yield* Effect.all( + [Stream.mkString(Stream.decodeText(handle.stdout)), Stream.mkString(Stream.decodeText(handle.stderr))], + { concurrency: 2 }, + ) + const code = yield* handle.exitCode + return { code, stdout, stderr } + }, + Effect.scoped, + Effect.orDie, + ) + + const methodImpl = Effect.fn("Installation.method")(function* () { + if (process.execPath.includes(path.join(".opencode", "bin"))) return "curl" as Method + if (process.execPath.includes(path.join(".local", "bin"))) return "curl" as Method + const exec = process.execPath.toLowerCase() + + const checks: Array<{ name: Method; command: () => Effect.Effect }> = [ + { name: "npm", command: () => text(["npm", "list", "-g", "--depth=0"]) }, + { name: "yarn", command: () => text(["yarn", "global", "list"]) }, + { name: "pnpm", command: () => text(["pnpm", "list", "-g", "--depth=0"]) }, + { name: "bun", command: () => text(["bun", "pm", "ls", "-g"]) }, + { name: "brew", command: () => text(["brew", "list", "--formula", "opencode"]) }, + { name: "scoop", command: () => text(["scoop", "list", "opencode"]) }, + { name: "choco", command: () => text(["choco", "list", "--limit-output", "opencode"]) }, + ] + + checks.sort((a, b) => { + const aMatches = exec.includes(a.name) + const bMatches = exec.includes(b.name) + if (aMatches && !bMatches) return -1 + if (!aMatches && bMatches) return 1 + return 0 + }) + + for (const check of checks) { + const output = yield* check.command() + const installedName = + check.name === "brew" || check.name === "choco" || check.name === "scoop" ? "opencode" : "opencode-ai" + if (output.includes(installedName)) { + return check.name + } + } + + return "unknown" as Method + }) + + const latestImpl = Effect.fn("Installation.latest")(function* (installMethod?: Method) { + const detectedMethod = installMethod || (yield* methodImpl()) + + if (detectedMethod === "brew") { + const formula = yield* getBrewFormula() + if (formula.includes("/")) { + const infoJson = yield* text(["brew", "info", "--json=v2", formula]) + const info = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(BrewInfoV2))(infoJson) + return info.formulae[0].versions.stable + } + const response = yield* httpOk.execute( + HttpClientRequest.get("https://formulae.brew.sh/api/formula/opencode.json").pipe( + HttpClientRequest.acceptJson, + ), + ) + const data = yield* HttpClientResponse.schemaBodyJson(BrewFormula)(response) + return data.versions.stable + } + + if (detectedMethod === "npm" || detectedMethod === "bun" || detectedMethod === "pnpm") { + const r = (yield* text(["npm", "config", "get", "registry"])).trim() + const reg = r || "https://registry.npmjs.org" + const registry = reg.endsWith("/") ? reg.slice(0, -1) : reg + const channel = CHANNEL + const response = yield* httpOk.execute( + HttpClientRequest.get(`${registry}/opencode-ai/${channel}`).pipe(HttpClientRequest.acceptJson), + ) + const data = yield* HttpClientResponse.schemaBodyJson(NpmPackage)(response) + return data.version + } + + if (detectedMethod === "choco") { + const response = yield* httpOk.execute( + HttpClientRequest.get( + "https://community.chocolatey.org/api/v2/Packages?$filter=Id%20eq%20%27opencode%27%20and%20IsLatestVersion&$select=Version", + ).pipe(HttpClientRequest.setHeaders({ Accept: "application/json;odata=verbose" })), + ) + const data = yield* HttpClientResponse.schemaBodyJson(ChocoPackage)(response) + return data.d.results[0].Version + } + + if (detectedMethod === "scoop") { + const response = yield* httpOk.execute( + HttpClientRequest.get( + "https://raw.githubusercontent.com/ScoopInstaller/Main/master/bucket/opencode.json", + ).pipe(HttpClientRequest.setHeaders({ Accept: "application/json" })), + ) + const data = yield* HttpClientResponse.schemaBodyJson(ScoopManifest)(response) + return data.version + } + + const response = yield* httpOk.execute( + HttpClientRequest.get("https://api.github.com/repos/anomalyco/opencode/releases/latest").pipe( + HttpClientRequest.acceptJson, + ), + ) + const data = yield* HttpClientResponse.schemaBodyJson(GitHubRelease)(response) + return data.tag_name.replace(/^v/, "") + }, Effect.orDie) + + const upgradeImpl = Effect.fn("Installation.upgrade")(function* (m: Method, target: string) { + let result: { code: ChildProcessSpawner.ExitCode; stdout: string; stderr: string } | undefined + switch (m) { + case "curl": + result = yield* upgradeCurl(target) + break + case "npm": + result = yield* run(["npm", "install", "-g", `opencode-ai@${target}`]) + break + case "pnpm": + result = yield* run(["pnpm", "install", "-g", `opencode-ai@${target}`]) + break + case "bun": + result = yield* run(["bun", "install", "-g", `opencode-ai@${target}`]) + break + case "brew": { + const formula = yield* getBrewFormula() + const env = { HOMEBREW_NO_AUTO_UPDATE: "1" } + if (formula.includes("/")) { + const tap = yield* run(["brew", "tap", "anomalyco/tap"], { env }) + if (tap.code !== 0) { + result = tap + break + } + const repo = yield* text(["brew", "--repo", "anomalyco/tap"]) + const dir = repo.trim() + if (dir) { + const pull = yield* run(["git", "pull", "--ff-only"], { cwd: dir, env }) + if (pull.code !== 0) { + result = pull + break + } + } + } + result = yield* run(["brew", "upgrade", formula], { env }) + break + } + case "choco": + result = yield* run(["choco", "upgrade", "opencode", `--version=${target}`, "-y"]) + break + case "scoop": + result = yield* run(["scoop", "install", `opencode@${target}`]) + break + default: + return yield* new UpgradeFailedError({ stderr: `Unknown method: ${m}` }) + } + if (!result || result.code !== 0) { + const stderr = m === "choco" ? "not running from an elevated command shell" : result?.stderr || "" + return yield* new UpgradeFailedError({ stderr }) + } + log.info("upgraded", { + method: m, + target, + stdout: result.stdout, + stderr: result.stderr, + }) + yield* text([process.execPath, "--version"]) + }) + + return Service.of({ + info: Effect.fn("Installation.info")(function* () { + return { + version: VERSION, + latest: yield* latestImpl(), + } + }), + method: methodImpl, + latest: latestImpl, + upgrade: upgradeImpl, + }) + }), + ) + +export const defaultLayer = layer.pipe( + Layer.provide(FetchHttpClient.layer), + Layer.provide(CrossSpawnSpawner.defaultLayer), +) From 26cdbc20b2f889d27d5e84c6b87774c61ec87f99 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 22:14:37 -0400 Subject: [PATCH 185/300] feat: unwrap ufile namespace to flat exports + barrel (#22702) --- packages/opencode/src/file/file.ts | 654 +++++++++++++++++++++++++++ packages/opencode/src/file/index.ts | 657 +--------------------------- 2 files changed, 655 insertions(+), 656 deletions(-) create mode 100644 packages/opencode/src/file/file.ts diff --git a/packages/opencode/src/file/file.ts b/packages/opencode/src/file/file.ts new file mode 100644 index 0000000000..657fe9a583 --- /dev/null +++ b/packages/opencode/src/file/file.ts @@ -0,0 +1,654 @@ +import { BusEvent } from "@/bus/bus-event" +import { InstanceState } from "@/effect/instance-state" + +import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { Git } from "@/git" +import { Effect, Layer, Context } from "effect" +import * as Stream from "effect/Stream" +import { formatPatch, structuredPatch } from "diff" +import fuzzysort from "fuzzysort" +import ignore from "ignore" +import path from "path" +import z from "zod" +import { Global } from "../global" +import { Instance } from "../project/instance" +import { Log } from "../util/log" +import { Protected } from "./protected" +import { Ripgrep } from "./ripgrep" + +export const Info = z + .object({ + path: z.string(), + added: z.number().int(), + removed: z.number().int(), + status: z.enum(["added", "deleted", "modified"]), + }) + .meta({ + ref: "File", + }) + +export type Info = z.infer + +export const Node = z + .object({ + name: z.string(), + path: z.string(), + absolute: z.string(), + type: z.enum(["file", "directory"]), + ignored: z.boolean(), + }) + .meta({ + ref: "FileNode", + }) +export type Node = z.infer + +export const Content = z + .object({ + type: z.enum(["text", "binary"]), + content: z.string(), + diff: z.string().optional(), + patch: z + .object({ + oldFileName: z.string(), + newFileName: z.string(), + oldHeader: z.string().optional(), + newHeader: z.string().optional(), + hunks: z.array( + z.object({ + oldStart: z.number(), + oldLines: z.number(), + newStart: z.number(), + newLines: z.number(), + lines: z.array(z.string()), + }), + ), + index: z.string().optional(), + }) + .optional(), + encoding: z.literal("base64").optional(), + mimeType: z.string().optional(), + }) + .meta({ + ref: "FileContent", + }) +export type Content = z.infer + +export const Event = { + Edited: BusEvent.define( + "file.edited", + z.object({ + file: z.string(), + }), + ), +} + +const log = Log.create({ service: "file" }) + +const binary = new Set([ + "exe", + "dll", + "pdb", + "bin", + "so", + "dylib", + "o", + "a", + "lib", + "wav", + "mp3", + "ogg", + "oga", + "ogv", + "ogx", + "flac", + "aac", + "wma", + "m4a", + "weba", + "mp4", + "avi", + "mov", + "wmv", + "flv", + "webm", + "mkv", + "zip", + "tar", + "gz", + "gzip", + "bz", + "bz2", + "bzip", + "bzip2", + "7z", + "rar", + "xz", + "lz", + "z", + "pdf", + "doc", + "docx", + "ppt", + "pptx", + "xls", + "xlsx", + "dmg", + "iso", + "img", + "vmdk", + "ttf", + "otf", + "woff", + "woff2", + "eot", + "sqlite", + "db", + "mdb", + "apk", + "ipa", + "aab", + "xapk", + "app", + "pkg", + "deb", + "rpm", + "snap", + "flatpak", + "appimage", + "msi", + "msp", + "jar", + "war", + "ear", + "class", + "kotlin_module", + "dex", + "vdex", + "odex", + "oat", + "art", + "wasm", + "wat", + "bc", + "ll", + "s", + "ko", + "sys", + "drv", + "efi", + "rom", + "com", +]) + +const image = new Set([ + "png", + "jpg", + "jpeg", + "gif", + "bmp", + "webp", + "ico", + "tif", + "tiff", + "svg", + "svgz", + "avif", + "apng", + "jxl", + "heic", + "heif", + "raw", + "cr2", + "nef", + "arw", + "dng", + "orf", + "raf", + "pef", + "x3f", +]) + +const text = new Set([ + "ts", + "tsx", + "mts", + "cts", + "mtsx", + "ctsx", + "js", + "jsx", + "mjs", + "cjs", + "sh", + "bash", + "zsh", + "fish", + "ps1", + "psm1", + "cmd", + "bat", + "json", + "jsonc", + "json5", + "yaml", + "yml", + "toml", + "md", + "mdx", + "txt", + "xml", + "html", + "htm", + "css", + "scss", + "sass", + "less", + "graphql", + "gql", + "sql", + "ini", + "cfg", + "conf", + "env", +]) + +const textName = new Set([ + "dockerfile", + "makefile", + ".gitignore", + ".gitattributes", + ".editorconfig", + ".npmrc", + ".nvmrc", + ".prettierrc", + ".eslintrc", +]) + +const mime: Record = { + png: "image/png", + jpg: "image/jpeg", + jpeg: "image/jpeg", + gif: "image/gif", + bmp: "image/bmp", + webp: "image/webp", + ico: "image/x-icon", + tif: "image/tiff", + tiff: "image/tiff", + svg: "image/svg+xml", + svgz: "image/svg+xml", + avif: "image/avif", + apng: "image/apng", + jxl: "image/jxl", + heic: "image/heic", + heif: "image/heif", +} + +type Entry = { files: string[]; dirs: string[] } + +const ext = (file: string) => path.extname(file).toLowerCase().slice(1) +const name = (file: string) => path.basename(file).toLowerCase() +const isImageByExtension = (file: string) => image.has(ext(file)) +const isTextByExtension = (file: string) => text.has(ext(file)) +const isTextByName = (file: string) => textName.has(name(file)) +const isBinaryByExtension = (file: string) => binary.has(ext(file)) +const isImage = (mimeType: string) => mimeType.startsWith("image/") +const getImageMimeType = (file: string) => mime[ext(file)] || "image/" + ext(file) + +function shouldEncode(mimeType: string) { + const type = mimeType.toLowerCase() + log.debug("shouldEncode", { type }) + if (!type) return false + if (type.startsWith("text/")) return false + if (type.includes("charset=")) return false + const top = type.split("/", 2)[0] + return ["image", "audio", "video", "font", "model", "multipart"].includes(top) +} + +const hidden = (item: string) => { + const normalized = item.replaceAll("\\", "/").replace(/\/+$/, "") + return normalized.split("/").some((part) => part.startsWith(".") && part.length > 1) +} + +const sortHiddenLast = (items: string[], prefer: boolean) => { + if (prefer) return items + const visible: string[] = [] + const hiddenItems: string[] = [] + for (const item of items) { + if (hidden(item)) hiddenItems.push(item) + else visible.push(item) + } + return [...visible, ...hiddenItems] +} + +interface State { + cache: Entry +} + +export interface Interface { + readonly init: () => Effect.Effect + readonly status: () => Effect.Effect + readonly read: (file: string) => Effect.Effect + readonly list: (dir?: string) => Effect.Effect + readonly search: (input: { + query: string + limit?: number + dirs?: boolean + type?: "file" | "directory" + }) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/File") {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const appFs = yield* AppFileSystem.Service + const rg = yield* Ripgrep.Service + const git = yield* Git.Service + + const state = yield* InstanceState.make( + Effect.fn("File.state")(() => + Effect.succeed({ + cache: { files: [], dirs: [] } as Entry, + }), + ), + ) + + const scan = Effect.fn("File.scan")(function* () { + if (Instance.directory === path.parse(Instance.directory).root) return + const isGlobalHome = Instance.directory === Global.Path.home && Instance.project.id === "global" + const next: Entry = { files: [], dirs: [] } + + if (isGlobalHome) { + const dirs = new Set() + const protectedNames = Protected.names() + const ignoreNested = new Set(["node_modules", "dist", "build", "target", "vendor"]) + const shouldIgnoreName = (name: string) => name.startsWith(".") || protectedNames.has(name) + const shouldIgnoreNested = (name: string) => name.startsWith(".") || ignoreNested.has(name) + const top = yield* appFs.readDirectoryEntries(Instance.directory).pipe(Effect.orElseSucceed(() => [])) + + for (const entry of top) { + if (entry.type !== "directory") continue + if (shouldIgnoreName(entry.name)) continue + dirs.add(entry.name + "/") + + const base = path.join(Instance.directory, entry.name) + const children = yield* appFs.readDirectoryEntries(base).pipe(Effect.orElseSucceed(() => [])) + for (const child of children) { + if (child.type !== "directory") continue + if (shouldIgnoreNested(child.name)) continue + dirs.add(entry.name + "/" + child.name + "/") + } + } + + next.dirs = Array.from(dirs).toSorted() + } else { + const files = yield* rg.files({ cwd: Instance.directory }).pipe( + Stream.runCollect, + Effect.map((chunk) => [...chunk]), + ) + const seen = new Set() + for (const file of files) { + next.files.push(file) + let current = file + while (true) { + const dir = path.dirname(current) + if (dir === ".") break + if (dir === current) break + current = dir + if (seen.has(dir)) continue + seen.add(dir) + next.dirs.push(dir + "/") + } + } + } + + const s = yield* InstanceState.get(state) + s.cache = next + }) + + let cachedScan = yield* Effect.cached(scan().pipe(Effect.catchCause(() => Effect.void))) + + const ensure = Effect.fn("File.ensure")(function* () { + yield* cachedScan + cachedScan = yield* Effect.cached(scan().pipe(Effect.catchCause(() => Effect.void))) + }) + + const gitText = Effect.fnUntraced(function* (args: string[]) { + return (yield* git.run(args, { cwd: Instance.directory })).text() + }) + + const init = Effect.fn("File.init")(function* () { + yield* ensure() + }) + + const status = Effect.fn("File.status")(function* () { + if (Instance.project.vcs !== "git") return [] + + const diffOutput = yield* gitText([ + "-c", + "core.fsmonitor=false", + "-c", + "core.quotepath=false", + "diff", + "--numstat", + "HEAD", + ]) + + const changed: Info[] = [] + + if (diffOutput.trim()) { + for (const line of diffOutput.trim().split("\n")) { + const [added, removed, file] = line.split("\t") + changed.push({ + path: file, + added: added === "-" ? 0 : parseInt(added, 10), + removed: removed === "-" ? 0 : parseInt(removed, 10), + status: "modified", + }) + } + } + + const untrackedOutput = yield* gitText([ + "-c", + "core.fsmonitor=false", + "-c", + "core.quotepath=false", + "ls-files", + "--others", + "--exclude-standard", + ]) + + if (untrackedOutput.trim()) { + for (const file of untrackedOutput.trim().split("\n")) { + const content = yield* appFs + .readFileString(path.join(Instance.directory, file)) + .pipe(Effect.catch(() => Effect.succeed(undefined))) + if (content === undefined) continue + changed.push({ + path: file, + added: content.split("\n").length, + removed: 0, + status: "added", + }) + } + } + + const deletedOutput = yield* gitText([ + "-c", + "core.fsmonitor=false", + "-c", + "core.quotepath=false", + "diff", + "--name-only", + "--diff-filter=D", + "HEAD", + ]) + + if (deletedOutput.trim()) { + for (const file of deletedOutput.trim().split("\n")) { + changed.push({ + path: file, + added: 0, + removed: 0, + status: "deleted", + }) + } + } + + return changed.map((item) => { + const full = path.isAbsolute(item.path) ? item.path : path.join(Instance.directory, item.path) + return { + ...item, + path: path.relative(Instance.directory, full), + } + }) + }) + + const read: Interface["read"] = Effect.fn("File.read")(function* (file: string) { + using _ = log.time("read", { file }) + const full = path.join(Instance.directory, file) + + if (!Instance.containsPath(full)) throw new Error("Access denied: path escapes project directory") + + if (isImageByExtension(file)) { + const exists = yield* appFs.existsSafe(full) + if (exists) { + const bytes = yield* appFs.readFile(full).pipe(Effect.catch(() => Effect.succeed(new Uint8Array()))) + return { + type: "text" as const, + content: Buffer.from(bytes).toString("base64"), + mimeType: getImageMimeType(file), + encoding: "base64" as const, + } + } + return { type: "text" as const, content: "" } + } + + const knownText = isTextByExtension(file) || isTextByName(file) + + if (isBinaryByExtension(file) && !knownText) return { type: "binary" as const, content: "" } + + const exists = yield* appFs.existsSafe(full) + if (!exists) return { type: "text" as const, content: "" } + + const mimeType = AppFileSystem.mimeType(full) + const encode = knownText ? false : shouldEncode(mimeType) + + if (encode && !isImage(mimeType)) return { type: "binary" as const, content: "", mimeType } + + if (encode) { + const bytes = yield* appFs.readFile(full).pipe(Effect.catch(() => Effect.succeed(new Uint8Array()))) + return { + type: "text" as const, + content: Buffer.from(bytes).toString("base64"), + mimeType, + encoding: "base64" as const, + } + } + + const content = yield* appFs.readFileString(full).pipe( + Effect.map((s) => s.trim()), + Effect.catch(() => Effect.succeed("")), + ) + + if (Instance.project.vcs === "git") { + let diff = yield* gitText(["-c", "core.fsmonitor=false", "diff", "--", file]) + if (!diff.trim()) { + diff = yield* gitText(["-c", "core.fsmonitor=false", "diff", "--staged", "--", file]) + } + if (diff.trim()) { + const original = yield* git.show(Instance.directory, "HEAD", file) + const patch = structuredPatch(file, file, original, content, "old", "new", { + context: Infinity, + ignoreWhitespace: true, + }) + return { type: "text" as const, content, patch, diff: formatPatch(patch) } + } + return { type: "text" as const, content } + } + + return { type: "text" as const, content } + }) + + const list = Effect.fn("File.list")(function* (dir?: string) { + const exclude = [".git", ".DS_Store"] + let ignored = (_: string) => false + if (Instance.project.vcs === "git") { + const ig = ignore() + const gitignore = path.join(Instance.project.worktree, ".gitignore") + const gitignoreText = yield* appFs.readFileString(gitignore).pipe(Effect.catch(() => Effect.succeed(""))) + if (gitignoreText) ig.add(gitignoreText) + const ignoreFile = path.join(Instance.project.worktree, ".ignore") + const ignoreText = yield* appFs.readFileString(ignoreFile).pipe(Effect.catch(() => Effect.succeed(""))) + if (ignoreText) ig.add(ignoreText) + ignored = ig.ignores.bind(ig) + } + + const resolved = dir ? path.join(Instance.directory, dir) : Instance.directory + if (!Instance.containsPath(resolved)) throw new Error("Access denied: path escapes project directory") + + const entries = yield* appFs.readDirectoryEntries(resolved).pipe(Effect.orElseSucceed(() => [])) + + const nodes: Node[] = [] + for (const entry of entries) { + if (exclude.includes(entry.name)) continue + const absolute = path.join(resolved, entry.name) + const file = path.relative(Instance.directory, absolute) + const type = entry.type === "directory" ? "directory" : "file" + nodes.push({ + name: entry.name, + path: file, + absolute, + type, + ignored: ignored(type === "directory" ? file + "/" : file), + }) + } + return nodes.sort((a, b) => { + if (a.type !== b.type) return a.type === "directory" ? -1 : 1 + return a.name.localeCompare(b.name) + }) + }) + + const search = Effect.fn("File.search")(function* (input: { + query: string + limit?: number + dirs?: boolean + type?: "file" | "directory" + }) { + yield* ensure() + const { cache } = yield* InstanceState.get(state) + + const query = input.query.trim() + const limit = input.limit ?? 100 + const kind = input.type ?? (input.dirs === false ? "file" : "all") + log.info("search", { query, kind }) + + const preferHidden = query.startsWith(".") || query.includes("/.") + + if (!query) { + if (kind === "file") return cache.files.slice(0, limit) + return sortHiddenLast(cache.dirs.toSorted(), preferHidden).slice(0, limit) + } + + const items = + kind === "file" ? cache.files : kind === "directory" ? cache.dirs : [...cache.files, ...cache.dirs] + + const searchLimit = kind === "directory" && !preferHidden ? limit * 20 : limit + const sorted = fuzzysort.go(query, items, { limit: searchLimit }).map((item) => item.target) + const output = kind === "directory" ? sortHiddenLast(sorted, preferHidden).slice(0, limit) : sorted + + log.info("search", { query, kind, results: output.length }) + return output + }) + + log.info("init") + return Service.of({ init, status, read, list, search }) + }), +) + +export const defaultLayer = layer.pipe( + Layer.provide(Ripgrep.defaultLayer), + Layer.provide(AppFileSystem.defaultLayer), + Layer.provide(Git.defaultLayer), +) diff --git a/packages/opencode/src/file/index.ts b/packages/opencode/src/file/index.ts index 909f1e61d2..b65ac9d686 100644 --- a/packages/opencode/src/file/index.ts +++ b/packages/opencode/src/file/index.ts @@ -1,656 +1 @@ -import { BusEvent } from "@/bus/bus-event" -import { InstanceState } from "@/effect/instance-state" - -import { AppFileSystem } from "@opencode-ai/shared/filesystem" -import { Git } from "@/git" -import { Effect, Layer, Context } from "effect" -import * as Stream from "effect/Stream" -import { formatPatch, structuredPatch } from "diff" -import fuzzysort from "fuzzysort" -import ignore from "ignore" -import path from "path" -import z from "zod" -import { Global } from "../global" -import { Instance } from "../project/instance" -import { Log } from "../util/log" -import { Protected } from "./protected" -import { Ripgrep } from "./ripgrep" - -export namespace File { - export const Info = z - .object({ - path: z.string(), - added: z.number().int(), - removed: z.number().int(), - status: z.enum(["added", "deleted", "modified"]), - }) - .meta({ - ref: "File", - }) - - export type Info = z.infer - - export const Node = z - .object({ - name: z.string(), - path: z.string(), - absolute: z.string(), - type: z.enum(["file", "directory"]), - ignored: z.boolean(), - }) - .meta({ - ref: "FileNode", - }) - export type Node = z.infer - - export const Content = z - .object({ - type: z.enum(["text", "binary"]), - content: z.string(), - diff: z.string().optional(), - patch: z - .object({ - oldFileName: z.string(), - newFileName: z.string(), - oldHeader: z.string().optional(), - newHeader: z.string().optional(), - hunks: z.array( - z.object({ - oldStart: z.number(), - oldLines: z.number(), - newStart: z.number(), - newLines: z.number(), - lines: z.array(z.string()), - }), - ), - index: z.string().optional(), - }) - .optional(), - encoding: z.literal("base64").optional(), - mimeType: z.string().optional(), - }) - .meta({ - ref: "FileContent", - }) - export type Content = z.infer - - export const Event = { - Edited: BusEvent.define( - "file.edited", - z.object({ - file: z.string(), - }), - ), - } - - const log = Log.create({ service: "file" }) - - const binary = new Set([ - "exe", - "dll", - "pdb", - "bin", - "so", - "dylib", - "o", - "a", - "lib", - "wav", - "mp3", - "ogg", - "oga", - "ogv", - "ogx", - "flac", - "aac", - "wma", - "m4a", - "weba", - "mp4", - "avi", - "mov", - "wmv", - "flv", - "webm", - "mkv", - "zip", - "tar", - "gz", - "gzip", - "bz", - "bz2", - "bzip", - "bzip2", - "7z", - "rar", - "xz", - "lz", - "z", - "pdf", - "doc", - "docx", - "ppt", - "pptx", - "xls", - "xlsx", - "dmg", - "iso", - "img", - "vmdk", - "ttf", - "otf", - "woff", - "woff2", - "eot", - "sqlite", - "db", - "mdb", - "apk", - "ipa", - "aab", - "xapk", - "app", - "pkg", - "deb", - "rpm", - "snap", - "flatpak", - "appimage", - "msi", - "msp", - "jar", - "war", - "ear", - "class", - "kotlin_module", - "dex", - "vdex", - "odex", - "oat", - "art", - "wasm", - "wat", - "bc", - "ll", - "s", - "ko", - "sys", - "drv", - "efi", - "rom", - "com", - ]) - - const image = new Set([ - "png", - "jpg", - "jpeg", - "gif", - "bmp", - "webp", - "ico", - "tif", - "tiff", - "svg", - "svgz", - "avif", - "apng", - "jxl", - "heic", - "heif", - "raw", - "cr2", - "nef", - "arw", - "dng", - "orf", - "raf", - "pef", - "x3f", - ]) - - const text = new Set([ - "ts", - "tsx", - "mts", - "cts", - "mtsx", - "ctsx", - "js", - "jsx", - "mjs", - "cjs", - "sh", - "bash", - "zsh", - "fish", - "ps1", - "psm1", - "cmd", - "bat", - "json", - "jsonc", - "json5", - "yaml", - "yml", - "toml", - "md", - "mdx", - "txt", - "xml", - "html", - "htm", - "css", - "scss", - "sass", - "less", - "graphql", - "gql", - "sql", - "ini", - "cfg", - "conf", - "env", - ]) - - const textName = new Set([ - "dockerfile", - "makefile", - ".gitignore", - ".gitattributes", - ".editorconfig", - ".npmrc", - ".nvmrc", - ".prettierrc", - ".eslintrc", - ]) - - const mime: Record = { - png: "image/png", - jpg: "image/jpeg", - jpeg: "image/jpeg", - gif: "image/gif", - bmp: "image/bmp", - webp: "image/webp", - ico: "image/x-icon", - tif: "image/tiff", - tiff: "image/tiff", - svg: "image/svg+xml", - svgz: "image/svg+xml", - avif: "image/avif", - apng: "image/apng", - jxl: "image/jxl", - heic: "image/heic", - heif: "image/heif", - } - - type Entry = { files: string[]; dirs: string[] } - - const ext = (file: string) => path.extname(file).toLowerCase().slice(1) - const name = (file: string) => path.basename(file).toLowerCase() - const isImageByExtension = (file: string) => image.has(ext(file)) - const isTextByExtension = (file: string) => text.has(ext(file)) - const isTextByName = (file: string) => textName.has(name(file)) - const isBinaryByExtension = (file: string) => binary.has(ext(file)) - const isImage = (mimeType: string) => mimeType.startsWith("image/") - const getImageMimeType = (file: string) => mime[ext(file)] || "image/" + ext(file) - - function shouldEncode(mimeType: string) { - const type = mimeType.toLowerCase() - log.debug("shouldEncode", { type }) - if (!type) return false - if (type.startsWith("text/")) return false - if (type.includes("charset=")) return false - const top = type.split("/", 2)[0] - return ["image", "audio", "video", "font", "model", "multipart"].includes(top) - } - - const hidden = (item: string) => { - const normalized = item.replaceAll("\\", "/").replace(/\/+$/, "") - return normalized.split("/").some((part) => part.startsWith(".") && part.length > 1) - } - - const sortHiddenLast = (items: string[], prefer: boolean) => { - if (prefer) return items - const visible: string[] = [] - const hiddenItems: string[] = [] - for (const item of items) { - if (hidden(item)) hiddenItems.push(item) - else visible.push(item) - } - return [...visible, ...hiddenItems] - } - - interface State { - cache: Entry - } - - export interface Interface { - readonly init: () => Effect.Effect - readonly status: () => Effect.Effect - readonly read: (file: string) => Effect.Effect - readonly list: (dir?: string) => Effect.Effect - readonly search: (input: { - query: string - limit?: number - dirs?: boolean - type?: "file" | "directory" - }) => Effect.Effect - } - - export class Service extends Context.Service()("@opencode/File") {} - - export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const appFs = yield* AppFileSystem.Service - const rg = yield* Ripgrep.Service - const git = yield* Git.Service - - const state = yield* InstanceState.make( - Effect.fn("File.state")(() => - Effect.succeed({ - cache: { files: [], dirs: [] } as Entry, - }), - ), - ) - - const scan = Effect.fn("File.scan")(function* () { - if (Instance.directory === path.parse(Instance.directory).root) return - const isGlobalHome = Instance.directory === Global.Path.home && Instance.project.id === "global" - const next: Entry = { files: [], dirs: [] } - - if (isGlobalHome) { - const dirs = new Set() - const protectedNames = Protected.names() - const ignoreNested = new Set(["node_modules", "dist", "build", "target", "vendor"]) - const shouldIgnoreName = (name: string) => name.startsWith(".") || protectedNames.has(name) - const shouldIgnoreNested = (name: string) => name.startsWith(".") || ignoreNested.has(name) - const top = yield* appFs.readDirectoryEntries(Instance.directory).pipe(Effect.orElseSucceed(() => [])) - - for (const entry of top) { - if (entry.type !== "directory") continue - if (shouldIgnoreName(entry.name)) continue - dirs.add(entry.name + "/") - - const base = path.join(Instance.directory, entry.name) - const children = yield* appFs.readDirectoryEntries(base).pipe(Effect.orElseSucceed(() => [])) - for (const child of children) { - if (child.type !== "directory") continue - if (shouldIgnoreNested(child.name)) continue - dirs.add(entry.name + "/" + child.name + "/") - } - } - - next.dirs = Array.from(dirs).toSorted() - } else { - const files = yield* rg.files({ cwd: Instance.directory }).pipe( - Stream.runCollect, - Effect.map((chunk) => [...chunk]), - ) - const seen = new Set() - for (const file of files) { - next.files.push(file) - let current = file - while (true) { - const dir = path.dirname(current) - if (dir === ".") break - if (dir === current) break - current = dir - if (seen.has(dir)) continue - seen.add(dir) - next.dirs.push(dir + "/") - } - } - } - - const s = yield* InstanceState.get(state) - s.cache = next - }) - - let cachedScan = yield* Effect.cached(scan().pipe(Effect.catchCause(() => Effect.void))) - - const ensure = Effect.fn("File.ensure")(function* () { - yield* cachedScan - cachedScan = yield* Effect.cached(scan().pipe(Effect.catchCause(() => Effect.void))) - }) - - const gitText = Effect.fnUntraced(function* (args: string[]) { - return (yield* git.run(args, { cwd: Instance.directory })).text() - }) - - const init = Effect.fn("File.init")(function* () { - yield* ensure() - }) - - const status = Effect.fn("File.status")(function* () { - if (Instance.project.vcs !== "git") return [] - - const diffOutput = yield* gitText([ - "-c", - "core.fsmonitor=false", - "-c", - "core.quotepath=false", - "diff", - "--numstat", - "HEAD", - ]) - - const changed: File.Info[] = [] - - if (diffOutput.trim()) { - for (const line of diffOutput.trim().split("\n")) { - const [added, removed, file] = line.split("\t") - changed.push({ - path: file, - added: added === "-" ? 0 : parseInt(added, 10), - removed: removed === "-" ? 0 : parseInt(removed, 10), - status: "modified", - }) - } - } - - const untrackedOutput = yield* gitText([ - "-c", - "core.fsmonitor=false", - "-c", - "core.quotepath=false", - "ls-files", - "--others", - "--exclude-standard", - ]) - - if (untrackedOutput.trim()) { - for (const file of untrackedOutput.trim().split("\n")) { - const content = yield* appFs - .readFileString(path.join(Instance.directory, file)) - .pipe(Effect.catch(() => Effect.succeed(undefined))) - if (content === undefined) continue - changed.push({ - path: file, - added: content.split("\n").length, - removed: 0, - status: "added", - }) - } - } - - const deletedOutput = yield* gitText([ - "-c", - "core.fsmonitor=false", - "-c", - "core.quotepath=false", - "diff", - "--name-only", - "--diff-filter=D", - "HEAD", - ]) - - if (deletedOutput.trim()) { - for (const file of deletedOutput.trim().split("\n")) { - changed.push({ - path: file, - added: 0, - removed: 0, - status: "deleted", - }) - } - } - - return changed.map((item) => { - const full = path.isAbsolute(item.path) ? item.path : path.join(Instance.directory, item.path) - return { - ...item, - path: path.relative(Instance.directory, full), - } - }) - }) - - const read: Interface["read"] = Effect.fn("File.read")(function* (file: string) { - using _ = log.time("read", { file }) - const full = path.join(Instance.directory, file) - - if (!Instance.containsPath(full)) throw new Error("Access denied: path escapes project directory") - - if (isImageByExtension(file)) { - const exists = yield* appFs.existsSafe(full) - if (exists) { - const bytes = yield* appFs.readFile(full).pipe(Effect.catch(() => Effect.succeed(new Uint8Array()))) - return { - type: "text" as const, - content: Buffer.from(bytes).toString("base64"), - mimeType: getImageMimeType(file), - encoding: "base64" as const, - } - } - return { type: "text" as const, content: "" } - } - - const knownText = isTextByExtension(file) || isTextByName(file) - - if (isBinaryByExtension(file) && !knownText) return { type: "binary" as const, content: "" } - - const exists = yield* appFs.existsSafe(full) - if (!exists) return { type: "text" as const, content: "" } - - const mimeType = AppFileSystem.mimeType(full) - const encode = knownText ? false : shouldEncode(mimeType) - - if (encode && !isImage(mimeType)) return { type: "binary" as const, content: "", mimeType } - - if (encode) { - const bytes = yield* appFs.readFile(full).pipe(Effect.catch(() => Effect.succeed(new Uint8Array()))) - return { - type: "text" as const, - content: Buffer.from(bytes).toString("base64"), - mimeType, - encoding: "base64" as const, - } - } - - const content = yield* appFs.readFileString(full).pipe( - Effect.map((s) => s.trim()), - Effect.catch(() => Effect.succeed("")), - ) - - if (Instance.project.vcs === "git") { - let diff = yield* gitText(["-c", "core.fsmonitor=false", "diff", "--", file]) - if (!diff.trim()) { - diff = yield* gitText(["-c", "core.fsmonitor=false", "diff", "--staged", "--", file]) - } - if (diff.trim()) { - const original = yield* git.show(Instance.directory, "HEAD", file) - const patch = structuredPatch(file, file, original, content, "old", "new", { - context: Infinity, - ignoreWhitespace: true, - }) - return { type: "text" as const, content, patch, diff: formatPatch(patch) } - } - return { type: "text" as const, content } - } - - return { type: "text" as const, content } - }) - - const list = Effect.fn("File.list")(function* (dir?: string) { - const exclude = [".git", ".DS_Store"] - let ignored = (_: string) => false - if (Instance.project.vcs === "git") { - const ig = ignore() - const gitignore = path.join(Instance.project.worktree, ".gitignore") - const gitignoreText = yield* appFs.readFileString(gitignore).pipe(Effect.catch(() => Effect.succeed(""))) - if (gitignoreText) ig.add(gitignoreText) - const ignoreFile = path.join(Instance.project.worktree, ".ignore") - const ignoreText = yield* appFs.readFileString(ignoreFile).pipe(Effect.catch(() => Effect.succeed(""))) - if (ignoreText) ig.add(ignoreText) - ignored = ig.ignores.bind(ig) - } - - const resolved = dir ? path.join(Instance.directory, dir) : Instance.directory - if (!Instance.containsPath(resolved)) throw new Error("Access denied: path escapes project directory") - - const entries = yield* appFs.readDirectoryEntries(resolved).pipe(Effect.orElseSucceed(() => [])) - - const nodes: File.Node[] = [] - for (const entry of entries) { - if (exclude.includes(entry.name)) continue - const absolute = path.join(resolved, entry.name) - const file = path.relative(Instance.directory, absolute) - const type = entry.type === "directory" ? "directory" : "file" - nodes.push({ - name: entry.name, - path: file, - absolute, - type, - ignored: ignored(type === "directory" ? file + "/" : file), - }) - } - return nodes.sort((a, b) => { - if (a.type !== b.type) return a.type === "directory" ? -1 : 1 - return a.name.localeCompare(b.name) - }) - }) - - const search = Effect.fn("File.search")(function* (input: { - query: string - limit?: number - dirs?: boolean - type?: "file" | "directory" - }) { - yield* ensure() - const { cache } = yield* InstanceState.get(state) - - const query = input.query.trim() - const limit = input.limit ?? 100 - const kind = input.type ?? (input.dirs === false ? "file" : "all") - log.info("search", { query, kind }) - - const preferHidden = query.startsWith(".") || query.includes("/.") - - if (!query) { - if (kind === "file") return cache.files.slice(0, limit) - return sortHiddenLast(cache.dirs.toSorted(), preferHidden).slice(0, limit) - } - - const items = - kind === "file" ? cache.files : kind === "directory" ? cache.dirs : [...cache.files, ...cache.dirs] - - const searchLimit = kind === "directory" && !preferHidden ? limit * 20 : limit - const sorted = fuzzysort.go(query, items, { limit: searchLimit }).map((item) => item.target) - const output = kind === "directory" ? sortHiddenLast(sorted, preferHidden).slice(0, limit) : sorted - - log.info("search", { query, kind, results: output.length }) - return output - }) - - log.info("init") - return Service.of({ init, status, read, list, search }) - }), - ) - - export const defaultLayer = layer.pipe( - Layer.provide(Ripgrep.defaultLayer), - Layer.provide(AppFileSystem.defaultLayer), - Layer.provide(Git.defaultLayer), - ) -} +export * as File from "./file" From d22b5f026d38562550a9394aff9dbe9839b09812 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 22:14:44 -0400 Subject: [PATCH 186/300] feat: unwrap unpm namespace to flat exports + barrel (#22708) --- packages/opencode/src/npm/index.ts | 189 +---------------------------- packages/opencode/src/npm/npm.ts | 186 ++++++++++++++++++++++++++++ 2 files changed, 187 insertions(+), 188 deletions(-) create mode 100644 packages/opencode/src/npm/npm.ts diff --git a/packages/opencode/src/npm/index.ts b/packages/opencode/src/npm/index.ts index e648fd899c..856ed2a2c6 100644 --- a/packages/opencode/src/npm/index.ts +++ b/packages/opencode/src/npm/index.ts @@ -1,188 +1 @@ -import semver from "semver" -import z from "zod" -import { NamedError } from "@opencode-ai/shared/util/error" -import { Global } from "../global" -import { Log } from "../util/log" -import path from "path" -import { readdir, rm } from "fs/promises" -import { Filesystem } from "@/util/filesystem" -import { Flock } from "@opencode-ai/shared/util/flock" -import { Arborist } from "@npmcli/arborist" - -export namespace Npm { - const log = Log.create({ service: "npm" }) - const illegal = process.platform === "win32" ? new Set(["<", ">", ":", '"', "|", "?", "*"]) : undefined - - export const InstallFailedError = NamedError.create( - "NpmInstallFailedError", - z.object({ - pkg: z.string(), - }), - ) - - export function sanitize(pkg: string) { - if (!illegal) return pkg - return Array.from(pkg, (char) => (illegal.has(char) || char.charCodeAt(0) < 32 ? "_" : char)).join("") - } - - function directory(pkg: string) { - return path.join(Global.Path.cache, "packages", sanitize(pkg)) - } - - function resolveEntryPoint(name: string, dir: string) { - let entrypoint: string | undefined - try { - entrypoint = typeof Bun !== "undefined" ? import.meta.resolve(name, dir) : import.meta.resolve(dir) - } catch {} - const result = { - directory: dir, - entrypoint, - } - return result - } - - export async function outdated(pkg: string, cachedVersion: string): Promise { - const response = await fetch(`https://registry.npmjs.org/${pkg}`) - if (!response.ok) { - log.warn("Failed to resolve latest version, using cached", { pkg, cachedVersion }) - return false - } - - const data = (await response.json()) as { "dist-tags"?: { latest?: string } } - const latestVersion = data?.["dist-tags"]?.latest - if (!latestVersion) { - log.warn("No latest version found, using cached", { pkg, cachedVersion }) - return false - } - - const range = /[\s^~*xX<>|=]/.test(cachedVersion) - if (range) return !semver.satisfies(latestVersion, cachedVersion) - - return semver.lt(cachedVersion, latestVersion) - } - - export async function add(pkg: string) { - const dir = directory(pkg) - await using _ = await Flock.acquire(`npm-install:${Filesystem.resolve(dir)}`) - log.info("installing package", { - pkg, - }) - - const arborist = new Arborist({ - path: dir, - binLinks: true, - progress: false, - savePrefix: "", - ignoreScripts: true, - }) - const tree = await arborist.loadVirtual().catch(() => {}) - if (tree) { - const first = tree.edgesOut.values().next().value?.to - if (first) { - return resolveEntryPoint(first.name, first.path) - } - } - - const result = await arborist - .reify({ - add: [pkg], - save: true, - saveType: "prod", - }) - .catch((cause) => { - throw new InstallFailedError( - { pkg }, - { - cause, - }, - ) - }) - - const first = result.edgesOut.values().next().value?.to - if (!first) throw new InstallFailedError({ pkg }) - return resolveEntryPoint(first.name, first.path) - } - - export async function install(dir: string) { - await using _ = await Flock.acquire(`npm-install:${dir}`) - log.info("checking dependencies", { dir }) - - const reify = async () => { - const arb = new Arborist({ - path: dir, - binLinks: true, - progress: false, - savePrefix: "", - ignoreScripts: true, - }) - await arb.reify().catch(() => {}) - } - - if (!(await Filesystem.exists(path.join(dir, "node_modules")))) { - log.info("node_modules missing, reifying") - await reify() - return - } - - const pkg = await Filesystem.readJson(path.join(dir, "package.json")).catch(() => ({})) - const lock = await Filesystem.readJson(path.join(dir, "package-lock.json")).catch(() => ({})) - - const declared = new Set([ - ...Object.keys(pkg.dependencies || {}), - ...Object.keys(pkg.devDependencies || {}), - ...Object.keys(pkg.peerDependencies || {}), - ...Object.keys(pkg.optionalDependencies || {}), - ]) - - const root = lock.packages?.[""] || {} - const locked = new Set([ - ...Object.keys(root.dependencies || {}), - ...Object.keys(root.devDependencies || {}), - ...Object.keys(root.peerDependencies || {}), - ...Object.keys(root.optionalDependencies || {}), - ]) - - for (const name of declared) { - if (!locked.has(name)) { - log.info("dependency not in lock file, reifying", { name }) - await reify() - return - } - } - - log.info("dependencies in sync") - } - - export async function which(pkg: string) { - const dir = directory(pkg) - const binDir = path.join(dir, "node_modules", ".bin") - - const pick = async () => { - const files = await readdir(binDir).catch(() => []) - if (files.length === 0) return undefined - if (files.length === 1) return files[0] - // Multiple binaries — resolve from package.json bin field like npx does - const pkgJson = await Filesystem.readJson<{ bin?: string | Record }>( - path.join(dir, "node_modules", pkg, "package.json"), - ).catch(() => undefined) - if (pkgJson?.bin) { - const unscoped = pkg.startsWith("@") ? pkg.split("/")[1] : pkg - const bin = pkgJson.bin - if (typeof bin === "string") return unscoped - const keys = Object.keys(bin) - if (keys.length === 1) return keys[0] - return bin[unscoped] ? unscoped : keys[0] - } - return files[0] - } - - const bin = await pick() - if (bin) return path.join(binDir, bin) - - await rm(path.join(dir, "package-lock.json"), { force: true }) - await add(pkg) - const resolved = await pick() - if (!resolved) return - return path.join(binDir, resolved) - } -} +export * as Npm from "./npm" diff --git a/packages/opencode/src/npm/npm.ts b/packages/opencode/src/npm/npm.ts new file mode 100644 index 0000000000..f905130719 --- /dev/null +++ b/packages/opencode/src/npm/npm.ts @@ -0,0 +1,186 @@ +import semver from "semver" +import z from "zod" +import { NamedError } from "@opencode-ai/shared/util/error" +import { Global } from "../global" +import { Log } from "../util/log" +import path from "path" +import { readdir, rm } from "fs/promises" +import { Filesystem } from "@/util/filesystem" +import { Flock } from "@opencode-ai/shared/util/flock" +import { Arborist } from "@npmcli/arborist" + +const log = Log.create({ service: "npm" }) +const illegal = process.platform === "win32" ? new Set(["<", ">", ":", '"', "|", "?", "*"]) : undefined + +export const InstallFailedError = NamedError.create( + "NpmInstallFailedError", + z.object({ + pkg: z.string(), + }), +) + +export function sanitize(pkg: string) { + if (!illegal) return pkg + return Array.from(pkg, (char) => (illegal.has(char) || char.charCodeAt(0) < 32 ? "_" : char)).join("") +} + +function directory(pkg: string) { + return path.join(Global.Path.cache, "packages", sanitize(pkg)) +} + +function resolveEntryPoint(name: string, dir: string) { + let entrypoint: string | undefined + try { + entrypoint = typeof Bun !== "undefined" ? import.meta.resolve(name, dir) : import.meta.resolve(dir) + } catch {} + const result = { + directory: dir, + entrypoint, + } + return result +} + +export async function outdated(pkg: string, cachedVersion: string): Promise { + const response = await fetch(`https://registry.npmjs.org/${pkg}`) + if (!response.ok) { + log.warn("Failed to resolve latest version, using cached", { pkg, cachedVersion }) + return false + } + + const data = (await response.json()) as { "dist-tags"?: { latest?: string } } + const latestVersion = data?.["dist-tags"]?.latest + if (!latestVersion) { + log.warn("No latest version found, using cached", { pkg, cachedVersion }) + return false + } + + const range = /[\s^~*xX<>|=]/.test(cachedVersion) + if (range) return !semver.satisfies(latestVersion, cachedVersion) + + return semver.lt(cachedVersion, latestVersion) +} + +export async function add(pkg: string) { + const dir = directory(pkg) + await using _ = await Flock.acquire(`npm-install:${Filesystem.resolve(dir)}`) + log.info("installing package", { + pkg, + }) + + const arborist = new Arborist({ + path: dir, + binLinks: true, + progress: false, + savePrefix: "", + ignoreScripts: true, + }) + const tree = await arborist.loadVirtual().catch(() => {}) + if (tree) { + const first = tree.edgesOut.values().next().value?.to + if (first) { + return resolveEntryPoint(first.name, first.path) + } + } + + const result = await arborist + .reify({ + add: [pkg], + save: true, + saveType: "prod", + }) + .catch((cause) => { + throw new InstallFailedError( + { pkg }, + { + cause, + }, + ) + }) + + const first = result.edgesOut.values().next().value?.to + if (!first) throw new InstallFailedError({ pkg }) + return resolveEntryPoint(first.name, first.path) +} + +export async function install(dir: string) { + await using _ = await Flock.acquire(`npm-install:${dir}`) + log.info("checking dependencies", { dir }) + + const reify = async () => { + const arb = new Arborist({ + path: dir, + binLinks: true, + progress: false, + savePrefix: "", + ignoreScripts: true, + }) + await arb.reify().catch(() => {}) + } + + if (!(await Filesystem.exists(path.join(dir, "node_modules")))) { + log.info("node_modules missing, reifying") + await reify() + return + } + + const pkg = await Filesystem.readJson(path.join(dir, "package.json")).catch(() => ({})) + const lock = await Filesystem.readJson(path.join(dir, "package-lock.json")).catch(() => ({})) + + const declared = new Set([ + ...Object.keys(pkg.dependencies || {}), + ...Object.keys(pkg.devDependencies || {}), + ...Object.keys(pkg.peerDependencies || {}), + ...Object.keys(pkg.optionalDependencies || {}), + ]) + + const root = lock.packages?.[""] || {} + const locked = new Set([ + ...Object.keys(root.dependencies || {}), + ...Object.keys(root.devDependencies || {}), + ...Object.keys(root.peerDependencies || {}), + ...Object.keys(root.optionalDependencies || {}), + ]) + + for (const name of declared) { + if (!locked.has(name)) { + log.info("dependency not in lock file, reifying", { name }) + await reify() + return + } + } + + log.info("dependencies in sync") +} + +export async function which(pkg: string) { + const dir = directory(pkg) + const binDir = path.join(dir, "node_modules", ".bin") + + const pick = async () => { + const files = await readdir(binDir).catch(() => []) + if (files.length === 0) return undefined + if (files.length === 1) return files[0] + // Multiple binaries — resolve from package.json bin field like npx does + const pkgJson = await Filesystem.readJson<{ bin?: string | Record }>( + path.join(dir, "node_modules", pkg, "package.json"), + ).catch(() => undefined) + if (pkgJson?.bin) { + const unscoped = pkg.startsWith("@") ? pkg.split("/")[1] : pkg + const bin = pkgJson.bin + if (typeof bin === "string") return unscoped + const keys = Object.keys(bin) + if (keys.length === 1) return keys[0] + return bin[unscoped] ? unscoped : keys[0] + } + return files[0] + } + + const bin = await pick() + if (bin) return path.join(binDir, bin) + + await rm(path.join(dir, "package-lock.json"), { force: true }) + await add(pkg) + const resolved = await pick() + if (!resolved) return + return path.join(binDir, resolved) +} From 47577ae8574dd64b3b0773ce9239d6d103fea8d3 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 22:14:59 -0400 Subject: [PATCH 187/300] feat: unwrap upermission namespace to flat exports + barrel (#22710) --- packages/opencode/src/permission/index.ts | 326 +----------------- .../opencode/src/permission/permission.ts | 323 +++++++++++++++++ 2 files changed, 324 insertions(+), 325 deletions(-) create mode 100644 packages/opencode/src/permission/permission.ts diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts index 0100485492..7d8a2fff82 100644 --- a/packages/opencode/src/permission/index.ts +++ b/packages/opencode/src/permission/index.ts @@ -1,325 +1 @@ -import { Bus } from "@/bus" -import { BusEvent } from "@/bus/bus-event" -import { Config } from "@/config" -import { InstanceState } from "@/effect/instance-state" -import { ProjectID } from "@/project/schema" -import { MessageID, SessionID } from "@/session/schema" -import { PermissionTable } from "@/session/session.sql" -import { Database, eq } from "@/storage/db" -import { zod } from "@/util/effect-zod" -import { Log } from "@/util/log" -import { withStatics } from "@/util/schema" -import { Wildcard } from "@/util/wildcard" -import { Deferred, Effect, Layer, Schema, Context } from "effect" -import os from "os" -import { evaluate as evalRule } from "./evaluate" -import { PermissionID } from "./schema" - -export namespace Permission { - const log = Log.create({ service: "permission" }) - - export const Action = Schema.Literals(["allow", "deny", "ask"]) - .annotate({ identifier: "PermissionAction" }) - .pipe(withStatics((s) => ({ zod: zod(s) }))) - export type Action = Schema.Schema.Type - - export class Rule extends Schema.Class("PermissionRule")({ - permission: Schema.String, - pattern: Schema.String, - action: Action, - }) { - static readonly zod = zod(this) - } - - export const Ruleset = Schema.mutable(Schema.Array(Rule)) - .annotate({ identifier: "PermissionRuleset" }) - .pipe(withStatics((s) => ({ zod: zod(s) }))) - export type Ruleset = Schema.Schema.Type - - export class Request extends Schema.Class("PermissionRequest")({ - id: PermissionID, - sessionID: SessionID, - permission: Schema.String, - patterns: Schema.Array(Schema.String), - metadata: Schema.Record(Schema.String, Schema.Unknown), - always: Schema.Array(Schema.String), - tool: Schema.optional( - Schema.Struct({ - messageID: MessageID, - callID: Schema.String, - }), - ), - }) { - static readonly zod = zod(this) - } - - export const Reply = Schema.Literals(["once", "always", "reject"]).pipe(withStatics((s) => ({ zod: zod(s) }))) - export type Reply = Schema.Schema.Type - - const reply = { - reply: Reply, - message: Schema.optional(Schema.String), - } - - export const ReplyBody = Schema.Struct(reply) - .annotate({ identifier: "PermissionReplyBody" }) - .pipe(withStatics((s) => ({ zod: zod(s) }))) - export type ReplyBody = Schema.Schema.Type - - export class Approval extends Schema.Class("PermissionApproval")({ - projectID: ProjectID, - patterns: Schema.Array(Schema.String), - }) { - static readonly zod = zod(this) - } - - export const Event = { - Asked: BusEvent.define("permission.asked", Request.zod), - Replied: BusEvent.define( - "permission.replied", - zod( - Schema.Struct({ - sessionID: SessionID, - requestID: PermissionID, - reply: Reply, - }), - ), - ), - } - - export class RejectedError extends Schema.TaggedErrorClass()("PermissionRejectedError", {}) { - override get message() { - return "The user rejected permission to use this specific tool call." - } - } - - export class CorrectedError extends Schema.TaggedErrorClass()("PermissionCorrectedError", { - feedback: Schema.String, - }) { - override get message() { - return `The user rejected permission to use this specific tool call with the following feedback: ${this.feedback}` - } - } - - export class DeniedError extends Schema.TaggedErrorClass()("PermissionDeniedError", { - ruleset: Schema.Any, - }) { - override get message() { - return `The user has specified a rule which prevents you from using this specific tool call. Here are some of the relevant rules ${JSON.stringify(this.ruleset)}` - } - } - - export type Error = DeniedError | RejectedError | CorrectedError - - export const AskInput = Schema.Struct({ - ...Request.fields, - id: Schema.optional(PermissionID), - ruleset: Ruleset, - }) - .annotate({ identifier: "PermissionAskInput" }) - .pipe(withStatics((s) => ({ zod: zod(s) }))) - export type AskInput = Schema.Schema.Type - - export const ReplyInput = Schema.Struct({ - requestID: PermissionID, - ...reply, - }) - .annotate({ identifier: "PermissionReplyInput" }) - .pipe(withStatics((s) => ({ zod: zod(s) }))) - export type ReplyInput = Schema.Schema.Type - - export interface Interface { - readonly ask: (input: AskInput) => Effect.Effect - readonly reply: (input: ReplyInput) => Effect.Effect - readonly list: () => Effect.Effect> - } - - interface PendingEntry { - info: Request - deferred: Deferred.Deferred - } - - interface State { - pending: Map - approved: Ruleset - } - - export function evaluate(permission: string, pattern: string, ...rulesets: Ruleset[]): Rule { - log.info("evaluate", { permission, pattern, ruleset: rulesets.flat() }) - return evalRule(permission, pattern, ...rulesets) - } - - export class Service extends Context.Service()("@opencode/Permission") {} - - export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const bus = yield* Bus.Service - const state = yield* InstanceState.make( - Effect.fn("Permission.state")(function* (ctx) { - const row = Database.use((db) => - db.select().from(PermissionTable).where(eq(PermissionTable.project_id, ctx.project.id)).get(), - ) - const state = { - pending: new Map(), - approved: row?.data ?? [], - } - - yield* Effect.addFinalizer(() => - Effect.gen(function* () { - for (const item of state.pending.values()) { - yield* Deferred.fail(item.deferred, new RejectedError()) - } - state.pending.clear() - }), - ) - - return state - }), - ) - - const ask = Effect.fn("Permission.ask")(function* (input: AskInput) { - const { approved, pending } = yield* InstanceState.get(state) - const { ruleset, ...request } = input - let needsAsk = false - - for (const pattern of request.patterns) { - const rule = evaluate(request.permission, pattern, ruleset, approved) - log.info("evaluated", { permission: request.permission, pattern, action: rule }) - if (rule.action === "deny") { - return yield* new DeniedError({ - ruleset: ruleset.filter((rule) => Wildcard.match(request.permission, rule.permission)), - }) - } - if (rule.action === "allow") continue - needsAsk = true - } - - if (!needsAsk) return - - const id = request.id ?? PermissionID.ascending() - const info = Schema.decodeUnknownSync(Request)({ - id, - ...request, - }) - log.info("asking", { id, permission: info.permission, patterns: info.patterns }) - - const deferred = yield* Deferred.make() - pending.set(id, { info, deferred }) - yield* bus.publish(Event.Asked, info) - return yield* Effect.ensuring( - Deferred.await(deferred), - Effect.sync(() => { - pending.delete(id) - }), - ) - }) - - const reply = Effect.fn("Permission.reply")(function* (input: ReplyInput) { - const { approved, pending } = yield* InstanceState.get(state) - const existing = pending.get(input.requestID) - if (!existing) return - - pending.delete(input.requestID) - yield* bus.publish(Event.Replied, { - sessionID: existing.info.sessionID, - requestID: existing.info.id, - reply: input.reply, - }) - - if (input.reply === "reject") { - yield* Deferred.fail( - existing.deferred, - input.message ? new CorrectedError({ feedback: input.message }) : new RejectedError(), - ) - - for (const [id, item] of pending.entries()) { - if (item.info.sessionID !== existing.info.sessionID) continue - pending.delete(id) - yield* bus.publish(Event.Replied, { - sessionID: item.info.sessionID, - requestID: item.info.id, - reply: "reject", - }) - yield* Deferred.fail(item.deferred, new RejectedError()) - } - return - } - - yield* Deferred.succeed(existing.deferred, undefined) - if (input.reply === "once") return - - for (const pattern of existing.info.always) { - approved.push({ - permission: existing.info.permission, - pattern, - action: "allow", - }) - } - - for (const [id, item] of pending.entries()) { - if (item.info.sessionID !== existing.info.sessionID) continue - const ok = item.info.patterns.every( - (pattern) => evaluate(item.info.permission, pattern, approved).action === "allow", - ) - if (!ok) continue - pending.delete(id) - yield* bus.publish(Event.Replied, { - sessionID: item.info.sessionID, - requestID: item.info.id, - reply: "always", - }) - yield* Deferred.succeed(item.deferred, undefined) - } - }) - - const list = Effect.fn("Permission.list")(function* () { - const pending = (yield* InstanceState.get(state)).pending - return Array.from(pending.values(), (item) => item.info) - }) - - return Service.of({ ask, reply, list }) - }), - ) - - function expand(pattern: string): string { - if (pattern.startsWith("~/")) return os.homedir() + pattern.slice(1) - if (pattern === "~") return os.homedir() - if (pattern.startsWith("$HOME/")) return os.homedir() + pattern.slice(5) - if (pattern.startsWith("$HOME")) return os.homedir() + pattern.slice(5) - return pattern - } - - export function fromConfig(permission: Config.Permission) { - const ruleset: Ruleset = [] - for (const [key, value] of Object.entries(permission)) { - if (typeof value === "string") { - ruleset.push({ permission: key, action: value, pattern: "*" }) - continue - } - ruleset.push( - ...Object.entries(value).map(([pattern, action]) => ({ permission: key, pattern: expand(pattern), action })), - ) - } - return ruleset - } - - export function merge(...rulesets: Ruleset[]): Ruleset { - return rulesets.flat() - } - - const EDIT_TOOLS = ["edit", "write", "apply_patch", "multiedit"] - - export function disabled(tools: string[], ruleset: Ruleset): Set { - const result = new Set() - for (const tool of tools) { - const permission = EDIT_TOOLS.includes(tool) ? "edit" : tool - const rule = ruleset.findLast((rule) => Wildcard.match(permission, rule.permission)) - if (!rule) continue - if (rule.pattern === "*" && rule.action === "deny") result.add(tool) - } - return result - } - - export const defaultLayer = layer.pipe(Layer.provide(Bus.layer)) -} +export * as Permission from "./permission" diff --git a/packages/opencode/src/permission/permission.ts b/packages/opencode/src/permission/permission.ts new file mode 100644 index 0000000000..a5f6ded329 --- /dev/null +++ b/packages/opencode/src/permission/permission.ts @@ -0,0 +1,323 @@ +import { Bus } from "@/bus" +import { BusEvent } from "@/bus/bus-event" +import { Config } from "@/config" +import { InstanceState } from "@/effect/instance-state" +import { ProjectID } from "@/project/schema" +import { MessageID, SessionID } from "@/session/schema" +import { PermissionTable } from "@/session/session.sql" +import { Database, eq } from "@/storage/db" +import { zod } from "@/util/effect-zod" +import { Log } from "@/util/log" +import { withStatics } from "@/util/schema" +import { Wildcard } from "@/util/wildcard" +import { Deferred, Effect, Layer, Schema, Context } from "effect" +import os from "os" +import { evaluate as evalRule } from "./evaluate" +import { PermissionID } from "./schema" + +const log = Log.create({ service: "permission" }) + +export const Action = Schema.Literals(["allow", "deny", "ask"]) + .annotate({ identifier: "PermissionAction" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) +export type Action = Schema.Schema.Type + +export class Rule extends Schema.Class("PermissionRule")({ + permission: Schema.String, + pattern: Schema.String, + action: Action, +}) { + static readonly zod = zod(this) +} + +export const Ruleset = Schema.mutable(Schema.Array(Rule)) + .annotate({ identifier: "PermissionRuleset" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) +export type Ruleset = Schema.Schema.Type + +export class Request extends Schema.Class("PermissionRequest")({ + id: PermissionID, + sessionID: SessionID, + permission: Schema.String, + patterns: Schema.Array(Schema.String), + metadata: Schema.Record(Schema.String, Schema.Unknown), + always: Schema.Array(Schema.String), + tool: Schema.optional( + Schema.Struct({ + messageID: MessageID, + callID: Schema.String, + }), + ), +}) { + static readonly zod = zod(this) +} + +export const Reply = Schema.Literals(["once", "always", "reject"]).pipe(withStatics((s) => ({ zod: zod(s) }))) +export type Reply = Schema.Schema.Type + +const reply = { + reply: Reply, + message: Schema.optional(Schema.String), +} + +export const ReplyBody = Schema.Struct(reply) + .annotate({ identifier: "PermissionReplyBody" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) +export type ReplyBody = Schema.Schema.Type + +export class Approval extends Schema.Class("PermissionApproval")({ + projectID: ProjectID, + patterns: Schema.Array(Schema.String), +}) { + static readonly zod = zod(this) +} + +export const Event = { + Asked: BusEvent.define("permission.asked", Request.zod), + Replied: BusEvent.define( + "permission.replied", + zod( + Schema.Struct({ + sessionID: SessionID, + requestID: PermissionID, + reply: Reply, + }), + ), + ), +} + +export class RejectedError extends Schema.TaggedErrorClass()("PermissionRejectedError", {}) { + override get message() { + return "The user rejected permission to use this specific tool call." + } +} + +export class CorrectedError extends Schema.TaggedErrorClass()("PermissionCorrectedError", { + feedback: Schema.String, +}) { + override get message() { + return `The user rejected permission to use this specific tool call with the following feedback: ${this.feedback}` + } +} + +export class DeniedError extends Schema.TaggedErrorClass()("PermissionDeniedError", { + ruleset: Schema.Any, +}) { + override get message() { + return `The user has specified a rule which prevents you from using this specific tool call. Here are some of the relevant rules ${JSON.stringify(this.ruleset)}` + } +} + +export type Error = DeniedError | RejectedError | CorrectedError + +export const AskInput = Schema.Struct({ + ...Request.fields, + id: Schema.optional(PermissionID), + ruleset: Ruleset, +}) + .annotate({ identifier: "PermissionAskInput" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) +export type AskInput = Schema.Schema.Type + +export const ReplyInput = Schema.Struct({ + requestID: PermissionID, + ...reply, +}) + .annotate({ identifier: "PermissionReplyInput" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) +export type ReplyInput = Schema.Schema.Type + +export interface Interface { + readonly ask: (input: AskInput) => Effect.Effect + readonly reply: (input: ReplyInput) => Effect.Effect + readonly list: () => Effect.Effect> +} + +interface PendingEntry { + info: Request + deferred: Deferred.Deferred +} + +interface State { + pending: Map + approved: Ruleset +} + +export function evaluate(permission: string, pattern: string, ...rulesets: Ruleset[]): Rule { + log.info("evaluate", { permission, pattern, ruleset: rulesets.flat() }) + return evalRule(permission, pattern, ...rulesets) +} + +export class Service extends Context.Service()("@opencode/Permission") {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const bus = yield* Bus.Service + const state = yield* InstanceState.make( + Effect.fn("Permission.state")(function* (ctx) { + const row = Database.use((db) => + db.select().from(PermissionTable).where(eq(PermissionTable.project_id, ctx.project.id)).get(), + ) + const state = { + pending: new Map(), + approved: row?.data ?? [], + } + + yield* Effect.addFinalizer(() => + Effect.gen(function* () { + for (const item of state.pending.values()) { + yield* Deferred.fail(item.deferred, new RejectedError()) + } + state.pending.clear() + }), + ) + + return state + }), + ) + + const ask = Effect.fn("Permission.ask")(function* (input: AskInput) { + const { approved, pending } = yield* InstanceState.get(state) + const { ruleset, ...request } = input + let needsAsk = false + + for (const pattern of request.patterns) { + const rule = evaluate(request.permission, pattern, ruleset, approved) + log.info("evaluated", { permission: request.permission, pattern, action: rule }) + if (rule.action === "deny") { + return yield* new DeniedError({ + ruleset: ruleset.filter((rule) => Wildcard.match(request.permission, rule.permission)), + }) + } + if (rule.action === "allow") continue + needsAsk = true + } + + if (!needsAsk) return + + const id = request.id ?? PermissionID.ascending() + const info = Schema.decodeUnknownSync(Request)({ + id, + ...request, + }) + log.info("asking", { id, permission: info.permission, patterns: info.patterns }) + + const deferred = yield* Deferred.make() + pending.set(id, { info, deferred }) + yield* bus.publish(Event.Asked, info) + return yield* Effect.ensuring( + Deferred.await(deferred), + Effect.sync(() => { + pending.delete(id) + }), + ) + }) + + const reply = Effect.fn("Permission.reply")(function* (input: ReplyInput) { + const { approved, pending } = yield* InstanceState.get(state) + const existing = pending.get(input.requestID) + if (!existing) return + + pending.delete(input.requestID) + yield* bus.publish(Event.Replied, { + sessionID: existing.info.sessionID, + requestID: existing.info.id, + reply: input.reply, + }) + + if (input.reply === "reject") { + yield* Deferred.fail( + existing.deferred, + input.message ? new CorrectedError({ feedback: input.message }) : new RejectedError(), + ) + + for (const [id, item] of pending.entries()) { + if (item.info.sessionID !== existing.info.sessionID) continue + pending.delete(id) + yield* bus.publish(Event.Replied, { + sessionID: item.info.sessionID, + requestID: item.info.id, + reply: "reject", + }) + yield* Deferred.fail(item.deferred, new RejectedError()) + } + return + } + + yield* Deferred.succeed(existing.deferred, undefined) + if (input.reply === "once") return + + for (const pattern of existing.info.always) { + approved.push({ + permission: existing.info.permission, + pattern, + action: "allow", + }) + } + + for (const [id, item] of pending.entries()) { + if (item.info.sessionID !== existing.info.sessionID) continue + const ok = item.info.patterns.every( + (pattern) => evaluate(item.info.permission, pattern, approved).action === "allow", + ) + if (!ok) continue + pending.delete(id) + yield* bus.publish(Event.Replied, { + sessionID: item.info.sessionID, + requestID: item.info.id, + reply: "always", + }) + yield* Deferred.succeed(item.deferred, undefined) + } + }) + + const list = Effect.fn("Permission.list")(function* () { + const pending = (yield* InstanceState.get(state)).pending + return Array.from(pending.values(), (item) => item.info) + }) + + return Service.of({ ask, reply, list }) + }), +) + +function expand(pattern: string): string { + if (pattern.startsWith("~/")) return os.homedir() + pattern.slice(1) + if (pattern === "~") return os.homedir() + if (pattern.startsWith("$HOME/")) return os.homedir() + pattern.slice(5) + if (pattern.startsWith("$HOME")) return os.homedir() + pattern.slice(5) + return pattern +} + +export function fromConfig(permission: Config.Permission) { + const ruleset: Ruleset = [] + for (const [key, value] of Object.entries(permission)) { + if (typeof value === "string") { + ruleset.push({ permission: key, action: value, pattern: "*" }) + continue + } + ruleset.push( + ...Object.entries(value).map(([pattern, action]) => ({ permission: key, pattern: expand(pattern), action })), + ) + } + return ruleset +} + +export function merge(...rulesets: Ruleset[]): Ruleset { + return rulesets.flat() +} + +const EDIT_TOOLS = ["edit", "write", "apply_patch", "multiedit"] + +export function disabled(tools: string[], ruleset: Ruleset): Set { + const result = new Set() + for (const tool of tools) { + const permission = EDIT_TOOLS.includes(tool) ? "edit" : tool + const rule = ruleset.findLast((rule) => Wildcard.match(permission, rule.permission)) + if (!rule) continue + if (rule.pattern === "*" && rule.action === "deny") result.add(tool) + } + return result +} + +export const defaultLayer = layer.pipe(Layer.provide(Bus.layer)) From 18538e359b22ff52231766b6880d70a9bdf9a063 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 22:15:17 -0400 Subject: [PATCH 188/300] feat: unwrap usession namespace to flat exports + barrel (#22713) --- packages/opencode/src/session/index.ts | 819 +------------------- packages/opencode/src/session/projectors.ts | 2 +- packages/opencode/src/session/session.ts | 816 +++++++++++++++++++ 3 files changed, 818 insertions(+), 819 deletions(-) create mode 100644 packages/opencode/src/session/session.ts diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 585b9a135d..1b79fd01a4 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -1,818 +1 @@ -import { Slug } from "@opencode-ai/shared/util/slug" -import path from "path" -import { BusEvent } from "@/bus/bus-event" -import { Bus } from "@/bus" -import { Decimal } from "decimal.js" -import z from "zod" -import { type ProviderMetadata, type LanguageModelUsage } from "ai" -import { Flag } from "../flag/flag" -import { Installation } from "../installation" - -import { Database, NotFoundError, eq, and, gte, isNull, desc, like, inArray, lt } from "../storage/db" -import { SyncEvent } from "../sync" -import type { SQL } from "../storage/db" -import { PartTable, SessionTable } from "./session.sql" -import { ProjectTable } from "../project/project.sql" -import { Storage } from "@/storage/storage" -import { Log } from "../util/log" -import { updateSchema } from "../util/update-schema" -import { MessageV2 } from "./message-v2" -import { Instance } from "../project/instance" -import { InstanceState } from "@/effect/instance-state" -import { Snapshot } from "@/snapshot" -import { ProjectID } from "../project/schema" -import { WorkspaceID } from "../control-plane/schema" -import { SessionID, MessageID, PartID } from "./schema" - -import type { Provider } from "@/provider" -import { Permission } from "@/permission" -import { Global } from "@/global" -import { Effect, Layer, Option, Context } from "effect" - -export namespace Session { - const log = Log.create({ service: "session" }) - - const parentTitlePrefix = "New session - " - const childTitlePrefix = "Child session - " - - function createDefaultTitle(isChild = false) { - return (isChild ? childTitlePrefix : parentTitlePrefix) + new Date().toISOString() - } - - export function isDefaultTitle(title: string) { - return new RegExp( - `^(${parentTitlePrefix}|${childTitlePrefix})\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$`, - ).test(title) - } - - type SessionRow = typeof SessionTable.$inferSelect - - export function fromRow(row: SessionRow): Info { - const summary = - row.summary_additions !== null || row.summary_deletions !== null || row.summary_files !== null - ? { - additions: row.summary_additions ?? 0, - deletions: row.summary_deletions ?? 0, - files: row.summary_files ?? 0, - diffs: row.summary_diffs ?? undefined, - } - : undefined - const share = row.share_url ? { url: row.share_url } : undefined - const revert = row.revert ?? undefined - return { - id: row.id, - slug: row.slug, - projectID: row.project_id, - workspaceID: row.workspace_id ?? undefined, - directory: row.directory, - parentID: row.parent_id ?? undefined, - title: row.title, - version: row.version, - summary, - share, - revert, - permission: row.permission ?? undefined, - time: { - created: row.time_created, - updated: row.time_updated, - compacting: row.time_compacting ?? undefined, - archived: row.time_archived ?? undefined, - }, - } - } - - export function toRow(info: Info) { - return { - id: info.id, - project_id: info.projectID, - workspace_id: info.workspaceID, - parent_id: info.parentID, - slug: info.slug, - directory: info.directory, - title: info.title, - version: info.version, - share_url: info.share?.url, - summary_additions: info.summary?.additions, - summary_deletions: info.summary?.deletions, - summary_files: info.summary?.files, - summary_diffs: info.summary?.diffs, - revert: info.revert ?? null, - permission: info.permission, - time_created: info.time.created, - time_updated: info.time.updated, - time_compacting: info.time.compacting, - time_archived: info.time.archived, - } - } - - function getForkedTitle(title: string): string { - const match = title.match(/^(.+) \(fork #(\d+)\)$/) - if (match) { - const base = match[1] - const num = parseInt(match[2], 10) - return `${base} (fork #${num + 1})` - } - return `${title} (fork #1)` - } - - export const Info = z - .object({ - id: SessionID.zod, - slug: z.string(), - projectID: ProjectID.zod, - workspaceID: WorkspaceID.zod.optional(), - directory: z.string(), - parentID: SessionID.zod.optional(), - summary: z - .object({ - additions: z.number(), - deletions: z.number(), - files: z.number(), - diffs: Snapshot.FileDiff.array().optional(), - }) - .optional(), - share: z - .object({ - url: z.string(), - }) - .optional(), - title: z.string(), - version: z.string(), - time: z.object({ - created: z.number(), - updated: z.number(), - compacting: z.number().optional(), - archived: z.number().optional(), - }), - permission: Permission.Ruleset.zod.optional(), - revert: z - .object({ - messageID: MessageID.zod, - partID: PartID.zod.optional(), - snapshot: z.string().optional(), - diff: z.string().optional(), - }) - .optional(), - }) - .meta({ - ref: "Session", - }) - export type Info = z.output - - export const ProjectInfo = z - .object({ - id: ProjectID.zod, - name: z.string().optional(), - worktree: z.string(), - }) - .meta({ - ref: "ProjectSummary", - }) - export type ProjectInfo = z.output - - export const GlobalInfo = Info.extend({ - project: ProjectInfo.nullable(), - }).meta({ - ref: "GlobalSession", - }) - export type GlobalInfo = z.output - - export const CreateInput = z - .object({ - parentID: SessionID.zod.optional(), - title: z.string().optional(), - permission: Info.shape.permission, - workspaceID: WorkspaceID.zod.optional(), - }) - .optional() - export type CreateInput = z.output - - export const ForkInput = z.object({ sessionID: SessionID.zod, messageID: MessageID.zod.optional() }) - export const GetInput = SessionID.zod - export const ChildrenInput = SessionID.zod - export const RemoveInput = SessionID.zod - export const SetTitleInput = z.object({ sessionID: SessionID.zod, title: z.string() }) - export const SetArchivedInput = z.object({ sessionID: SessionID.zod, time: z.number().optional() }) - export const SetPermissionInput = z.object({ sessionID: SessionID.zod, permission: Permission.Ruleset.zod }) - export const SetRevertInput = z.object({ - sessionID: SessionID.zod, - revert: Info.shape.revert, - summary: Info.shape.summary, - }) - export const MessagesInput = z.object({ sessionID: SessionID.zod, limit: z.number().optional() }) - - export const Event = { - Created: SyncEvent.define({ - type: "session.created", - version: 1, - aggregate: "sessionID", - schema: z.object({ - sessionID: SessionID.zod, - info: Info, - }), - }), - Updated: SyncEvent.define({ - type: "session.updated", - version: 1, - aggregate: "sessionID", - schema: z.object({ - sessionID: SessionID.zod, - info: updateSchema(Info).extend({ - share: updateSchema(Info.shape.share.unwrap()).optional(), - time: updateSchema(Info.shape.time).optional(), - }), - }), - busSchema: z.object({ - sessionID: SessionID.zod, - info: Info, - }), - }), - Deleted: SyncEvent.define({ - type: "session.deleted", - version: 1, - aggregate: "sessionID", - schema: z.object({ - sessionID: SessionID.zod, - info: Info, - }), - }), - Diff: BusEvent.define( - "session.diff", - z.object({ - sessionID: SessionID.zod, - diff: Snapshot.FileDiff.array(), - }), - ), - Error: BusEvent.define( - "session.error", - z.object({ - sessionID: SessionID.zod.optional(), - error: MessageV2.Assistant.shape.error, - }), - ), - } - - export function plan(input: { slug: string; time: { created: number } }) { - const base = Instance.project.vcs - ? path.join(Instance.worktree, ".opencode", "plans") - : path.join(Global.Path.data, "plans") - return path.join(base, [input.time.created, input.slug].join("-") + ".md") - } - - export const getUsage = (input: { - model: Provider.Model - usage: LanguageModelUsage - metadata?: ProviderMetadata - }) => { - const safe = (value: number) => { - if (!Number.isFinite(value)) return 0 - return value - } - const inputTokens = safe(input.usage.inputTokens ?? 0) - const outputTokens = safe(input.usage.outputTokens ?? 0) - const reasoningTokens = safe(input.usage.outputTokenDetails?.reasoningTokens ?? input.usage.reasoningTokens ?? 0) - - const cacheReadInputTokens = safe( - input.usage.inputTokenDetails?.cacheReadTokens ?? input.usage.cachedInputTokens ?? 0, - ) - const cacheWriteInputTokens = safe( - (input.usage.inputTokenDetails?.cacheWriteTokens ?? - input.metadata?.["anthropic"]?.["cacheCreationInputTokens"] ?? - // google-vertex-anthropic returns metadata under "vertex" key - // (AnthropicMessagesLanguageModel custom provider key from 'vertex.anthropic.messages') - input.metadata?.["vertex"]?.["cacheCreationInputTokens"] ?? - // @ts-expect-error - input.metadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ?? - // @ts-expect-error - input.metadata?.["venice"]?.["usage"]?.["cacheCreationInputTokens"] ?? - 0) as number, - ) - - // AI SDK v6 normalized inputTokens to include cached tokens across all providers - // (including Anthropic/Bedrock which previously excluded them). Always subtract cache - // tokens to get the non-cached input count for separate cost calculation. - const adjustedInputTokens = safe(inputTokens - cacheReadInputTokens - cacheWriteInputTokens) - - const total = input.usage.totalTokens - - const tokens = { - total, - input: adjustedInputTokens, - output: safe(outputTokens - reasoningTokens), - reasoning: reasoningTokens, - cache: { - write: cacheWriteInputTokens, - read: cacheReadInputTokens, - }, - } - - const costInfo = - input.model.cost?.experimentalOver200K && tokens.input + tokens.cache.read > 200_000 - ? input.model.cost.experimentalOver200K - : input.model.cost - return { - cost: safe( - new Decimal(0) - .add(new Decimal(tokens.input).mul(costInfo?.input ?? 0).div(1_000_000)) - .add(new Decimal(tokens.output).mul(costInfo?.output ?? 0).div(1_000_000)) - .add(new Decimal(tokens.cache.read).mul(costInfo?.cache?.read ?? 0).div(1_000_000)) - .add(new Decimal(tokens.cache.write).mul(costInfo?.cache?.write ?? 0).div(1_000_000)) - // TODO: update models.dev to have better pricing model, for now: - // charge reasoning tokens at the same rate as output tokens - .add(new Decimal(tokens.reasoning).mul(costInfo?.output ?? 0).div(1_000_000)) - .toNumber(), - ), - tokens, - } - } - - export class BusyError extends Error { - constructor(public readonly sessionID: string) { - super(`Session ${sessionID} is busy`) - } - } - - export interface Interface { - readonly create: (input?: { - parentID?: SessionID - title?: string - permission?: Permission.Ruleset - workspaceID?: WorkspaceID - }) => Effect.Effect - readonly fork: (input: { sessionID: SessionID; messageID?: MessageID }) => Effect.Effect - readonly touch: (sessionID: SessionID) => Effect.Effect - readonly get: (id: SessionID) => Effect.Effect - readonly setTitle: (input: { sessionID: SessionID; title: string }) => Effect.Effect - readonly setArchived: (input: { sessionID: SessionID; time?: number }) => Effect.Effect - readonly setPermission: (input: { sessionID: SessionID; permission: Permission.Ruleset }) => Effect.Effect - readonly setRevert: (input: { - sessionID: SessionID - revert: Info["revert"] - summary: Info["summary"] - }) => Effect.Effect - readonly clearRevert: (sessionID: SessionID) => Effect.Effect - readonly setSummary: (input: { sessionID: SessionID; summary: Info["summary"] }) => Effect.Effect - readonly diff: (sessionID: SessionID) => Effect.Effect - readonly messages: (input: { sessionID: SessionID; limit?: number }) => Effect.Effect - readonly children: (parentID: SessionID) => Effect.Effect - readonly remove: (sessionID: SessionID) => Effect.Effect - readonly updateMessage: (msg: T) => Effect.Effect - readonly removeMessage: (input: { sessionID: SessionID; messageID: MessageID }) => Effect.Effect - readonly removePart: (input: { - sessionID: SessionID - messageID: MessageID - partID: PartID - }) => Effect.Effect - readonly getPart: (input: { - sessionID: SessionID - messageID: MessageID - partID: PartID - }) => Effect.Effect - readonly updatePart: (part: T) => Effect.Effect - readonly updatePartDelta: (input: { - sessionID: SessionID - messageID: MessageID - partID: PartID - field: string - delta: string - }) => Effect.Effect - /** Finds the first message matching the predicate, searching newest-first. */ - readonly findMessage: ( - sessionID: SessionID, - predicate: (msg: MessageV2.WithParts) => boolean, - ) => Effect.Effect> - } - - export class Service extends Context.Service()("@opencode/Session") {} - - type Patch = z.infer["info"] - - const db = (fn: (d: Parameters[0] extends (trx: infer D) => any ? D : never) => T) => - Effect.sync(() => Database.use(fn)) - - export const layer: Layer.Layer = Layer.effect( - Service, - Effect.gen(function* () { - const bus = yield* Bus.Service - const storage = yield* Storage.Service - - const createNext = Effect.fn("Session.createNext")(function* (input: { - id?: SessionID - title?: string - parentID?: SessionID - workspaceID?: WorkspaceID - directory: string - permission?: Permission.Ruleset - }) { - const ctx = yield* InstanceState.context - const result: Info = { - id: SessionID.descending(input.id), - slug: Slug.create(), - version: Installation.VERSION, - projectID: ctx.project.id, - directory: input.directory, - workspaceID: input.workspaceID, - parentID: input.parentID, - title: input.title ?? createDefaultTitle(!!input.parentID), - permission: input.permission, - time: { - created: Date.now(), - updated: Date.now(), - }, - } - log.info("created", result) - - yield* Effect.sync(() => SyncEvent.run(Event.Created, { sessionID: result.id, info: result })) - - if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) { - // This only exist for backwards compatibility. We should not be - // manually publishing this event; it is a sync event now - yield* bus.publish(Event.Updated, { - sessionID: result.id, - info: result, - }) - } - - return result - }) - - const get = Effect.fn("Session.get")(function* (id: SessionID) { - const row = yield* db((d) => d.select().from(SessionTable).where(eq(SessionTable.id, id)).get()) - if (!row) throw new NotFoundError({ message: `Session not found: ${id}` }) - return fromRow(row) - }) - - const children = Effect.fn("Session.children")(function* (parentID: SessionID) { - const rows = yield* db((d) => - d - .select() - .from(SessionTable) - .where(and(eq(SessionTable.parent_id, parentID))) - .all(), - ) - return rows.map(fromRow) - }) - - const remove: Interface["remove"] = Effect.fnUntraced(function* (sessionID: SessionID) { - try { - const session = yield* get(sessionID) - const kids = yield* children(sessionID) - for (const child of kids) { - yield* remove(child.id) - } - - // `remove` needs to work in all cases, such as a broken - // sessions that run cleanup. In certain cases these will - // run without any instance state, so we need to turn off - // publishing of events in that case - const hasInstance = yield* InstanceState.directory.pipe( - Effect.as(true), - Effect.catchCause(() => Effect.succeed(false)), - ) - - yield* Effect.sync(() => { - SyncEvent.run(Event.Deleted, { sessionID, info: session }, { publish: hasInstance }) - SyncEvent.remove(sessionID) - }) - } catch (e) { - log.error(e) - } - }) - - const updateMessage = (msg: T): Effect.Effect => - Effect.gen(function* () { - yield* Effect.sync(() => SyncEvent.run(MessageV2.Event.Updated, { sessionID: msg.sessionID, info: msg })) - return msg - }).pipe(Effect.withSpan("Session.updateMessage")) - - const updatePart = (part: T): Effect.Effect => - Effect.gen(function* () { - yield* Effect.sync(() => - SyncEvent.run(MessageV2.Event.PartUpdated, { - sessionID: part.sessionID, - part: structuredClone(part), - time: Date.now(), - }), - ) - return part - }).pipe(Effect.withSpan("Session.updatePart")) - - const getPart: Interface["getPart"] = Effect.fn("Session.getPart")(function* (input) { - const row = Database.use((db) => - db - .select() - .from(PartTable) - .where( - and( - eq(PartTable.session_id, input.sessionID), - eq(PartTable.message_id, input.messageID), - eq(PartTable.id, input.partID), - ), - ) - .get(), - ) - if (!row) return - return { - ...row.data, - id: row.id, - sessionID: row.session_id, - messageID: row.message_id, - } as MessageV2.Part - }) - - const create = Effect.fn("Session.create")(function* (input?: { - parentID?: SessionID - title?: string - permission?: Permission.Ruleset - workspaceID?: WorkspaceID - }) { - const directory = yield* InstanceState.directory - return yield* createNext({ - parentID: input?.parentID, - directory, - title: input?.title, - permission: input?.permission, - workspaceID: input?.workspaceID, - }) - }) - - const fork = Effect.fn("Session.fork")(function* (input: { sessionID: SessionID; messageID?: MessageID }) { - const directory = yield* InstanceState.directory - const original = yield* get(input.sessionID) - const title = getForkedTitle(original.title) - const session = yield* createNext({ - directory, - workspaceID: original.workspaceID, - title, - }) - const msgs = yield* messages({ sessionID: input.sessionID }) - const idMap = new Map() - - for (const msg of msgs) { - if (input.messageID && msg.info.id >= input.messageID) break - const newID = MessageID.ascending() - idMap.set(msg.info.id, newID) - - const parentID = msg.info.role === "assistant" && msg.info.parentID ? idMap.get(msg.info.parentID) : undefined - const cloned = yield* updateMessage({ - ...msg.info, - sessionID: session.id, - id: newID, - ...(parentID && { parentID }), - }) - - for (const part of msg.parts) { - yield* updatePart({ - ...part, - id: PartID.ascending(), - messageID: cloned.id, - sessionID: session.id, - }) - } - } - return session - }) - - const patch = (sessionID: SessionID, info: Patch) => - Effect.sync(() => SyncEvent.run(Event.Updated, { sessionID, info })) - - const touch = Effect.fn("Session.touch")(function* (sessionID: SessionID) { - yield* patch(sessionID, { time: { updated: Date.now() } }) - }) - - const setTitle = Effect.fn("Session.setTitle")(function* (input: { sessionID: SessionID; title: string }) { - yield* patch(input.sessionID, { title: input.title }) - }) - - const setArchived = Effect.fn("Session.setArchived")(function* (input: { sessionID: SessionID; time?: number }) { - yield* patch(input.sessionID, { time: { archived: input.time } }) - }) - - const setPermission = Effect.fn("Session.setPermission")(function* (input: { - sessionID: SessionID - permission: Permission.Ruleset - }) { - yield* patch(input.sessionID, { permission: input.permission, time: { updated: Date.now() } }) - }) - - const setRevert = Effect.fn("Session.setRevert")(function* (input: { - sessionID: SessionID - revert: Info["revert"] - summary: Info["summary"] - }) { - yield* patch(input.sessionID, { summary: input.summary, time: { updated: Date.now() }, revert: input.revert }) - }) - - const clearRevert = Effect.fn("Session.clearRevert")(function* (sessionID: SessionID) { - yield* patch(sessionID, { time: { updated: Date.now() }, revert: null }) - }) - - const setSummary = Effect.fn("Session.setSummary")(function* (input: { - sessionID: SessionID - summary: Info["summary"] - }) { - yield* patch(input.sessionID, { time: { updated: Date.now() }, summary: input.summary }) - }) - - const diff = Effect.fn("Session.diff")(function* (sessionID: SessionID) { - return yield* storage - .read(["session_diff", sessionID]) - .pipe(Effect.orElseSucceed((): Snapshot.FileDiff[] => [])) - }) - - const messages = Effect.fn("Session.messages")(function* (input: { sessionID: SessionID; limit?: number }) { - if (input.limit) { - return MessageV2.page({ sessionID: input.sessionID, limit: input.limit }).items - } - return Array.from(MessageV2.stream(input.sessionID)).reverse() - }) - - const removeMessage = Effect.fn("Session.removeMessage")(function* (input: { - sessionID: SessionID - messageID: MessageID - }) { - yield* Effect.sync(() => - SyncEvent.run(MessageV2.Event.Removed, { - sessionID: input.sessionID, - messageID: input.messageID, - }), - ) - return input.messageID - }) - - const removePart = Effect.fn("Session.removePart")(function* (input: { - sessionID: SessionID - messageID: MessageID - partID: PartID - }) { - yield* Effect.sync(() => - SyncEvent.run(MessageV2.Event.PartRemoved, { - sessionID: input.sessionID, - messageID: input.messageID, - partID: input.partID, - }), - ) - return input.partID - }) - - const updatePartDelta = Effect.fn("Session.updatePartDelta")(function* (input: { - sessionID: SessionID - messageID: MessageID - partID: PartID - field: string - delta: string - }) { - yield* bus.publish(MessageV2.Event.PartDelta, input) - }) - - /** Finds the first message matching the predicate, searching newest-first. */ - const findMessage = Effect.fn("Session.findMessage")(function* ( - sessionID: SessionID, - predicate: (msg: MessageV2.WithParts) => boolean, - ) { - for (const item of MessageV2.stream(sessionID)) { - if (predicate(item)) return Option.some(item) - } - return Option.none() - }) - - return Service.of({ - create, - fork, - touch, - get, - setTitle, - setArchived, - setPermission, - setRevert, - clearRevert, - setSummary, - diff, - messages, - children, - remove, - updateMessage, - removeMessage, - removePart, - updatePart, - getPart, - updatePartDelta, - findMessage, - }) - }), - ) - - export const defaultLayer = layer.pipe(Layer.provide(Bus.layer), Layer.provide(Storage.defaultLayer)) - - export function* list(input?: { - directory?: string - workspaceID?: WorkspaceID - roots?: boolean - start?: number - search?: string - limit?: number - }) { - const project = Instance.project - const conditions = [eq(SessionTable.project_id, project.id)] - - if (input?.workspaceID) { - conditions.push(eq(SessionTable.workspace_id, input.workspaceID)) - } - if (input?.directory) { - conditions.push(eq(SessionTable.directory, input.directory)) - } - if (input?.roots) { - conditions.push(isNull(SessionTable.parent_id)) - } - if (input?.start) { - conditions.push(gte(SessionTable.time_updated, input.start)) - } - if (input?.search) { - conditions.push(like(SessionTable.title, `%${input.search}%`)) - } - - const limit = input?.limit ?? 100 - - const rows = Database.use((db) => - db - .select() - .from(SessionTable) - .where(and(...conditions)) - .orderBy(desc(SessionTable.time_updated)) - .limit(limit) - .all(), - ) - for (const row of rows) { - yield fromRow(row) - } - } - - export function* listGlobal(input?: { - directory?: string - roots?: boolean - start?: number - cursor?: number - search?: string - limit?: number - archived?: boolean - }) { - const conditions: SQL[] = [] - - if (input?.directory) { - conditions.push(eq(SessionTable.directory, input.directory)) - } - if (input?.roots) { - conditions.push(isNull(SessionTable.parent_id)) - } - if (input?.start) { - conditions.push(gte(SessionTable.time_updated, input.start)) - } - if (input?.cursor) { - conditions.push(lt(SessionTable.time_updated, input.cursor)) - } - if (input?.search) { - conditions.push(like(SessionTable.title, `%${input.search}%`)) - } - if (!input?.archived) { - conditions.push(isNull(SessionTable.time_archived)) - } - - const limit = input?.limit ?? 100 - - const rows = Database.use((db) => { - const query = - conditions.length > 0 - ? db - .select() - .from(SessionTable) - .where(and(...conditions)) - : db.select().from(SessionTable) - return query.orderBy(desc(SessionTable.time_updated), desc(SessionTable.id)).limit(limit).all() - }) - - const ids = [...new Set(rows.map((row) => row.project_id))] - const projects = new Map() - - if (ids.length > 0) { - const items = Database.use((db) => - db - .select({ id: ProjectTable.id, name: ProjectTable.name, worktree: ProjectTable.worktree }) - .from(ProjectTable) - .where(inArray(ProjectTable.id, ids)) - .all(), - ) - for (const item of items) { - projects.set(item.id, { - id: item.id, - name: item.name ?? undefined, - worktree: item.worktree, - }) - } - } - - for (const row of rows) { - const project = projects.get(row.project_id) ?? null - yield { ...fromRow(row), project } - } - } -} +export * as Session from "./session" diff --git a/packages/opencode/src/session/projectors.ts b/packages/opencode/src/session/projectors.ts index a1b2e401d0..bc083105c2 100644 --- a/packages/opencode/src/session/projectors.ts +++ b/packages/opencode/src/session/projectors.ts @@ -1,6 +1,6 @@ import { NotFoundError, eq, and } from "../storage/db" import { SyncEvent } from "@/sync" -import { Session } from "./index" +import { Session } from "." import { MessageV2 } from "./message-v2" import { SessionTable, MessageTable, PartTable } from "./session.sql" import { Log } from "../util/log" diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts new file mode 100644 index 0000000000..369a2085ff --- /dev/null +++ b/packages/opencode/src/session/session.ts @@ -0,0 +1,816 @@ +import { Slug } from "@opencode-ai/shared/util/slug" +import path from "path" +import { BusEvent } from "@/bus/bus-event" +import { Bus } from "@/bus" +import { Decimal } from "decimal.js" +import z from "zod" +import { type ProviderMetadata, type LanguageModelUsage } from "ai" +import { Flag } from "../flag/flag" +import { Installation } from "../installation" + +import { Database, NotFoundError, eq, and, gte, isNull, desc, like, inArray, lt } from "../storage/db" +import { SyncEvent } from "../sync" +import type { SQL } from "../storage/db" +import { PartTable, SessionTable } from "./session.sql" +import { ProjectTable } from "../project/project.sql" +import { Storage } from "@/storage/storage" +import { Log } from "../util/log" +import { updateSchema } from "../util/update-schema" +import { MessageV2 } from "./message-v2" +import { Instance } from "../project/instance" +import { InstanceState } from "@/effect/instance-state" +import { Snapshot } from "@/snapshot" +import { ProjectID } from "../project/schema" +import { WorkspaceID } from "../control-plane/schema" +import { SessionID, MessageID, PartID } from "./schema" + +import type { Provider } from "@/provider" +import { Permission } from "@/permission" +import { Global } from "@/global" +import { Effect, Layer, Option, Context } from "effect" + +const log = Log.create({ service: "session" }) + +const parentTitlePrefix = "New session - " +const childTitlePrefix = "Child session - " + +function createDefaultTitle(isChild = false) { + return (isChild ? childTitlePrefix : parentTitlePrefix) + new Date().toISOString() +} + +export function isDefaultTitle(title: string) { + return new RegExp( + `^(${parentTitlePrefix}|${childTitlePrefix})\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$`, + ).test(title) +} + +type SessionRow = typeof SessionTable.$inferSelect + +export function fromRow(row: SessionRow): Info { + const summary = + row.summary_additions !== null || row.summary_deletions !== null || row.summary_files !== null + ? { + additions: row.summary_additions ?? 0, + deletions: row.summary_deletions ?? 0, + files: row.summary_files ?? 0, + diffs: row.summary_diffs ?? undefined, + } + : undefined + const share = row.share_url ? { url: row.share_url } : undefined + const revert = row.revert ?? undefined + return { + id: row.id, + slug: row.slug, + projectID: row.project_id, + workspaceID: row.workspace_id ?? undefined, + directory: row.directory, + parentID: row.parent_id ?? undefined, + title: row.title, + version: row.version, + summary, + share, + revert, + permission: row.permission ?? undefined, + time: { + created: row.time_created, + updated: row.time_updated, + compacting: row.time_compacting ?? undefined, + archived: row.time_archived ?? undefined, + }, + } +} + +export function toRow(info: Info) { + return { + id: info.id, + project_id: info.projectID, + workspace_id: info.workspaceID, + parent_id: info.parentID, + slug: info.slug, + directory: info.directory, + title: info.title, + version: info.version, + share_url: info.share?.url, + summary_additions: info.summary?.additions, + summary_deletions: info.summary?.deletions, + summary_files: info.summary?.files, + summary_diffs: info.summary?.diffs, + revert: info.revert ?? null, + permission: info.permission, + time_created: info.time.created, + time_updated: info.time.updated, + time_compacting: info.time.compacting, + time_archived: info.time.archived, + } +} + +function getForkedTitle(title: string): string { + const match = title.match(/^(.+) \(fork #(\d+)\)$/) + if (match) { + const base = match[1] + const num = parseInt(match[2], 10) + return `${base} (fork #${num + 1})` + } + return `${title} (fork #1)` +} + +export const Info = z + .object({ + id: SessionID.zod, + slug: z.string(), + projectID: ProjectID.zod, + workspaceID: WorkspaceID.zod.optional(), + directory: z.string(), + parentID: SessionID.zod.optional(), + summary: z + .object({ + additions: z.number(), + deletions: z.number(), + files: z.number(), + diffs: Snapshot.FileDiff.array().optional(), + }) + .optional(), + share: z + .object({ + url: z.string(), + }) + .optional(), + title: z.string(), + version: z.string(), + time: z.object({ + created: z.number(), + updated: z.number(), + compacting: z.number().optional(), + archived: z.number().optional(), + }), + permission: Permission.Ruleset.zod.optional(), + revert: z + .object({ + messageID: MessageID.zod, + partID: PartID.zod.optional(), + snapshot: z.string().optional(), + diff: z.string().optional(), + }) + .optional(), + }) + .meta({ + ref: "Session", + }) +export type Info = z.output + +export const ProjectInfo = z + .object({ + id: ProjectID.zod, + name: z.string().optional(), + worktree: z.string(), + }) + .meta({ + ref: "ProjectSummary", + }) +export type ProjectInfo = z.output + +export const GlobalInfo = Info.extend({ + project: ProjectInfo.nullable(), +}).meta({ + ref: "GlobalSession", +}) +export type GlobalInfo = z.output + +export const CreateInput = z + .object({ + parentID: SessionID.zod.optional(), + title: z.string().optional(), + permission: Info.shape.permission, + workspaceID: WorkspaceID.zod.optional(), + }) + .optional() +export type CreateInput = z.output + +export const ForkInput = z.object({ sessionID: SessionID.zod, messageID: MessageID.zod.optional() }) +export const GetInput = SessionID.zod +export const ChildrenInput = SessionID.zod +export const RemoveInput = SessionID.zod +export const SetTitleInput = z.object({ sessionID: SessionID.zod, title: z.string() }) +export const SetArchivedInput = z.object({ sessionID: SessionID.zod, time: z.number().optional() }) +export const SetPermissionInput = z.object({ sessionID: SessionID.zod, permission: Permission.Ruleset.zod }) +export const SetRevertInput = z.object({ + sessionID: SessionID.zod, + revert: Info.shape.revert, + summary: Info.shape.summary, +}) +export const MessagesInput = z.object({ sessionID: SessionID.zod, limit: z.number().optional() }) + +export const Event = { + Created: SyncEvent.define({ + type: "session.created", + version: 1, + aggregate: "sessionID", + schema: z.object({ + sessionID: SessionID.zod, + info: Info, + }), + }), + Updated: SyncEvent.define({ + type: "session.updated", + version: 1, + aggregate: "sessionID", + schema: z.object({ + sessionID: SessionID.zod, + info: updateSchema(Info).extend({ + share: updateSchema(Info.shape.share.unwrap()).optional(), + time: updateSchema(Info.shape.time).optional(), + }), + }), + busSchema: z.object({ + sessionID: SessionID.zod, + info: Info, + }), + }), + Deleted: SyncEvent.define({ + type: "session.deleted", + version: 1, + aggregate: "sessionID", + schema: z.object({ + sessionID: SessionID.zod, + info: Info, + }), + }), + Diff: BusEvent.define( + "session.diff", + z.object({ + sessionID: SessionID.zod, + diff: Snapshot.FileDiff.array(), + }), + ), + Error: BusEvent.define( + "session.error", + z.object({ + sessionID: SessionID.zod.optional(), + error: MessageV2.Assistant.shape.error, + }), + ), +} + +export function plan(input: { slug: string; time: { created: number } }) { + const base = Instance.project.vcs + ? path.join(Instance.worktree, ".opencode", "plans") + : path.join(Global.Path.data, "plans") + return path.join(base, [input.time.created, input.slug].join("-") + ".md") +} + +export const getUsage = (input: { + model: Provider.Model + usage: LanguageModelUsage + metadata?: ProviderMetadata +}) => { + const safe = (value: number) => { + if (!Number.isFinite(value)) return 0 + return value + } + const inputTokens = safe(input.usage.inputTokens ?? 0) + const outputTokens = safe(input.usage.outputTokens ?? 0) + const reasoningTokens = safe(input.usage.outputTokenDetails?.reasoningTokens ?? input.usage.reasoningTokens ?? 0) + + const cacheReadInputTokens = safe( + input.usage.inputTokenDetails?.cacheReadTokens ?? input.usage.cachedInputTokens ?? 0, + ) + const cacheWriteInputTokens = safe( + (input.usage.inputTokenDetails?.cacheWriteTokens ?? + input.metadata?.["anthropic"]?.["cacheCreationInputTokens"] ?? + // google-vertex-anthropic returns metadata under "vertex" key + // (AnthropicMessagesLanguageModel custom provider key from 'vertex.anthropic.messages') + input.metadata?.["vertex"]?.["cacheCreationInputTokens"] ?? + // @ts-expect-error + input.metadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ?? + // @ts-expect-error + input.metadata?.["venice"]?.["usage"]?.["cacheCreationInputTokens"] ?? + 0) as number, + ) + + // AI SDK v6 normalized inputTokens to include cached tokens across all providers + // (including Anthropic/Bedrock which previously excluded them). Always subtract cache + // tokens to get the non-cached input count for separate cost calculation. + const adjustedInputTokens = safe(inputTokens - cacheReadInputTokens - cacheWriteInputTokens) + + const total = input.usage.totalTokens + + const tokens = { + total, + input: adjustedInputTokens, + output: safe(outputTokens - reasoningTokens), + reasoning: reasoningTokens, + cache: { + write: cacheWriteInputTokens, + read: cacheReadInputTokens, + }, + } + + const costInfo = + input.model.cost?.experimentalOver200K && tokens.input + tokens.cache.read > 200_000 + ? input.model.cost.experimentalOver200K + : input.model.cost + return { + cost: safe( + new Decimal(0) + .add(new Decimal(tokens.input).mul(costInfo?.input ?? 0).div(1_000_000)) + .add(new Decimal(tokens.output).mul(costInfo?.output ?? 0).div(1_000_000)) + .add(new Decimal(tokens.cache.read).mul(costInfo?.cache?.read ?? 0).div(1_000_000)) + .add(new Decimal(tokens.cache.write).mul(costInfo?.cache?.write ?? 0).div(1_000_000)) + // TODO: update models.dev to have better pricing model, for now: + // charge reasoning tokens at the same rate as output tokens + .add(new Decimal(tokens.reasoning).mul(costInfo?.output ?? 0).div(1_000_000)) + .toNumber(), + ), + tokens, + } +} + +export class BusyError extends Error { + constructor(public readonly sessionID: string) { + super(`Session ${sessionID} is busy`) + } +} + +export interface Interface { + readonly create: (input?: { + parentID?: SessionID + title?: string + permission?: Permission.Ruleset + workspaceID?: WorkspaceID + }) => Effect.Effect + readonly fork: (input: { sessionID: SessionID; messageID?: MessageID }) => Effect.Effect + readonly touch: (sessionID: SessionID) => Effect.Effect + readonly get: (id: SessionID) => Effect.Effect + readonly setTitle: (input: { sessionID: SessionID; title: string }) => Effect.Effect + readonly setArchived: (input: { sessionID: SessionID; time?: number }) => Effect.Effect + readonly setPermission: (input: { sessionID: SessionID; permission: Permission.Ruleset }) => Effect.Effect + readonly setRevert: (input: { + sessionID: SessionID + revert: Info["revert"] + summary: Info["summary"] + }) => Effect.Effect + readonly clearRevert: (sessionID: SessionID) => Effect.Effect + readonly setSummary: (input: { sessionID: SessionID; summary: Info["summary"] }) => Effect.Effect + readonly diff: (sessionID: SessionID) => Effect.Effect + readonly messages: (input: { sessionID: SessionID; limit?: number }) => Effect.Effect + readonly children: (parentID: SessionID) => Effect.Effect + readonly remove: (sessionID: SessionID) => Effect.Effect + readonly updateMessage: (msg: T) => Effect.Effect + readonly removeMessage: (input: { sessionID: SessionID; messageID: MessageID }) => Effect.Effect + readonly removePart: (input: { + sessionID: SessionID + messageID: MessageID + partID: PartID + }) => Effect.Effect + readonly getPart: (input: { + sessionID: SessionID + messageID: MessageID + partID: PartID + }) => Effect.Effect + readonly updatePart: (part: T) => Effect.Effect + readonly updatePartDelta: (input: { + sessionID: SessionID + messageID: MessageID + partID: PartID + field: string + delta: string + }) => Effect.Effect + /** Finds the first message matching the predicate, searching newest-first. */ + readonly findMessage: ( + sessionID: SessionID, + predicate: (msg: MessageV2.WithParts) => boolean, + ) => Effect.Effect> +} + +export class Service extends Context.Service()("@opencode/Session") {} + +type Patch = z.infer["info"] + +const db = (fn: (d: Parameters[0] extends (trx: infer D) => any ? D : never) => T) => + Effect.sync(() => Database.use(fn)) + +export const layer: Layer.Layer = Layer.effect( + Service, + Effect.gen(function* () { + const bus = yield* Bus.Service + const storage = yield* Storage.Service + + const createNext = Effect.fn("Session.createNext")(function* (input: { + id?: SessionID + title?: string + parentID?: SessionID + workspaceID?: WorkspaceID + directory: string + permission?: Permission.Ruleset + }) { + const ctx = yield* InstanceState.context + const result: Info = { + id: SessionID.descending(input.id), + slug: Slug.create(), + version: Installation.VERSION, + projectID: ctx.project.id, + directory: input.directory, + workspaceID: input.workspaceID, + parentID: input.parentID, + title: input.title ?? createDefaultTitle(!!input.parentID), + permission: input.permission, + time: { + created: Date.now(), + updated: Date.now(), + }, + } + log.info("created", result) + + yield* Effect.sync(() => SyncEvent.run(Event.Created, { sessionID: result.id, info: result })) + + if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) { + // This only exist for backwards compatibility. We should not be + // manually publishing this event; it is a sync event now + yield* bus.publish(Event.Updated, { + sessionID: result.id, + info: result, + }) + } + + return result + }) + + const get = Effect.fn("Session.get")(function* (id: SessionID) { + const row = yield* db((d) => d.select().from(SessionTable).where(eq(SessionTable.id, id)).get()) + if (!row) throw new NotFoundError({ message: `Session not found: ${id}` }) + return fromRow(row) + }) + + const children = Effect.fn("Session.children")(function* (parentID: SessionID) { + const rows = yield* db((d) => + d + .select() + .from(SessionTable) + .where(and(eq(SessionTable.parent_id, parentID))) + .all(), + ) + return rows.map(fromRow) + }) + + const remove: Interface["remove"] = Effect.fnUntraced(function* (sessionID: SessionID) { + try { + const session = yield* get(sessionID) + const kids = yield* children(sessionID) + for (const child of kids) { + yield* remove(child.id) + } + + // `remove` needs to work in all cases, such as a broken + // sessions that run cleanup. In certain cases these will + // run without any instance state, so we need to turn off + // publishing of events in that case + const hasInstance = yield* InstanceState.directory.pipe( + Effect.as(true), + Effect.catchCause(() => Effect.succeed(false)), + ) + + yield* Effect.sync(() => { + SyncEvent.run(Event.Deleted, { sessionID, info: session }, { publish: hasInstance }) + SyncEvent.remove(sessionID) + }) + } catch (e) { + log.error(e) + } + }) + + const updateMessage = (msg: T): Effect.Effect => + Effect.gen(function* () { + yield* Effect.sync(() => SyncEvent.run(MessageV2.Event.Updated, { sessionID: msg.sessionID, info: msg })) + return msg + }).pipe(Effect.withSpan("Session.updateMessage")) + + const updatePart = (part: T): Effect.Effect => + Effect.gen(function* () { + yield* Effect.sync(() => + SyncEvent.run(MessageV2.Event.PartUpdated, { + sessionID: part.sessionID, + part: structuredClone(part), + time: Date.now(), + }), + ) + return part + }).pipe(Effect.withSpan("Session.updatePart")) + + const getPart: Interface["getPart"] = Effect.fn("Session.getPart")(function* (input) { + const row = Database.use((db) => + db + .select() + .from(PartTable) + .where( + and( + eq(PartTable.session_id, input.sessionID), + eq(PartTable.message_id, input.messageID), + eq(PartTable.id, input.partID), + ), + ) + .get(), + ) + if (!row) return + return { + ...row.data, + id: row.id, + sessionID: row.session_id, + messageID: row.message_id, + } as MessageV2.Part + }) + + const create = Effect.fn("Session.create")(function* (input?: { + parentID?: SessionID + title?: string + permission?: Permission.Ruleset + workspaceID?: WorkspaceID + }) { + const directory = yield* InstanceState.directory + return yield* createNext({ + parentID: input?.parentID, + directory, + title: input?.title, + permission: input?.permission, + workspaceID: input?.workspaceID, + }) + }) + + const fork = Effect.fn("Session.fork")(function* (input: { sessionID: SessionID; messageID?: MessageID }) { + const directory = yield* InstanceState.directory + const original = yield* get(input.sessionID) + const title = getForkedTitle(original.title) + const session = yield* createNext({ + directory, + workspaceID: original.workspaceID, + title, + }) + const msgs = yield* messages({ sessionID: input.sessionID }) + const idMap = new Map() + + for (const msg of msgs) { + if (input.messageID && msg.info.id >= input.messageID) break + const newID = MessageID.ascending() + idMap.set(msg.info.id, newID) + + const parentID = msg.info.role === "assistant" && msg.info.parentID ? idMap.get(msg.info.parentID) : undefined + const cloned = yield* updateMessage({ + ...msg.info, + sessionID: session.id, + id: newID, + ...(parentID && { parentID }), + }) + + for (const part of msg.parts) { + yield* updatePart({ + ...part, + id: PartID.ascending(), + messageID: cloned.id, + sessionID: session.id, + }) + } + } + return session + }) + + const patch = (sessionID: SessionID, info: Patch) => + Effect.sync(() => SyncEvent.run(Event.Updated, { sessionID, info })) + + const touch = Effect.fn("Session.touch")(function* (sessionID: SessionID) { + yield* patch(sessionID, { time: { updated: Date.now() } }) + }) + + const setTitle = Effect.fn("Session.setTitle")(function* (input: { sessionID: SessionID; title: string }) { + yield* patch(input.sessionID, { title: input.title }) + }) + + const setArchived = Effect.fn("Session.setArchived")(function* (input: { sessionID: SessionID; time?: number }) { + yield* patch(input.sessionID, { time: { archived: input.time } }) + }) + + const setPermission = Effect.fn("Session.setPermission")(function* (input: { + sessionID: SessionID + permission: Permission.Ruleset + }) { + yield* patch(input.sessionID, { permission: input.permission, time: { updated: Date.now() } }) + }) + + const setRevert = Effect.fn("Session.setRevert")(function* (input: { + sessionID: SessionID + revert: Info["revert"] + summary: Info["summary"] + }) { + yield* patch(input.sessionID, { summary: input.summary, time: { updated: Date.now() }, revert: input.revert }) + }) + + const clearRevert = Effect.fn("Session.clearRevert")(function* (sessionID: SessionID) { + yield* patch(sessionID, { time: { updated: Date.now() }, revert: null }) + }) + + const setSummary = Effect.fn("Session.setSummary")(function* (input: { + sessionID: SessionID + summary: Info["summary"] + }) { + yield* patch(input.sessionID, { time: { updated: Date.now() }, summary: input.summary }) + }) + + const diff = Effect.fn("Session.diff")(function* (sessionID: SessionID) { + return yield* storage + .read(["session_diff", sessionID]) + .pipe(Effect.orElseSucceed((): Snapshot.FileDiff[] => [])) + }) + + const messages = Effect.fn("Session.messages")(function* (input: { sessionID: SessionID; limit?: number }) { + if (input.limit) { + return MessageV2.page({ sessionID: input.sessionID, limit: input.limit }).items + } + return Array.from(MessageV2.stream(input.sessionID)).reverse() + }) + + const removeMessage = Effect.fn("Session.removeMessage")(function* (input: { + sessionID: SessionID + messageID: MessageID + }) { + yield* Effect.sync(() => + SyncEvent.run(MessageV2.Event.Removed, { + sessionID: input.sessionID, + messageID: input.messageID, + }), + ) + return input.messageID + }) + + const removePart = Effect.fn("Session.removePart")(function* (input: { + sessionID: SessionID + messageID: MessageID + partID: PartID + }) { + yield* Effect.sync(() => + SyncEvent.run(MessageV2.Event.PartRemoved, { + sessionID: input.sessionID, + messageID: input.messageID, + partID: input.partID, + }), + ) + return input.partID + }) + + const updatePartDelta = Effect.fn("Session.updatePartDelta")(function* (input: { + sessionID: SessionID + messageID: MessageID + partID: PartID + field: string + delta: string + }) { + yield* bus.publish(MessageV2.Event.PartDelta, input) + }) + + /** Finds the first message matching the predicate, searching newest-first. */ + const findMessage = Effect.fn("Session.findMessage")(function* ( + sessionID: SessionID, + predicate: (msg: MessageV2.WithParts) => boolean, + ) { + for (const item of MessageV2.stream(sessionID)) { + if (predicate(item)) return Option.some(item) + } + return Option.none() + }) + + return Service.of({ + create, + fork, + touch, + get, + setTitle, + setArchived, + setPermission, + setRevert, + clearRevert, + setSummary, + diff, + messages, + children, + remove, + updateMessage, + removeMessage, + removePart, + updatePart, + getPart, + updatePartDelta, + findMessage, + }) + }), +) + +export const defaultLayer = layer.pipe(Layer.provide(Bus.layer), Layer.provide(Storage.defaultLayer)) + +export function* list(input?: { + directory?: string + workspaceID?: WorkspaceID + roots?: boolean + start?: number + search?: string + limit?: number +}) { + const project = Instance.project + const conditions = [eq(SessionTable.project_id, project.id)] + + if (input?.workspaceID) { + conditions.push(eq(SessionTable.workspace_id, input.workspaceID)) + } + if (input?.directory) { + conditions.push(eq(SessionTable.directory, input.directory)) + } + if (input?.roots) { + conditions.push(isNull(SessionTable.parent_id)) + } + if (input?.start) { + conditions.push(gte(SessionTable.time_updated, input.start)) + } + if (input?.search) { + conditions.push(like(SessionTable.title, `%${input.search}%`)) + } + + const limit = input?.limit ?? 100 + + const rows = Database.use((db) => + db + .select() + .from(SessionTable) + .where(and(...conditions)) + .orderBy(desc(SessionTable.time_updated)) + .limit(limit) + .all(), + ) + for (const row of rows) { + yield fromRow(row) + } +} + +export function* listGlobal(input?: { + directory?: string + roots?: boolean + start?: number + cursor?: number + search?: string + limit?: number + archived?: boolean +}) { + const conditions: SQL[] = [] + + if (input?.directory) { + conditions.push(eq(SessionTable.directory, input.directory)) + } + if (input?.roots) { + conditions.push(isNull(SessionTable.parent_id)) + } + if (input?.start) { + conditions.push(gte(SessionTable.time_updated, input.start)) + } + if (input?.cursor) { + conditions.push(lt(SessionTable.time_updated, input.cursor)) + } + if (input?.search) { + conditions.push(like(SessionTable.title, `%${input.search}%`)) + } + if (!input?.archived) { + conditions.push(isNull(SessionTable.time_archived)) + } + + const limit = input?.limit ?? 100 + + const rows = Database.use((db) => { + const query = + conditions.length > 0 + ? db + .select() + .from(SessionTable) + .where(and(...conditions)) + : db.select().from(SessionTable) + return query.orderBy(desc(SessionTable.time_updated), desc(SessionTable.id)).limit(limit).all() + }) + + const ids = [...new Set(rows.map((row) => row.project_id))] + const projects = new Map() + + if (ids.length > 0) { + const items = Database.use((db) => + db + .select({ id: ProjectTable.id, name: ProjectTable.name, worktree: ProjectTable.worktree }) + .from(ProjectTable) + .where(inArray(ProjectTable.id, ids)) + .all(), + ) + for (const item of items) { + projects.set(item.id, { + id: item.id, + name: item.name ?? undefined, + worktree: item.worktree, + }) + } + } + + for (const row of rows) { + const project = projects.get(row.project_id) ?? null + yield { ...fromRow(row), project } + } +} From 5ae91aa81047d3fa7e50e9e2d260835f100409c7 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 22:15:19 -0400 Subject: [PATCH 189/300] feat: unwrap uplugin namespace to flat exports + barrel (#22711) --- packages/opencode/src/plugin/index.ts | 290 +----------------- packages/opencode/src/plugin/plugin.ts | 287 +++++++++++++++++ .../test/plugin/auth-override.test.ts | 2 +- 3 files changed, 289 insertions(+), 290 deletions(-) create mode 100644 packages/opencode/src/plugin/plugin.ts diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index f31e0b9ff2..20f38c41c2 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -1,289 +1 @@ -import type { - Hooks, - PluginInput, - Plugin as PluginInstance, - PluginModule, - WorkspaceAdaptor as PluginWorkspaceAdaptor, -} from "@opencode-ai/plugin" -import { Config } from "../config" -import { Bus } from "../bus" -import { Log } from "../util/log" -import { createOpencodeClient } from "@opencode-ai/sdk" -import { Flag } from "../flag/flag" -import { CodexAuthPlugin } from "./codex" -import { Session } from "../session" -import { NamedError } from "@opencode-ai/shared/util/error" -import { CopilotAuthPlugin } from "./github-copilot/copilot" -import { gitlabAuthPlugin as GitlabAuthPlugin } from "opencode-gitlab-auth" -import { PoeAuthPlugin } from "opencode-poe-auth" -import { CloudflareAIGatewayAuthPlugin, CloudflareWorkersAuthPlugin } from "./cloudflare" -import { Effect, Layer, Context, Stream } from "effect" -import { EffectBridge } from "@/effect/bridge" -import { InstanceState } from "@/effect/instance-state" -import { errorMessage } from "@/util/error" -import { PluginLoader } from "./loader" -import { parsePluginSpecifier, readPluginId, readV1Plugin, resolvePluginId } from "./shared" -import { registerAdaptor } from "@/control-plane/adaptors" -import type { WorkspaceAdaptor } from "@/control-plane/types" - -export namespace Plugin { - const log = Log.create({ service: "plugin" }) - - type State = { - hooks: Hooks[] - } - - // Hook names that follow the (input, output) => Promise trigger pattern - type TriggerName = { - [K in keyof Hooks]-?: NonNullable extends (input: any, output: any) => Promise ? K : never - }[keyof Hooks] - - export interface Interface { - readonly trigger: < - Name extends TriggerName, - Input = Parameters[Name]>[0], - Output = Parameters[Name]>[1], - >( - name: Name, - input: Input, - output: Output, - ) => Effect.Effect - readonly list: () => Effect.Effect - readonly init: () => Effect.Effect - } - - export class Service extends Context.Service()("@opencode/Plugin") {} - - // Built-in plugins that are directly imported (not installed from npm) - const INTERNAL_PLUGINS: PluginInstance[] = [ - CodexAuthPlugin, - CopilotAuthPlugin, - GitlabAuthPlugin, - PoeAuthPlugin, - CloudflareWorkersAuthPlugin, - CloudflareAIGatewayAuthPlugin, - ] - - function isServerPlugin(value: unknown): value is PluginInstance { - return typeof value === "function" - } - - function getServerPlugin(value: unknown) { - if (isServerPlugin(value)) return value - if (!value || typeof value !== "object" || !("server" in value)) return - if (!isServerPlugin(value.server)) return - return value.server - } - - function getLegacyPlugins(mod: Record) { - const seen = new Set() - const result: PluginInstance[] = [] - - for (const entry of Object.values(mod)) { - if (seen.has(entry)) continue - seen.add(entry) - const plugin = getServerPlugin(entry) - if (!plugin) throw new TypeError("Plugin export is not a function") - result.push(plugin) - } - - return result - } - - async function applyPlugin(load: PluginLoader.Loaded, input: PluginInput, hooks: Hooks[]) { - const plugin = readV1Plugin(load.mod, load.spec, "server", "detect") - if (plugin) { - await resolvePluginId(load.source, load.spec, load.target, readPluginId(plugin.id, load.spec), load.pkg) - hooks.push(await (plugin as PluginModule).server(input, load.options)) - return - } - - for (const server of getLegacyPlugins(load.mod)) { - hooks.push(await server(input, load.options)) - } - } - - export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const bus = yield* Bus.Service - const config = yield* Config.Service - - const state = yield* InstanceState.make( - Effect.fn("Plugin.state")(function* (ctx) { - const hooks: Hooks[] = [] - const bridge = yield* EffectBridge.make() - - function publishPluginError(message: string) { - bridge.fork(bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() })) - } - - const { Server } = yield* Effect.promise(() => import("../server/server")) - - const client = createOpencodeClient({ - baseUrl: "http://localhost:4096", - directory: ctx.directory, - headers: Flag.OPENCODE_SERVER_PASSWORD - ? { - Authorization: `Basic ${Buffer.from(`${Flag.OPENCODE_SERVER_USERNAME ?? "opencode"}:${Flag.OPENCODE_SERVER_PASSWORD}`).toString("base64")}`, - } - : undefined, - fetch: async (...args) => (await Server.Default()).app.fetch(...args), - }) - const cfg = yield* config.get() - const input: PluginInput = { - client, - project: ctx.project, - worktree: ctx.worktree, - directory: ctx.directory, - experimental_workspace: { - register(type: string, adaptor: PluginWorkspaceAdaptor) { - registerAdaptor(ctx.project.id, type, adaptor as WorkspaceAdaptor) - }, - }, - get serverUrl(): URL { - return Server.url ?? new URL("http://localhost:4096") - }, - // @ts-expect-error - $: typeof Bun === "undefined" ? undefined : Bun.$, - } - - for (const plugin of INTERNAL_PLUGINS) { - log.info("loading internal plugin", { name: plugin.name }) - const init = yield* Effect.tryPromise({ - try: () => plugin(input), - catch: (err) => { - log.error("failed to load internal plugin", { name: plugin.name, error: err }) - }, - }).pipe(Effect.option) - if (init._tag === "Some") hooks.push(init.value) - } - - const plugins = Flag.OPENCODE_PURE ? [] : (cfg.plugin_origins ?? []) - if (Flag.OPENCODE_PURE && cfg.plugin_origins?.length) { - log.info("skipping external plugins in pure mode", { count: cfg.plugin_origins.length }) - } - if (plugins.length) yield* config.waitForDependencies() - - const loaded = yield* Effect.promise(() => - PluginLoader.loadExternal({ - items: plugins, - kind: "server", - report: { - start(candidate) { - log.info("loading plugin", { path: candidate.plan.spec }) - }, - missing(candidate, _retry, message) { - log.warn("plugin has no server entrypoint", { path: candidate.plan.spec, message }) - }, - error(candidate, _retry, stage, error, resolved) { - const spec = candidate.plan.spec - const cause = error instanceof Error ? (error.cause ?? error) : error - const message = stage === "load" ? errorMessage(error) : errorMessage(cause) - - if (stage === "install") { - const parsed = parsePluginSpecifier(spec) - log.error("failed to install plugin", { pkg: parsed.pkg, version: parsed.version, error: message }) - publishPluginError(`Failed to install plugin ${parsed.pkg}@${parsed.version}: ${message}`) - return - } - - if (stage === "compatibility") { - log.warn("plugin incompatible", { path: spec, error: message }) - publishPluginError(`Plugin ${spec} skipped: ${message}`) - return - } - - if (stage === "entry") { - log.error("failed to resolve plugin server entry", { path: spec, error: message }) - publishPluginError(`Failed to load plugin ${spec}: ${message}`) - return - } - - log.error("failed to load plugin", { path: spec, target: resolved?.entry, error: message }) - publishPluginError(`Failed to load plugin ${spec}: ${message}`) - }, - }, - }), - ) - for (const load of loaded) { - if (!load) continue - - // Keep plugin execution sequential so hook registration and execution - // order remains deterministic across plugin runs. - yield* Effect.tryPromise({ - try: () => applyPlugin(load, input, hooks), - catch: (err) => { - const message = errorMessage(err) - log.error("failed to load plugin", { path: load.spec, error: message }) - return message - }, - }).pipe( - Effect.catch(() => { - // TODO: make proper events for this - // bus.publish(Session.Event.Error, { - // error: new NamedError.Unknown({ - // message: `Failed to load plugin ${load.spec}: ${message}`, - // }).toObject(), - // }) - return Effect.void - }), - ) - } - - // Notify plugins of current config - for (const hook of hooks) { - yield* Effect.tryPromise({ - try: () => Promise.resolve((hook as any).config?.(cfg)), - catch: (err) => { - log.error("plugin config hook failed", { error: err }) - }, - }).pipe(Effect.ignore) - } - - // Subscribe to bus events, fiber interrupted when scope closes - yield* bus.subscribeAll().pipe( - Stream.runForEach((input) => - Effect.sync(() => { - for (const hook of hooks) { - hook["event"]?.({ event: input as any }) - } - }), - ), - Effect.forkScoped, - ) - - return { hooks } - }), - ) - - const trigger = Effect.fn("Plugin.trigger")(function* < - Name extends TriggerName, - Input = Parameters[Name]>[0], - Output = Parameters[Name]>[1], - >(name: Name, input: Input, output: Output) { - if (!name) return output - const s = yield* InstanceState.get(state) - for (const hook of s.hooks) { - const fn = hook[name] as any - if (!fn) continue - yield* Effect.promise(async () => fn(input, output)) - } - return output - }) - - const list = Effect.fn("Plugin.list")(function* () { - const s = yield* InstanceState.get(state) - return s.hooks - }) - - const init = Effect.fn("Plugin.init")(function* () { - yield* InstanceState.get(state) - }) - - return Service.of({ trigger, list, init }) - }), - ) - - export const defaultLayer = layer.pipe(Layer.provide(Bus.layer), Layer.provide(Config.defaultLayer)) -} +export * as Plugin from "./plugin" diff --git a/packages/opencode/src/plugin/plugin.ts b/packages/opencode/src/plugin/plugin.ts new file mode 100644 index 0000000000..537794138a --- /dev/null +++ b/packages/opencode/src/plugin/plugin.ts @@ -0,0 +1,287 @@ +import type { + Hooks, + PluginInput, + Plugin as PluginInstance, + PluginModule, + WorkspaceAdaptor as PluginWorkspaceAdaptor, +} from "@opencode-ai/plugin" +import { Config } from "../config" +import { Bus } from "../bus" +import { Log } from "../util/log" +import { createOpencodeClient } from "@opencode-ai/sdk" +import { Flag } from "../flag/flag" +import { CodexAuthPlugin } from "./codex" +import { Session } from "../session" +import { NamedError } from "@opencode-ai/shared/util/error" +import { CopilotAuthPlugin } from "./github-copilot/copilot" +import { gitlabAuthPlugin as GitlabAuthPlugin } from "opencode-gitlab-auth" +import { PoeAuthPlugin } from "opencode-poe-auth" +import { CloudflareAIGatewayAuthPlugin, CloudflareWorkersAuthPlugin } from "./cloudflare" +import { Effect, Layer, Context, Stream } from "effect" +import { EffectBridge } from "@/effect/bridge" +import { InstanceState } from "@/effect/instance-state" +import { errorMessage } from "@/util/error" +import { PluginLoader } from "./loader" +import { parsePluginSpecifier, readPluginId, readV1Plugin, resolvePluginId } from "./shared" +import { registerAdaptor } from "@/control-plane/adaptors" +import type { WorkspaceAdaptor } from "@/control-plane/types" + +const log = Log.create({ service: "plugin" }) + +type State = { + hooks: Hooks[] +} + +// Hook names that follow the (input, output) => Promise trigger pattern +type TriggerName = { + [K in keyof Hooks]-?: NonNullable extends (input: any, output: any) => Promise ? K : never +}[keyof Hooks] + +export interface Interface { + readonly trigger: < + Name extends TriggerName, + Input = Parameters[Name]>[0], + Output = Parameters[Name]>[1], + >( + name: Name, + input: Input, + output: Output, + ) => Effect.Effect + readonly list: () => Effect.Effect + readonly init: () => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/Plugin") {} + +// Built-in plugins that are directly imported (not installed from npm) +const INTERNAL_PLUGINS: PluginInstance[] = [ + CodexAuthPlugin, + CopilotAuthPlugin, + GitlabAuthPlugin, + PoeAuthPlugin, + CloudflareWorkersAuthPlugin, + CloudflareAIGatewayAuthPlugin, +] + +function isServerPlugin(value: unknown): value is PluginInstance { + return typeof value === "function" +} + +function getServerPlugin(value: unknown) { + if (isServerPlugin(value)) return value + if (!value || typeof value !== "object" || !("server" in value)) return + if (!isServerPlugin(value.server)) return + return value.server +} + +function getLegacyPlugins(mod: Record) { + const seen = new Set() + const result: PluginInstance[] = [] + + for (const entry of Object.values(mod)) { + if (seen.has(entry)) continue + seen.add(entry) + const plugin = getServerPlugin(entry) + if (!plugin) throw new TypeError("Plugin export is not a function") + result.push(plugin) + } + + return result +} + +async function applyPlugin(load: PluginLoader.Loaded, input: PluginInput, hooks: Hooks[]) { + const plugin = readV1Plugin(load.mod, load.spec, "server", "detect") + if (plugin) { + await resolvePluginId(load.source, load.spec, load.target, readPluginId(plugin.id, load.spec), load.pkg) + hooks.push(await (plugin as PluginModule).server(input, load.options)) + return + } + + for (const server of getLegacyPlugins(load.mod)) { + hooks.push(await server(input, load.options)) + } +} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const bus = yield* Bus.Service + const config = yield* Config.Service + + const state = yield* InstanceState.make( + Effect.fn("Plugin.state")(function* (ctx) { + const hooks: Hooks[] = [] + const bridge = yield* EffectBridge.make() + + function publishPluginError(message: string) { + bridge.fork(bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() })) + } + + const { Server } = yield* Effect.promise(() => import("../server/server")) + + const client = createOpencodeClient({ + baseUrl: "http://localhost:4096", + directory: ctx.directory, + headers: Flag.OPENCODE_SERVER_PASSWORD + ? { + Authorization: `Basic ${Buffer.from(`${Flag.OPENCODE_SERVER_USERNAME ?? "opencode"}:${Flag.OPENCODE_SERVER_PASSWORD}`).toString("base64")}`, + } + : undefined, + fetch: async (...args) => (await Server.Default()).app.fetch(...args), + }) + const cfg = yield* config.get() + const input: PluginInput = { + client, + project: ctx.project, + worktree: ctx.worktree, + directory: ctx.directory, + experimental_workspace: { + register(type: string, adaptor: PluginWorkspaceAdaptor) { + registerAdaptor(ctx.project.id, type, adaptor as WorkspaceAdaptor) + }, + }, + get serverUrl(): URL { + return Server.url ?? new URL("http://localhost:4096") + }, + // @ts-expect-error + $: typeof Bun === "undefined" ? undefined : Bun.$, + } + + for (const plugin of INTERNAL_PLUGINS) { + log.info("loading internal plugin", { name: plugin.name }) + const init = yield* Effect.tryPromise({ + try: () => plugin(input), + catch: (err) => { + log.error("failed to load internal plugin", { name: plugin.name, error: err }) + }, + }).pipe(Effect.option) + if (init._tag === "Some") hooks.push(init.value) + } + + const plugins = Flag.OPENCODE_PURE ? [] : (cfg.plugin_origins ?? []) + if (Flag.OPENCODE_PURE && cfg.plugin_origins?.length) { + log.info("skipping external plugins in pure mode", { count: cfg.plugin_origins.length }) + } + if (plugins.length) yield* config.waitForDependencies() + + const loaded = yield* Effect.promise(() => + PluginLoader.loadExternal({ + items: plugins, + kind: "server", + report: { + start(candidate) { + log.info("loading plugin", { path: candidate.plan.spec }) + }, + missing(candidate, _retry, message) { + log.warn("plugin has no server entrypoint", { path: candidate.plan.spec, message }) + }, + error(candidate, _retry, stage, error, resolved) { + const spec = candidate.plan.spec + const cause = error instanceof Error ? (error.cause ?? error) : error + const message = stage === "load" ? errorMessage(error) : errorMessage(cause) + + if (stage === "install") { + const parsed = parsePluginSpecifier(spec) + log.error("failed to install plugin", { pkg: parsed.pkg, version: parsed.version, error: message }) + publishPluginError(`Failed to install plugin ${parsed.pkg}@${parsed.version}: ${message}`) + return + } + + if (stage === "compatibility") { + log.warn("plugin incompatible", { path: spec, error: message }) + publishPluginError(`Plugin ${spec} skipped: ${message}`) + return + } + + if (stage === "entry") { + log.error("failed to resolve plugin server entry", { path: spec, error: message }) + publishPluginError(`Failed to load plugin ${spec}: ${message}`) + return + } + + log.error("failed to load plugin", { path: spec, target: resolved?.entry, error: message }) + publishPluginError(`Failed to load plugin ${spec}: ${message}`) + }, + }, + }), + ) + for (const load of loaded) { + if (!load) continue + + // Keep plugin execution sequential so hook registration and execution + // order remains deterministic across plugin runs. + yield* Effect.tryPromise({ + try: () => applyPlugin(load, input, hooks), + catch: (err) => { + const message = errorMessage(err) + log.error("failed to load plugin", { path: load.spec, error: message }) + return message + }, + }).pipe( + Effect.catch(() => { + // TODO: make proper events for this + // bus.publish(Session.Event.Error, { + // error: new NamedError.Unknown({ + // message: `Failed to load plugin ${load.spec}: ${message}`, + // }).toObject(), + // }) + return Effect.void + }), + ) + } + + // Notify plugins of current config + for (const hook of hooks) { + yield* Effect.tryPromise({ + try: () => Promise.resolve((hook as any).config?.(cfg)), + catch: (err) => { + log.error("plugin config hook failed", { error: err }) + }, + }).pipe(Effect.ignore) + } + + // Subscribe to bus events, fiber interrupted when scope closes + yield* bus.subscribeAll().pipe( + Stream.runForEach((input) => + Effect.sync(() => { + for (const hook of hooks) { + hook["event"]?.({ event: input as any }) + } + }), + ), + Effect.forkScoped, + ) + + return { hooks } + }), + ) + + const trigger = Effect.fn("Plugin.trigger")(function* < + Name extends TriggerName, + Input = Parameters[Name]>[0], + Output = Parameters[Name]>[1], + >(name: Name, input: Input, output: Output) { + if (!name) return output + const s = yield* InstanceState.get(state) + for (const hook of s.hooks) { + const fn = hook[name] as any + if (!fn) continue + yield* Effect.promise(async () => fn(input, output)) + } + return output + }) + + const list = Effect.fn("Plugin.list")(function* () { + const s = yield* InstanceState.get(state) + return s.hooks + }) + + const init = Effect.fn("Plugin.init")(function* () { + yield* InstanceState.get(state) + }) + + return Service.of({ trigger, list, init }) + }), +) + +export const defaultLayer = layer.pipe(Layer.provide(Bus.layer), Layer.provide(Config.defaultLayer)) diff --git a/packages/opencode/test/plugin/auth-override.test.ts b/packages/opencode/test/plugin/auth-override.test.ts index 36a02058ea..0c619c2edc 100644 --- a/packages/opencode/test/plugin/auth-override.test.ts +++ b/packages/opencode/test/plugin/auth-override.test.ts @@ -63,7 +63,7 @@ describe("plugin.auth-override", () => { }, 30000) // Increased timeout for plugin installation }) -const file = path.join(import.meta.dir, "../../src/plugin/index.ts") +const file = path.join(import.meta.dir, "../../src/plugin/plugin.ts") describe("plugin.config-hook-error-isolation", () => { test("config hooks are individually error-isolated in the layer factory", async () => { From d7a072dd464e09a3b5ef21943cf126f374884c4b Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 22:15:20 -0400 Subject: [PATCH 190/300] feat: unwrap usnapshot namespace to flat exports + barrel (#22715) --- packages/opencode/src/snapshot/index.ts | 780 +-------------------- packages/opencode/src/snapshot/snapshot.ts | 777 ++++++++++++++++++++ 2 files changed, 778 insertions(+), 779 deletions(-) create mode 100644 packages/opencode/src/snapshot/snapshot.ts diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 83963e3511..49eafe4450 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -1,779 +1 @@ -import { Cause, Duration, Effect, Layer, Schedule, Semaphore, Context, Stream } from "effect" -import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" -import { formatPatch, structuredPatch } from "diff" -import path from "path" -import z from "zod" -import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" -import { InstanceState } from "@/effect/instance-state" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" -import { Hash } from "@opencode-ai/shared/util/hash" -import { Config } from "../config" -import { Global } from "../global" -import { Log } from "../util/log" - -export namespace Snapshot { - export const Patch = z.object({ - hash: z.string(), - files: z.string().array(), - }) - export type Patch = z.infer - - export const FileDiff = z - .object({ - file: z.string(), - patch: z.string(), - additions: z.number(), - deletions: z.number(), - status: z.enum(["added", "deleted", "modified"]).optional(), - }) - .meta({ - ref: "SnapshotFileDiff", - }) - export type FileDiff = z.infer - - const log = Log.create({ service: "snapshot" }) - const prune = "7.days" - const limit = 2 * 1024 * 1024 - const core = ["-c", "core.longpaths=true", "-c", "core.symlinks=true"] - const cfg = ["-c", "core.autocrlf=false", ...core] - const quote = [...cfg, "-c", "core.quotepath=false"] - interface GitResult { - readonly code: ChildProcessSpawner.ExitCode - readonly text: string - readonly stderr: string - } - - type State = Omit - - export interface Interface { - readonly init: () => Effect.Effect - readonly cleanup: () => Effect.Effect - readonly track: () => Effect.Effect - readonly patch: (hash: string) => Effect.Effect - readonly restore: (snapshot: string) => Effect.Effect - readonly revert: (patches: Snapshot.Patch[]) => Effect.Effect - readonly diff: (hash: string) => Effect.Effect - readonly diffFull: (from: string, to: string) => Effect.Effect - } - - export class Service extends Context.Service()("@opencode/Snapshot") {} - - export const layer: Layer.Layer< - Service, - never, - AppFileSystem.Service | ChildProcessSpawner.ChildProcessSpawner | Config.Service - > = Layer.effect( - Service, - Effect.gen(function* () { - const fs = yield* AppFileSystem.Service - const spawner = yield* ChildProcessSpawner.ChildProcessSpawner - const config = yield* Config.Service - const locks = new Map() - - const lock = (key: string) => { - const hit = locks.get(key) - if (hit) return hit - - const next = Semaphore.makeUnsafe(1) - locks.set(key, next) - return next - } - - const state = yield* InstanceState.make( - Effect.fn("Snapshot.state")(function* (ctx) { - const state = { - directory: ctx.directory, - worktree: ctx.worktree, - gitdir: path.join(Global.Path.data, "snapshot", ctx.project.id, Hash.fast(ctx.worktree)), - vcs: ctx.project.vcs, - } - - const args = (cmd: string[]) => ["--git-dir", state.gitdir, "--work-tree", state.worktree, ...cmd] - - const enc = new TextEncoder() - const feed = (list: string[]) => Stream.make(enc.encode(list.join("\0") + "\0")) - - const git = Effect.fnUntraced( - function* ( - cmd: string[], - opts?: { cwd?: string; env?: Record; stdin?: ChildProcess.CommandInput }, - ) { - const proc = ChildProcess.make("git", cmd, { - cwd: opts?.cwd, - env: opts?.env, - extendEnv: true, - stdin: opts?.stdin, - }) - const handle = yield* spawner.spawn(proc) - const [text, stderr] = yield* Effect.all( - [Stream.mkString(Stream.decodeText(handle.stdout)), Stream.mkString(Stream.decodeText(handle.stderr))], - { concurrency: 2 }, - ) - const code = yield* handle.exitCode - return { code, text, stderr } satisfies GitResult - }, - Effect.scoped, - Effect.catch((err) => - Effect.succeed({ - code: ChildProcessSpawner.ExitCode(1), - text: "", - stderr: String(err), - }), - ), - ) - - const ignore = Effect.fnUntraced(function* (files: string[]) { - if (!files.length) return new Set() - const check = yield* git( - [ - ...quote, - "--git-dir", - path.join(state.worktree, ".git"), - "--work-tree", - state.worktree, - "check-ignore", - "--no-index", - "--stdin", - "-z", - ], - { - cwd: state.directory, - stdin: feed(files), - }, - ) - if (check.code !== 0 && check.code !== 1) return new Set() - return new Set(check.text.split("\0").filter(Boolean)) - }) - - const drop = Effect.fnUntraced(function* (files: string[]) { - if (!files.length) return - yield* git( - [ - ...cfg, - ...args(["rm", "--cached", "-f", "--ignore-unmatch", "--pathspec-from-file=-", "--pathspec-file-nul"]), - ], - { - cwd: state.directory, - stdin: feed(files), - }, - ) - }) - - const stage = Effect.fnUntraced(function* (files: string[]) { - if (!files.length) return - const result = yield* git( - [...cfg, ...args(["add", "--all", "--sparse", "--pathspec-from-file=-", "--pathspec-file-nul"])], - { - cwd: state.directory, - stdin: feed(files), - }, - ) - if (result.code === 0) return - log.warn("failed to add snapshot files", { - exitCode: result.code, - stderr: result.stderr, - }) - }) - - const exists = (file: string) => fs.exists(file).pipe(Effect.orDie) - const read = (file: string) => fs.readFileString(file).pipe(Effect.catch(() => Effect.succeed(""))) - const remove = (file: string) => fs.remove(file).pipe(Effect.catch(() => Effect.void)) - const locked = (fx: Effect.Effect) => lock(state.gitdir).withPermits(1)(fx) - - const enabled = Effect.fnUntraced(function* () { - if (state.vcs !== "git") return false - return (yield* config.get()).snapshot !== false - }) - - const excludes = Effect.fnUntraced(function* () { - const result = yield* git(["rev-parse", "--path-format=absolute", "--git-path", "info/exclude"], { - cwd: state.worktree, - }) - const file = result.text.trim() - if (!file) return - if (!(yield* exists(file))) return - return file - }) - - const sync = Effect.fnUntraced(function* (list: string[] = []) { - const file = yield* excludes() - const target = path.join(state.gitdir, "info", "exclude") - const text = [ - file ? (yield* read(file)).trimEnd() : "", - ...list.map((item) => `/${item.replaceAll("\\", "/")}`), - ] - .filter(Boolean) - .join("\n") - yield* fs.ensureDir(path.join(state.gitdir, "info")).pipe(Effect.orDie) - yield* fs.writeFileString(target, text ? `${text}\n` : "").pipe(Effect.orDie) - }) - - const add = Effect.fnUntraced(function* () { - yield* sync() - const [diff, other] = yield* Effect.all( - [ - git([...quote, ...args(["diff-files", "--name-only", "-z", "--", "."])], { - cwd: state.directory, - }), - git([...quote, ...args(["ls-files", "--others", "--exclude-standard", "-z", "--", "."])], { - cwd: state.directory, - }), - ], - { concurrency: 2 }, - ) - if (diff.code !== 0 || other.code !== 0) { - log.warn("failed to list snapshot files", { - diffCode: diff.code, - diffStderr: diff.stderr, - otherCode: other.code, - otherStderr: other.stderr, - }) - return - } - - const tracked = diff.text.split("\0").filter(Boolean) - const untracked = other.text.split("\0").filter(Boolean) - const all = Array.from(new Set([...tracked, ...untracked])) - if (!all.length) return - - // Resolve source-repo ignore rules against the exact candidate set. - // --no-index keeps this pattern-based even when a path is already tracked. - const ignored = yield* ignore(all) - - // Remove newly-ignored files from snapshot index to prevent re-adding - if (ignored.size > 0) { - const ignoredFiles = Array.from(ignored) - log.info("removing gitignored files from snapshot", { count: ignoredFiles.length }) - yield* drop(ignoredFiles) - } - - const allow = all.filter((item) => !ignored.has(item)) - if (!allow.length) return - - const large = new Set( - (yield* Effect.all( - allow.map((item) => - fs - .stat(path.join(state.directory, item)) - .pipe(Effect.catch(() => Effect.void)) - .pipe( - Effect.map((stat) => { - if (!stat || stat.type !== "File") return - const size = typeof stat.size === "bigint" ? Number(stat.size) : stat.size - return size > limit ? item : undefined - }), - ), - ), - { concurrency: 8 }, - )).filter((item): item is string => Boolean(item)), - ) - const block = new Set(untracked.filter((item) => large.has(item))) - yield* sync(Array.from(block)) - // Stage only the allowed candidate paths so snapshot updates stay scoped. - yield* stage(allow.filter((item) => !block.has(item))) - }) - - const cleanup = Effect.fnUntraced(function* () { - return yield* locked( - Effect.gen(function* () { - if (!(yield* enabled())) return - if (!(yield* exists(state.gitdir))) return - const result = yield* git(args(["gc", `--prune=${prune}`]), { cwd: state.directory }) - if (result.code !== 0) { - log.warn("cleanup failed", { - exitCode: result.code, - stderr: result.stderr, - }) - return - } - log.info("cleanup", { prune }) - }), - ) - }) - - const track = Effect.fnUntraced(function* () { - return yield* locked( - Effect.gen(function* () { - if (!(yield* enabled())) return - const existed = yield* exists(state.gitdir) - yield* fs.ensureDir(state.gitdir).pipe(Effect.orDie) - if (!existed) { - yield* git(["init"], { - env: { GIT_DIR: state.gitdir, GIT_WORK_TREE: state.worktree }, - }) - yield* git(["--git-dir", state.gitdir, "config", "core.autocrlf", "false"]) - yield* git(["--git-dir", state.gitdir, "config", "core.longpaths", "true"]) - yield* git(["--git-dir", state.gitdir, "config", "core.symlinks", "true"]) - yield* git(["--git-dir", state.gitdir, "config", "core.fsmonitor", "false"]) - log.info("initialized") - } - yield* add() - const result = yield* git(args(["write-tree"]), { cwd: state.directory }) - const hash = result.text.trim() - log.info("tracking", { hash, cwd: state.directory, git: state.gitdir }) - return hash - }), - ) - }) - - const patch = Effect.fnUntraced(function* (hash: string) { - return yield* locked( - Effect.gen(function* () { - yield* add() - const result = yield* git( - [...quote, ...args(["diff", "--cached", "--no-ext-diff", "--name-only", hash, "--", "."])], - { - cwd: state.directory, - }, - ) - if (result.code !== 0) { - log.warn("failed to get diff", { hash, exitCode: result.code }) - return { hash, files: [] } - } - const files = result.text - .trim() - .split("\n") - .map((x) => x.trim()) - .filter(Boolean) - - // Hide ignored-file removals from the user-facing patch output. - const ignored = yield* ignore(files) - - return { - hash, - files: files - .filter((item) => !ignored.has(item)) - .map((x) => path.join(state.worktree, x).replaceAll("\\", "/")), - } - }), - ) - }) - - const restore = Effect.fnUntraced(function* (snapshot: string) { - return yield* locked( - Effect.gen(function* () { - log.info("restore", { commit: snapshot }) - const result = yield* git([...core, ...args(["read-tree", snapshot])], { cwd: state.worktree }) - if (result.code === 0) { - const checkout = yield* git([...core, ...args(["checkout-index", "-a", "-f"])], { - cwd: state.worktree, - }) - if (checkout.code === 0) return - log.error("failed to restore snapshot", { - snapshot, - exitCode: checkout.code, - stderr: checkout.stderr, - }) - return - } - log.error("failed to restore snapshot", { - snapshot, - exitCode: result.code, - stderr: result.stderr, - }) - }), - ) - }) - - const revert = Effect.fnUntraced(function* (patches: Snapshot.Patch[]) { - return yield* locked( - Effect.gen(function* () { - const ops: { hash: string; file: string; rel: string }[] = [] - const seen = new Set() - for (const item of patches) { - for (const file of item.files) { - if (seen.has(file)) continue - seen.add(file) - ops.push({ - hash: item.hash, - file, - rel: path.relative(state.worktree, file).replaceAll("\\", "/"), - }) - } - } - - const single = Effect.fnUntraced(function* (op: (typeof ops)[number]) { - log.info("reverting", { file: op.file, hash: op.hash }) - const result = yield* git([...core, ...args(["checkout", op.hash, "--", op.file])], { - cwd: state.worktree, - }) - if (result.code === 0) return - const tree = yield* git([...core, ...args(["ls-tree", op.hash, "--", op.rel])], { - cwd: state.worktree, - }) - if (tree.code === 0 && tree.text.trim()) { - log.info("file existed in snapshot but checkout failed, keeping", { file: op.file, hash: op.hash }) - return - } - log.info("file did not exist in snapshot, deleting", { file: op.file, hash: op.hash }) - yield* remove(op.file) - }) - - const clash = (a: string, b: string) => a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`) - - for (let i = 0; i < ops.length; ) { - const first = ops[i]! - const run = [first] - let j = i + 1 - // Only batch adjacent files when their paths cannot affect each other. - while (j < ops.length && run.length < 100) { - const next = ops[j]! - if (next.hash !== first.hash) break - if (run.some((item) => clash(item.rel, next.rel))) break - run.push(next) - j += 1 - } - - if (run.length === 1) { - yield* single(first) - i = j - continue - } - - const tree = yield* git( - [...core, ...args(["ls-tree", "--name-only", first.hash, "--", ...run.map((item) => item.rel)])], - { - cwd: state.worktree, - }, - ) - - if (tree.code !== 0) { - log.info("batched ls-tree failed, falling back to single-file revert", { - hash: first.hash, - files: run.length, - }) - for (const op of run) { - yield* single(op) - } - i = j - continue - } - - const have = new Set( - tree.text - .trim() - .split("\n") - .map((item) => item.trim()) - .filter(Boolean), - ) - const list = run.filter((item) => have.has(item.rel)) - if (list.length) { - log.info("reverting", { hash: first.hash, files: list.length }) - const result = yield* git( - [...core, ...args(["checkout", first.hash, "--", ...list.map((item) => item.file)])], - { - cwd: state.worktree, - }, - ) - if (result.code !== 0) { - log.info("batched checkout failed, falling back to single-file revert", { - hash: first.hash, - files: list.length, - }) - for (const op of run) { - yield* single(op) - } - i = j - continue - } - } - - for (const op of run) { - if (have.has(op.rel)) continue - log.info("file did not exist in snapshot, deleting", { file: op.file, hash: op.hash }) - yield* remove(op.file) - } - - i = j - } - }), - ) - }) - - const diff = Effect.fnUntraced(function* (hash: string) { - return yield* locked( - Effect.gen(function* () { - yield* add() - const result = yield* git([...quote, ...args(["diff", "--cached", "--no-ext-diff", hash, "--", "."])], { - cwd: state.worktree, - }) - if (result.code !== 0) { - log.warn("failed to get diff", { - hash, - exitCode: result.code, - stderr: result.stderr, - }) - return "" - } - return result.text.trim() - }), - ) - }) - - const diffFull = Effect.fnUntraced(function* (from: string, to: string) { - return yield* locked( - Effect.gen(function* () { - type Row = { - file: string - status: "added" | "deleted" | "modified" - binary: boolean - additions: number - deletions: number - } - - type Ref = { - file: string - side: "before" | "after" - ref: string - } - - const show = Effect.fnUntraced(function* (row: Row) { - if (row.binary) return ["", ""] - if (row.status === "added") { - return [ - "", - yield* git([...cfg, ...args(["show", `${to}:${row.file}`])]).pipe( - Effect.map((item) => item.text), - ), - ] - } - if (row.status === "deleted") { - return [ - yield* git([...cfg, ...args(["show", `${from}:${row.file}`])]).pipe( - Effect.map((item) => item.text), - ), - "", - ] - } - return yield* Effect.all( - [ - git([...cfg, ...args(["show", `${from}:${row.file}`])]).pipe(Effect.map((item) => item.text)), - git([...cfg, ...args(["show", `${to}:${row.file}`])]).pipe(Effect.map((item) => item.text)), - ], - { concurrency: 2 }, - ) - }) - - const load = Effect.fnUntraced( - function* (rows: Row[]) { - const refs = rows.flatMap((row) => { - if (row.binary) return [] - if (row.status === "added") - return [{ file: row.file, side: "after", ref: `${to}:${row.file}` } satisfies Ref] - if (row.status === "deleted") { - return [{ file: row.file, side: "before", ref: `${from}:${row.file}` } satisfies Ref] - } - return [ - { file: row.file, side: "before", ref: `${from}:${row.file}` } satisfies Ref, - { file: row.file, side: "after", ref: `${to}:${row.file}` } satisfies Ref, - ] - }) - if (!refs.length) return new Map() - - const proc = ChildProcess.make("git", [...cfg, ...args(["cat-file", "--batch"])], { - cwd: state.directory, - extendEnv: true, - stdin: Stream.make(new TextEncoder().encode(refs.map((item) => item.ref).join("\n") + "\n")), - }) - const handle = yield* spawner.spawn(proc) - const [out, err] = yield* Effect.all( - [Stream.mkUint8Array(handle.stdout), Stream.mkString(Stream.decodeText(handle.stderr))], - { concurrency: 2 }, - ) - const code = yield* handle.exitCode - if (code !== 0) { - log.info("git cat-file --batch failed during snapshot diff, falling back to per-file git show", { - stderr: err, - refs: refs.length, - }) - return - } - - const fail = (msg: string, extra?: Record) => { - log.info(msg, { ...extra, refs: refs.length }) - return undefined - } - - const map = new Map() - const dec = new TextDecoder() - let i = 0 - for (const ref of refs) { - let end = i - while (end < out.length && out[end] !== 10) end += 1 - if (end >= out.length) { - return fail( - "git cat-file --batch returned a truncated header during snapshot diff, falling back to per-file git show", - ) - } - - const head = dec.decode(out.slice(i, end)) - i = end + 1 - const hit = map.get(ref.file) ?? { before: "", after: "" } - if (head.endsWith(" missing")) { - map.set(ref.file, hit) - continue - } - - const match = head.match(/^[0-9a-f]+ blob (\d+)$/) - if (!match) { - return fail( - "git cat-file --batch returned an unexpected header during snapshot diff, falling back to per-file git show", - { head }, - ) - } - - const size = Number(match[1]) - if (!Number.isInteger(size) || size < 0 || i + size >= out.length || out[i + size] !== 10) { - return fail( - "git cat-file --batch returned truncated content during snapshot diff, falling back to per-file git show", - { head }, - ) - } - - const text = dec.decode(out.slice(i, i + size)) - if (ref.side === "before") hit.before = text - if (ref.side === "after") hit.after = text - map.set(ref.file, hit) - i += size + 1 - } - - if (i !== out.length) { - return fail( - "git cat-file --batch returned trailing data during snapshot diff, falling back to per-file git show", - ) - } - - return map - }, - Effect.scoped, - Effect.catch(() => - Effect.succeed | undefined>(undefined), - ), - ) - - const result: Snapshot.FileDiff[] = [] - const status = new Map() - - const statuses = yield* git( - [...quote, ...args(["diff", "--no-ext-diff", "--name-status", "--no-renames", from, to, "--", "."])], - { cwd: state.directory }, - ) - - for (const line of statuses.text.trim().split("\n")) { - if (!line) continue - const [code, file] = line.split("\t") - if (!code || !file) continue - status.set(file, code.startsWith("A") ? "added" : code.startsWith("D") ? "deleted" : "modified") - } - - const numstat = yield* git( - [...quote, ...args(["diff", "--no-ext-diff", "--no-renames", "--numstat", from, to, "--", "."])], - { - cwd: state.directory, - }, - ) - - const rows = numstat.text - .trim() - .split("\n") - .filter(Boolean) - .flatMap((line) => { - const [adds, dels, file] = line.split("\t") - if (!file) return [] - const binary = adds === "-" && dels === "-" - const additions = binary ? 0 : parseInt(adds) - const deletions = binary ? 0 : parseInt(dels) - return [ - { - file, - status: status.get(file) ?? "modified", - binary, - additions: Number.isFinite(additions) ? additions : 0, - deletions: Number.isFinite(deletions) ? deletions : 0, - } satisfies Row, - ] - }) - - // Hide ignored-file removals from the user-facing diff output. - const ignored = yield* ignore(rows.map((r) => r.file)) - if (ignored.size > 0) { - const filtered = rows.filter((r) => !ignored.has(r.file)) - rows.length = 0 - rows.push(...filtered) - } - - const step = 100 - const patch = (file: string, before: string, after: string) => - formatPatch(structuredPatch(file, file, before, after, "", "", { context: Number.MAX_SAFE_INTEGER })) - - for (let i = 0; i < rows.length; i += step) { - const run = rows.slice(i, i + step) - const text = yield* load(run) - - for (const row of run) { - const hit = text?.get(row.file) ?? { before: "", after: "" } - const [before, after] = row.binary ? ["", ""] : text ? [hit.before, hit.after] : yield* show(row) - result.push({ - file: row.file, - patch: row.binary ? "" : patch(row.file, before, after), - additions: row.additions, - deletions: row.deletions, - status: row.status, - }) - } - } - - return result - }), - ) - }) - - yield* cleanup().pipe( - Effect.catchCause((cause) => { - log.error("cleanup loop failed", { cause: Cause.pretty(cause) }) - return Effect.void - }), - Effect.repeat(Schedule.spaced(Duration.hours(1))), - Effect.delay(Duration.minutes(1)), - Effect.forkScoped, - ) - - return { cleanup, track, patch, restore, revert, diff, diffFull } - }), - ) - - return Service.of({ - init: Effect.fn("Snapshot.init")(function* () { - yield* InstanceState.get(state) - }), - cleanup: Effect.fn("Snapshot.cleanup")(function* () { - return yield* InstanceState.useEffect(state, (s) => s.cleanup()) - }), - track: Effect.fn("Snapshot.track")(function* () { - return yield* InstanceState.useEffect(state, (s) => s.track()) - }), - patch: Effect.fn("Snapshot.patch")(function* (hash: string) { - return yield* InstanceState.useEffect(state, (s) => s.patch(hash)) - }), - restore: Effect.fn("Snapshot.restore")(function* (snapshot: string) { - return yield* InstanceState.useEffect(state, (s) => s.restore(snapshot)) - }), - revert: Effect.fn("Snapshot.revert")(function* (patches: Snapshot.Patch[]) { - return yield* InstanceState.useEffect(state, (s) => s.revert(patches)) - }), - diff: Effect.fn("Snapshot.diff")(function* (hash: string) { - return yield* InstanceState.useEffect(state, (s) => s.diff(hash)) - }), - diffFull: Effect.fn("Snapshot.diffFull")(function* (from: string, to: string) { - return yield* InstanceState.useEffect(state, (s) => s.diffFull(from, to)) - }), - }) - }), - ) - - export const defaultLayer = layer.pipe( - Layer.provide(CrossSpawnSpawner.defaultLayer), - Layer.provide(AppFileSystem.defaultLayer), - Layer.provide(Config.defaultLayer), - ) -} +export * as Snapshot from "./snapshot" diff --git a/packages/opencode/src/snapshot/snapshot.ts b/packages/opencode/src/snapshot/snapshot.ts new file mode 100644 index 0000000000..32c637a216 --- /dev/null +++ b/packages/opencode/src/snapshot/snapshot.ts @@ -0,0 +1,777 @@ +import { Cause, Duration, Effect, Layer, Schedule, Semaphore, Context, Stream } from "effect" +import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" +import { formatPatch, structuredPatch } from "diff" +import path from "path" +import z from "zod" +import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" +import { InstanceState } from "@/effect/instance-state" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { Hash } from "@opencode-ai/shared/util/hash" +import { Config } from "../config" +import { Global } from "../global" +import { Log } from "../util/log" + +export const Patch = z.object({ + hash: z.string(), + files: z.string().array(), +}) +export type Patch = z.infer + +export const FileDiff = z + .object({ + file: z.string(), + patch: z.string(), + additions: z.number(), + deletions: z.number(), + status: z.enum(["added", "deleted", "modified"]).optional(), + }) + .meta({ + ref: "SnapshotFileDiff", + }) +export type FileDiff = z.infer + +const log = Log.create({ service: "snapshot" }) +const prune = "7.days" +const limit = 2 * 1024 * 1024 +const core = ["-c", "core.longpaths=true", "-c", "core.symlinks=true"] +const cfg = ["-c", "core.autocrlf=false", ...core] +const quote = [...cfg, "-c", "core.quotepath=false"] +interface GitResult { + readonly code: ChildProcessSpawner.ExitCode + readonly text: string + readonly stderr: string +} + +type State = Omit + +export interface Interface { + readonly init: () => Effect.Effect + readonly cleanup: () => Effect.Effect + readonly track: () => Effect.Effect + readonly patch: (hash: string) => Effect.Effect + readonly restore: (snapshot: string) => Effect.Effect + readonly revert: (patches: Patch[]) => Effect.Effect + readonly diff: (hash: string) => Effect.Effect + readonly diffFull: (from: string, to: string) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/Snapshot") {} + +export const layer: Layer.Layer< + Service, + never, + AppFileSystem.Service | ChildProcessSpawner.ChildProcessSpawner | Config.Service +> = Layer.effect( + Service, + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner + const config = yield* Config.Service + const locks = new Map() + + const lock = (key: string) => { + const hit = locks.get(key) + if (hit) return hit + + const next = Semaphore.makeUnsafe(1) + locks.set(key, next) + return next + } + + const state = yield* InstanceState.make( + Effect.fn("Snapshot.state")(function* (ctx) { + const state = { + directory: ctx.directory, + worktree: ctx.worktree, + gitdir: path.join(Global.Path.data, "snapshot", ctx.project.id, Hash.fast(ctx.worktree)), + vcs: ctx.project.vcs, + } + + const args = (cmd: string[]) => ["--git-dir", state.gitdir, "--work-tree", state.worktree, ...cmd] + + const enc = new TextEncoder() + const feed = (list: string[]) => Stream.make(enc.encode(list.join("\0") + "\0")) + + const git = Effect.fnUntraced( + function* ( + cmd: string[], + opts?: { cwd?: string; env?: Record; stdin?: ChildProcess.CommandInput }, + ) { + const proc = ChildProcess.make("git", cmd, { + cwd: opts?.cwd, + env: opts?.env, + extendEnv: true, + stdin: opts?.stdin, + }) + const handle = yield* spawner.spawn(proc) + const [text, stderr] = yield* Effect.all( + [Stream.mkString(Stream.decodeText(handle.stdout)), Stream.mkString(Stream.decodeText(handle.stderr))], + { concurrency: 2 }, + ) + const code = yield* handle.exitCode + return { code, text, stderr } satisfies GitResult + }, + Effect.scoped, + Effect.catch((err) => + Effect.succeed({ + code: ChildProcessSpawner.ExitCode(1), + text: "", + stderr: String(err), + }), + ), + ) + + const ignore = Effect.fnUntraced(function* (files: string[]) { + if (!files.length) return new Set() + const check = yield* git( + [ + ...quote, + "--git-dir", + path.join(state.worktree, ".git"), + "--work-tree", + state.worktree, + "check-ignore", + "--no-index", + "--stdin", + "-z", + ], + { + cwd: state.directory, + stdin: feed(files), + }, + ) + if (check.code !== 0 && check.code !== 1) return new Set() + return new Set(check.text.split("\0").filter(Boolean)) + }) + + const drop = Effect.fnUntraced(function* (files: string[]) { + if (!files.length) return + yield* git( + [ + ...cfg, + ...args(["rm", "--cached", "-f", "--ignore-unmatch", "--pathspec-from-file=-", "--pathspec-file-nul"]), + ], + { + cwd: state.directory, + stdin: feed(files), + }, + ) + }) + + const stage = Effect.fnUntraced(function* (files: string[]) { + if (!files.length) return + const result = yield* git( + [...cfg, ...args(["add", "--all", "--sparse", "--pathspec-from-file=-", "--pathspec-file-nul"])], + { + cwd: state.directory, + stdin: feed(files), + }, + ) + if (result.code === 0) return + log.warn("failed to add snapshot files", { + exitCode: result.code, + stderr: result.stderr, + }) + }) + + const exists = (file: string) => fs.exists(file).pipe(Effect.orDie) + const read = (file: string) => fs.readFileString(file).pipe(Effect.catch(() => Effect.succeed(""))) + const remove = (file: string) => fs.remove(file).pipe(Effect.catch(() => Effect.void)) + const locked = (fx: Effect.Effect) => lock(state.gitdir).withPermits(1)(fx) + + const enabled = Effect.fnUntraced(function* () { + if (state.vcs !== "git") return false + return (yield* config.get()).snapshot !== false + }) + + const excludes = Effect.fnUntraced(function* () { + const result = yield* git(["rev-parse", "--path-format=absolute", "--git-path", "info/exclude"], { + cwd: state.worktree, + }) + const file = result.text.trim() + if (!file) return + if (!(yield* exists(file))) return + return file + }) + + const sync = Effect.fnUntraced(function* (list: string[] = []) { + const file = yield* excludes() + const target = path.join(state.gitdir, "info", "exclude") + const text = [ + file ? (yield* read(file)).trimEnd() : "", + ...list.map((item) => `/${item.replaceAll("\\", "/")}`), + ] + .filter(Boolean) + .join("\n") + yield* fs.ensureDir(path.join(state.gitdir, "info")).pipe(Effect.orDie) + yield* fs.writeFileString(target, text ? `${text}\n` : "").pipe(Effect.orDie) + }) + + const add = Effect.fnUntraced(function* () { + yield* sync() + const [diff, other] = yield* Effect.all( + [ + git([...quote, ...args(["diff-files", "--name-only", "-z", "--", "."])], { + cwd: state.directory, + }), + git([...quote, ...args(["ls-files", "--others", "--exclude-standard", "-z", "--", "."])], { + cwd: state.directory, + }), + ], + { concurrency: 2 }, + ) + if (diff.code !== 0 || other.code !== 0) { + log.warn("failed to list snapshot files", { + diffCode: diff.code, + diffStderr: diff.stderr, + otherCode: other.code, + otherStderr: other.stderr, + }) + return + } + + const tracked = diff.text.split("\0").filter(Boolean) + const untracked = other.text.split("\0").filter(Boolean) + const all = Array.from(new Set([...tracked, ...untracked])) + if (!all.length) return + + // Resolve source-repo ignore rules against the exact candidate set. + // --no-index keeps this pattern-based even when a path is already tracked. + const ignored = yield* ignore(all) + + // Remove newly-ignored files from snapshot index to prevent re-adding + if (ignored.size > 0) { + const ignoredFiles = Array.from(ignored) + log.info("removing gitignored files from snapshot", { count: ignoredFiles.length }) + yield* drop(ignoredFiles) + } + + const allow = all.filter((item) => !ignored.has(item)) + if (!allow.length) return + + const large = new Set( + (yield* Effect.all( + allow.map((item) => + fs + .stat(path.join(state.directory, item)) + .pipe(Effect.catch(() => Effect.void)) + .pipe( + Effect.map((stat) => { + if (!stat || stat.type !== "File") return + const size = typeof stat.size === "bigint" ? Number(stat.size) : stat.size + return size > limit ? item : undefined + }), + ), + ), + { concurrency: 8 }, + )).filter((item): item is string => Boolean(item)), + ) + const block = new Set(untracked.filter((item) => large.has(item))) + yield* sync(Array.from(block)) + // Stage only the allowed candidate paths so snapshot updates stay scoped. + yield* stage(allow.filter((item) => !block.has(item))) + }) + + const cleanup = Effect.fnUntraced(function* () { + return yield* locked( + Effect.gen(function* () { + if (!(yield* enabled())) return + if (!(yield* exists(state.gitdir))) return + const result = yield* git(args(["gc", `--prune=${prune}`]), { cwd: state.directory }) + if (result.code !== 0) { + log.warn("cleanup failed", { + exitCode: result.code, + stderr: result.stderr, + }) + return + } + log.info("cleanup", { prune }) + }), + ) + }) + + const track = Effect.fnUntraced(function* () { + return yield* locked( + Effect.gen(function* () { + if (!(yield* enabled())) return + const existed = yield* exists(state.gitdir) + yield* fs.ensureDir(state.gitdir).pipe(Effect.orDie) + if (!existed) { + yield* git(["init"], { + env: { GIT_DIR: state.gitdir, GIT_WORK_TREE: state.worktree }, + }) + yield* git(["--git-dir", state.gitdir, "config", "core.autocrlf", "false"]) + yield* git(["--git-dir", state.gitdir, "config", "core.longpaths", "true"]) + yield* git(["--git-dir", state.gitdir, "config", "core.symlinks", "true"]) + yield* git(["--git-dir", state.gitdir, "config", "core.fsmonitor", "false"]) + log.info("initialized") + } + yield* add() + const result = yield* git(args(["write-tree"]), { cwd: state.directory }) + const hash = result.text.trim() + log.info("tracking", { hash, cwd: state.directory, git: state.gitdir }) + return hash + }), + ) + }) + + const patch = Effect.fnUntraced(function* (hash: string) { + return yield* locked( + Effect.gen(function* () { + yield* add() + const result = yield* git( + [...quote, ...args(["diff", "--cached", "--no-ext-diff", "--name-only", hash, "--", "."])], + { + cwd: state.directory, + }, + ) + if (result.code !== 0) { + log.warn("failed to get diff", { hash, exitCode: result.code }) + return { hash, files: [] } + } + const files = result.text + .trim() + .split("\n") + .map((x) => x.trim()) + .filter(Boolean) + + // Hide ignored-file removals from the user-facing patch output. + const ignored = yield* ignore(files) + + return { + hash, + files: files + .filter((item) => !ignored.has(item)) + .map((x) => path.join(state.worktree, x).replaceAll("\\", "/")), + } + }), + ) + }) + + const restore = Effect.fnUntraced(function* (snapshot: string) { + return yield* locked( + Effect.gen(function* () { + log.info("restore", { commit: snapshot }) + const result = yield* git([...core, ...args(["read-tree", snapshot])], { cwd: state.worktree }) + if (result.code === 0) { + const checkout = yield* git([...core, ...args(["checkout-index", "-a", "-f"])], { + cwd: state.worktree, + }) + if (checkout.code === 0) return + log.error("failed to restore snapshot", { + snapshot, + exitCode: checkout.code, + stderr: checkout.stderr, + }) + return + } + log.error("failed to restore snapshot", { + snapshot, + exitCode: result.code, + stderr: result.stderr, + }) + }), + ) + }) + + const revert = Effect.fnUntraced(function* (patches: Patch[]) { + return yield* locked( + Effect.gen(function* () { + const ops: { hash: string; file: string; rel: string }[] = [] + const seen = new Set() + for (const item of patches) { + for (const file of item.files) { + if (seen.has(file)) continue + seen.add(file) + ops.push({ + hash: item.hash, + file, + rel: path.relative(state.worktree, file).replaceAll("\\", "/"), + }) + } + } + + const single = Effect.fnUntraced(function* (op: (typeof ops)[number]) { + log.info("reverting", { file: op.file, hash: op.hash }) + const result = yield* git([...core, ...args(["checkout", op.hash, "--", op.file])], { + cwd: state.worktree, + }) + if (result.code === 0) return + const tree = yield* git([...core, ...args(["ls-tree", op.hash, "--", op.rel])], { + cwd: state.worktree, + }) + if (tree.code === 0 && tree.text.trim()) { + log.info("file existed in snapshot but checkout failed, keeping", { file: op.file, hash: op.hash }) + return + } + log.info("file did not exist in snapshot, deleting", { file: op.file, hash: op.hash }) + yield* remove(op.file) + }) + + const clash = (a: string, b: string) => a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`) + + for (let i = 0; i < ops.length; ) { + const first = ops[i]! + const run = [first] + let j = i + 1 + // Only batch adjacent files when their paths cannot affect each other. + while (j < ops.length && run.length < 100) { + const next = ops[j]! + if (next.hash !== first.hash) break + if (run.some((item) => clash(item.rel, next.rel))) break + run.push(next) + j += 1 + } + + if (run.length === 1) { + yield* single(first) + i = j + continue + } + + const tree = yield* git( + [...core, ...args(["ls-tree", "--name-only", first.hash, "--", ...run.map((item) => item.rel)])], + { + cwd: state.worktree, + }, + ) + + if (tree.code !== 0) { + log.info("batched ls-tree failed, falling back to single-file revert", { + hash: first.hash, + files: run.length, + }) + for (const op of run) { + yield* single(op) + } + i = j + continue + } + + const have = new Set( + tree.text + .trim() + .split("\n") + .map((item) => item.trim()) + .filter(Boolean), + ) + const list = run.filter((item) => have.has(item.rel)) + if (list.length) { + log.info("reverting", { hash: first.hash, files: list.length }) + const result = yield* git( + [...core, ...args(["checkout", first.hash, "--", ...list.map((item) => item.file)])], + { + cwd: state.worktree, + }, + ) + if (result.code !== 0) { + log.info("batched checkout failed, falling back to single-file revert", { + hash: first.hash, + files: list.length, + }) + for (const op of run) { + yield* single(op) + } + i = j + continue + } + } + + for (const op of run) { + if (have.has(op.rel)) continue + log.info("file did not exist in snapshot, deleting", { file: op.file, hash: op.hash }) + yield* remove(op.file) + } + + i = j + } + }), + ) + }) + + const diff = Effect.fnUntraced(function* (hash: string) { + return yield* locked( + Effect.gen(function* () { + yield* add() + const result = yield* git([...quote, ...args(["diff", "--cached", "--no-ext-diff", hash, "--", "."])], { + cwd: state.worktree, + }) + if (result.code !== 0) { + log.warn("failed to get diff", { + hash, + exitCode: result.code, + stderr: result.stderr, + }) + return "" + } + return result.text.trim() + }), + ) + }) + + const diffFull = Effect.fnUntraced(function* (from: string, to: string) { + return yield* locked( + Effect.gen(function* () { + type Row = { + file: string + status: "added" | "deleted" | "modified" + binary: boolean + additions: number + deletions: number + } + + type Ref = { + file: string + side: "before" | "after" + ref: string + } + + const show = Effect.fnUntraced(function* (row: Row) { + if (row.binary) return ["", ""] + if (row.status === "added") { + return [ + "", + yield* git([...cfg, ...args(["show", `${to}:${row.file}`])]).pipe( + Effect.map((item) => item.text), + ), + ] + } + if (row.status === "deleted") { + return [ + yield* git([...cfg, ...args(["show", `${from}:${row.file}`])]).pipe( + Effect.map((item) => item.text), + ), + "", + ] + } + return yield* Effect.all( + [ + git([...cfg, ...args(["show", `${from}:${row.file}`])]).pipe(Effect.map((item) => item.text)), + git([...cfg, ...args(["show", `${to}:${row.file}`])]).pipe(Effect.map((item) => item.text)), + ], + { concurrency: 2 }, + ) + }) + + const load = Effect.fnUntraced( + function* (rows: Row[]) { + const refs = rows.flatMap((row) => { + if (row.binary) return [] + if (row.status === "added") + return [{ file: row.file, side: "after", ref: `${to}:${row.file}` } satisfies Ref] + if (row.status === "deleted") { + return [{ file: row.file, side: "before", ref: `${from}:${row.file}` } satisfies Ref] + } + return [ + { file: row.file, side: "before", ref: `${from}:${row.file}` } satisfies Ref, + { file: row.file, side: "after", ref: `${to}:${row.file}` } satisfies Ref, + ] + }) + if (!refs.length) return new Map() + + const proc = ChildProcess.make("git", [...cfg, ...args(["cat-file", "--batch"])], { + cwd: state.directory, + extendEnv: true, + stdin: Stream.make(new TextEncoder().encode(refs.map((item) => item.ref).join("\n") + "\n")), + }) + const handle = yield* spawner.spawn(proc) + const [out, err] = yield* Effect.all( + [Stream.mkUint8Array(handle.stdout), Stream.mkString(Stream.decodeText(handle.stderr))], + { concurrency: 2 }, + ) + const code = yield* handle.exitCode + if (code !== 0) { + log.info("git cat-file --batch failed during snapshot diff, falling back to per-file git show", { + stderr: err, + refs: refs.length, + }) + return + } + + const fail = (msg: string, extra?: Record) => { + log.info(msg, { ...extra, refs: refs.length }) + return undefined + } + + const map = new Map() + const dec = new TextDecoder() + let i = 0 + for (const ref of refs) { + let end = i + while (end < out.length && out[end] !== 10) end += 1 + if (end >= out.length) { + return fail( + "git cat-file --batch returned a truncated header during snapshot diff, falling back to per-file git show", + ) + } + + const head = dec.decode(out.slice(i, end)) + i = end + 1 + const hit = map.get(ref.file) ?? { before: "", after: "" } + if (head.endsWith(" missing")) { + map.set(ref.file, hit) + continue + } + + const match = head.match(/^[0-9a-f]+ blob (\d+)$/) + if (!match) { + return fail( + "git cat-file --batch returned an unexpected header during snapshot diff, falling back to per-file git show", + { head }, + ) + } + + const size = Number(match[1]) + if (!Number.isInteger(size) || size < 0 || i + size >= out.length || out[i + size] !== 10) { + return fail( + "git cat-file --batch returned truncated content during snapshot diff, falling back to per-file git show", + { head }, + ) + } + + const text = dec.decode(out.slice(i, i + size)) + if (ref.side === "before") hit.before = text + if (ref.side === "after") hit.after = text + map.set(ref.file, hit) + i += size + 1 + } + + if (i !== out.length) { + return fail( + "git cat-file --batch returned trailing data during snapshot diff, falling back to per-file git show", + ) + } + + return map + }, + Effect.scoped, + Effect.catch(() => + Effect.succeed | undefined>(undefined), + ), + ) + + const result: FileDiff[] = [] + const status = new Map() + + const statuses = yield* git( + [...quote, ...args(["diff", "--no-ext-diff", "--name-status", "--no-renames", from, to, "--", "."])], + { cwd: state.directory }, + ) + + for (const line of statuses.text.trim().split("\n")) { + if (!line) continue + const [code, file] = line.split("\t") + if (!code || !file) continue + status.set(file, code.startsWith("A") ? "added" : code.startsWith("D") ? "deleted" : "modified") + } + + const numstat = yield* git( + [...quote, ...args(["diff", "--no-ext-diff", "--no-renames", "--numstat", from, to, "--", "."])], + { + cwd: state.directory, + }, + ) + + const rows = numstat.text + .trim() + .split("\n") + .filter(Boolean) + .flatMap((line) => { + const [adds, dels, file] = line.split("\t") + if (!file) return [] + const binary = adds === "-" && dels === "-" + const additions = binary ? 0 : parseInt(adds) + const deletions = binary ? 0 : parseInt(dels) + return [ + { + file, + status: status.get(file) ?? "modified", + binary, + additions: Number.isFinite(additions) ? additions : 0, + deletions: Number.isFinite(deletions) ? deletions : 0, + } satisfies Row, + ] + }) + + // Hide ignored-file removals from the user-facing diff output. + const ignored = yield* ignore(rows.map((r) => r.file)) + if (ignored.size > 0) { + const filtered = rows.filter((r) => !ignored.has(r.file)) + rows.length = 0 + rows.push(...filtered) + } + + const step = 100 + const patch = (file: string, before: string, after: string) => + formatPatch(structuredPatch(file, file, before, after, "", "", { context: Number.MAX_SAFE_INTEGER })) + + for (let i = 0; i < rows.length; i += step) { + const run = rows.slice(i, i + step) + const text = yield* load(run) + + for (const row of run) { + const hit = text?.get(row.file) ?? { before: "", after: "" } + const [before, after] = row.binary ? ["", ""] : text ? [hit.before, hit.after] : yield* show(row) + result.push({ + file: row.file, + patch: row.binary ? "" : patch(row.file, before, after), + additions: row.additions, + deletions: row.deletions, + status: row.status, + }) + } + } + + return result + }), + ) + }) + + yield* cleanup().pipe( + Effect.catchCause((cause) => { + log.error("cleanup loop failed", { cause: Cause.pretty(cause) }) + return Effect.void + }), + Effect.repeat(Schedule.spaced(Duration.hours(1))), + Effect.delay(Duration.minutes(1)), + Effect.forkScoped, + ) + + return { cleanup, track, patch, restore, revert, diff, diffFull } + }), + ) + + return Service.of({ + init: Effect.fn("Snapshot.init")(function* () { + yield* InstanceState.get(state) + }), + cleanup: Effect.fn("Snapshot.cleanup")(function* () { + return yield* InstanceState.useEffect(state, (s) => s.cleanup()) + }), + track: Effect.fn("Snapshot.track")(function* () { + return yield* InstanceState.useEffect(state, (s) => s.track()) + }), + patch: Effect.fn("Snapshot.patch")(function* (hash: string) { + return yield* InstanceState.useEffect(state, (s) => s.patch(hash)) + }), + restore: Effect.fn("Snapshot.restore")(function* (snapshot: string) { + return yield* InstanceState.useEffect(state, (s) => s.restore(snapshot)) + }), + revert: Effect.fn("Snapshot.revert")(function* (patches: Patch[]) { + return yield* InstanceState.useEffect(state, (s) => s.revert(patches)) + }), + diff: Effect.fn("Snapshot.diff")(function* (hash: string) { + return yield* InstanceState.useEffect(state, (s) => s.diff(hash)) + }), + diffFull: Effect.fn("Snapshot.diffFull")(function* (from: string, to: string) { + return yield* InstanceState.useEffect(state, (s) => s.diffFull(from, to)) + }), + }) + }), +) + +export const defaultLayer = layer.pipe( + Layer.provide(CrossSpawnSpawner.defaultLayer), + Layer.provide(AppFileSystem.defaultLayer), + Layer.provide(Config.defaultLayer), +) From dc16488bd78ccecb10613cf8dbe017be4c9b2408 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 22:15:21 -0400 Subject: [PATCH 191/300] feat: unwrap uide namespace to flat exports + barrel (#22706) --- packages/opencode/src/ide/ide.ts | 71 ++++++++++++++++++++++++++++ packages/opencode/src/ide/index.ts | 74 +----------------------------- 2 files changed, 72 insertions(+), 73 deletions(-) create mode 100644 packages/opencode/src/ide/ide.ts diff --git a/packages/opencode/src/ide/ide.ts b/packages/opencode/src/ide/ide.ts new file mode 100644 index 0000000000..cbced9c3d8 --- /dev/null +++ b/packages/opencode/src/ide/ide.ts @@ -0,0 +1,71 @@ +import { BusEvent } from "@/bus/bus-event" +import z from "zod" +import { NamedError } from "@opencode-ai/shared/util/error" +import { Log } from "../util/log" +import { Process } from "@/util/process" + +const SUPPORTED_IDES = [ + { name: "Windsurf" as const, cmd: "windsurf" }, + { name: "Visual Studio Code - Insiders" as const, cmd: "code-insiders" }, + { name: "Visual Studio Code" as const, cmd: "code" }, + { name: "Cursor" as const, cmd: "cursor" }, + { name: "VSCodium" as const, cmd: "codium" }, +] + +const log = Log.create({ service: "ide" }) + +export const Event = { + Installed: BusEvent.define( + "ide.installed", + z.object({ + ide: z.string(), + }), + ), +} + +export const AlreadyInstalledError = NamedError.create("AlreadyInstalledError", z.object({})) + +export const InstallFailedError = NamedError.create( + "InstallFailedError", + z.object({ + stderr: z.string(), + }), +) + +export function ide() { + if (process.env["TERM_PROGRAM"] === "vscode") { + const v = process.env["GIT_ASKPASS"] + for (const ide of SUPPORTED_IDES) { + if (v?.includes(ide.name)) return ide.name + } + } + return "unknown" +} + +export function alreadyInstalled() { + return process.env["OPENCODE_CALLER"] === "vscode" || process.env["OPENCODE_CALLER"] === "vscode-insiders" +} + +export async function install(ide: (typeof SUPPORTED_IDES)[number]["name"]) { + const cmd = SUPPORTED_IDES.find((i) => i.name === ide)?.cmd + if (!cmd) throw new Error(`Unknown IDE: ${ide}`) + + const p = await Process.run([cmd, "--install-extension", "sst-dev.opencode"], { + nothrow: true, + }) + const stdout = p.stdout.toString() + const stderr = p.stderr.toString() + + log.info("installed", { + ide, + stdout, + stderr, + }) + + if (p.code !== 0) { + throw new InstallFailedError({ stderr }) + } + if (stdout.includes("already installed")) { + throw new AlreadyInstalledError({}) + } +} diff --git a/packages/opencode/src/ide/index.ts b/packages/opencode/src/ide/index.ts index 24ba53f82e..9716ecbc74 100644 --- a/packages/opencode/src/ide/index.ts +++ b/packages/opencode/src/ide/index.ts @@ -1,73 +1 @@ -import { BusEvent } from "@/bus/bus-event" -import z from "zod" -import { NamedError } from "@opencode-ai/shared/util/error" -import { Log } from "../util/log" -import { Process } from "@/util/process" - -const SUPPORTED_IDES = [ - { name: "Windsurf" as const, cmd: "windsurf" }, - { name: "Visual Studio Code - Insiders" as const, cmd: "code-insiders" }, - { name: "Visual Studio Code" as const, cmd: "code" }, - { name: "Cursor" as const, cmd: "cursor" }, - { name: "VSCodium" as const, cmd: "codium" }, -] - -export namespace Ide { - const log = Log.create({ service: "ide" }) - - export const Event = { - Installed: BusEvent.define( - "ide.installed", - z.object({ - ide: z.string(), - }), - ), - } - - export const AlreadyInstalledError = NamedError.create("AlreadyInstalledError", z.object({})) - - export const InstallFailedError = NamedError.create( - "InstallFailedError", - z.object({ - stderr: z.string(), - }), - ) - - export function ide() { - if (process.env["TERM_PROGRAM"] === "vscode") { - const v = process.env["GIT_ASKPASS"] - for (const ide of SUPPORTED_IDES) { - if (v?.includes(ide.name)) return ide.name - } - } - return "unknown" - } - - export function alreadyInstalled() { - return process.env["OPENCODE_CALLER"] === "vscode" || process.env["OPENCODE_CALLER"] === "vscode-insiders" - } - - export async function install(ide: (typeof SUPPORTED_IDES)[number]["name"]) { - const cmd = SUPPORTED_IDES.find((i) => i.name === ide)?.cmd - if (!cmd) throw new Error(`Unknown IDE: ${ide}`) - - const p = await Process.run([cmd, "--install-extension", "sst-dev.opencode"], { - nothrow: true, - }) - const stdout = p.stdout.toString() - const stderr = p.stderr.toString() - - log.info("installed", { - ide, - stdout, - stderr, - }) - - if (p.code !== 0) { - throw new InstallFailedError({ stderr }) - } - if (stdout.includes("already installed")) { - throw new AlreadyInstalledError({}) - } - } -} +export * as Ide from "./ide" From f7edffc11aeceeab1000026cfcd7063671a1e7bb Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 22:15:36 -0400 Subject: [PATCH 192/300] feat: unwrap uglobal namespace to flat exports + barrel (#22705) --- packages/opencode/src/global/global.ts | 56 ++++++++++++++++++++++++ packages/opencode/src/global/index.ts | 59 +------------------------- 2 files changed, 57 insertions(+), 58 deletions(-) create mode 100644 packages/opencode/src/global/global.ts diff --git a/packages/opencode/src/global/global.ts b/packages/opencode/src/global/global.ts new file mode 100644 index 0000000000..1bbb5968c9 --- /dev/null +++ b/packages/opencode/src/global/global.ts @@ -0,0 +1,56 @@ +import fs from "fs/promises" +import { xdgData, xdgCache, xdgConfig, xdgState } from "xdg-basedir" +import path from "path" +import os from "os" +import { Filesystem } from "../util/filesystem" +import { Flock } from "@opencode-ai/shared/util/flock" + +const app = "opencode" + +const data = path.join(xdgData!, app) +const cache = path.join(xdgCache!, app) +const config = path.join(xdgConfig!, app) +const state = path.join(xdgState!, app) + +export const Path = { + // Allow override via OPENCODE_TEST_HOME for test isolation + get home() { + return process.env.OPENCODE_TEST_HOME || os.homedir() + }, + data, + bin: path.join(cache, "bin"), + log: path.join(data, "log"), + cache, + config, + state, +} + +// Initialize Flock with global state path +Flock.setGlobal({ state }) + +await Promise.all([ + fs.mkdir(Path.data, { recursive: true }), + fs.mkdir(Path.config, { recursive: true }), + fs.mkdir(Path.state, { recursive: true }), + fs.mkdir(Path.log, { recursive: true }), + fs.mkdir(Path.bin, { recursive: true }), +]) + +const CACHE_VERSION = "21" + +const version = await Filesystem.readText(path.join(Path.cache, "version")).catch(() => "0") + +if (version !== CACHE_VERSION) { + try { + const contents = await fs.readdir(Path.cache) + await Promise.all( + contents.map((item) => + fs.rm(path.join(Path.cache, item), { + recursive: true, + force: true, + }), + ), + ) + } catch {} + await Filesystem.write(path.join(Path.cache, "version"), CACHE_VERSION) +} diff --git a/packages/opencode/src/global/index.ts b/packages/opencode/src/global/index.ts index df46397816..9262bf2a93 100644 --- a/packages/opencode/src/global/index.ts +++ b/packages/opencode/src/global/index.ts @@ -1,58 +1 @@ -import fs from "fs/promises" -import { xdgData, xdgCache, xdgConfig, xdgState } from "xdg-basedir" -import path from "path" -import os from "os" -import { Filesystem } from "../util/filesystem" -import { Flock } from "@opencode-ai/shared/util/flock" - -const app = "opencode" - -const data = path.join(xdgData!, app) -const cache = path.join(xdgCache!, app) -const config = path.join(xdgConfig!, app) -const state = path.join(xdgState!, app) - -export namespace Global { - export const Path = { - // Allow override via OPENCODE_TEST_HOME for test isolation - get home() { - return process.env.OPENCODE_TEST_HOME || os.homedir() - }, - data, - bin: path.join(cache, "bin"), - log: path.join(data, "log"), - cache, - config, - state, - } -} - -// Initialize Flock with global state path -Flock.setGlobal({ state }) - -await Promise.all([ - fs.mkdir(Global.Path.data, { recursive: true }), - fs.mkdir(Global.Path.config, { recursive: true }), - fs.mkdir(Global.Path.state, { recursive: true }), - fs.mkdir(Global.Path.log, { recursive: true }), - fs.mkdir(Global.Path.bin, { recursive: true }), -]) - -const CACHE_VERSION = "21" - -const version = await Filesystem.readText(path.join(Global.Path.cache, "version")).catch(() => "0") - -if (version !== CACHE_VERSION) { - try { - const contents = await fs.readdir(Global.Path.cache) - await Promise.all( - contents.map((item) => - fs.rm(path.join(Global.Path.cache, item), { - recursive: true, - force: true, - }), - ), - ) - } catch {} - await Filesystem.write(path.join(Global.Path.cache, "version"), CACHE_VERSION) -} +export * as Global from "./global" From a653a4b8871e5d58c56f588e4cd3b2001f8bc6a1 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 22:15:46 -0400 Subject: [PATCH 193/300] feat: unwrap usync namespace to flat exports + barrel (#22716) --- packages/opencode/src/sync/index.ts | 283 +---------------------- packages/opencode/src/sync/sync-event.ts | 280 ++++++++++++++++++++++ 2 files changed, 281 insertions(+), 282 deletions(-) create mode 100644 packages/opencode/src/sync/sync-event.ts diff --git a/packages/opencode/src/sync/index.ts b/packages/opencode/src/sync/index.ts index e89d57e181..a6dec180bd 100644 --- a/packages/opencode/src/sync/index.ts +++ b/packages/opencode/src/sync/index.ts @@ -1,282 +1 @@ -import z from "zod" -import type { ZodObject } from "zod" -import { Database, eq } from "@/storage/db" -import { GlobalBus } from "@/bus/global" -import { Bus as ProjectBus } from "@/bus" -import { BusEvent } from "@/bus/bus-event" -import { Instance } from "@/project/instance" -import { EventSequenceTable, EventTable } from "./event.sql" -import { WorkspaceContext } from "@/control-plane/workspace-context" -import { EventID } from "./schema" -import { Flag } from "@/flag/flag" - -export namespace SyncEvent { - export type Definition = { - type: string - version: number - aggregate: string - schema: z.ZodObject - - // This is temporary and only exists for compatibility with bus - // event definitions - properties: z.ZodObject - } - - export type Event = { - id: string - seq: number - aggregateID: string - data: z.infer - } - - export type SerializedEvent = Event & { type: string } - - type ProjectorFunc = (db: Database.TxOrDb, data: unknown) => void - - export const registry = new Map() - let projectors: Map | undefined - const versions = new Map() - let frozen = false - let convertEvent: (type: string, event: Event["data"]) => Promise> | Record - - export function reset() { - frozen = false - projectors = undefined - convertEvent = (_, data) => data - } - - export function init(input: { projectors: Array<[Definition, ProjectorFunc]>; convertEvent?: typeof convertEvent }) { - projectors = new Map(input.projectors) - - // Install all the latest event defs to the bus. We only ever emit - // latest versions from code, and keep around old versions for - // replaying. Replaying does not go through the bus, and it - // simplifies the bus to only use unversioned latest events - for (let [type, version] of versions.entries()) { - let def = registry.get(versionedType(type, version))! - - BusEvent.define(def.type, def.properties || def.schema) - } - - // Freeze the system so it clearly errors if events are defined - // after `init` which would cause bugs - frozen = true - convertEvent = input.convertEvent || ((_, data) => data) - } - - export function versionedType(type: A): A - export function versionedType(type: A, version: B): `${A}/${B}` - export function versionedType(type: string, version?: number) { - return version ? `${type}.${version}` : type - } - - export function define< - Type extends string, - Agg extends string, - Schema extends ZodObject>>, - BusSchema extends ZodObject = Schema, - >(input: { type: Type; version: number; aggregate: Agg; schema: Schema; busSchema?: BusSchema }) { - if (frozen) { - throw new Error("Error defining sync event: sync system has been frozen") - } - - const def = { - type: input.type, - version: input.version, - aggregate: input.aggregate, - schema: input.schema, - properties: input.busSchema ? input.busSchema : input.schema, - } - - versions.set(def.type, Math.max(def.version, versions.get(def.type) || 0)) - - registry.set(versionedType(def.type, def.version), def) - - return def - } - - export function project( - def: Def, - func: (db: Database.TxOrDb, data: Event["data"]) => void, - ): [Definition, ProjectorFunc] { - return [def, func as ProjectorFunc] - } - - function process(def: Def, event: Event, options: { publish: boolean }) { - if (projectors == null) { - throw new Error("No projectors available. Call `SyncEvent.init` to install projectors") - } - - const projector = projectors.get(def) - if (!projector) { - throw new Error(`Projector not found for event: ${def.type}`) - } - - // idempotent: need to ignore any events already logged - - Database.transaction((tx) => { - projector(tx, event.data) - - if (Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) { - tx.insert(EventSequenceTable) - .values({ - aggregate_id: event.aggregateID, - seq: event.seq, - }) - .onConflictDoUpdate({ - target: EventSequenceTable.aggregate_id, - set: { seq: event.seq }, - }) - .run() - tx.insert(EventTable) - .values({ - id: event.id, - seq: event.seq, - aggregate_id: event.aggregateID, - type: versionedType(def.type, def.version), - data: event.data as Record, - }) - .run() - } - - Database.effect(() => { - if (options?.publish) { - const result = convertEvent(def.type, event.data) - if (result instanceof Promise) { - result.then((data) => { - ProjectBus.publish({ type: def.type, properties: def.schema }, data) - }) - } else { - ProjectBus.publish({ type: def.type, properties: def.schema }, result) - } - - GlobalBus.emit("event", { - directory: Instance.directory, - project: Instance.project.id, - workspace: WorkspaceContext.workspaceID, - payload: { - type: "sync", - name: versionedType(def.type, def.version), - ...event, - }, - }) - } - }) - }) - } - - // TODO: - // - // * Support applying multiple events at one time. One transaction, - // and it validets all the sequence ids - // * when loading events from db, apply zod validation to ensure shape - - export function replay(event: SerializedEvent, options?: { publish: boolean }) { - const def = registry.get(event.type) - if (!def) { - throw new Error(`Unknown event type: ${event.type}`) - } - - const row = Database.use((db) => - db - .select({ seq: EventSequenceTable.seq }) - .from(EventSequenceTable) - .where(eq(EventSequenceTable.aggregate_id, event.aggregateID)) - .get(), - ) - - const latest = row?.seq ?? -1 - if (event.seq <= latest) { - return - } - - const expected = latest + 1 - if (event.seq !== expected) { - throw new Error(`Sequence mismatch for aggregate "${event.aggregateID}": expected ${expected}, got ${event.seq}`) - } - - process(def, event, { publish: !!options?.publish }) - } - - export function replayAll(events: SerializedEvent[], options?: { publish: boolean }) { - const source = events[0]?.aggregateID - if (!source) return - if (events.some((item) => item.aggregateID !== source)) { - throw new Error("Replay events must belong to the same session") - } - const start = events[0].seq - for (const [i, item] of events.entries()) { - const seq = start + i - if (item.seq !== seq) { - throw new Error(`Replay sequence mismatch at index ${i}: expected ${seq}, got ${item.seq}`) - } - } - for (const item of events) { - replay(item, options) - } - return source - } - - export function run(def: Def, data: Event["data"], options?: { publish?: boolean }) { - const agg = (data as Record)[def.aggregate] - // This should never happen: we've enforced it via typescript in - // the definition - if (agg == null) { - throw new Error(`SyncEvent.run: "${def.aggregate}" required but not found: ${JSON.stringify(data)}`) - } - - if (def.version !== versions.get(def.type)) { - throw new Error(`SyncEvent.run: running old versions of events is not allowed: ${def.type}`) - } - - const { publish = true } = options || {} - - // Note that this is an "immediate" transaction which is critical. - // We need to make sure we can safely read and write with nothing - // else changing the data from under us - Database.transaction( - (tx) => { - const id = EventID.ascending() - const row = tx - .select({ seq: EventSequenceTable.seq }) - .from(EventSequenceTable) - .where(eq(EventSequenceTable.aggregate_id, agg)) - .get() - const seq = row?.seq != null ? row.seq + 1 : 0 - - const event = { id, seq, aggregateID: agg, data } - process(def, event, { publish }) - }, - { - behavior: "immediate", - }, - ) - } - - export function remove(aggregateID: string) { - Database.transaction((tx) => { - tx.delete(EventSequenceTable).where(eq(EventSequenceTable.aggregate_id, aggregateID)).run() - tx.delete(EventTable).where(eq(EventTable.aggregate_id, aggregateID)).run() - }) - } - - export function payloads() { - return registry - .entries() - .map(([type, def]) => { - return z - .object({ - type: z.literal("sync"), - name: z.literal(type), - id: z.string(), - seq: z.number(), - aggregateID: z.literal(def.aggregate), - data: def.schema, - }) - .meta({ - ref: "SyncEvent" + "." + def.type, - }) - }) - .toArray() - } -} +export * as SyncEvent from "./sync-event" diff --git a/packages/opencode/src/sync/sync-event.ts b/packages/opencode/src/sync/sync-event.ts new file mode 100644 index 0000000000..2b1eb09810 --- /dev/null +++ b/packages/opencode/src/sync/sync-event.ts @@ -0,0 +1,280 @@ +import z from "zod" +import type { ZodObject } from "zod" +import { Database, eq } from "@/storage/db" +import { GlobalBus } from "@/bus/global" +import { Bus as ProjectBus } from "@/bus" +import { BusEvent } from "@/bus/bus-event" +import { Instance } from "@/project/instance" +import { EventSequenceTable, EventTable } from "./event.sql" +import { WorkspaceContext } from "@/control-plane/workspace-context" +import { EventID } from "./schema" +import { Flag } from "@/flag/flag" + +export type Definition = { + type: string + version: number + aggregate: string + schema: z.ZodObject + + // This is temporary and only exists for compatibility with bus + // event definitions + properties: z.ZodObject +} + +export type Event = { + id: string + seq: number + aggregateID: string + data: z.infer +} + +export type SerializedEvent = Event & { type: string } + +type ProjectorFunc = (db: Database.TxOrDb, data: unknown) => void + +export const registry = new Map() +let projectors: Map | undefined +const versions = new Map() +let frozen = false +let convertEvent: (type: string, event: Event["data"]) => Promise> | Record + +export function reset() { + frozen = false + projectors = undefined + convertEvent = (_, data) => data +} + +export function init(input: { projectors: Array<[Definition, ProjectorFunc]>; convertEvent?: typeof convertEvent }) { + projectors = new Map(input.projectors) + + // Install all the latest event defs to the bus. We only ever emit + // latest versions from code, and keep around old versions for + // replaying. Replaying does not go through the bus, and it + // simplifies the bus to only use unversioned latest events + for (let [type, version] of versions.entries()) { + let def = registry.get(versionedType(type, version))! + + BusEvent.define(def.type, def.properties || def.schema) + } + + // Freeze the system so it clearly errors if events are defined + // after `init` which would cause bugs + frozen = true + convertEvent = input.convertEvent || ((_, data) => data) +} + +export function versionedType(type: A): A +export function versionedType(type: A, version: B): `${A}/${B}` +export function versionedType(type: string, version?: number) { + return version ? `${type}.${version}` : type +} + +export function define< + Type extends string, + Agg extends string, + Schema extends ZodObject>>, + BusSchema extends ZodObject = Schema, +>(input: { type: Type; version: number; aggregate: Agg; schema: Schema; busSchema?: BusSchema }) { + if (frozen) { + throw new Error("Error defining sync event: sync system has been frozen") + } + + const def = { + type: input.type, + version: input.version, + aggregate: input.aggregate, + schema: input.schema, + properties: input.busSchema ? input.busSchema : input.schema, + } + + versions.set(def.type, Math.max(def.version, versions.get(def.type) || 0)) + + registry.set(versionedType(def.type, def.version), def) + + return def +} + +export function project( + def: Def, + func: (db: Database.TxOrDb, data: Event["data"]) => void, +): [Definition, ProjectorFunc] { + return [def, func as ProjectorFunc] +} + +function process(def: Def, event: Event, options: { publish: boolean }) { + if (projectors == null) { + throw new Error("No projectors available. Call `SyncEvent.init` to install projectors") + } + + const projector = projectors.get(def) + if (!projector) { + throw new Error(`Projector not found for event: ${def.type}`) + } + + // idempotent: need to ignore any events already logged + + Database.transaction((tx) => { + projector(tx, event.data) + + if (Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) { + tx.insert(EventSequenceTable) + .values({ + aggregate_id: event.aggregateID, + seq: event.seq, + }) + .onConflictDoUpdate({ + target: EventSequenceTable.aggregate_id, + set: { seq: event.seq }, + }) + .run() + tx.insert(EventTable) + .values({ + id: event.id, + seq: event.seq, + aggregate_id: event.aggregateID, + type: versionedType(def.type, def.version), + data: event.data as Record, + }) + .run() + } + + Database.effect(() => { + if (options?.publish) { + const result = convertEvent(def.type, event.data) + if (result instanceof Promise) { + result.then((data) => { + ProjectBus.publish({ type: def.type, properties: def.schema }, data) + }) + } else { + ProjectBus.publish({ type: def.type, properties: def.schema }, result) + } + + GlobalBus.emit("event", { + directory: Instance.directory, + project: Instance.project.id, + workspace: WorkspaceContext.workspaceID, + payload: { + type: "sync", + name: versionedType(def.type, def.version), + ...event, + }, + }) + } + }) + }) +} + +// TODO: +// +// * Support applying multiple events at one time. One transaction, +// and it validets all the sequence ids +// * when loading events from db, apply zod validation to ensure shape + +export function replay(event: SerializedEvent, options?: { publish: boolean }) { + const def = registry.get(event.type) + if (!def) { + throw new Error(`Unknown event type: ${event.type}`) + } + + const row = Database.use((db) => + db + .select({ seq: EventSequenceTable.seq }) + .from(EventSequenceTable) + .where(eq(EventSequenceTable.aggregate_id, event.aggregateID)) + .get(), + ) + + const latest = row?.seq ?? -1 + if (event.seq <= latest) { + return + } + + const expected = latest + 1 + if (event.seq !== expected) { + throw new Error(`Sequence mismatch for aggregate "${event.aggregateID}": expected ${expected}, got ${event.seq}`) + } + + process(def, event, { publish: !!options?.publish }) +} + +export function replayAll(events: SerializedEvent[], options?: { publish: boolean }) { + const source = events[0]?.aggregateID + if (!source) return + if (events.some((item) => item.aggregateID !== source)) { + throw new Error("Replay events must belong to the same session") + } + const start = events[0].seq + for (const [i, item] of events.entries()) { + const seq = start + i + if (item.seq !== seq) { + throw new Error(`Replay sequence mismatch at index ${i}: expected ${seq}, got ${item.seq}`) + } + } + for (const item of events) { + replay(item, options) + } + return source +} + +export function run(def: Def, data: Event["data"], options?: { publish?: boolean }) { + const agg = (data as Record)[def.aggregate] + // This should never happen: we've enforced it via typescript in + // the definition + if (agg == null) { + throw new Error(`SyncEvent.run: "${def.aggregate}" required but not found: ${JSON.stringify(data)}`) + } + + if (def.version !== versions.get(def.type)) { + throw new Error(`SyncEvent.run: running old versions of events is not allowed: ${def.type}`) + } + + const { publish = true } = options || {} + + // Note that this is an "immediate" transaction which is critical. + // We need to make sure we can safely read and write with nothing + // else changing the data from under us + Database.transaction( + (tx) => { + const id = EventID.ascending() + const row = tx + .select({ seq: EventSequenceTable.seq }) + .from(EventSequenceTable) + .where(eq(EventSequenceTable.aggregate_id, agg)) + .get() + const seq = row?.seq != null ? row.seq + 1 : 0 + + const event = { id, seq, aggregateID: agg, data } + process(def, event, { publish }) + }, + { + behavior: "immediate", + }, + ) +} + +export function remove(aggregateID: string) { + Database.transaction((tx) => { + tx.delete(EventSequenceTable).where(eq(EventSequenceTable.aggregate_id, aggregateID)).run() + tx.delete(EventTable).where(eq(EventTable.aggregate_id, aggregateID)).run() + }) +} + +export function payloads() { + return registry + .entries() + .map(([type, def]) => { + return z + .object({ + type: z.literal("sync"), + name: z.literal(type), + id: z.string(), + seq: z.number(), + aggregateID: z.literal(def.aggregate), + data: def.schema, + }) + .meta({ + ref: "SyncEvent" + "." + def.type, + }) + }) + .toArray() +} From e3677c2ba2077d2dfdd188c31a55f8cdc1efa209 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 22:15:58 -0400 Subject: [PATCH 194/300] feat: unwrap upatch namespace to flat exports + barrel (#22709) --- packages/opencode/src/patch/index.ts | 681 +-------------------------- packages/opencode/src/patch/patch.ts | 678 ++++++++++++++++++++++++++ 2 files changed, 679 insertions(+), 680 deletions(-) create mode 100644 packages/opencode/src/patch/patch.ts diff --git a/packages/opencode/src/patch/index.ts b/packages/opencode/src/patch/index.ts index f003606c4d..cec24614d8 100644 --- a/packages/opencode/src/patch/index.ts +++ b/packages/opencode/src/patch/index.ts @@ -1,680 +1 @@ -import z from "zod" -import * as path from "path" -import * as fs from "fs/promises" -import { readFileSync } from "fs" -import { Log } from "../util/log" - -export namespace Patch { - const log = Log.create({ service: "patch" }) - - // Schema definitions - export const PatchSchema = z.object({ - patchText: z.string().describe("The full patch text that describes all changes to be made"), - }) - - export type PatchParams = z.infer - - // Core types matching the Rust implementation - export interface ApplyPatchArgs { - patch: string - hunks: Hunk[] - workdir?: string - } - - export type Hunk = - | { type: "add"; path: string; contents: string } - | { type: "delete"; path: string } - | { type: "update"; path: string; move_path?: string; chunks: UpdateFileChunk[] } - - export interface UpdateFileChunk { - old_lines: string[] - new_lines: string[] - change_context?: string - is_end_of_file?: boolean - } - - export interface ApplyPatchAction { - changes: Map - patch: string - cwd: string - } - - export type ApplyPatchFileChange = - | { type: "add"; content: string } - | { type: "delete"; content: string } - | { type: "update"; unified_diff: string; move_path?: string; new_content: string } - - export interface AffectedPaths { - added: string[] - modified: string[] - deleted: string[] - } - - export enum ApplyPatchError { - ParseError = "ParseError", - IoError = "IoError", - ComputeReplacements = "ComputeReplacements", - ImplicitInvocation = "ImplicitInvocation", - } - - export enum MaybeApplyPatch { - Body = "Body", - ShellParseError = "ShellParseError", - PatchParseError = "PatchParseError", - NotApplyPatch = "NotApplyPatch", - } - - export enum MaybeApplyPatchVerified { - Body = "Body", - ShellParseError = "ShellParseError", - CorrectnessError = "CorrectnessError", - NotApplyPatch = "NotApplyPatch", - } - - // Parser implementation - function parsePatchHeader( - lines: string[], - startIdx: number, - ): { filePath: string; movePath?: string; nextIdx: number } | null { - const line = lines[startIdx] - - if (line.startsWith("*** Add File:")) { - const filePath = line.slice("*** Add File:".length).trim() - return filePath ? { filePath, nextIdx: startIdx + 1 } : null - } - - if (line.startsWith("*** Delete File:")) { - const filePath = line.slice("*** Delete File:".length).trim() - return filePath ? { filePath, nextIdx: startIdx + 1 } : null - } - - if (line.startsWith("*** Update File:")) { - const filePath = line.slice("*** Update File:".length).trim() - let movePath: string | undefined - let nextIdx = startIdx + 1 - - // Check for move directive - if (nextIdx < lines.length && lines[nextIdx].startsWith("*** Move to:")) { - movePath = lines[nextIdx].slice("*** Move to:".length).trim() - nextIdx++ - } - - return filePath ? { filePath, movePath, nextIdx } : null - } - - return null - } - - function parseUpdateFileChunks(lines: string[], startIdx: number): { chunks: UpdateFileChunk[]; nextIdx: number } { - const chunks: UpdateFileChunk[] = [] - let i = startIdx - - while (i < lines.length && !lines[i].startsWith("***")) { - if (lines[i].startsWith("@@")) { - // Parse context line - const contextLine = lines[i].substring(2).trim() - i++ - - const oldLines: string[] = [] - const newLines: string[] = [] - let isEndOfFile = false - - // Parse change lines - while (i < lines.length && !lines[i].startsWith("@@") && !lines[i].startsWith("***")) { - const changeLine = lines[i] - - if (changeLine === "*** End of File") { - isEndOfFile = true - i++ - break - } - - if (changeLine.startsWith(" ")) { - // Keep line - appears in both old and new - const content = changeLine.substring(1) - oldLines.push(content) - newLines.push(content) - } else if (changeLine.startsWith("-")) { - // Remove line - only in old - oldLines.push(changeLine.substring(1)) - } else if (changeLine.startsWith("+")) { - // Add line - only in new - newLines.push(changeLine.substring(1)) - } - - i++ - } - - chunks.push({ - old_lines: oldLines, - new_lines: newLines, - change_context: contextLine || undefined, - is_end_of_file: isEndOfFile || undefined, - }) - } else { - i++ - } - } - - return { chunks, nextIdx: i } - } - - function parseAddFileContent(lines: string[], startIdx: number): { content: string; nextIdx: number } { - let content = "" - let i = startIdx - - while (i < lines.length && !lines[i].startsWith("***")) { - if (lines[i].startsWith("+")) { - content += lines[i].substring(1) + "\n" - } - i++ - } - - // Remove trailing newline - if (content.endsWith("\n")) { - content = content.slice(0, -1) - } - - return { content, nextIdx: i } - } - - function stripHeredoc(input: string): string { - // Match heredoc patterns like: cat <<'EOF'\n...\nEOF or < line.trim() === beginMarker) - const endIdx = lines.findIndex((line) => line.trim() === endMarker) - - if (beginIdx === -1 || endIdx === -1 || beginIdx >= endIdx) { - throw new Error("Invalid patch format: missing Begin/End markers") - } - - // Parse content between markers - i = beginIdx + 1 - - while (i < endIdx) { - const header = parsePatchHeader(lines, i) - if (!header) { - i++ - continue - } - - if (lines[i].startsWith("*** Add File:")) { - const { content, nextIdx } = parseAddFileContent(lines, header.nextIdx) - hunks.push({ - type: "add", - path: header.filePath, - contents: content, - }) - i = nextIdx - } else if (lines[i].startsWith("*** Delete File:")) { - hunks.push({ - type: "delete", - path: header.filePath, - }) - i = header.nextIdx - } else if (lines[i].startsWith("*** Update File:")) { - const { chunks, nextIdx } = parseUpdateFileChunks(lines, header.nextIdx) - hunks.push({ - type: "update", - path: header.filePath, - move_path: header.movePath, - chunks, - }) - i = nextIdx - } else { - i++ - } - } - - return { hunks } - } - - // Apply patch functionality - export function maybeParseApplyPatch( - argv: string[], - ): - | { type: MaybeApplyPatch.Body; args: ApplyPatchArgs } - | { type: MaybeApplyPatch.PatchParseError; error: Error } - | { type: MaybeApplyPatch.NotApplyPatch } { - const APPLY_PATCH_COMMANDS = ["apply_patch", "applypatch"] - - // Direct invocation: apply_patch - if (argv.length === 2 && APPLY_PATCH_COMMANDS.includes(argv[0])) { - try { - const { hunks } = parsePatch(argv[1]) - return { - type: MaybeApplyPatch.Body, - args: { - patch: argv[1], - hunks, - }, - } - } catch (error) { - return { - type: MaybeApplyPatch.PatchParseError, - error: error as Error, - } - } - } - - // Bash heredoc form: bash -lc 'apply_patch <<"EOF" ...' - if (argv.length === 3 && argv[0] === "bash" && argv[1] === "-lc") { - // Simple extraction - in real implementation would need proper bash parsing - const script = argv[2] - const heredocMatch = script.match(/apply_patch\s*<<['"](\w+)['"]\s*\n([\s\S]*?)\n\1/) - - if (heredocMatch) { - const patchContent = heredocMatch[2] - try { - const { hunks } = parsePatch(patchContent) - return { - type: MaybeApplyPatch.Body, - args: { - patch: patchContent, - hunks, - }, - } - } catch (error) { - return { - type: MaybeApplyPatch.PatchParseError, - error: error as Error, - } - } - } - } - - return { type: MaybeApplyPatch.NotApplyPatch } - } - - // File content manipulation - interface ApplyPatchFileUpdate { - unified_diff: string - content: string - } - - export function deriveNewContentsFromChunks(filePath: string, chunks: UpdateFileChunk[]): ApplyPatchFileUpdate { - // Read original file content - let originalContent: string - try { - originalContent = readFileSync(filePath, "utf-8") - } catch (error) { - throw new Error(`Failed to read file ${filePath}: ${error}`) - } - - let originalLines = originalContent.split("\n") - - // Drop trailing empty element for consistent line counting - if (originalLines.length > 0 && originalLines[originalLines.length - 1] === "") { - originalLines.pop() - } - - const replacements = computeReplacements(originalLines, filePath, chunks) - let newLines = applyReplacements(originalLines, replacements) - - // Ensure trailing newline - if (newLines.length === 0 || newLines[newLines.length - 1] !== "") { - newLines.push("") - } - - const newContent = newLines.join("\n") - - // Generate unified diff - const unifiedDiff = generateUnifiedDiff(originalContent, newContent) - - return { - unified_diff: unifiedDiff, - content: newContent, - } - } - - function computeReplacements( - originalLines: string[], - filePath: string, - chunks: UpdateFileChunk[], - ): Array<[number, number, string[]]> { - const replacements: Array<[number, number, string[]]> = [] - let lineIndex = 0 - - for (const chunk of chunks) { - // Handle context-based seeking - if (chunk.change_context) { - const contextIdx = seekSequence(originalLines, [chunk.change_context], lineIndex) - if (contextIdx === -1) { - throw new Error(`Failed to find context '${chunk.change_context}' in ${filePath}`) - } - lineIndex = contextIdx + 1 - } - - // Handle pure addition (no old lines) - if (chunk.old_lines.length === 0) { - const insertionIdx = - originalLines.length > 0 && originalLines[originalLines.length - 1] === "" - ? originalLines.length - 1 - : originalLines.length - replacements.push([insertionIdx, 0, chunk.new_lines]) - continue - } - - // Try to match old lines in the file - let pattern = chunk.old_lines - let newSlice = chunk.new_lines - let found = seekSequence(originalLines, pattern, lineIndex, chunk.is_end_of_file) - - // Retry without trailing empty line if not found - if (found === -1 && pattern.length > 0 && pattern[pattern.length - 1] === "") { - pattern = pattern.slice(0, -1) - if (newSlice.length > 0 && newSlice[newSlice.length - 1] === "") { - newSlice = newSlice.slice(0, -1) - } - found = seekSequence(originalLines, pattern, lineIndex, chunk.is_end_of_file) - } - - if (found !== -1) { - replacements.push([found, pattern.length, newSlice]) - lineIndex = found + pattern.length - } else { - throw new Error(`Failed to find expected lines in ${filePath}:\n${chunk.old_lines.join("\n")}`) - } - } - - // Sort replacements by index to apply in order - replacements.sort((a, b) => a[0] - b[0]) - - return replacements - } - - function applyReplacements(lines: string[], replacements: Array<[number, number, string[]]>): string[] { - // Apply replacements in reverse order to avoid index shifting - const result = [...lines] - - for (let i = replacements.length - 1; i >= 0; i--) { - const [startIdx, oldLen, newSegment] = replacements[i] - - // Remove old lines - result.splice(startIdx, oldLen) - - // Insert new lines - for (let j = 0; j < newSegment.length; j++) { - result.splice(startIdx + j, 0, newSegment[j]) - } - } - - return result - } - - // Normalize Unicode punctuation to ASCII equivalents (like Rust's normalize_unicode) - function normalizeUnicode(str: string): string { - return str - .replace(/[\u2018\u2019\u201A\u201B]/g, "'") // single quotes - .replace(/[\u201C\u201D\u201E\u201F]/g, '"') // double quotes - .replace(/[\u2010\u2011\u2012\u2013\u2014\u2015]/g, "-") // dashes - .replace(/\u2026/g, "...") // ellipsis - .replace(/\u00A0/g, " ") // non-breaking space - } - - type Comparator = (a: string, b: string) => boolean - - function tryMatch(lines: string[], pattern: string[], startIndex: number, compare: Comparator, eof: boolean): number { - // If EOF anchor, try matching from end of file first - if (eof) { - const fromEnd = lines.length - pattern.length - if (fromEnd >= startIndex) { - let matches = true - for (let j = 0; j < pattern.length; j++) { - if (!compare(lines[fromEnd + j], pattern[j])) { - matches = false - break - } - } - if (matches) return fromEnd - } - } - - // Forward search from startIndex - for (let i = startIndex; i <= lines.length - pattern.length; i++) { - let matches = true - for (let j = 0; j < pattern.length; j++) { - if (!compare(lines[i + j], pattern[j])) { - matches = false - break - } - } - if (matches) return i - } - - return -1 - } - - function seekSequence(lines: string[], pattern: string[], startIndex: number, eof = false): number { - if (pattern.length === 0) return -1 - - // Pass 1: exact match - const exact = tryMatch(lines, pattern, startIndex, (a, b) => a === b, eof) - if (exact !== -1) return exact - - // Pass 2: rstrip (trim trailing whitespace) - const rstrip = tryMatch(lines, pattern, startIndex, (a, b) => a.trimEnd() === b.trimEnd(), eof) - if (rstrip !== -1) return rstrip - - // Pass 3: trim (both ends) - const trim = tryMatch(lines, pattern, startIndex, (a, b) => a.trim() === b.trim(), eof) - if (trim !== -1) return trim - - // Pass 4: normalized (Unicode punctuation to ASCII) - const normalized = tryMatch( - lines, - pattern, - startIndex, - (a, b) => normalizeUnicode(a.trim()) === normalizeUnicode(b.trim()), - eof, - ) - return normalized - } - - function generateUnifiedDiff(oldContent: string, newContent: string): string { - const oldLines = oldContent.split("\n") - const newLines = newContent.split("\n") - - // Simple diff generation - in a real implementation you'd use a proper diff algorithm - let diff = "@@ -1 +1 @@\n" - - // Find changes (simplified approach) - const maxLen = Math.max(oldLines.length, newLines.length) - let hasChanges = false - - for (let i = 0; i < maxLen; i++) { - const oldLine = oldLines[i] || "" - const newLine = newLines[i] || "" - - if (oldLine !== newLine) { - if (oldLine) diff += `-${oldLine}\n` - if (newLine) diff += `+${newLine}\n` - hasChanges = true - } else if (oldLine) { - diff += ` ${oldLine}\n` - } - } - - return hasChanges ? diff : "" - } - - // Apply hunks to filesystem - export async function applyHunksToFiles(hunks: Hunk[]): Promise { - if (hunks.length === 0) { - throw new Error("No files were modified.") - } - - const added: string[] = [] - const modified: string[] = [] - const deleted: string[] = [] - - for (const hunk of hunks) { - switch (hunk.type) { - case "add": - // Create parent directories - const addDir = path.dirname(hunk.path) - if (addDir !== "." && addDir !== "/") { - await fs.mkdir(addDir, { recursive: true }) - } - - await fs.writeFile(hunk.path, hunk.contents, "utf-8") - added.push(hunk.path) - log.info(`Added file: ${hunk.path}`) - break - - case "delete": - await fs.unlink(hunk.path) - deleted.push(hunk.path) - log.info(`Deleted file: ${hunk.path}`) - break - - case "update": - const fileUpdate = deriveNewContentsFromChunks(hunk.path, hunk.chunks) - - if (hunk.move_path) { - // Handle file move - const moveDir = path.dirname(hunk.move_path) - if (moveDir !== "." && moveDir !== "/") { - await fs.mkdir(moveDir, { recursive: true }) - } - - await fs.writeFile(hunk.move_path, fileUpdate.content, "utf-8") - await fs.unlink(hunk.path) - modified.push(hunk.move_path) - log.info(`Moved file: ${hunk.path} -> ${hunk.move_path}`) - } else { - // Regular update - await fs.writeFile(hunk.path, fileUpdate.content, "utf-8") - modified.push(hunk.path) - log.info(`Updated file: ${hunk.path}`) - } - break - } - } - - return { added, modified, deleted } - } - - // Main patch application function - export async function applyPatch(patchText: string): Promise { - const { hunks } = parsePatch(patchText) - return applyHunksToFiles(hunks) - } - - // Async version of maybeParseApplyPatchVerified - export async function maybeParseApplyPatchVerified( - argv: string[], - cwd: string, - ): Promise< - | { type: MaybeApplyPatchVerified.Body; action: ApplyPatchAction } - | { type: MaybeApplyPatchVerified.CorrectnessError; error: Error } - | { type: MaybeApplyPatchVerified.NotApplyPatch } - > { - // Detect implicit patch invocation (raw patch without apply_patch command) - if (argv.length === 1) { - try { - parsePatch(argv[0]) - return { - type: MaybeApplyPatchVerified.CorrectnessError, - error: new Error(ApplyPatchError.ImplicitInvocation), - } - } catch { - // Not a patch, continue - } - } - - const result = maybeParseApplyPatch(argv) - - switch (result.type) { - case MaybeApplyPatch.Body: - const { args } = result - const effectiveCwd = args.workdir ? path.resolve(cwd, args.workdir) : cwd - const changes = new Map() - - for (const hunk of args.hunks) { - const resolvedPath = path.resolve( - effectiveCwd, - hunk.type === "update" && hunk.move_path ? hunk.move_path : hunk.path, - ) - - switch (hunk.type) { - case "add": - changes.set(resolvedPath, { - type: "add", - content: hunk.contents, - }) - break - - case "delete": - // For delete, we need to read the current content - const deletePath = path.resolve(effectiveCwd, hunk.path) - try { - const content = await fs.readFile(deletePath, "utf-8") - changes.set(resolvedPath, { - type: "delete", - content, - }) - } catch { - return { - type: MaybeApplyPatchVerified.CorrectnessError, - error: new Error(`Failed to read file for deletion: ${deletePath}`), - } - } - break - - case "update": - const updatePath = path.resolve(effectiveCwd, hunk.path) - try { - const fileUpdate = deriveNewContentsFromChunks(updatePath, hunk.chunks) - changes.set(resolvedPath, { - type: "update", - unified_diff: fileUpdate.unified_diff, - move_path: hunk.move_path ? path.resolve(effectiveCwd, hunk.move_path) : undefined, - new_content: fileUpdate.content, - }) - } catch (error) { - return { - type: MaybeApplyPatchVerified.CorrectnessError, - error: error as Error, - } - } - break - } - } - - return { - type: MaybeApplyPatchVerified.Body, - action: { - changes, - patch: args.patch, - cwd: effectiveCwd, - }, - } - - case MaybeApplyPatch.PatchParseError: - return { - type: MaybeApplyPatchVerified.CorrectnessError, - error: result.error, - } - - case MaybeApplyPatch.NotApplyPatch: - return { type: MaybeApplyPatchVerified.NotApplyPatch } - } - } -} +export * as Patch from "./patch" diff --git a/packages/opencode/src/patch/patch.ts b/packages/opencode/src/patch/patch.ts new file mode 100644 index 0000000000..d36ec72c72 --- /dev/null +++ b/packages/opencode/src/patch/patch.ts @@ -0,0 +1,678 @@ +import z from "zod" +import * as path from "path" +import * as fs from "fs/promises" +import { readFileSync } from "fs" +import { Log } from "../util/log" + +const log = Log.create({ service: "patch" }) + +// Schema definitions +export const PatchSchema = z.object({ + patchText: z.string().describe("The full patch text that describes all changes to be made"), +}) + +export type PatchParams = z.infer + +// Core types matching the Rust implementation +export interface ApplyPatchArgs { + patch: string + hunks: Hunk[] + workdir?: string +} + +export type Hunk = + | { type: "add"; path: string; contents: string } + | { type: "delete"; path: string } + | { type: "update"; path: string; move_path?: string; chunks: UpdateFileChunk[] } + +export interface UpdateFileChunk { + old_lines: string[] + new_lines: string[] + change_context?: string + is_end_of_file?: boolean +} + +export interface ApplyPatchAction { + changes: Map + patch: string + cwd: string +} + +export type ApplyPatchFileChange = + | { type: "add"; content: string } + | { type: "delete"; content: string } + | { type: "update"; unified_diff: string; move_path?: string; new_content: string } + +export interface AffectedPaths { + added: string[] + modified: string[] + deleted: string[] +} + +export enum ApplyPatchError { + ParseError = "ParseError", + IoError = "IoError", + ComputeReplacements = "ComputeReplacements", + ImplicitInvocation = "ImplicitInvocation", +} + +export enum MaybeApplyPatch { + Body = "Body", + ShellParseError = "ShellParseError", + PatchParseError = "PatchParseError", + NotApplyPatch = "NotApplyPatch", +} + +export enum MaybeApplyPatchVerified { + Body = "Body", + ShellParseError = "ShellParseError", + CorrectnessError = "CorrectnessError", + NotApplyPatch = "NotApplyPatch", +} + +// Parser implementation +function parsePatchHeader( + lines: string[], + startIdx: number, +): { filePath: string; movePath?: string; nextIdx: number } | null { + const line = lines[startIdx] + + if (line.startsWith("*** Add File:")) { + const filePath = line.slice("*** Add File:".length).trim() + return filePath ? { filePath, nextIdx: startIdx + 1 } : null + } + + if (line.startsWith("*** Delete File:")) { + const filePath = line.slice("*** Delete File:".length).trim() + return filePath ? { filePath, nextIdx: startIdx + 1 } : null + } + + if (line.startsWith("*** Update File:")) { + const filePath = line.slice("*** Update File:".length).trim() + let movePath: string | undefined + let nextIdx = startIdx + 1 + + // Check for move directive + if (nextIdx < lines.length && lines[nextIdx].startsWith("*** Move to:")) { + movePath = lines[nextIdx].slice("*** Move to:".length).trim() + nextIdx++ + } + + return filePath ? { filePath, movePath, nextIdx } : null + } + + return null +} + +function parseUpdateFileChunks(lines: string[], startIdx: number): { chunks: UpdateFileChunk[]; nextIdx: number } { + const chunks: UpdateFileChunk[] = [] + let i = startIdx + + while (i < lines.length && !lines[i].startsWith("***")) { + if (lines[i].startsWith("@@")) { + // Parse context line + const contextLine = lines[i].substring(2).trim() + i++ + + const oldLines: string[] = [] + const newLines: string[] = [] + let isEndOfFile = false + + // Parse change lines + while (i < lines.length && !lines[i].startsWith("@@") && !lines[i].startsWith("***")) { + const changeLine = lines[i] + + if (changeLine === "*** End of File") { + isEndOfFile = true + i++ + break + } + + if (changeLine.startsWith(" ")) { + // Keep line - appears in both old and new + const content = changeLine.substring(1) + oldLines.push(content) + newLines.push(content) + } else if (changeLine.startsWith("-")) { + // Remove line - only in old + oldLines.push(changeLine.substring(1)) + } else if (changeLine.startsWith("+")) { + // Add line - only in new + newLines.push(changeLine.substring(1)) + } + + i++ + } + + chunks.push({ + old_lines: oldLines, + new_lines: newLines, + change_context: contextLine || undefined, + is_end_of_file: isEndOfFile || undefined, + }) + } else { + i++ + } + } + + return { chunks, nextIdx: i } +} + +function parseAddFileContent(lines: string[], startIdx: number): { content: string; nextIdx: number } { + let content = "" + let i = startIdx + + while (i < lines.length && !lines[i].startsWith("***")) { + if (lines[i].startsWith("+")) { + content += lines[i].substring(1) + "\n" + } + i++ + } + + // Remove trailing newline + if (content.endsWith("\n")) { + content = content.slice(0, -1) + } + + return { content, nextIdx: i } +} + +function stripHeredoc(input: string): string { + // Match heredoc patterns like: cat <<'EOF'\n...\nEOF or < line.trim() === beginMarker) + const endIdx = lines.findIndex((line) => line.trim() === endMarker) + + if (beginIdx === -1 || endIdx === -1 || beginIdx >= endIdx) { + throw new Error("Invalid patch format: missing Begin/End markers") + } + + // Parse content between markers + i = beginIdx + 1 + + while (i < endIdx) { + const header = parsePatchHeader(lines, i) + if (!header) { + i++ + continue + } + + if (lines[i].startsWith("*** Add File:")) { + const { content, nextIdx } = parseAddFileContent(lines, header.nextIdx) + hunks.push({ + type: "add", + path: header.filePath, + contents: content, + }) + i = nextIdx + } else if (lines[i].startsWith("*** Delete File:")) { + hunks.push({ + type: "delete", + path: header.filePath, + }) + i = header.nextIdx + } else if (lines[i].startsWith("*** Update File:")) { + const { chunks, nextIdx } = parseUpdateFileChunks(lines, header.nextIdx) + hunks.push({ + type: "update", + path: header.filePath, + move_path: header.movePath, + chunks, + }) + i = nextIdx + } else { + i++ + } + } + + return { hunks } +} + +// Apply patch functionality +export function maybeParseApplyPatch( + argv: string[], +): + | { type: MaybeApplyPatch.Body; args: ApplyPatchArgs } + | { type: MaybeApplyPatch.PatchParseError; error: Error } + | { type: MaybeApplyPatch.NotApplyPatch } { + const APPLY_PATCH_COMMANDS = ["apply_patch", "applypatch"] + + // Direct invocation: apply_patch + if (argv.length === 2 && APPLY_PATCH_COMMANDS.includes(argv[0])) { + try { + const { hunks } = parsePatch(argv[1]) + return { + type: MaybeApplyPatch.Body, + args: { + patch: argv[1], + hunks, + }, + } + } catch (error) { + return { + type: MaybeApplyPatch.PatchParseError, + error: error as Error, + } + } + } + + // Bash heredoc form: bash -lc 'apply_patch <<"EOF" ...' + if (argv.length === 3 && argv[0] === "bash" && argv[1] === "-lc") { + // Simple extraction - in real implementation would need proper bash parsing + const script = argv[2] + const heredocMatch = script.match(/apply_patch\s*<<['"](\w+)['"]\s*\n([\s\S]*?)\n\1/) + + if (heredocMatch) { + const patchContent = heredocMatch[2] + try { + const { hunks } = parsePatch(patchContent) + return { + type: MaybeApplyPatch.Body, + args: { + patch: patchContent, + hunks, + }, + } + } catch (error) { + return { + type: MaybeApplyPatch.PatchParseError, + error: error as Error, + } + } + } + } + + return { type: MaybeApplyPatch.NotApplyPatch } +} + +// File content manipulation +interface ApplyPatchFileUpdate { + unified_diff: string + content: string +} + +export function deriveNewContentsFromChunks(filePath: string, chunks: UpdateFileChunk[]): ApplyPatchFileUpdate { + // Read original file content + let originalContent: string + try { + originalContent = readFileSync(filePath, "utf-8") + } catch (error) { + throw new Error(`Failed to read file ${filePath}: ${error}`) + } + + let originalLines = originalContent.split("\n") + + // Drop trailing empty element for consistent line counting + if (originalLines.length > 0 && originalLines[originalLines.length - 1] === "") { + originalLines.pop() + } + + const replacements = computeReplacements(originalLines, filePath, chunks) + let newLines = applyReplacements(originalLines, replacements) + + // Ensure trailing newline + if (newLines.length === 0 || newLines[newLines.length - 1] !== "") { + newLines.push("") + } + + const newContent = newLines.join("\n") + + // Generate unified diff + const unifiedDiff = generateUnifiedDiff(originalContent, newContent) + + return { + unified_diff: unifiedDiff, + content: newContent, + } +} + +function computeReplacements( + originalLines: string[], + filePath: string, + chunks: UpdateFileChunk[], +): Array<[number, number, string[]]> { + const replacements: Array<[number, number, string[]]> = [] + let lineIndex = 0 + + for (const chunk of chunks) { + // Handle context-based seeking + if (chunk.change_context) { + const contextIdx = seekSequence(originalLines, [chunk.change_context], lineIndex) + if (contextIdx === -1) { + throw new Error(`Failed to find context '${chunk.change_context}' in ${filePath}`) + } + lineIndex = contextIdx + 1 + } + + // Handle pure addition (no old lines) + if (chunk.old_lines.length === 0) { + const insertionIdx = + originalLines.length > 0 && originalLines[originalLines.length - 1] === "" + ? originalLines.length - 1 + : originalLines.length + replacements.push([insertionIdx, 0, chunk.new_lines]) + continue + } + + // Try to match old lines in the file + let pattern = chunk.old_lines + let newSlice = chunk.new_lines + let found = seekSequence(originalLines, pattern, lineIndex, chunk.is_end_of_file) + + // Retry without trailing empty line if not found + if (found === -1 && pattern.length > 0 && pattern[pattern.length - 1] === "") { + pattern = pattern.slice(0, -1) + if (newSlice.length > 0 && newSlice[newSlice.length - 1] === "") { + newSlice = newSlice.slice(0, -1) + } + found = seekSequence(originalLines, pattern, lineIndex, chunk.is_end_of_file) + } + + if (found !== -1) { + replacements.push([found, pattern.length, newSlice]) + lineIndex = found + pattern.length + } else { + throw new Error(`Failed to find expected lines in ${filePath}:\n${chunk.old_lines.join("\n")}`) + } + } + + // Sort replacements by index to apply in order + replacements.sort((a, b) => a[0] - b[0]) + + return replacements +} + +function applyReplacements(lines: string[], replacements: Array<[number, number, string[]]>): string[] { + // Apply replacements in reverse order to avoid index shifting + const result = [...lines] + + for (let i = replacements.length - 1; i >= 0; i--) { + const [startIdx, oldLen, newSegment] = replacements[i] + + // Remove old lines + result.splice(startIdx, oldLen) + + // Insert new lines + for (let j = 0; j < newSegment.length; j++) { + result.splice(startIdx + j, 0, newSegment[j]) + } + } + + return result +} + +// Normalize Unicode punctuation to ASCII equivalents (like Rust's normalize_unicode) +function normalizeUnicode(str: string): string { + return str + .replace(/[\u2018\u2019\u201A\u201B]/g, "'") // single quotes + .replace(/[\u201C\u201D\u201E\u201F]/g, '"') // double quotes + .replace(/[\u2010\u2011\u2012\u2013\u2014\u2015]/g, "-") // dashes + .replace(/\u2026/g, "...") // ellipsis + .replace(/\u00A0/g, " ") // non-breaking space +} + +type Comparator = (a: string, b: string) => boolean + +function tryMatch(lines: string[], pattern: string[], startIndex: number, compare: Comparator, eof: boolean): number { + // If EOF anchor, try matching from end of file first + if (eof) { + const fromEnd = lines.length - pattern.length + if (fromEnd >= startIndex) { + let matches = true + for (let j = 0; j < pattern.length; j++) { + if (!compare(lines[fromEnd + j], pattern[j])) { + matches = false + break + } + } + if (matches) return fromEnd + } + } + + // Forward search from startIndex + for (let i = startIndex; i <= lines.length - pattern.length; i++) { + let matches = true + for (let j = 0; j < pattern.length; j++) { + if (!compare(lines[i + j], pattern[j])) { + matches = false + break + } + } + if (matches) return i + } + + return -1 +} + +function seekSequence(lines: string[], pattern: string[], startIndex: number, eof = false): number { + if (pattern.length === 0) return -1 + + // Pass 1: exact match + const exact = tryMatch(lines, pattern, startIndex, (a, b) => a === b, eof) + if (exact !== -1) return exact + + // Pass 2: rstrip (trim trailing whitespace) + const rstrip = tryMatch(lines, pattern, startIndex, (a, b) => a.trimEnd() === b.trimEnd(), eof) + if (rstrip !== -1) return rstrip + + // Pass 3: trim (both ends) + const trim = tryMatch(lines, pattern, startIndex, (a, b) => a.trim() === b.trim(), eof) + if (trim !== -1) return trim + + // Pass 4: normalized (Unicode punctuation to ASCII) + const normalized = tryMatch( + lines, + pattern, + startIndex, + (a, b) => normalizeUnicode(a.trim()) === normalizeUnicode(b.trim()), + eof, + ) + return normalized +} + +function generateUnifiedDiff(oldContent: string, newContent: string): string { + const oldLines = oldContent.split("\n") + const newLines = newContent.split("\n") + + // Simple diff generation - in a real implementation you'd use a proper diff algorithm + let diff = "@@ -1 +1 @@\n" + + // Find changes (simplified approach) + const maxLen = Math.max(oldLines.length, newLines.length) + let hasChanges = false + + for (let i = 0; i < maxLen; i++) { + const oldLine = oldLines[i] || "" + const newLine = newLines[i] || "" + + if (oldLine !== newLine) { + if (oldLine) diff += `-${oldLine}\n` + if (newLine) diff += `+${newLine}\n` + hasChanges = true + } else if (oldLine) { + diff += ` ${oldLine}\n` + } + } + + return hasChanges ? diff : "" +} + +// Apply hunks to filesystem +export async function applyHunksToFiles(hunks: Hunk[]): Promise { + if (hunks.length === 0) { + throw new Error("No files were modified.") + } + + const added: string[] = [] + const modified: string[] = [] + const deleted: string[] = [] + + for (const hunk of hunks) { + switch (hunk.type) { + case "add": + // Create parent directories + const addDir = path.dirname(hunk.path) + if (addDir !== "." && addDir !== "/") { + await fs.mkdir(addDir, { recursive: true }) + } + + await fs.writeFile(hunk.path, hunk.contents, "utf-8") + added.push(hunk.path) + log.info(`Added file: ${hunk.path}`) + break + + case "delete": + await fs.unlink(hunk.path) + deleted.push(hunk.path) + log.info(`Deleted file: ${hunk.path}`) + break + + case "update": + const fileUpdate = deriveNewContentsFromChunks(hunk.path, hunk.chunks) + + if (hunk.move_path) { + // Handle file move + const moveDir = path.dirname(hunk.move_path) + if (moveDir !== "." && moveDir !== "/") { + await fs.mkdir(moveDir, { recursive: true }) + } + + await fs.writeFile(hunk.move_path, fileUpdate.content, "utf-8") + await fs.unlink(hunk.path) + modified.push(hunk.move_path) + log.info(`Moved file: ${hunk.path} -> ${hunk.move_path}`) + } else { + // Regular update + await fs.writeFile(hunk.path, fileUpdate.content, "utf-8") + modified.push(hunk.path) + log.info(`Updated file: ${hunk.path}`) + } + break + } + } + + return { added, modified, deleted } +} + +// Main patch application function +export async function applyPatch(patchText: string): Promise { + const { hunks } = parsePatch(patchText) + return applyHunksToFiles(hunks) +} + +// Async version of maybeParseApplyPatchVerified +export async function maybeParseApplyPatchVerified( + argv: string[], + cwd: string, +): Promise< + | { type: MaybeApplyPatchVerified.Body; action: ApplyPatchAction } + | { type: MaybeApplyPatchVerified.CorrectnessError; error: Error } + | { type: MaybeApplyPatchVerified.NotApplyPatch } +> { + // Detect implicit patch invocation (raw patch without apply_patch command) + if (argv.length === 1) { + try { + parsePatch(argv[0]) + return { + type: MaybeApplyPatchVerified.CorrectnessError, + error: new Error(ApplyPatchError.ImplicitInvocation), + } + } catch { + // Not a patch, continue + } + } + + const result = maybeParseApplyPatch(argv) + + switch (result.type) { + case MaybeApplyPatch.Body: + const { args } = result + const effectiveCwd = args.workdir ? path.resolve(cwd, args.workdir) : cwd + const changes = new Map() + + for (const hunk of args.hunks) { + const resolvedPath = path.resolve( + effectiveCwd, + hunk.type === "update" && hunk.move_path ? hunk.move_path : hunk.path, + ) + + switch (hunk.type) { + case "add": + changes.set(resolvedPath, { + type: "add", + content: hunk.contents, + }) + break + + case "delete": + // For delete, we need to read the current content + const deletePath = path.resolve(effectiveCwd, hunk.path) + try { + const content = await fs.readFile(deletePath, "utf-8") + changes.set(resolvedPath, { + type: "delete", + content, + }) + } catch { + return { + type: MaybeApplyPatchVerified.CorrectnessError, + error: new Error(`Failed to read file for deletion: ${deletePath}`), + } + } + break + + case "update": + const updatePath = path.resolve(effectiveCwd, hunk.path) + try { + const fileUpdate = deriveNewContentsFromChunks(updatePath, hunk.chunks) + changes.set(resolvedPath, { + type: "update", + unified_diff: fileUpdate.unified_diff, + move_path: hunk.move_path ? path.resolve(effectiveCwd, hunk.move_path) : undefined, + new_content: fileUpdate.content, + }) + } catch (error) { + return { + type: MaybeApplyPatchVerified.CorrectnessError, + error: error as Error, + } + } + break + } + } + + return { + type: MaybeApplyPatchVerified.Body, + action: { + changes, + patch: args.patch, + cwd: effectiveCwd, + }, + } + + case MaybeApplyPatch.PatchParseError: + return { + type: MaybeApplyPatchVerified.CorrectnessError, + error: result.error, + } + + case MaybeApplyPatch.NotApplyPatch: + return { type: MaybeApplyPatchVerified.NotApplyPatch } + } +} From ce4e47a2e3456924b9a8306d63ab2241772d02f5 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 22:16:01 -0400 Subject: [PATCH 195/300] feat: unwrap uformat namespace to flat exports + barrel (#22703) --- packages/opencode/src/format/format.ts | 192 ++++++++++++++++++++++++ packages/opencode/src/format/index.ts | 195 +------------------------ 2 files changed, 193 insertions(+), 194 deletions(-) create mode 100644 packages/opencode/src/format/format.ts diff --git a/packages/opencode/src/format/format.ts b/packages/opencode/src/format/format.ts new file mode 100644 index 0000000000..6df00d3db3 --- /dev/null +++ b/packages/opencode/src/format/format.ts @@ -0,0 +1,192 @@ +import { Effect, Layer, Context } from "effect" +import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" +import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" +import { InstanceState } from "@/effect/instance-state" +import path from "path" +import { mergeDeep } from "remeda" +import z from "zod" +import { Config } from "../config" +import { Log } from "../util/log" +import * as Formatter from "./formatter" + +const log = Log.create({ service: "format" }) + +export const Status = z + .object({ + name: z.string(), + extensions: z.string().array(), + enabled: z.boolean(), + }) + .meta({ + ref: "FormatterStatus", + }) +export type Status = z.infer + +export interface Interface { + readonly init: () => Effect.Effect + readonly status: () => Effect.Effect + readonly file: (filepath: string) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/Format") {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const config = yield* Config.Service + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner + + const state = yield* InstanceState.make( + Effect.fn("Format.state")(function* (_ctx) { + const commands: Record = {} + const formatters: Record = {} + + const cfg = yield* config.get() + + if (cfg.formatter !== false) { + for (const item of Object.values(Formatter)) { + formatters[item.name] = item + } + for (const [name, item] of Object.entries(cfg.formatter ?? {})) { + // Ruff and uv are both the same formatter, so disabling either should disable both. + if (["ruff", "uv"].includes(name) && (cfg.formatter?.ruff?.disabled || cfg.formatter?.uv?.disabled)) { + // TODO combine formatters so shared backends like Ruff/uv don't need linked disable handling here. + delete formatters.ruff + delete formatters.uv + continue + } + if (item.disabled) { + delete formatters[name] + continue + } + const info = mergeDeep(formatters[name] ?? {}, { + extensions: [], + ...item, + }) + + formatters[name] = { + ...info, + name, + enabled: async () => info.command ?? false, + } + } + } else { + log.info("all formatters are disabled") + } + + async function getCommand(item: Formatter.Info) { + let cmd = commands[item.name] + if (cmd === false || cmd === undefined) { + cmd = await item.enabled() + commands[item.name] = cmd + } + return cmd + } + + async function isEnabled(item: Formatter.Info) { + const cmd = await getCommand(item) + return cmd !== false + } + + async function getFormatter(ext: string) { + const matching = Object.values(formatters).filter((item) => item.extensions.includes(ext)) + const checks = await Promise.all( + matching.map(async (item) => { + log.info("checking", { name: item.name, ext }) + const cmd = await getCommand(item) + if (cmd) { + log.info("enabled", { name: item.name, ext }) + } + return { + item, + cmd, + } + }), + ) + return checks.filter((x) => x.cmd).map((x) => ({ item: x.item, cmd: x.cmd! })) + } + + function formatFile(filepath: string) { + return Effect.gen(function* () { + log.info("formatting", { file: filepath }) + const ext = path.extname(filepath) + + for (const { item, cmd } of yield* Effect.promise(() => getFormatter(ext))) { + if (cmd === false) continue + log.info("running", { command: cmd }) + const replaced = cmd.map((x) => x.replace("$FILE", filepath)) + const dir = yield* InstanceState.directory + const code = yield* spawner + .spawn( + ChildProcess.make(replaced[0]!, replaced.slice(1), { + cwd: dir, + env: item.environment, + extendEnv: true, + }), + ) + .pipe( + Effect.flatMap((handle) => handle.exitCode), + Effect.scoped, + Effect.catch(() => + Effect.sync(() => { + log.error("failed to format file", { + error: "spawn failed", + command: cmd, + ...item.environment, + file: filepath, + }) + return ChildProcessSpawner.ExitCode(1) + }), + ), + ) + if (code !== 0) { + log.error("failed", { + command: cmd, + ...item.environment, + }) + } + } + }) + } + + log.info("init") + + return { + formatters, + isEnabled, + formatFile, + } + }), + ) + + const init = Effect.fn("Format.init")(function* () { + yield* InstanceState.get(state) + }) + + const status = Effect.fn("Format.status")(function* () { + const { formatters, isEnabled } = yield* InstanceState.get(state) + const result: Status[] = [] + for (const formatter of Object.values(formatters)) { + const isOn = yield* Effect.promise(() => isEnabled(formatter)) + result.push({ + name: formatter.name, + extensions: formatter.extensions, + enabled: isOn, + }) + } + return result + }) + + const file = Effect.fn("Format.file")(function* (filepath: string) { + const { formatFile } = yield* InstanceState.get(state) + yield* formatFile(filepath) + }) + + return Service.of({ init, status, file }) + }), +) + +export const defaultLayer = layer.pipe( + Layer.provide(Config.defaultLayer), + Layer.provide(CrossSpawnSpawner.defaultLayer), +) diff --git a/packages/opencode/src/format/index.ts b/packages/opencode/src/format/index.ts index d65ed2944e..435c517ac7 100644 --- a/packages/opencode/src/format/index.ts +++ b/packages/opencode/src/format/index.ts @@ -1,194 +1 @@ -import { Effect, Layer, Context } from "effect" -import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" -import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" -import { InstanceState } from "@/effect/instance-state" -import path from "path" -import { mergeDeep } from "remeda" -import z from "zod" -import { Config } from "../config" -import { Log } from "../util/log" -import * as Formatter from "./formatter" - -export namespace Format { - const log = Log.create({ service: "format" }) - - export const Status = z - .object({ - name: z.string(), - extensions: z.string().array(), - enabled: z.boolean(), - }) - .meta({ - ref: "FormatterStatus", - }) - export type Status = z.infer - - export interface Interface { - readonly init: () => Effect.Effect - readonly status: () => Effect.Effect - readonly file: (filepath: string) => Effect.Effect - } - - export class Service extends Context.Service()("@opencode/Format") {} - - export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const config = yield* Config.Service - const spawner = yield* ChildProcessSpawner.ChildProcessSpawner - - const state = yield* InstanceState.make( - Effect.fn("Format.state")(function* (_ctx) { - const commands: Record = {} - const formatters: Record = {} - - const cfg = yield* config.get() - - if (cfg.formatter !== false) { - for (const item of Object.values(Formatter)) { - formatters[item.name] = item - } - for (const [name, item] of Object.entries(cfg.formatter ?? {})) { - // Ruff and uv are both the same formatter, so disabling either should disable both. - if (["ruff", "uv"].includes(name) && (cfg.formatter?.ruff?.disabled || cfg.formatter?.uv?.disabled)) { - // TODO combine formatters so shared backends like Ruff/uv don't need linked disable handling here. - delete formatters.ruff - delete formatters.uv - continue - } - if (item.disabled) { - delete formatters[name] - continue - } - const info = mergeDeep(formatters[name] ?? {}, { - extensions: [], - ...item, - }) - - formatters[name] = { - ...info, - name, - enabled: async () => info.command ?? false, - } - } - } else { - log.info("all formatters are disabled") - } - - async function getCommand(item: Formatter.Info) { - let cmd = commands[item.name] - if (cmd === false || cmd === undefined) { - cmd = await item.enabled() - commands[item.name] = cmd - } - return cmd - } - - async function isEnabled(item: Formatter.Info) { - const cmd = await getCommand(item) - return cmd !== false - } - - async function getFormatter(ext: string) { - const matching = Object.values(formatters).filter((item) => item.extensions.includes(ext)) - const checks = await Promise.all( - matching.map(async (item) => { - log.info("checking", { name: item.name, ext }) - const cmd = await getCommand(item) - if (cmd) { - log.info("enabled", { name: item.name, ext }) - } - return { - item, - cmd, - } - }), - ) - return checks.filter((x) => x.cmd).map((x) => ({ item: x.item, cmd: x.cmd! })) - } - - function formatFile(filepath: string) { - return Effect.gen(function* () { - log.info("formatting", { file: filepath }) - const ext = path.extname(filepath) - - for (const { item, cmd } of yield* Effect.promise(() => getFormatter(ext))) { - if (cmd === false) continue - log.info("running", { command: cmd }) - const replaced = cmd.map((x) => x.replace("$FILE", filepath)) - const dir = yield* InstanceState.directory - const code = yield* spawner - .spawn( - ChildProcess.make(replaced[0]!, replaced.slice(1), { - cwd: dir, - env: item.environment, - extendEnv: true, - }), - ) - .pipe( - Effect.flatMap((handle) => handle.exitCode), - Effect.scoped, - Effect.catch(() => - Effect.sync(() => { - log.error("failed to format file", { - error: "spawn failed", - command: cmd, - ...item.environment, - file: filepath, - }) - return ChildProcessSpawner.ExitCode(1) - }), - ), - ) - if (code !== 0) { - log.error("failed", { - command: cmd, - ...item.environment, - }) - } - } - }) - } - - log.info("init") - - return { - formatters, - isEnabled, - formatFile, - } - }), - ) - - const init = Effect.fn("Format.init")(function* () { - yield* InstanceState.get(state) - }) - - const status = Effect.fn("Format.status")(function* () { - const { formatters, isEnabled } = yield* InstanceState.get(state) - const result: Status[] = [] - for (const formatter of Object.values(formatters)) { - const isOn = yield* Effect.promise(() => isEnabled(formatter)) - result.push({ - name: formatter.name, - extensions: formatter.extensions, - enabled: isOn, - }) - } - return result - }) - - const file = Effect.fn("Format.file")(function* (filepath: string) { - const { formatFile } = yield* InstanceState.get(state) - yield* formatFile(filepath) - }) - - return Service.of({ init, status, file }) - }), - ) - - export const defaultLayer = layer.pipe( - Layer.provide(Config.defaultLayer), - Layer.provide(CrossSpawnSpawner.defaultLayer), - ) -} +export * as Format from "./format" From bb90aa6cb2e9c39e43420da29927250f384e1ca0 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 22:16:17 -0400 Subject: [PATCH 196/300] feat: unwrap uworktree namespace to flat exports + barrel (#22717) --- packages/opencode/src/worktree/index.ts | 601 +-------------------- packages/opencode/src/worktree/worktree.ts | 598 ++++++++++++++++++++ 2 files changed, 599 insertions(+), 600 deletions(-) create mode 100644 packages/opencode/src/worktree/worktree.ts diff --git a/packages/opencode/src/worktree/index.ts b/packages/opencode/src/worktree/index.ts index 14a3a0dc9b..39bf94d69b 100644 --- a/packages/opencode/src/worktree/index.ts +++ b/packages/opencode/src/worktree/index.ts @@ -1,600 +1 @@ -import z from "zod" -import { NamedError } from "@opencode-ai/shared/util/error" -import { Global } from "../global" -import { Instance } from "../project/instance" -import { InstanceBootstrap } from "../project/bootstrap" -import { Project } from "../project/project" -import { Database, eq } from "../storage/db" -import { ProjectTable } from "../project/project.sql" -import type { ProjectID } from "../project/schema" -import { Log } from "../util/log" -import { Slug } from "@opencode-ai/shared/util/slug" -import { errorMessage } from "../util/error" -import { BusEvent } from "@/bus/bus-event" -import { GlobalBus } from "@/bus/global" -import { Git } from "@/git" -import { Effect, Layer, Path, Scope, Context, Stream } from "effect" -import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" -import { NodePath } from "@effect/platform-node" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" -import { BootstrapRuntime } from "@/effect/bootstrap-runtime" -import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" -import { InstanceState } from "@/effect/instance-state" - -export namespace Worktree { - const log = Log.create({ service: "worktree" }) - - export const Event = { - Ready: BusEvent.define( - "worktree.ready", - z.object({ - name: z.string(), - branch: z.string(), - }), - ), - Failed: BusEvent.define( - "worktree.failed", - z.object({ - message: z.string(), - }), - ), - } - - export const Info = z - .object({ - name: z.string(), - branch: z.string(), - directory: z.string(), - }) - .meta({ - ref: "Worktree", - }) - - export type Info = z.infer - - export const CreateInput = z - .object({ - name: z.string().optional(), - startCommand: z - .string() - .optional() - .describe("Additional startup script to run after the project's start command"), - }) - .meta({ - ref: "WorktreeCreateInput", - }) - - export type CreateInput = z.infer - - export const RemoveInput = z - .object({ - directory: z.string(), - }) - .meta({ - ref: "WorktreeRemoveInput", - }) - - export type RemoveInput = z.infer - - export const ResetInput = z - .object({ - directory: z.string(), - }) - .meta({ - ref: "WorktreeResetInput", - }) - - export type ResetInput = z.infer - - export const NotGitError = NamedError.create( - "WorktreeNotGitError", - z.object({ - message: z.string(), - }), - ) - - export const NameGenerationFailedError = NamedError.create( - "WorktreeNameGenerationFailedError", - z.object({ - message: z.string(), - }), - ) - - export const CreateFailedError = NamedError.create( - "WorktreeCreateFailedError", - z.object({ - message: z.string(), - }), - ) - - export const StartCommandFailedError = NamedError.create( - "WorktreeStartCommandFailedError", - z.object({ - message: z.string(), - }), - ) - - export const RemoveFailedError = NamedError.create( - "WorktreeRemoveFailedError", - z.object({ - message: z.string(), - }), - ) - - export const ResetFailedError = NamedError.create( - "WorktreeResetFailedError", - z.object({ - message: z.string(), - }), - ) - - function slugify(input: string) { - return input - .trim() - .toLowerCase() - .replace(/[^a-z0-9]+/g, "-") - .replace(/^-+/, "") - .replace(/-+$/, "") - } - - function failedRemoves(...chunks: string[]) { - return chunks.filter(Boolean).flatMap((chunk) => - chunk - .split("\n") - .map((line) => line.trim()) - .flatMap((line) => { - const match = line.match(/^warning:\s+failed to remove\s+(.+):\s+/i) - if (!match) return [] - const value = match[1]?.trim().replace(/^['"]|['"]$/g, "") - if (!value) return [] - return [value] - }), - ) - } - - // --------------------------------------------------------------------------- - // Effect service - // --------------------------------------------------------------------------- - - export interface Interface { - readonly makeWorktreeInfo: (name?: string) => Effect.Effect - readonly createFromInfo: (info: Info, startCommand?: string) => Effect.Effect - readonly create: (input?: CreateInput) => Effect.Effect - readonly remove: (input: RemoveInput) => Effect.Effect - readonly reset: (input: ResetInput) => Effect.Effect - } - - export class Service extends Context.Service()("@opencode/Worktree") {} - - type GitResult = { code: number; text: string; stderr: string } - - export const layer: Layer.Layer< - Service, - never, - AppFileSystem.Service | Path.Path | ChildProcessSpawner.ChildProcessSpawner | Git.Service | Project.Service - > = Layer.effect( - Service, - Effect.gen(function* () { - const scope = yield* Scope.Scope - const fs = yield* AppFileSystem.Service - const pathSvc = yield* Path.Path - const spawner = yield* ChildProcessSpawner.ChildProcessSpawner - const gitSvc = yield* Git.Service - const project = yield* Project.Service - - const git = Effect.fnUntraced( - function* (args: string[], opts?: { cwd?: string }) { - const handle = yield* spawner.spawn( - ChildProcess.make("git", args, { cwd: opts?.cwd, extendEnv: true, stdin: "ignore" }), - ) - const [text, stderr] = yield* Effect.all( - [Stream.mkString(Stream.decodeText(handle.stdout)), Stream.mkString(Stream.decodeText(handle.stderr))], - { concurrency: 2 }, - ) - const code = yield* handle.exitCode - return { code, text, stderr } satisfies GitResult - }, - Effect.scoped, - Effect.catch((e) => - Effect.succeed({ code: 1, text: "", stderr: e instanceof Error ? e.message : String(e) } satisfies GitResult), - ), - ) - - const MAX_NAME_ATTEMPTS = 26 - const candidate = Effect.fn("Worktree.candidate")(function* (root: string, base?: string) { - const ctx = yield* InstanceState.context - for (const attempt of Array.from({ length: MAX_NAME_ATTEMPTS }, (_, i) => i)) { - const name = base ? (attempt === 0 ? base : `${base}-${Slug.create()}`) : Slug.create() - const branch = `opencode/${name}` - const directory = pathSvc.join(root, name) - - if (yield* fs.exists(directory).pipe(Effect.orDie)) continue - - const ref = `refs/heads/${branch}` - const branchCheck = yield* git(["show-ref", "--verify", "--quiet", ref], { cwd: ctx.worktree }) - if (branchCheck.code === 0) continue - - return Info.parse({ name, branch, directory }) - } - throw new NameGenerationFailedError({ message: "Failed to generate a unique worktree name" }) - }) - - const makeWorktreeInfo = Effect.fn("Worktree.makeWorktreeInfo")(function* (name?: string) { - const ctx = yield* InstanceState.context - if (ctx.project.vcs !== "git") { - throw new NotGitError({ message: "Worktrees are only supported for git projects" }) - } - - const root = pathSvc.join(Global.Path.data, "worktree", ctx.project.id) - yield* fs.makeDirectory(root, { recursive: true }).pipe(Effect.orDie) - - const base = name ? slugify(name) : "" - return yield* candidate(root, base || undefined) - }) - - const setup = Effect.fnUntraced(function* (info: Info) { - const ctx = yield* InstanceState.context - const created = yield* git(["worktree", "add", "--no-checkout", "-b", info.branch, info.directory], { - cwd: ctx.worktree, - }) - if (created.code !== 0) { - throw new CreateFailedError({ message: created.stderr || created.text || "Failed to create git worktree" }) - } - - yield* project.addSandbox(ctx.project.id, info.directory).pipe(Effect.catch(() => Effect.void)) - }) - - const boot = Effect.fnUntraced(function* (info: Info, startCommand?: string) { - const ctx = yield* InstanceState.context - const workspaceID = yield* InstanceState.workspaceID - const projectID = ctx.project.id - const extra = startCommand?.trim() - - const populated = yield* git(["reset", "--hard"], { cwd: info.directory }) - if (populated.code !== 0) { - const message = populated.stderr || populated.text || "Failed to populate worktree" - log.error("worktree checkout failed", { directory: info.directory, message }) - GlobalBus.emit("event", { - directory: info.directory, - project: ctx.project.id, - workspace: workspaceID, - payload: { type: Event.Failed.type, properties: { message } }, - }) - return - } - - const booted = yield* Effect.promise(() => - Instance.provide({ - directory: info.directory, - init: () => BootstrapRuntime.runPromise(InstanceBootstrap), - fn: () => undefined, - }) - .then(() => true) - .catch((error) => { - const message = errorMessage(error) - log.error("worktree bootstrap failed", { directory: info.directory, message }) - GlobalBus.emit("event", { - directory: info.directory, - project: ctx.project.id, - workspace: workspaceID, - payload: { type: Event.Failed.type, properties: { message } }, - }) - return false - }), - ) - if (!booted) return - - GlobalBus.emit("event", { - directory: info.directory, - project: ctx.project.id, - workspace: workspaceID, - payload: { - type: Event.Ready.type, - properties: { name: info.name, branch: info.branch }, - }, - }) - - yield* runStartScripts(info.directory, { projectID, extra }) - }) - - const createFromInfo = Effect.fn("Worktree.createFromInfo")(function* (info: Info, startCommand?: string) { - yield* setup(info) - yield* boot(info, startCommand) - }) - - const create = Effect.fn("Worktree.create")(function* (input?: CreateInput) { - const info = yield* makeWorktreeInfo(input?.name) - yield* setup(info) - yield* boot(info, input?.startCommand).pipe( - Effect.catchCause((cause) => Effect.sync(() => log.error("worktree bootstrap failed", { cause }))), - Effect.forkIn(scope), - ) - return info - }) - - const canonical = Effect.fnUntraced(function* (input: string) { - const abs = pathSvc.resolve(input) - const real = yield* fs.realPath(abs).pipe(Effect.catch(() => Effect.succeed(abs))) - const normalized = pathSvc.normalize(real) - return process.platform === "win32" ? normalized.toLowerCase() : normalized - }) - - function parseWorktreeList(text: string) { - return text - .split("\n") - .map((line) => line.trim()) - .reduce<{ path?: string; branch?: string }[]>((acc, line) => { - if (!line) return acc - if (line.startsWith("worktree ")) { - acc.push({ path: line.slice("worktree ".length).trim() }) - return acc - } - const current = acc[acc.length - 1] - if (!current) return acc - if (line.startsWith("branch ")) { - current.branch = line.slice("branch ".length).trim() - } - return acc - }, []) - } - - const locateWorktree = Effect.fnUntraced(function* ( - entries: { path?: string; branch?: string }[], - directory: string, - ) { - for (const item of entries) { - if (!item.path) continue - const key = yield* canonical(item.path) - if (key === directory) return item - } - return undefined - }) - - function stopFsmonitor(target: string) { - return fs.exists(target).pipe( - Effect.orDie, - Effect.flatMap((exists) => (exists ? git(["fsmonitor--daemon", "stop"], { cwd: target }) : Effect.void)), - ) - } - - function cleanDirectory(target: string) { - return Effect.promise(() => - import("fs/promises") - .then((fsp) => fsp.rm(target, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 })) - .catch((error) => { - const message = errorMessage(error) - throw new RemoveFailedError({ message: message || "Failed to remove git worktree directory" }) - }), - ) - } - - const remove = Effect.fn("Worktree.remove")(function* (input: RemoveInput) { - if (Instance.project.vcs !== "git") { - throw new NotGitError({ message: "Worktrees are only supported for git projects" }) - } - - const directory = yield* canonical(input.directory) - - const list = yield* git(["worktree", "list", "--porcelain"], { cwd: Instance.worktree }) - if (list.code !== 0) { - throw new RemoveFailedError({ message: list.stderr || list.text || "Failed to read git worktrees" }) - } - - const entries = parseWorktreeList(list.text) - const entry = yield* locateWorktree(entries, directory) - - if (!entry?.path) { - const directoryExists = yield* fs.exists(directory).pipe(Effect.orDie) - if (directoryExists) { - yield* stopFsmonitor(directory) - yield* cleanDirectory(directory) - } - return true - } - - yield* stopFsmonitor(entry.path) - const removed = yield* git(["worktree", "remove", "--force", entry.path], { cwd: Instance.worktree }) - if (removed.code !== 0) { - const next = yield* git(["worktree", "list", "--porcelain"], { cwd: Instance.worktree }) - if (next.code !== 0) { - throw new RemoveFailedError({ - message: removed.stderr || removed.text || next.stderr || next.text || "Failed to remove git worktree", - }) - } - - const stale = yield* locateWorktree(parseWorktreeList(next.text), directory) - if (stale?.path) { - throw new RemoveFailedError({ message: removed.stderr || removed.text || "Failed to remove git worktree" }) - } - } - - yield* cleanDirectory(entry.path) - - const branch = entry.branch?.replace(/^refs\/heads\//, "") - if (branch) { - const deleted = yield* git(["branch", "-D", branch], { cwd: Instance.worktree }) - if (deleted.code !== 0) { - throw new RemoveFailedError({ - message: deleted.stderr || deleted.text || "Failed to delete worktree branch", - }) - } - } - - return true - }) - - const gitExpect = Effect.fnUntraced(function* ( - args: string[], - opts: { cwd: string }, - error: (r: GitResult) => Error, - ) { - const result = yield* git(args, opts) - if (result.code !== 0) throw error(result) - return result - }) - - const runStartCommand = Effect.fnUntraced( - function* (directory: string, cmd: string) { - const [shell, args] = process.platform === "win32" ? ["cmd", ["/c", cmd]] : ["bash", ["-lc", cmd]] - const handle = yield* spawner.spawn( - ChildProcess.make(shell, args, { cwd: directory, extendEnv: true, stdin: "ignore" }), - ) - // Drain stdout, capture stderr for error reporting - const [, stderr] = yield* Effect.all( - [Stream.runDrain(handle.stdout), Stream.mkString(Stream.decodeText(handle.stderr))], - { concurrency: 2 }, - ).pipe(Effect.orDie) - const code = yield* handle.exitCode - return { code, stderr } - }, - Effect.scoped, - Effect.catch(() => Effect.succeed({ code: 1, stderr: "" })), - ) - - const runStartScript = Effect.fnUntraced(function* (directory: string, cmd: string, kind: string) { - const text = cmd.trim() - if (!text) return true - const result = yield* runStartCommand(directory, text) - if (result.code === 0) return true - log.error("worktree start command failed", { kind, directory, message: result.stderr }) - return false - }) - - const runStartScripts = Effect.fnUntraced(function* ( - directory: string, - input: { projectID: ProjectID; extra?: string }, - ) { - const row = yield* Effect.sync(() => - Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, input.projectID)).get()), - ) - const project = row ? Project.fromRow(row) : undefined - const startup = project?.commands?.start?.trim() ?? "" - const ok = yield* runStartScript(directory, startup, "project") - if (!ok) return false - yield* runStartScript(directory, input.extra ?? "", "worktree") - return true - }) - - const prune = Effect.fnUntraced(function* (root: string, entries: string[]) { - const base = yield* canonical(root) - yield* Effect.forEach( - entries, - (entry) => - Effect.gen(function* () { - const target = yield* canonical(pathSvc.resolve(root, entry)) - if (target === base) return - if (!target.startsWith(`${base}${pathSvc.sep}`)) return - yield* fs.remove(target, { recursive: true }).pipe(Effect.ignore) - }), - { concurrency: "unbounded" }, - ) - }) - - const sweep = Effect.fnUntraced(function* (root: string) { - const first = yield* git(["clean", "-ffdx"], { cwd: root }) - if (first.code === 0) return first - - const entries = failedRemoves(first.stderr, first.text) - if (!entries.length) return first - - yield* prune(root, entries) - return yield* git(["clean", "-ffdx"], { cwd: root }) - }) - - const reset = Effect.fn("Worktree.reset")(function* (input: ResetInput) { - if (Instance.project.vcs !== "git") { - throw new NotGitError({ message: "Worktrees are only supported for git projects" }) - } - - const directory = yield* canonical(input.directory) - const primary = yield* canonical(Instance.worktree) - if (directory === primary) { - throw new ResetFailedError({ message: "Cannot reset the primary workspace" }) - } - - const list = yield* git(["worktree", "list", "--porcelain"], { cwd: Instance.worktree }) - if (list.code !== 0) { - throw new ResetFailedError({ message: list.stderr || list.text || "Failed to read git worktrees" }) - } - - const entry = yield* locateWorktree(parseWorktreeList(list.text), directory) - if (!entry?.path) { - throw new ResetFailedError({ message: "Worktree not found" }) - } - - const worktreePath = entry.path - - const base = yield* gitSvc.defaultBranch(Instance.worktree) - if (!base) { - throw new ResetFailedError({ message: "Default branch not found" }) - } - - const sep = base.ref.indexOf("/") - if (base.ref !== base.name && sep > 0) { - const remote = base.ref.slice(0, sep) - const branch = base.ref.slice(sep + 1) - yield* gitExpect( - ["fetch", remote, branch], - { cwd: Instance.worktree }, - (r) => new ResetFailedError({ message: r.stderr || r.text || `Failed to fetch ${base.ref}` }), - ) - } - - yield* gitExpect( - ["reset", "--hard", base.ref], - { cwd: worktreePath }, - (r) => new ResetFailedError({ message: r.stderr || r.text || "Failed to reset worktree to target" }), - ) - - const cleanResult = yield* sweep(worktreePath) - if (cleanResult.code !== 0) { - throw new ResetFailedError({ message: cleanResult.stderr || cleanResult.text || "Failed to clean worktree" }) - } - - yield* gitExpect( - ["submodule", "update", "--init", "--recursive", "--force"], - { cwd: worktreePath }, - (r) => new ResetFailedError({ message: r.stderr || r.text || "Failed to update submodules" }), - ) - - yield* gitExpect( - ["submodule", "foreach", "--recursive", "git", "reset", "--hard"], - { cwd: worktreePath }, - (r) => new ResetFailedError({ message: r.stderr || r.text || "Failed to reset submodules" }), - ) - - yield* gitExpect( - ["submodule", "foreach", "--recursive", "git", "clean", "-fdx"], - { cwd: worktreePath }, - (r) => new ResetFailedError({ message: r.stderr || r.text || "Failed to clean submodules" }), - ) - - const status = yield* git(["-c", "core.fsmonitor=false", "status", "--porcelain=v1"], { cwd: worktreePath }) - if (status.code !== 0) { - throw new ResetFailedError({ message: status.stderr || status.text || "Failed to read git status" }) - } - - if (status.text.trim()) { - throw new ResetFailedError({ message: `Worktree reset left local changes:\n${status.text.trim()}` }) - } - - yield* runStartScripts(worktreePath, { projectID: Instance.project.id }).pipe( - Effect.catchCause((cause) => Effect.sync(() => log.error("worktree start task failed", { cause }))), - Effect.forkIn(scope), - ) - - return true - }) - - return Service.of({ makeWorktreeInfo, createFromInfo, create, remove, reset }) - }), - ) - - export const defaultLayer = layer.pipe( - Layer.provide(Git.defaultLayer), - Layer.provide(CrossSpawnSpawner.defaultLayer), - Layer.provide(Project.defaultLayer), - Layer.provide(AppFileSystem.defaultLayer), - Layer.provide(NodePath.layer), - ) -} +export * as Worktree from "./worktree" diff --git a/packages/opencode/src/worktree/worktree.ts b/packages/opencode/src/worktree/worktree.ts new file mode 100644 index 0000000000..9280b7a52e --- /dev/null +++ b/packages/opencode/src/worktree/worktree.ts @@ -0,0 +1,598 @@ +import z from "zod" +import { NamedError } from "@opencode-ai/shared/util/error" +import { Global } from "../global" +import { Instance } from "../project/instance" +import { InstanceBootstrap } from "../project/bootstrap" +import { Project } from "../project/project" +import { Database, eq } from "../storage/db" +import { ProjectTable } from "../project/project.sql" +import type { ProjectID } from "../project/schema" +import { Log } from "../util/log" +import { Slug } from "@opencode-ai/shared/util/slug" +import { errorMessage } from "../util/error" +import { BusEvent } from "@/bus/bus-event" +import { GlobalBus } from "@/bus/global" +import { Git } from "@/git" +import { Effect, Layer, Path, Scope, Context, Stream } from "effect" +import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" +import { NodePath } from "@effect/platform-node" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { BootstrapRuntime } from "@/effect/bootstrap-runtime" +import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" +import { InstanceState } from "@/effect/instance-state" + +const log = Log.create({ service: "worktree" }) + +export const Event = { + Ready: BusEvent.define( + "worktree.ready", + z.object({ + name: z.string(), + branch: z.string(), + }), + ), + Failed: BusEvent.define( + "worktree.failed", + z.object({ + message: z.string(), + }), + ), +} + +export const Info = z + .object({ + name: z.string(), + branch: z.string(), + directory: z.string(), + }) + .meta({ + ref: "Worktree", + }) + +export type Info = z.infer + +export const CreateInput = z + .object({ + name: z.string().optional(), + startCommand: z + .string() + .optional() + .describe("Additional startup script to run after the project's start command"), + }) + .meta({ + ref: "WorktreeCreateInput", + }) + +export type CreateInput = z.infer + +export const RemoveInput = z + .object({ + directory: z.string(), + }) + .meta({ + ref: "WorktreeRemoveInput", + }) + +export type RemoveInput = z.infer + +export const ResetInput = z + .object({ + directory: z.string(), + }) + .meta({ + ref: "WorktreeResetInput", + }) + +export type ResetInput = z.infer + +export const NotGitError = NamedError.create( + "WorktreeNotGitError", + z.object({ + message: z.string(), + }), +) + +export const NameGenerationFailedError = NamedError.create( + "WorktreeNameGenerationFailedError", + z.object({ + message: z.string(), + }), +) + +export const CreateFailedError = NamedError.create( + "WorktreeCreateFailedError", + z.object({ + message: z.string(), + }), +) + +export const StartCommandFailedError = NamedError.create( + "WorktreeStartCommandFailedError", + z.object({ + message: z.string(), + }), +) + +export const RemoveFailedError = NamedError.create( + "WorktreeRemoveFailedError", + z.object({ + message: z.string(), + }), +) + +export const ResetFailedError = NamedError.create( + "WorktreeResetFailedError", + z.object({ + message: z.string(), + }), +) + +function slugify(input: string) { + return input + .trim() + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+/, "") + .replace(/-+$/, "") +} + +function failedRemoves(...chunks: string[]) { + return chunks.filter(Boolean).flatMap((chunk) => + chunk + .split("\n") + .map((line) => line.trim()) + .flatMap((line) => { + const match = line.match(/^warning:\s+failed to remove\s+(.+):\s+/i) + if (!match) return [] + const value = match[1]?.trim().replace(/^['"]|['"]$/g, "") + if (!value) return [] + return [value] + }), + ) +} + +// --------------------------------------------------------------------------- +// Effect service +// --------------------------------------------------------------------------- + +export interface Interface { + readonly makeWorktreeInfo: (name?: string) => Effect.Effect + readonly createFromInfo: (info: Info, startCommand?: string) => Effect.Effect + readonly create: (input?: CreateInput) => Effect.Effect + readonly remove: (input: RemoveInput) => Effect.Effect + readonly reset: (input: ResetInput) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/Worktree") {} + +type GitResult = { code: number; text: string; stderr: string } + +export const layer: Layer.Layer< + Service, + never, + AppFileSystem.Service | Path.Path | ChildProcessSpawner.ChildProcessSpawner | Git.Service | Project.Service +> = Layer.effect( + Service, + Effect.gen(function* () { + const scope = yield* Scope.Scope + const fs = yield* AppFileSystem.Service + const pathSvc = yield* Path.Path + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner + const gitSvc = yield* Git.Service + const project = yield* Project.Service + + const git = Effect.fnUntraced( + function* (args: string[], opts?: { cwd?: string }) { + const handle = yield* spawner.spawn( + ChildProcess.make("git", args, { cwd: opts?.cwd, extendEnv: true, stdin: "ignore" }), + ) + const [text, stderr] = yield* Effect.all( + [Stream.mkString(Stream.decodeText(handle.stdout)), Stream.mkString(Stream.decodeText(handle.stderr))], + { concurrency: 2 }, + ) + const code = yield* handle.exitCode + return { code, text, stderr } satisfies GitResult + }, + Effect.scoped, + Effect.catch((e) => + Effect.succeed({ code: 1, text: "", stderr: e instanceof Error ? e.message : String(e) } satisfies GitResult), + ), + ) + + const MAX_NAME_ATTEMPTS = 26 + const candidate = Effect.fn("Worktree.candidate")(function* (root: string, base?: string) { + const ctx = yield* InstanceState.context + for (const attempt of Array.from({ length: MAX_NAME_ATTEMPTS }, (_, i) => i)) { + const name = base ? (attempt === 0 ? base : `${base}-${Slug.create()}`) : Slug.create() + const branch = `opencode/${name}` + const directory = pathSvc.join(root, name) + + if (yield* fs.exists(directory).pipe(Effect.orDie)) continue + + const ref = `refs/heads/${branch}` + const branchCheck = yield* git(["show-ref", "--verify", "--quiet", ref], { cwd: ctx.worktree }) + if (branchCheck.code === 0) continue + + return Info.parse({ name, branch, directory }) + } + throw new NameGenerationFailedError({ message: "Failed to generate a unique worktree name" }) + }) + + const makeWorktreeInfo = Effect.fn("Worktree.makeWorktreeInfo")(function* (name?: string) { + const ctx = yield* InstanceState.context + if (ctx.project.vcs !== "git") { + throw new NotGitError({ message: "Worktrees are only supported for git projects" }) + } + + const root = pathSvc.join(Global.Path.data, "worktree", ctx.project.id) + yield* fs.makeDirectory(root, { recursive: true }).pipe(Effect.orDie) + + const base = name ? slugify(name) : "" + return yield* candidate(root, base || undefined) + }) + + const setup = Effect.fnUntraced(function* (info: Info) { + const ctx = yield* InstanceState.context + const created = yield* git(["worktree", "add", "--no-checkout", "-b", info.branch, info.directory], { + cwd: ctx.worktree, + }) + if (created.code !== 0) { + throw new CreateFailedError({ message: created.stderr || created.text || "Failed to create git worktree" }) + } + + yield* project.addSandbox(ctx.project.id, info.directory).pipe(Effect.catch(() => Effect.void)) + }) + + const boot = Effect.fnUntraced(function* (info: Info, startCommand?: string) { + const ctx = yield* InstanceState.context + const workspaceID = yield* InstanceState.workspaceID + const projectID = ctx.project.id + const extra = startCommand?.trim() + + const populated = yield* git(["reset", "--hard"], { cwd: info.directory }) + if (populated.code !== 0) { + const message = populated.stderr || populated.text || "Failed to populate worktree" + log.error("worktree checkout failed", { directory: info.directory, message }) + GlobalBus.emit("event", { + directory: info.directory, + project: ctx.project.id, + workspace: workspaceID, + payload: { type: Event.Failed.type, properties: { message } }, + }) + return + } + + const booted = yield* Effect.promise(() => + Instance.provide({ + directory: info.directory, + init: () => BootstrapRuntime.runPromise(InstanceBootstrap), + fn: () => undefined, + }) + .then(() => true) + .catch((error) => { + const message = errorMessage(error) + log.error("worktree bootstrap failed", { directory: info.directory, message }) + GlobalBus.emit("event", { + directory: info.directory, + project: ctx.project.id, + workspace: workspaceID, + payload: { type: Event.Failed.type, properties: { message } }, + }) + return false + }), + ) + if (!booted) return + + GlobalBus.emit("event", { + directory: info.directory, + project: ctx.project.id, + workspace: workspaceID, + payload: { + type: Event.Ready.type, + properties: { name: info.name, branch: info.branch }, + }, + }) + + yield* runStartScripts(info.directory, { projectID, extra }) + }) + + const createFromInfo = Effect.fn("Worktree.createFromInfo")(function* (info: Info, startCommand?: string) { + yield* setup(info) + yield* boot(info, startCommand) + }) + + const create = Effect.fn("Worktree.create")(function* (input?: CreateInput) { + const info = yield* makeWorktreeInfo(input?.name) + yield* setup(info) + yield* boot(info, input?.startCommand).pipe( + Effect.catchCause((cause) => Effect.sync(() => log.error("worktree bootstrap failed", { cause }))), + Effect.forkIn(scope), + ) + return info + }) + + const canonical = Effect.fnUntraced(function* (input: string) { + const abs = pathSvc.resolve(input) + const real = yield* fs.realPath(abs).pipe(Effect.catch(() => Effect.succeed(abs))) + const normalized = pathSvc.normalize(real) + return process.platform === "win32" ? normalized.toLowerCase() : normalized + }) + + function parseWorktreeList(text: string) { + return text + .split("\n") + .map((line) => line.trim()) + .reduce<{ path?: string; branch?: string }[]>((acc, line) => { + if (!line) return acc + if (line.startsWith("worktree ")) { + acc.push({ path: line.slice("worktree ".length).trim() }) + return acc + } + const current = acc[acc.length - 1] + if (!current) return acc + if (line.startsWith("branch ")) { + current.branch = line.slice("branch ".length).trim() + } + return acc + }, []) + } + + const locateWorktree = Effect.fnUntraced(function* ( + entries: { path?: string; branch?: string }[], + directory: string, + ) { + for (const item of entries) { + if (!item.path) continue + const key = yield* canonical(item.path) + if (key === directory) return item + } + return undefined + }) + + function stopFsmonitor(target: string) { + return fs.exists(target).pipe( + Effect.orDie, + Effect.flatMap((exists) => (exists ? git(["fsmonitor--daemon", "stop"], { cwd: target }) : Effect.void)), + ) + } + + function cleanDirectory(target: string) { + return Effect.promise(() => + import("fs/promises") + .then((fsp) => fsp.rm(target, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 })) + .catch((error) => { + const message = errorMessage(error) + throw new RemoveFailedError({ message: message || "Failed to remove git worktree directory" }) + }), + ) + } + + const remove = Effect.fn("Worktree.remove")(function* (input: RemoveInput) { + if (Instance.project.vcs !== "git") { + throw new NotGitError({ message: "Worktrees are only supported for git projects" }) + } + + const directory = yield* canonical(input.directory) + + const list = yield* git(["worktree", "list", "--porcelain"], { cwd: Instance.worktree }) + if (list.code !== 0) { + throw new RemoveFailedError({ message: list.stderr || list.text || "Failed to read git worktrees" }) + } + + const entries = parseWorktreeList(list.text) + const entry = yield* locateWorktree(entries, directory) + + if (!entry?.path) { + const directoryExists = yield* fs.exists(directory).pipe(Effect.orDie) + if (directoryExists) { + yield* stopFsmonitor(directory) + yield* cleanDirectory(directory) + } + return true + } + + yield* stopFsmonitor(entry.path) + const removed = yield* git(["worktree", "remove", "--force", entry.path], { cwd: Instance.worktree }) + if (removed.code !== 0) { + const next = yield* git(["worktree", "list", "--porcelain"], { cwd: Instance.worktree }) + if (next.code !== 0) { + throw new RemoveFailedError({ + message: removed.stderr || removed.text || next.stderr || next.text || "Failed to remove git worktree", + }) + } + + const stale = yield* locateWorktree(parseWorktreeList(next.text), directory) + if (stale?.path) { + throw new RemoveFailedError({ message: removed.stderr || removed.text || "Failed to remove git worktree" }) + } + } + + yield* cleanDirectory(entry.path) + + const branch = entry.branch?.replace(/^refs\/heads\//, "") + if (branch) { + const deleted = yield* git(["branch", "-D", branch], { cwd: Instance.worktree }) + if (deleted.code !== 0) { + throw new RemoveFailedError({ + message: deleted.stderr || deleted.text || "Failed to delete worktree branch", + }) + } + } + + return true + }) + + const gitExpect = Effect.fnUntraced(function* ( + args: string[], + opts: { cwd: string }, + error: (r: GitResult) => Error, + ) { + const result = yield* git(args, opts) + if (result.code !== 0) throw error(result) + return result + }) + + const runStartCommand = Effect.fnUntraced( + function* (directory: string, cmd: string) { + const [shell, args] = process.platform === "win32" ? ["cmd", ["/c", cmd]] : ["bash", ["-lc", cmd]] + const handle = yield* spawner.spawn( + ChildProcess.make(shell, args, { cwd: directory, extendEnv: true, stdin: "ignore" }), + ) + // Drain stdout, capture stderr for error reporting + const [, stderr] = yield* Effect.all( + [Stream.runDrain(handle.stdout), Stream.mkString(Stream.decodeText(handle.stderr))], + { concurrency: 2 }, + ).pipe(Effect.orDie) + const code = yield* handle.exitCode + return { code, stderr } + }, + Effect.scoped, + Effect.catch(() => Effect.succeed({ code: 1, stderr: "" })), + ) + + const runStartScript = Effect.fnUntraced(function* (directory: string, cmd: string, kind: string) { + const text = cmd.trim() + if (!text) return true + const result = yield* runStartCommand(directory, text) + if (result.code === 0) return true + log.error("worktree start command failed", { kind, directory, message: result.stderr }) + return false + }) + + const runStartScripts = Effect.fnUntraced(function* ( + directory: string, + input: { projectID: ProjectID; extra?: string }, + ) { + const row = yield* Effect.sync(() => + Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, input.projectID)).get()), + ) + const project = row ? Project.fromRow(row) : undefined + const startup = project?.commands?.start?.trim() ?? "" + const ok = yield* runStartScript(directory, startup, "project") + if (!ok) return false + yield* runStartScript(directory, input.extra ?? "", "worktree") + return true + }) + + const prune = Effect.fnUntraced(function* (root: string, entries: string[]) { + const base = yield* canonical(root) + yield* Effect.forEach( + entries, + (entry) => + Effect.gen(function* () { + const target = yield* canonical(pathSvc.resolve(root, entry)) + if (target === base) return + if (!target.startsWith(`${base}${pathSvc.sep}`)) return + yield* fs.remove(target, { recursive: true }).pipe(Effect.ignore) + }), + { concurrency: "unbounded" }, + ) + }) + + const sweep = Effect.fnUntraced(function* (root: string) { + const first = yield* git(["clean", "-ffdx"], { cwd: root }) + if (first.code === 0) return first + + const entries = failedRemoves(first.stderr, first.text) + if (!entries.length) return first + + yield* prune(root, entries) + return yield* git(["clean", "-ffdx"], { cwd: root }) + }) + + const reset = Effect.fn("Worktree.reset")(function* (input: ResetInput) { + if (Instance.project.vcs !== "git") { + throw new NotGitError({ message: "Worktrees are only supported for git projects" }) + } + + const directory = yield* canonical(input.directory) + const primary = yield* canonical(Instance.worktree) + if (directory === primary) { + throw new ResetFailedError({ message: "Cannot reset the primary workspace" }) + } + + const list = yield* git(["worktree", "list", "--porcelain"], { cwd: Instance.worktree }) + if (list.code !== 0) { + throw new ResetFailedError({ message: list.stderr || list.text || "Failed to read git worktrees" }) + } + + const entry = yield* locateWorktree(parseWorktreeList(list.text), directory) + if (!entry?.path) { + throw new ResetFailedError({ message: "Worktree not found" }) + } + + const worktreePath = entry.path + + const base = yield* gitSvc.defaultBranch(Instance.worktree) + if (!base) { + throw new ResetFailedError({ message: "Default branch not found" }) + } + + const sep = base.ref.indexOf("/") + if (base.ref !== base.name && sep > 0) { + const remote = base.ref.slice(0, sep) + const branch = base.ref.slice(sep + 1) + yield* gitExpect( + ["fetch", remote, branch], + { cwd: Instance.worktree }, + (r) => new ResetFailedError({ message: r.stderr || r.text || `Failed to fetch ${base.ref}` }), + ) + } + + yield* gitExpect( + ["reset", "--hard", base.ref], + { cwd: worktreePath }, + (r) => new ResetFailedError({ message: r.stderr || r.text || "Failed to reset worktree to target" }), + ) + + const cleanResult = yield* sweep(worktreePath) + if (cleanResult.code !== 0) { + throw new ResetFailedError({ message: cleanResult.stderr || cleanResult.text || "Failed to clean worktree" }) + } + + yield* gitExpect( + ["submodule", "update", "--init", "--recursive", "--force"], + { cwd: worktreePath }, + (r) => new ResetFailedError({ message: r.stderr || r.text || "Failed to update submodules" }), + ) + + yield* gitExpect( + ["submodule", "foreach", "--recursive", "git", "reset", "--hard"], + { cwd: worktreePath }, + (r) => new ResetFailedError({ message: r.stderr || r.text || "Failed to reset submodules" }), + ) + + yield* gitExpect( + ["submodule", "foreach", "--recursive", "git", "clean", "-fdx"], + { cwd: worktreePath }, + (r) => new ResetFailedError({ message: r.stderr || r.text || "Failed to clean submodules" }), + ) + + const status = yield* git(["-c", "core.fsmonitor=false", "status", "--porcelain=v1"], { cwd: worktreePath }) + if (status.code !== 0) { + throw new ResetFailedError({ message: status.stderr || status.text || "Failed to read git status" }) + } + + if (status.text.trim()) { + throw new ResetFailedError({ message: `Worktree reset left local changes:\n${status.text.trim()}` }) + } + + yield* runStartScripts(worktreePath, { projectID: Instance.project.id }).pipe( + Effect.catchCause((cause) => Effect.sync(() => log.error("worktree start task failed", { cause }))), + Effect.forkIn(scope), + ) + + return true + }) + + return Service.of({ makeWorktreeInfo, createFromInfo, create, remove, reset }) + }), +) + +export const defaultLayer = layer.pipe( + Layer.provide(Git.defaultLayer), + Layer.provide(CrossSpawnSpawner.defaultLayer), + Layer.provide(Project.defaultLayer), + Layer.provide(AppFileSystem.defaultLayer), + Layer.provide(NodePath.layer), +) From 0b975b01fbb4e2774fdd6d690675fd92b81d6029 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 22:16:42 -0400 Subject: [PATCH 197/300] feat: unwrap ugit namespace to flat exports + barrel (#22704) --- packages/opencode/src/git/git.ts | 258 ++++++++++++++++++++++++++++ packages/opencode/src/git/index.ts | 261 +---------------------------- 2 files changed, 259 insertions(+), 260 deletions(-) create mode 100644 packages/opencode/src/git/git.ts diff --git a/packages/opencode/src/git/git.ts b/packages/opencode/src/git/git.ts new file mode 100644 index 0000000000..908c718521 --- /dev/null +++ b/packages/opencode/src/git/git.ts @@ -0,0 +1,258 @@ +import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" +import { Effect, Layer, Context, Stream } from "effect" +import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" + +const cfg = [ + "--no-optional-locks", + "-c", + "core.autocrlf=false", + "-c", + "core.fsmonitor=false", + "-c", + "core.longpaths=true", + "-c", + "core.symlinks=true", + "-c", + "core.quotepath=false", +] as const + +const out = (result: { text(): string }) => result.text().trim() +const nuls = (text: string) => text.split("\0").filter(Boolean) +const fail = (err: unknown) => + ({ + exitCode: 1, + text: () => "", + stdout: Buffer.alloc(0), + stderr: Buffer.from(err instanceof Error ? err.message : String(err)), + }) satisfies Result + +export type Kind = "added" | "deleted" | "modified" + +export type Base = { + readonly name: string + readonly ref: string +} + +export type Item = { + readonly file: string + readonly code: string + readonly status: Kind +} + +export type Stat = { + readonly file: string + readonly additions: number + readonly deletions: number +} + +export interface Result { + readonly exitCode: number + readonly text: () => string + readonly stdout: Buffer + readonly stderr: Buffer +} + +export interface Options { + readonly cwd: string + readonly env?: Record +} + +export interface Interface { + readonly run: (args: string[], opts: Options) => Effect.Effect + readonly branch: (cwd: string) => Effect.Effect + readonly prefix: (cwd: string) => Effect.Effect + readonly defaultBranch: (cwd: string) => Effect.Effect + readonly hasHead: (cwd: string) => Effect.Effect + readonly mergeBase: (cwd: string, base: string, head?: string) => Effect.Effect + readonly show: (cwd: string, ref: string, file: string, prefix?: string) => Effect.Effect + readonly status: (cwd: string) => Effect.Effect + readonly diff: (cwd: string, ref: string) => Effect.Effect + readonly stats: (cwd: string, ref: string) => Effect.Effect +} + +const kind = (code: string): Kind => { + if (code === "??") return "added" + if (code.includes("U")) return "modified" + if (code.includes("A") && !code.includes("D")) return "added" + if (code.includes("D") && !code.includes("A")) return "deleted" + return "modified" +} + +export class Service extends Context.Service()("@opencode/Git") {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner + + const run = Effect.fn("Git.run")( + function* (args: string[], opts: Options) { + const proc = ChildProcess.make("git", [...cfg, ...args], { + cwd: opts.cwd, + env: opts.env, + extendEnv: true, + stdin: "ignore", + stdout: "pipe", + stderr: "pipe", + }) + const handle = yield* spawner.spawn(proc) + const [stdout, stderr] = yield* Effect.all( + [Stream.mkString(Stream.decodeText(handle.stdout)), Stream.mkString(Stream.decodeText(handle.stderr))], + { concurrency: 2 }, + ) + return { + exitCode: yield* handle.exitCode, + text: () => stdout, + stdout: Buffer.from(stdout), + stderr: Buffer.from(stderr), + } satisfies Result + }, + Effect.scoped, + Effect.catch((err) => Effect.succeed(fail(err))), + ) + + const text = Effect.fn("Git.text")(function* (args: string[], opts: Options) { + return (yield* run(args, opts)).text() + }) + + const lines = Effect.fn("Git.lines")(function* (args: string[], opts: Options) { + return (yield* text(args, opts)) + .split(/\r?\n/) + .map((item) => item.trim()) + .filter(Boolean) + }) + + const refs = Effect.fnUntraced(function* (cwd: string) { + return yield* lines(["for-each-ref", "--format=%(refname:short)", "refs/heads"], { cwd }) + }) + + const configured = Effect.fnUntraced(function* (cwd: string, list: string[]) { + const result = yield* run(["config", "init.defaultBranch"], { cwd }) + const name = out(result) + if (!name || !list.includes(name)) return + return { name, ref: name } satisfies Base + }) + + const primary = Effect.fnUntraced(function* (cwd: string) { + const list = yield* lines(["remote"], { cwd }) + if (list.includes("origin")) return "origin" + if (list.length === 1) return list[0] + if (list.includes("upstream")) return "upstream" + return list[0] + }) + + const branch = Effect.fn("Git.branch")(function* (cwd: string) { + const result = yield* run(["symbolic-ref", "--quiet", "--short", "HEAD"], { cwd }) + if (result.exitCode !== 0) return + const text = out(result) + return text || undefined + }) + + const prefix = Effect.fn("Git.prefix")(function* (cwd: string) { + const result = yield* run(["rev-parse", "--show-prefix"], { cwd }) + if (result.exitCode !== 0) return "" + return out(result) + }) + + const defaultBranch = Effect.fn("Git.defaultBranch")(function* (cwd: string) { + const remote = yield* primary(cwd) + if (remote) { + const head = yield* run(["symbolic-ref", `refs/remotes/${remote}/HEAD`], { cwd }) + if (head.exitCode === 0) { + const ref = out(head).replace(/^refs\/remotes\//, "") + const name = ref.startsWith(`${remote}/`) ? ref.slice(`${remote}/`.length) : "" + if (name) return { name, ref } satisfies Base + } + } + + const list = yield* refs(cwd) + const next = yield* configured(cwd, list) + if (next) return next + if (list.includes("main")) return { name: "main", ref: "main" } satisfies Base + if (list.includes("master")) return { name: "master", ref: "master" } satisfies Base + }) + + const hasHead = Effect.fn("Git.hasHead")(function* (cwd: string) { + const result = yield* run(["rev-parse", "--verify", "HEAD"], { cwd }) + return result.exitCode === 0 + }) + + const mergeBase = Effect.fn("Git.mergeBase")(function* (cwd: string, base: string, head = "HEAD") { + const result = yield* run(["merge-base", base, head], { cwd }) + if (result.exitCode !== 0) return + const text = out(result) + return text || undefined + }) + + const show = Effect.fn("Git.show")(function* (cwd: string, ref: string, file: string, prefix = "") { + const target = prefix ? `${prefix}${file}` : file + const result = yield* run(["show", `${ref}:${target}`], { cwd }) + if (result.exitCode !== 0) return "" + if (result.stdout.includes(0)) return "" + return result.text() + }) + + const status = Effect.fn("Git.status")(function* (cwd: string) { + return nuls( + yield* text(["status", "--porcelain=v1", "--untracked-files=all", "--no-renames", "-z", "--", "."], { + cwd, + }), + ).flatMap((item) => { + const file = item.slice(3) + if (!file) return [] + const code = item.slice(0, 2) + return [{ file, code, status: kind(code) } satisfies Item] + }) + }) + + const diff = Effect.fn("Git.diff")(function* (cwd: string, ref: string) { + const list = nuls( + yield* text(["diff", "--no-ext-diff", "--no-renames", "--name-status", "-z", ref, "--", "."], { cwd }), + ) + return list.flatMap((code, idx) => { + if (idx % 2 !== 0) return [] + const file = list[idx + 1] + if (!code || !file) return [] + return [{ file, code, status: kind(code) } satisfies Item] + }) + }) + + const stats = Effect.fn("Git.stats")(function* (cwd: string, ref: string) { + return nuls( + yield* text(["diff", "--no-ext-diff", "--no-renames", "--numstat", "-z", ref, "--", "."], { cwd }), + ).flatMap((item) => { + const a = item.indexOf("\t") + const b = item.indexOf("\t", a + 1) + if (a === -1 || b === -1) return [] + const file = item.slice(b + 1) + if (!file) return [] + const adds = item.slice(0, a) + const dels = item.slice(a + 1, b) + const additions = adds === "-" ? 0 : Number.parseInt(adds || "0", 10) + const deletions = dels === "-" ? 0 : Number.parseInt(dels || "0", 10) + return [ + { + file, + additions: Number.isFinite(additions) ? additions : 0, + deletions: Number.isFinite(deletions) ? deletions : 0, + } satisfies Stat, + ] + }) + }) + + return Service.of({ + run, + branch, + prefix, + defaultBranch, + hasHead, + mergeBase, + show, + status, + diff, + stats, + }) + }), +) + +export const defaultLayer = layer.pipe(Layer.provide(CrossSpawnSpawner.defaultLayer)) diff --git a/packages/opencode/src/git/index.ts b/packages/opencode/src/git/index.ts index ac964ee0a0..019819d6e3 100644 --- a/packages/opencode/src/git/index.ts +++ b/packages/opencode/src/git/index.ts @@ -1,260 +1 @@ -import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" -import { Effect, Layer, Context, Stream } from "effect" -import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" - -export namespace Git { - const cfg = [ - "--no-optional-locks", - "-c", - "core.autocrlf=false", - "-c", - "core.fsmonitor=false", - "-c", - "core.longpaths=true", - "-c", - "core.symlinks=true", - "-c", - "core.quotepath=false", - ] as const - - const out = (result: { text(): string }) => result.text().trim() - const nuls = (text: string) => text.split("\0").filter(Boolean) - const fail = (err: unknown) => - ({ - exitCode: 1, - text: () => "", - stdout: Buffer.alloc(0), - stderr: Buffer.from(err instanceof Error ? err.message : String(err)), - }) satisfies Result - - export type Kind = "added" | "deleted" | "modified" - - export type Base = { - readonly name: string - readonly ref: string - } - - export type Item = { - readonly file: string - readonly code: string - readonly status: Kind - } - - export type Stat = { - readonly file: string - readonly additions: number - readonly deletions: number - } - - export interface Result { - readonly exitCode: number - readonly text: () => string - readonly stdout: Buffer - readonly stderr: Buffer - } - - export interface Options { - readonly cwd: string - readonly env?: Record - } - - export interface Interface { - readonly run: (args: string[], opts: Options) => Effect.Effect - readonly branch: (cwd: string) => Effect.Effect - readonly prefix: (cwd: string) => Effect.Effect - readonly defaultBranch: (cwd: string) => Effect.Effect - readonly hasHead: (cwd: string) => Effect.Effect - readonly mergeBase: (cwd: string, base: string, head?: string) => Effect.Effect - readonly show: (cwd: string, ref: string, file: string, prefix?: string) => Effect.Effect - readonly status: (cwd: string) => Effect.Effect - readonly diff: (cwd: string, ref: string) => Effect.Effect - readonly stats: (cwd: string, ref: string) => Effect.Effect - } - - const kind = (code: string): Kind => { - if (code === "??") return "added" - if (code.includes("U")) return "modified" - if (code.includes("A") && !code.includes("D")) return "added" - if (code.includes("D") && !code.includes("A")) return "deleted" - return "modified" - } - - export class Service extends Context.Service()("@opencode/Git") {} - - export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const spawner = yield* ChildProcessSpawner.ChildProcessSpawner - - const run = Effect.fn("Git.run")( - function* (args: string[], opts: Options) { - const proc = ChildProcess.make("git", [...cfg, ...args], { - cwd: opts.cwd, - env: opts.env, - extendEnv: true, - stdin: "ignore", - stdout: "pipe", - stderr: "pipe", - }) - const handle = yield* spawner.spawn(proc) - const [stdout, stderr] = yield* Effect.all( - [Stream.mkString(Stream.decodeText(handle.stdout)), Stream.mkString(Stream.decodeText(handle.stderr))], - { concurrency: 2 }, - ) - return { - exitCode: yield* handle.exitCode, - text: () => stdout, - stdout: Buffer.from(stdout), - stderr: Buffer.from(stderr), - } satisfies Result - }, - Effect.scoped, - Effect.catch((err) => Effect.succeed(fail(err))), - ) - - const text = Effect.fn("Git.text")(function* (args: string[], opts: Options) { - return (yield* run(args, opts)).text() - }) - - const lines = Effect.fn("Git.lines")(function* (args: string[], opts: Options) { - return (yield* text(args, opts)) - .split(/\r?\n/) - .map((item) => item.trim()) - .filter(Boolean) - }) - - const refs = Effect.fnUntraced(function* (cwd: string) { - return yield* lines(["for-each-ref", "--format=%(refname:short)", "refs/heads"], { cwd }) - }) - - const configured = Effect.fnUntraced(function* (cwd: string, list: string[]) { - const result = yield* run(["config", "init.defaultBranch"], { cwd }) - const name = out(result) - if (!name || !list.includes(name)) return - return { name, ref: name } satisfies Base - }) - - const primary = Effect.fnUntraced(function* (cwd: string) { - const list = yield* lines(["remote"], { cwd }) - if (list.includes("origin")) return "origin" - if (list.length === 1) return list[0] - if (list.includes("upstream")) return "upstream" - return list[0] - }) - - const branch = Effect.fn("Git.branch")(function* (cwd: string) { - const result = yield* run(["symbolic-ref", "--quiet", "--short", "HEAD"], { cwd }) - if (result.exitCode !== 0) return - const text = out(result) - return text || undefined - }) - - const prefix = Effect.fn("Git.prefix")(function* (cwd: string) { - const result = yield* run(["rev-parse", "--show-prefix"], { cwd }) - if (result.exitCode !== 0) return "" - return out(result) - }) - - const defaultBranch = Effect.fn("Git.defaultBranch")(function* (cwd: string) { - const remote = yield* primary(cwd) - if (remote) { - const head = yield* run(["symbolic-ref", `refs/remotes/${remote}/HEAD`], { cwd }) - if (head.exitCode === 0) { - const ref = out(head).replace(/^refs\/remotes\//, "") - const name = ref.startsWith(`${remote}/`) ? ref.slice(`${remote}/`.length) : "" - if (name) return { name, ref } satisfies Base - } - } - - const list = yield* refs(cwd) - const next = yield* configured(cwd, list) - if (next) return next - if (list.includes("main")) return { name: "main", ref: "main" } satisfies Base - if (list.includes("master")) return { name: "master", ref: "master" } satisfies Base - }) - - const hasHead = Effect.fn("Git.hasHead")(function* (cwd: string) { - const result = yield* run(["rev-parse", "--verify", "HEAD"], { cwd }) - return result.exitCode === 0 - }) - - const mergeBase = Effect.fn("Git.mergeBase")(function* (cwd: string, base: string, head = "HEAD") { - const result = yield* run(["merge-base", base, head], { cwd }) - if (result.exitCode !== 0) return - const text = out(result) - return text || undefined - }) - - const show = Effect.fn("Git.show")(function* (cwd: string, ref: string, file: string, prefix = "") { - const target = prefix ? `${prefix}${file}` : file - const result = yield* run(["show", `${ref}:${target}`], { cwd }) - if (result.exitCode !== 0) return "" - if (result.stdout.includes(0)) return "" - return result.text() - }) - - const status = Effect.fn("Git.status")(function* (cwd: string) { - return nuls( - yield* text(["status", "--porcelain=v1", "--untracked-files=all", "--no-renames", "-z", "--", "."], { - cwd, - }), - ).flatMap((item) => { - const file = item.slice(3) - if (!file) return [] - const code = item.slice(0, 2) - return [{ file, code, status: kind(code) } satisfies Item] - }) - }) - - const diff = Effect.fn("Git.diff")(function* (cwd: string, ref: string) { - const list = nuls( - yield* text(["diff", "--no-ext-diff", "--no-renames", "--name-status", "-z", ref, "--", "."], { cwd }), - ) - return list.flatMap((code, idx) => { - if (idx % 2 !== 0) return [] - const file = list[idx + 1] - if (!code || !file) return [] - return [{ file, code, status: kind(code) } satisfies Item] - }) - }) - - const stats = Effect.fn("Git.stats")(function* (cwd: string, ref: string) { - return nuls( - yield* text(["diff", "--no-ext-diff", "--no-renames", "--numstat", "-z", ref, "--", "."], { cwd }), - ).flatMap((item) => { - const a = item.indexOf("\t") - const b = item.indexOf("\t", a + 1) - if (a === -1 || b === -1) return [] - const file = item.slice(b + 1) - if (!file) return [] - const adds = item.slice(0, a) - const dels = item.slice(a + 1, b) - const additions = adds === "-" ? 0 : Number.parseInt(adds || "0", 10) - const deletions = dels === "-" ? 0 : Number.parseInt(dels || "0", 10) - return [ - { - file, - additions: Number.isFinite(additions) ? additions : 0, - deletions: Number.isFinite(deletions) ? deletions : 0, - } satisfies Stat, - ] - }) - }) - - return Service.of({ - run, - branch, - prefix, - defaultBranch, - hasHead, - mergeBase, - show, - status, - diff, - stats, - }) - }), - ) - - export const defaultLayer = layer.pipe(Layer.provide(CrossSpawnSpawner.defaultLayer)) -} +export * as Git from "./git" From 62ddb9d3ad774f97d34c4004ee607fc86e69ddfc Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 22:17:19 -0400 Subject: [PATCH 198/300] feat: unwrap uskill namespace to flat exports + barrel (#22714) --- packages/opencode/src/skill/index.ts | 265 +-------------------------- packages/opencode/src/skill/skill.ts | 262 ++++++++++++++++++++++++++ 2 files changed, 263 insertions(+), 264 deletions(-) create mode 100644 packages/opencode/src/skill/skill.ts diff --git a/packages/opencode/src/skill/index.ts b/packages/opencode/src/skill/index.ts index 4bf5d0cfed..6d7b428dfb 100644 --- a/packages/opencode/src/skill/index.ts +++ b/packages/opencode/src/skill/index.ts @@ -1,264 +1 @@ -import os from "os" -import path from "path" -import { pathToFileURL } from "url" -import z from "zod" -import { Effect, Layer, Context } from "effect" -import { NamedError } from "@opencode-ai/shared/util/error" -import type { Agent } from "@/agent/agent" -import { Bus } from "@/bus" -import { InstanceState } from "@/effect/instance-state" -import { Flag } from "@/flag/flag" -import { Global } from "@/global" -import { Permission } from "@/permission" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" -import { Config } from "../config" -import { ConfigMarkdown } from "../config/markdown" -import { Glob } from "@opencode-ai/shared/util/glob" -import { Log } from "../util/log" -import { Discovery } from "./discovery" - -export namespace Skill { - const log = Log.create({ service: "skill" }) - const EXTERNAL_DIRS = [".claude", ".agents"] - const EXTERNAL_SKILL_PATTERN = "skills/**/SKILL.md" - const OPENCODE_SKILL_PATTERN = "{skill,skills}/**/SKILL.md" - const SKILL_PATTERN = "**/SKILL.md" - - export const Info = z.object({ - name: z.string(), - description: z.string(), - location: z.string(), - content: z.string(), - }) - export type Info = z.infer - - export const InvalidError = NamedError.create( - "SkillInvalidError", - z.object({ - path: z.string(), - message: z.string().optional(), - issues: z.custom().optional(), - }), - ) - - export const NameMismatchError = NamedError.create( - "SkillNameMismatchError", - z.object({ - path: z.string(), - expected: z.string(), - actual: z.string(), - }), - ) - - type State = { - skills: Record - dirs: Set - } - - export interface Interface { - readonly get: (name: string) => Effect.Effect - readonly all: () => Effect.Effect - readonly dirs: () => Effect.Effect - readonly available: (agent?: Agent.Info) => Effect.Effect - } - - const add = Effect.fnUntraced(function* (state: State, match: string, bus: Bus.Interface) { - const md = yield* Effect.tryPromise({ - try: () => ConfigMarkdown.parse(match), - catch: (err) => err, - }).pipe( - Effect.catch( - Effect.fnUntraced(function* (err) { - const message = ConfigMarkdown.FrontmatterError.isInstance(err) - ? err.data.message - : `Failed to parse skill ${match}` - const { Session } = yield* Effect.promise(() => import("@/session")) - yield* bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() }) - log.error("failed to load skill", { skill: match, err }) - return undefined - }), - ), - ) - - if (!md) return - - const parsed = Info.pick({ name: true, description: true }).safeParse(md.data) - if (!parsed.success) return - - if (state.skills[parsed.data.name]) { - log.warn("duplicate skill name", { - name: parsed.data.name, - existing: state.skills[parsed.data.name].location, - duplicate: match, - }) - } - - state.dirs.add(path.dirname(match)) - state.skills[parsed.data.name] = { - name: parsed.data.name, - description: parsed.data.description, - location: match, - content: md.content, - } - }) - - const scan = Effect.fnUntraced(function* ( - state: State, - bus: Bus.Interface, - root: string, - pattern: string, - opts?: { dot?: boolean; scope?: string }, - ) { - const matches = yield* Effect.tryPromise({ - try: () => - Glob.scan(pattern, { - cwd: root, - absolute: true, - include: "file", - symlink: true, - dot: opts?.dot, - }), - catch: (error) => error, - }).pipe( - Effect.catch((error) => { - if (!opts?.scope) return Effect.die(error) - log.error(`failed to scan ${opts.scope} skills`, { dir: root, error }) - return Effect.succeed([] as string[]) - }), - ) - - yield* Effect.forEach(matches, (match) => add(state, match, bus), { - concurrency: "unbounded", - discard: true, - }) - }) - - const loadSkills = Effect.fnUntraced(function* ( - state: State, - config: Config.Interface, - discovery: Discovery.Interface, - bus: Bus.Interface, - fsys: AppFileSystem.Interface, - directory: string, - worktree: string, - ) { - if (!Flag.OPENCODE_DISABLE_EXTERNAL_SKILLS) { - for (const dir of EXTERNAL_DIRS) { - const root = path.join(Global.Path.home, dir) - if (!(yield* fsys.isDir(root))) continue - yield* scan(state, bus, root, EXTERNAL_SKILL_PATTERN, { dot: true, scope: "global" }) - } - - const upDirs = yield* fsys - .up({ targets: EXTERNAL_DIRS, start: directory, stop: worktree }) - .pipe(Effect.catch(() => Effect.succeed([] as string[]))) - - for (const root of upDirs) { - yield* scan(state, bus, root, EXTERNAL_SKILL_PATTERN, { dot: true, scope: "project" }) - } - } - - const configDirs = yield* config.directories() - for (const dir of configDirs) { - yield* scan(state, bus, dir, OPENCODE_SKILL_PATTERN) - } - - const cfg = yield* config.get() - for (const item of cfg.skills?.paths ?? []) { - const expanded = item.startsWith("~/") ? path.join(os.homedir(), item.slice(2)) : item - const dir = path.isAbsolute(expanded) ? expanded : path.join(directory, expanded) - if (!(yield* fsys.isDir(dir))) { - log.warn("skill path not found", { path: dir }) - continue - } - - yield* scan(state, bus, dir, SKILL_PATTERN) - } - - for (const url of cfg.skills?.urls ?? []) { - const pulledDirs = yield* discovery.pull(url) - for (const dir of pulledDirs) { - state.dirs.add(dir) - yield* scan(state, bus, dir, SKILL_PATTERN) - } - } - - log.info("init", { count: Object.keys(state.skills).length }) - }) - - export class Service extends Context.Service()("@opencode/Skill") {} - - export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const discovery = yield* Discovery.Service - const config = yield* Config.Service - const bus = yield* Bus.Service - const fsys = yield* AppFileSystem.Service - const state = yield* InstanceState.make( - Effect.fn("Skill.state")(function* (ctx) { - const s: State = { skills: {}, dirs: new Set() } - yield* loadSkills(s, config, discovery, bus, fsys, ctx.directory, ctx.worktree) - return s - }), - ) - - const get = Effect.fn("Skill.get")(function* (name: string) { - const s = yield* InstanceState.get(state) - return s.skills[name] - }) - - const all = Effect.fn("Skill.all")(function* () { - const s = yield* InstanceState.get(state) - return Object.values(s.skills) - }) - - const dirs = Effect.fn("Skill.dirs")(function* () { - const s = yield* InstanceState.get(state) - return Array.from(s.dirs) - }) - - const available = Effect.fn("Skill.available")(function* (agent?: Agent.Info) { - const s = yield* InstanceState.get(state) - const list = Object.values(s.skills).toSorted((a, b) => a.name.localeCompare(b.name)) - if (!agent) return list - return list.filter((skill) => Permission.evaluate("skill", skill.name, agent.permission).action !== "deny") - }) - - return Service.of({ get, all, dirs, available }) - }), - ) - - export const defaultLayer = layer.pipe( - Layer.provide(Discovery.defaultLayer), - Layer.provide(Config.defaultLayer), - Layer.provide(Bus.layer), - Layer.provide(AppFileSystem.defaultLayer), - ) - - export function fmt(list: Info[], opts: { verbose: boolean }) { - if (list.length === 0) return "No skills are currently available." - if (opts.verbose) { - return [ - "", - ...list - .sort((a, b) => a.name.localeCompare(b.name)) - .flatMap((skill) => [ - " ", - ` ${skill.name}`, - ` ${skill.description}`, - ` ${pathToFileURL(skill.location).href}`, - " ", - ]), - "", - ].join("\n") - } - - return [ - "## Available Skills", - ...list - .toSorted((a, b) => a.name.localeCompare(b.name)) - .map((skill) => `- **${skill.name}**: ${skill.description}`), - ].join("\n") - } -} +export * as Skill from "./skill" diff --git a/packages/opencode/src/skill/skill.ts b/packages/opencode/src/skill/skill.ts new file mode 100644 index 0000000000..afc6446678 --- /dev/null +++ b/packages/opencode/src/skill/skill.ts @@ -0,0 +1,262 @@ +import os from "os" +import path from "path" +import { pathToFileURL } from "url" +import z from "zod" +import { Effect, Layer, Context } from "effect" +import { NamedError } from "@opencode-ai/shared/util/error" +import type { Agent } from "@/agent/agent" +import { Bus } from "@/bus" +import { InstanceState } from "@/effect/instance-state" +import { Flag } from "@/flag/flag" +import { Global } from "@/global" +import { Permission } from "@/permission" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { Config } from "../config" +import { ConfigMarkdown } from "../config/markdown" +import { Glob } from "@opencode-ai/shared/util/glob" +import { Log } from "../util/log" +import { Discovery } from "./discovery" + +const log = Log.create({ service: "skill" }) +const EXTERNAL_DIRS = [".claude", ".agents"] +const EXTERNAL_SKILL_PATTERN = "skills/**/SKILL.md" +const OPENCODE_SKILL_PATTERN = "{skill,skills}/**/SKILL.md" +const SKILL_PATTERN = "**/SKILL.md" + +export const Info = z.object({ + name: z.string(), + description: z.string(), + location: z.string(), + content: z.string(), +}) +export type Info = z.infer + +export const InvalidError = NamedError.create( + "SkillInvalidError", + z.object({ + path: z.string(), + message: z.string().optional(), + issues: z.custom().optional(), + }), +) + +export const NameMismatchError = NamedError.create( + "SkillNameMismatchError", + z.object({ + path: z.string(), + expected: z.string(), + actual: z.string(), + }), +) + +type State = { + skills: Record + dirs: Set +} + +export interface Interface { + readonly get: (name: string) => Effect.Effect + readonly all: () => Effect.Effect + readonly dirs: () => Effect.Effect + readonly available: (agent?: Agent.Info) => Effect.Effect +} + +const add = Effect.fnUntraced(function* (state: State, match: string, bus: Bus.Interface) { + const md = yield* Effect.tryPromise({ + try: () => ConfigMarkdown.parse(match), + catch: (err) => err, + }).pipe( + Effect.catch( + Effect.fnUntraced(function* (err) { + const message = ConfigMarkdown.FrontmatterError.isInstance(err) + ? err.data.message + : `Failed to parse skill ${match}` + const { Session } = yield* Effect.promise(() => import("@/session")) + yield* bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() }) + log.error("failed to load skill", { skill: match, err }) + return undefined + }), + ), + ) + + if (!md) return + + const parsed = Info.pick({ name: true, description: true }).safeParse(md.data) + if (!parsed.success) return + + if (state.skills[parsed.data.name]) { + log.warn("duplicate skill name", { + name: parsed.data.name, + existing: state.skills[parsed.data.name].location, + duplicate: match, + }) + } + + state.dirs.add(path.dirname(match)) + state.skills[parsed.data.name] = { + name: parsed.data.name, + description: parsed.data.description, + location: match, + content: md.content, + } +}) + +const scan = Effect.fnUntraced(function* ( + state: State, + bus: Bus.Interface, + root: string, + pattern: string, + opts?: { dot?: boolean; scope?: string }, +) { + const matches = yield* Effect.tryPromise({ + try: () => + Glob.scan(pattern, { + cwd: root, + absolute: true, + include: "file", + symlink: true, + dot: opts?.dot, + }), + catch: (error) => error, + }).pipe( + Effect.catch((error) => { + if (!opts?.scope) return Effect.die(error) + log.error(`failed to scan ${opts.scope} skills`, { dir: root, error }) + return Effect.succeed([] as string[]) + }), + ) + + yield* Effect.forEach(matches, (match) => add(state, match, bus), { + concurrency: "unbounded", + discard: true, + }) +}) + +const loadSkills = Effect.fnUntraced(function* ( + state: State, + config: Config.Interface, + discovery: Discovery.Interface, + bus: Bus.Interface, + fsys: AppFileSystem.Interface, + directory: string, + worktree: string, +) { + if (!Flag.OPENCODE_DISABLE_EXTERNAL_SKILLS) { + for (const dir of EXTERNAL_DIRS) { + const root = path.join(Global.Path.home, dir) + if (!(yield* fsys.isDir(root))) continue + yield* scan(state, bus, root, EXTERNAL_SKILL_PATTERN, { dot: true, scope: "global" }) + } + + const upDirs = yield* fsys + .up({ targets: EXTERNAL_DIRS, start: directory, stop: worktree }) + .pipe(Effect.catch(() => Effect.succeed([] as string[]))) + + for (const root of upDirs) { + yield* scan(state, bus, root, EXTERNAL_SKILL_PATTERN, { dot: true, scope: "project" }) + } + } + + const configDirs = yield* config.directories() + for (const dir of configDirs) { + yield* scan(state, bus, dir, OPENCODE_SKILL_PATTERN) + } + + const cfg = yield* config.get() + for (const item of cfg.skills?.paths ?? []) { + const expanded = item.startsWith("~/") ? path.join(os.homedir(), item.slice(2)) : item + const dir = path.isAbsolute(expanded) ? expanded : path.join(directory, expanded) + if (!(yield* fsys.isDir(dir))) { + log.warn("skill path not found", { path: dir }) + continue + } + + yield* scan(state, bus, dir, SKILL_PATTERN) + } + + for (const url of cfg.skills?.urls ?? []) { + const pulledDirs = yield* discovery.pull(url) + for (const dir of pulledDirs) { + state.dirs.add(dir) + yield* scan(state, bus, dir, SKILL_PATTERN) + } + } + + log.info("init", { count: Object.keys(state.skills).length }) +}) + +export class Service extends Context.Service()("@opencode/Skill") {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const discovery = yield* Discovery.Service + const config = yield* Config.Service + const bus = yield* Bus.Service + const fsys = yield* AppFileSystem.Service + const state = yield* InstanceState.make( + Effect.fn("Skill.state")(function* (ctx) { + const s: State = { skills: {}, dirs: new Set() } + yield* loadSkills(s, config, discovery, bus, fsys, ctx.directory, ctx.worktree) + return s + }), + ) + + const get = Effect.fn("Skill.get")(function* (name: string) { + const s = yield* InstanceState.get(state) + return s.skills[name] + }) + + const all = Effect.fn("Skill.all")(function* () { + const s = yield* InstanceState.get(state) + return Object.values(s.skills) + }) + + const dirs = Effect.fn("Skill.dirs")(function* () { + const s = yield* InstanceState.get(state) + return Array.from(s.dirs) + }) + + const available = Effect.fn("Skill.available")(function* (agent?: Agent.Info) { + const s = yield* InstanceState.get(state) + const list = Object.values(s.skills).toSorted((a, b) => a.name.localeCompare(b.name)) + if (!agent) return list + return list.filter((skill) => Permission.evaluate("skill", skill.name, agent.permission).action !== "deny") + }) + + return Service.of({ get, all, dirs, available }) + }), +) + +export const defaultLayer = layer.pipe( + Layer.provide(Discovery.defaultLayer), + Layer.provide(Config.defaultLayer), + Layer.provide(Bus.layer), + Layer.provide(AppFileSystem.defaultLayer), +) + +export function fmt(list: Info[], opts: { verbose: boolean }) { + if (list.length === 0) return "No skills are currently available." + if (opts.verbose) { + return [ + "", + ...list + .sort((a, b) => a.name.localeCompare(b.name)) + .flatMap((skill) => [ + " ", + ` ${skill.name}`, + ` ${skill.description}`, + ` ${pathToFileURL(skill.location).href}`, + " ", + ]), + "", + ].join("\n") + } + + return [ + "## Available Skills", + ...list + .toSorted((a, b) => a.name.localeCompare(b.name)) + .map((skill) => `- **${skill.name}**: ${skill.description}`), + ].join("\n") +} From cf423d27693ab5718eb836bdba7ba3ed357204b0 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 22:17:59 -0400 Subject: [PATCH 199/300] fix: remove 10 unused type-only imports and declarations (#22696) --- packages/app/src/components/file-tree.tsx | 1 - packages/app/src/context/global-sync/types.ts | 1 - packages/app/src/i18n/ko.ts | 2 -- packages/opencode/src/cli/cmd/tui/context/sdk.tsx | 2 +- packages/opencode/src/cli/cmd/tui/plugin/api.tsx | 1 - .../opencode/src/cli/cmd/tui/ui/dialog-export-options.tsx | 2 +- packages/opencode/test/fixture/plugin-meta-worker.ts | 7 ------- packages/opencode/test/mcp/oauth-auto-connect.test.ts | 1 - packages/opencode/test/session/prompt-effect.test.ts | 1 - packages/plugin/src/tui.ts | 1 - 10 files changed, 2 insertions(+), 17 deletions(-) diff --git a/packages/app/src/components/file-tree.tsx b/packages/app/src/components/file-tree.tsx index 8fbecf6712..211ce05ef0 100644 --- a/packages/app/src/components/file-tree.tsx +++ b/packages/app/src/components/file-tree.tsx @@ -14,7 +14,6 @@ import { Switch, untrack, type ComponentProps, - type JSXElement, type ParentProps, } from "solid-js" import { Dynamic } from "solid-js/web" diff --git a/packages/app/src/context/global-sync/types.ts b/packages/app/src/context/global-sync/types.ts index b0f340a902..e3ec83c5ee 100644 --- a/packages/app/src/context/global-sync/types.ts +++ b/packages/app/src/context/global-sync/types.ts @@ -8,7 +8,6 @@ import type { Part, Path, PermissionRequest, - Project, ProviderListResponse, QuestionRequest, Session, diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts index 0f2f7647ab..1c15720091 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -1,7 +1,5 @@ import { dict as en } from "./en" -type Keys = keyof typeof en - export const dict = { "command.category.suggested": "추천", "command.category.view": "보기", diff --git a/packages/opencode/src/cli/cmd/tui/context/sdk.tsx b/packages/opencode/src/cli/cmd/tui/context/sdk.tsx index ad35aa45c2..14d3062886 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sdk.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sdk.tsx @@ -1,5 +1,5 @@ import { createOpencodeClient } from "@opencode-ai/sdk/v2" -import type { GlobalEvent, Event } from "@opencode-ai/sdk/v2" +import type { GlobalEvent } from "@opencode-ai/sdk/v2" import { createSimpleContext } from "./helper" import { createGlobalEmitter } from "@solid-primitives/event-bus" import { batch, onCleanup, onMount } from "solid-js" diff --git a/packages/opencode/src/cli/cmd/tui/plugin/api.tsx b/packages/opencode/src/cli/cmd/tui/plugin/api.tsx index 42bf78adbf..3af70d8c25 100644 --- a/packages/opencode/src/cli/cmd/tui/plugin/api.tsx +++ b/packages/opencode/src/cli/cmd/tui/plugin/api.tsx @@ -19,7 +19,6 @@ import { Prompt } from "../component/prompt" import { Slot as HostSlot } from "./slots" import type { useToast } from "../ui/toast" import { Installation } from "@/installation" -import { type OpencodeClient } from "@opencode-ai/sdk/v2" type RouteEntry = { key: symbol diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-export-options.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-export-options.tsx index 4442eb9e60..513d34910b 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-export-options.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-export-options.tsx @@ -2,7 +2,7 @@ import { TextareaRenderable, TextAttributes } from "@opentui/core" import { useTheme } from "../context/theme" import { useDialog, type DialogContext } from "./dialog" import { createStore } from "solid-js/store" -import { onMount, Show, type JSX } from "solid-js" +import { onMount, Show } from "solid-js" import { useKeyboard } from "@opentui/solid" export type DialogExportOptionsProps = { diff --git a/packages/opencode/test/fixture/plugin-meta-worker.ts b/packages/opencode/test/fixture/plugin-meta-worker.ts index 86284b4c73..c02b448ae7 100644 --- a/packages/opencode/test/fixture/plugin-meta-worker.ts +++ b/packages/opencode/test/fixture/plugin-meta-worker.ts @@ -1,10 +1,3 @@ -type Msg = { - file: string - spec: string - target: string - id: string -} - const raw = process.argv[2] if (!raw) throw new Error("Missing worker payload") diff --git a/packages/opencode/test/mcp/oauth-auto-connect.test.ts b/packages/opencode/test/mcp/oauth-auto-connect.test.ts index 13ae0bb34d..89edd09084 100644 --- a/packages/opencode/test/mcp/oauth-auto-connect.test.ts +++ b/packages/opencode/test/mcp/oauth-auto-connect.test.ts @@ -1,6 +1,5 @@ import { test, expect, mock, beforeEach } from "bun:test" import { Effect } from "effect" -import type { MCP as MCPNS } from "../../src/mcp/index" // Mock UnauthorizedError to match the SDK's class class MockUnauthorizedError extends Error { diff --git a/packages/opencode/test/session/prompt-effect.test.ts b/packages/opencode/test/session/prompt-effect.test.ts index 7a118cb050..5ff8bf3424 100644 --- a/packages/opencode/test/session/prompt-effect.test.ts +++ b/packages/opencode/test/session/prompt-effect.test.ts @@ -14,7 +14,6 @@ import { Permission } from "../../src/permission" import { Plugin } from "../../src/plugin" import { Provider as ProviderSvc } from "../../src/provider" import { Env } from "../../src/env" -import type { Provider } from "../../src/provider" import { ModelID, ProviderID } from "../../src/provider/schema" import { Question } from "../../src/question" import { Todo } from "../../src/session/todo" diff --git a/packages/plugin/src/tui.ts b/packages/plugin/src/tui.ts index e6f832f7e1..099cf27580 100644 --- a/packages/plugin/src/tui.ts +++ b/packages/plugin/src/tui.ts @@ -13,7 +13,6 @@ import type { QuestionRequest, SessionStatus, TextPart, - Workspace, Config as SdkConfig, } from "@opencode-ai/sdk/v2" import type { CliRenderer, ParsedKey, RGBA, SlotMode } from "@opentui/core" From 069cef8a44f134a9a3517144b54ea5b1046bf6ff Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Thu, 16 Apr 2026 02:18:58 +0000 Subject: [PATCH 200/300] chore: generate --- packages/opencode/src/file/file.ts | 3 +-- packages/opencode/src/session/session.ts | 12 ++---------- packages/opencode/src/snapshot/snapshot.ts | 4 +--- packages/opencode/src/worktree/worktree.ts | 5 +---- 4 files changed, 5 insertions(+), 19 deletions(-) diff --git a/packages/opencode/src/file/file.ts b/packages/opencode/src/file/file.ts index 657fe9a583..a101574f61 100644 --- a/packages/opencode/src/file/file.ts +++ b/packages/opencode/src/file/file.ts @@ -631,8 +631,7 @@ export const layer = Layer.effect( return sortHiddenLast(cache.dirs.toSorted(), preferHidden).slice(0, limit) } - const items = - kind === "file" ? cache.files : kind === "directory" ? cache.dirs : [...cache.files, ...cache.dirs] + const items = kind === "file" ? cache.files : kind === "directory" ? cache.dirs : [...cache.files, ...cache.dirs] const searchLimit = kind === "directory" && !preferHidden ? limit * 20 : limit const sorted = fuzzysort.go(query, items, { limit: searchLimit }).map((item) => item.target) diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts index 369a2085ff..12ecd85529 100644 --- a/packages/opencode/src/session/session.ts +++ b/packages/opencode/src/session/session.ts @@ -258,11 +258,7 @@ export function plan(input: { slug: string; time: { created: number } }) { return path.join(base, [input.time.created, input.slug].join("-") + ".md") } -export const getUsage = (input: { - model: Provider.Model - usage: LanguageModelUsage - metadata?: ProviderMetadata -}) => { +export const getUsage = (input: { model: Provider.Model; usage: LanguageModelUsage; metadata?: ProviderMetadata }) => { const safe = (value: number) => { if (!Number.isFinite(value)) return 0 return value @@ -357,11 +353,7 @@ export interface Interface { readonly remove: (sessionID: SessionID) => Effect.Effect readonly updateMessage: (msg: T) => Effect.Effect readonly removeMessage: (input: { sessionID: SessionID; messageID: MessageID }) => Effect.Effect - readonly removePart: (input: { - sessionID: SessionID - messageID: MessageID - partID: PartID - }) => Effect.Effect + readonly removePart: (input: { sessionID: SessionID; messageID: MessageID; partID: PartID }) => Effect.Effect readonly getPart: (input: { sessionID: SessionID messageID: MessageID diff --git a/packages/opencode/src/snapshot/snapshot.ts b/packages/opencode/src/snapshot/snapshot.ts index 32c637a216..6624dee986 100644 --- a/packages/opencode/src/snapshot/snapshot.ts +++ b/packages/opencode/src/snapshot/snapshot.ts @@ -531,9 +531,7 @@ export const layer: Layer.Layer< if (row.status === "added") { return [ "", - yield* git([...cfg, ...args(["show", `${to}:${row.file}`])]).pipe( - Effect.map((item) => item.text), - ), + yield* git([...cfg, ...args(["show", `${to}:${row.file}`])]).pipe(Effect.map((item) => item.text)), ] } if (row.status === "deleted") { diff --git a/packages/opencode/src/worktree/worktree.ts b/packages/opencode/src/worktree/worktree.ts index 9280b7a52e..674d4d7570 100644 --- a/packages/opencode/src/worktree/worktree.ts +++ b/packages/opencode/src/worktree/worktree.ts @@ -54,10 +54,7 @@ export type Info = z.infer export const CreateInput = z .object({ name: z.string().optional(), - startCommand: z - .string() - .optional() - .describe("Additional startup script to run after the project's start command"), + startCommand: z.string().optional().describe("Additional startup script to run after the project's start command"), }) .meta({ ref: "WorktreeCreateInput", From 60c927cf4faa5e95c7c7a3aeb1a43e5431a069fb Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 22:21:46 -0400 Subject: [PATCH 201/300] feat: unwrap Pty namespace to flat exports + barrel (#22719) --- packages/opencode/script/unwrap-namespace.ts | 8 +- packages/opencode/src/pty/index.ts | 365 +------------------ packages/opencode/src/pty/service.ts | 362 ++++++++++++++++++ 3 files changed, 368 insertions(+), 367 deletions(-) create mode 100644 packages/opencode/src/pty/service.ts diff --git a/packages/opencode/script/unwrap-namespace.ts b/packages/opencode/script/unwrap-namespace.ts index bdb49a7fcf..45c16f6c73 100644 --- a/packages/opencode/script/unwrap-namespace.ts +++ b/packages/opencode/script/unwrap-namespace.ts @@ -5,6 +5,7 @@ * Usage: * bun script/unwrap-namespace.ts src/bus/index.ts * bun script/unwrap-namespace.ts src/bus/index.ts --dry-run + * bun script/unwrap-namespace.ts src/pty/index.ts --name service # avoid collision with pty.ts * * What it does: * 1. Reads the file and finds the `export namespace Foo { ... }` block @@ -24,10 +25,11 @@ import fs from "fs" const args = process.argv.slice(2) const dryRun = args.includes("--dry-run") -const filePath = args.find((a) => !a.startsWith("--")) +const nameFlag = args.find((a, i) => args[i - 1] === "--name") +const filePath = args.find((a) => !a.startsWith("--") && args[args.indexOf(a) - 1] !== "--name") if (!filePath) { - console.error("Usage: bun script/unwrap-namespace.ts [--dry-run]") + console.error("Usage: bun script/unwrap-namespace.ts [--dry-run] [--name ]") process.exit(1) } @@ -188,7 +190,7 @@ if (exportedNames.size > 0) { const dir = path.dirname(absPath) const basename = path.basename(absPath, ".ts") const isIndex = basename === "index" -const implName = isIndex ? nsName.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase() : basename +const implName = nameFlag ?? (isIndex ? nsName.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase() : basename) const implFile = path.join(dir, `${implName}.ts`) const indexFile = path.join(dir, "index.ts") const barrelLine = `export * as ${nsName} from "./${implName}"\n` diff --git a/packages/opencode/src/pty/index.ts b/packages/opencode/src/pty/index.ts index 1c969b4b93..37cb4e49a8 100644 --- a/packages/opencode/src/pty/index.ts +++ b/packages/opencode/src/pty/index.ts @@ -1,364 +1 @@ -import { BusEvent } from "@/bus/bus-event" -import { Bus } from "@/bus" -import { InstanceState } from "@/effect/instance-state" -import { Instance } from "@/project/instance" -import type { Proc } from "#pty" -import z from "zod" -import { Log } from "../util/log" -import { lazy } from "@opencode-ai/shared/util/lazy" -import { Shell } from "@/shell/shell" -import { Plugin } from "@/plugin" -import { PtyID } from "./schema" -import { Effect, Layer, Context } from "effect" -import { EffectBridge } from "@/effect/bridge" - -export namespace Pty { - const log = Log.create({ service: "pty" }) - - const BUFFER_LIMIT = 1024 * 1024 * 2 - const BUFFER_CHUNK = 64 * 1024 - const encoder = new TextEncoder() - - type Socket = { - readyState: number - data?: unknown - send: (data: string | Uint8Array | ArrayBuffer) => void - close: (code?: number, reason?: string) => void - } - - const sock = (ws: Socket) => (ws.data && typeof ws.data === "object" ? ws.data : ws) - - type Active = { - info: Info - process: Proc - buffer: string - bufferCursor: number - cursor: number - subscribers: Map - } - - type State = { - dir: string - sessions: Map - } - - // WebSocket control frame: 0x00 + UTF-8 JSON. - const meta = (cursor: number) => { - const json = JSON.stringify({ cursor }) - const bytes = encoder.encode(json) - const out = new Uint8Array(bytes.length + 1) - out[0] = 0 - out.set(bytes, 1) - return out - } - - const pty = lazy(() => import("#pty")) - - export const Info = z - .object({ - id: PtyID.zod, - title: z.string(), - command: z.string(), - args: z.array(z.string()), - cwd: z.string(), - status: z.enum(["running", "exited"]), - pid: z.number(), - }) - .meta({ ref: "Pty" }) - - export type Info = z.infer - - export const CreateInput = z.object({ - command: z.string().optional(), - args: z.array(z.string()).optional(), - cwd: z.string().optional(), - title: z.string().optional(), - env: z.record(z.string(), z.string()).optional(), - }) - - export type CreateInput = z.infer - - export const UpdateInput = z.object({ - title: z.string().optional(), - size: z - .object({ - rows: z.number(), - cols: z.number(), - }) - .optional(), - }) - - export type UpdateInput = z.infer - - export const Event = { - Created: BusEvent.define("pty.created", z.object({ info: Info })), - Updated: BusEvent.define("pty.updated", z.object({ info: Info })), - Exited: BusEvent.define("pty.exited", z.object({ id: PtyID.zod, exitCode: z.number() })), - Deleted: BusEvent.define("pty.deleted", z.object({ id: PtyID.zod })), - } - - export interface Interface { - readonly list: () => Effect.Effect - readonly get: (id: PtyID) => Effect.Effect - readonly create: (input: CreateInput) => Effect.Effect - readonly update: (id: PtyID, input: UpdateInput) => Effect.Effect - readonly remove: (id: PtyID) => Effect.Effect - readonly resize: (id: PtyID, cols: number, rows: number) => Effect.Effect - readonly write: (id: PtyID, data: string) => Effect.Effect - readonly connect: ( - id: PtyID, - ws: Socket, - cursor?: number, - ) => Effect.Effect<{ onMessage: (message: string | ArrayBuffer) => void; onClose: () => void } | undefined> - } - - export class Service extends Context.Service()("@opencode/Pty") {} - - export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const bus = yield* Bus.Service - const plugin = yield* Plugin.Service - function teardown(session: Active) { - try { - session.process.kill() - } catch {} - for (const [sub, ws] of session.subscribers.entries()) { - try { - if (sock(ws) === sub) ws.close() - } catch {} - } - session.subscribers.clear() - } - - const state = yield* InstanceState.make( - Effect.fn("Pty.state")(function* (ctx) { - const state = { - dir: ctx.directory, - sessions: new Map(), - } - - yield* Effect.addFinalizer(() => - Effect.sync(() => { - for (const session of state.sessions.values()) { - teardown(session) - } - state.sessions.clear() - }), - ) - - return state - }), - ) - - const remove = Effect.fn("Pty.remove")(function* (id: PtyID) { - const s = yield* InstanceState.get(state) - const session = s.sessions.get(id) - if (!session) return - s.sessions.delete(id) - log.info("removing session", { id }) - teardown(session) - yield* bus.publish(Event.Deleted, { id: session.info.id }) - }) - - const list = Effect.fn("Pty.list")(function* () { - const s = yield* InstanceState.get(state) - return Array.from(s.sessions.values()).map((session) => session.info) - }) - - const get = Effect.fn("Pty.get")(function* (id: PtyID) { - const s = yield* InstanceState.get(state) - return s.sessions.get(id)?.info - }) - - const create = Effect.fn("Pty.create")(function* (input: CreateInput) { - const s = yield* InstanceState.get(state) - const bridge = yield* EffectBridge.make() - const id = PtyID.ascending() - const command = input.command || Shell.preferred() - const args = input.args || [] - if (Shell.login(command)) { - args.push("-l") - } - - const cwd = input.cwd || s.dir - const shell = yield* plugin.trigger("shell.env", { cwd }, { env: {} }) - const env = { - ...process.env, - ...input.env, - ...shell.env, - TERM: "xterm-256color", - OPENCODE_TERMINAL: "1", - } as Record - - if (process.platform === "win32") { - env.LC_ALL = "C.UTF-8" - env.LC_CTYPE = "C.UTF-8" - env.LANG = "C.UTF-8" - } - log.info("creating session", { id, cmd: command, args, cwd }) - - const { spawn } = yield* Effect.promise(() => pty()) - const proc = yield* Effect.sync(() => - spawn(command, args, { - name: "xterm-256color", - cwd, - env, - }), - ) - - const info = { - id, - title: input.title || `Terminal ${id.slice(-4)}`, - command, - args, - cwd, - status: "running", - pid: proc.pid, - } as const - const session: Active = { - info, - process: proc, - buffer: "", - bufferCursor: 0, - cursor: 0, - subscribers: new Map(), - } - s.sessions.set(id, session) - proc.onData( - Instance.bind((chunk) => { - session.cursor += chunk.length - - for (const [key, ws] of session.subscribers.entries()) { - if (ws.readyState !== 1) { - session.subscribers.delete(key) - continue - } - if (sock(ws) !== key) { - session.subscribers.delete(key) - continue - } - try { - ws.send(chunk) - } catch { - session.subscribers.delete(key) - } - } - - session.buffer += chunk - if (session.buffer.length <= BUFFER_LIMIT) return - const excess = session.buffer.length - BUFFER_LIMIT - session.buffer = session.buffer.slice(excess) - session.bufferCursor += excess - }), - ) - proc.onExit( - Instance.bind(({ exitCode }) => { - if (session.info.status === "exited") return - log.info("session exited", { id, exitCode }) - session.info.status = "exited" - bridge.fork(bus.publish(Event.Exited, { id, exitCode })) - bridge.fork(remove(id)) - }), - ) - yield* bus.publish(Event.Created, { info }) - return info - }) - - const update = Effect.fn("Pty.update")(function* (id: PtyID, input: UpdateInput) { - const s = yield* InstanceState.get(state) - const session = s.sessions.get(id) - if (!session) return - if (input.title) { - session.info.title = input.title - } - if (input.size) { - session.process.resize(input.size.cols, input.size.rows) - } - yield* bus.publish(Event.Updated, { info: session.info }) - return session.info - }) - - const resize = Effect.fn("Pty.resize")(function* (id: PtyID, cols: number, rows: number) { - const s = yield* InstanceState.get(state) - const session = s.sessions.get(id) - if (session && session.info.status === "running") { - session.process.resize(cols, rows) - } - }) - - const write = Effect.fn("Pty.write")(function* (id: PtyID, data: string) { - const s = yield* InstanceState.get(state) - const session = s.sessions.get(id) - if (session && session.info.status === "running") { - session.process.write(data) - } - }) - - const connect = Effect.fn("Pty.connect")(function* (id: PtyID, ws: Socket, cursor?: number) { - const s = yield* InstanceState.get(state) - const session = s.sessions.get(id) - if (!session) { - ws.close() - return - } - log.info("client connected to session", { id }) - - const sub = sock(ws) - session.subscribers.delete(sub) - session.subscribers.set(sub, ws) - - const cleanup = () => { - session.subscribers.delete(sub) - } - - const start = session.bufferCursor - const end = session.cursor - const from = - cursor === -1 ? end : typeof cursor === "number" && Number.isSafeInteger(cursor) ? Math.max(0, cursor) : 0 - - const data = (() => { - if (!session.buffer) return "" - if (from >= end) return "" - const offset = Math.max(0, from - start) - if (offset >= session.buffer.length) return "" - return session.buffer.slice(offset) - })() - - if (data) { - try { - for (let i = 0; i < data.length; i += BUFFER_CHUNK) { - ws.send(data.slice(i, i + BUFFER_CHUNK)) - } - } catch { - cleanup() - ws.close() - return - } - } - - try { - ws.send(meta(end)) - } catch { - cleanup() - ws.close() - return - } - - return { - onMessage: (message: string | ArrayBuffer) => { - session.process.write(String(message)) - }, - onClose: () => { - log.info("client disconnected from session", { id }) - cleanup() - }, - } - }) - - return Service.of({ list, get, create, update, remove, resize, write, connect }) - }), - ) - - export const defaultLayer = layer.pipe(Layer.provide(Bus.layer), Layer.provide(Plugin.defaultLayer)) -} +export * as Pty from "./service" diff --git a/packages/opencode/src/pty/service.ts b/packages/opencode/src/pty/service.ts new file mode 100644 index 0000000000..3359d0aabf --- /dev/null +++ b/packages/opencode/src/pty/service.ts @@ -0,0 +1,362 @@ +import { BusEvent } from "@/bus/bus-event" +import { Bus } from "@/bus" +import { InstanceState } from "@/effect/instance-state" +import { Instance } from "@/project/instance" +import type { Proc } from "#pty" +import z from "zod" +import { Log } from "../util/log" +import { lazy } from "@opencode-ai/shared/util/lazy" +import { Shell } from "@/shell/shell" +import { Plugin } from "@/plugin" +import { PtyID } from "./schema" +import { Effect, Layer, Context } from "effect" +import { EffectBridge } from "@/effect/bridge" + +const log = Log.create({ service: "pty" }) + +const BUFFER_LIMIT = 1024 * 1024 * 2 +const BUFFER_CHUNK = 64 * 1024 +const encoder = new TextEncoder() + +type Socket = { + readyState: number + data?: unknown + send: (data: string | Uint8Array | ArrayBuffer) => void + close: (code?: number, reason?: string) => void +} + +const sock = (ws: Socket) => (ws.data && typeof ws.data === "object" ? ws.data : ws) + +type Active = { + info: Info + process: Proc + buffer: string + bufferCursor: number + cursor: number + subscribers: Map +} + +type State = { + dir: string + sessions: Map +} + +// WebSocket control frame: 0x00 + UTF-8 JSON. +const meta = (cursor: number) => { + const json = JSON.stringify({ cursor }) + const bytes = encoder.encode(json) + const out = new Uint8Array(bytes.length + 1) + out[0] = 0 + out.set(bytes, 1) + return out +} + +const pty = lazy(() => import("#pty")) + +export const Info = z + .object({ + id: PtyID.zod, + title: z.string(), + command: z.string(), + args: z.array(z.string()), + cwd: z.string(), + status: z.enum(["running", "exited"]), + pid: z.number(), + }) + .meta({ ref: "Pty" }) + +export type Info = z.infer + +export const CreateInput = z.object({ + command: z.string().optional(), + args: z.array(z.string()).optional(), + cwd: z.string().optional(), + title: z.string().optional(), + env: z.record(z.string(), z.string()).optional(), +}) + +export type CreateInput = z.infer + +export const UpdateInput = z.object({ + title: z.string().optional(), + size: z + .object({ + rows: z.number(), + cols: z.number(), + }) + .optional(), +}) + +export type UpdateInput = z.infer + +export const Event = { + Created: BusEvent.define("pty.created", z.object({ info: Info })), + Updated: BusEvent.define("pty.updated", z.object({ info: Info })), + Exited: BusEvent.define("pty.exited", z.object({ id: PtyID.zod, exitCode: z.number() })), + Deleted: BusEvent.define("pty.deleted", z.object({ id: PtyID.zod })), +} + +export interface Interface { + readonly list: () => Effect.Effect + readonly get: (id: PtyID) => Effect.Effect + readonly create: (input: CreateInput) => Effect.Effect + readonly update: (id: PtyID, input: UpdateInput) => Effect.Effect + readonly remove: (id: PtyID) => Effect.Effect + readonly resize: (id: PtyID, cols: number, rows: number) => Effect.Effect + readonly write: (id: PtyID, data: string) => Effect.Effect + readonly connect: ( + id: PtyID, + ws: Socket, + cursor?: number, + ) => Effect.Effect<{ onMessage: (message: string | ArrayBuffer) => void; onClose: () => void } | undefined> +} + +export class Service extends Context.Service()("@opencode/Pty") {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const bus = yield* Bus.Service + const plugin = yield* Plugin.Service + function teardown(session: Active) { + try { + session.process.kill() + } catch {} + for (const [sub, ws] of session.subscribers.entries()) { + try { + if (sock(ws) === sub) ws.close() + } catch {} + } + session.subscribers.clear() + } + + const state = yield* InstanceState.make( + Effect.fn("Pty.state")(function* (ctx) { + const state = { + dir: ctx.directory, + sessions: new Map(), + } + + yield* Effect.addFinalizer(() => + Effect.sync(() => { + for (const session of state.sessions.values()) { + teardown(session) + } + state.sessions.clear() + }), + ) + + return state + }), + ) + + const remove = Effect.fn("Pty.remove")(function* (id: PtyID) { + const s = yield* InstanceState.get(state) + const session = s.sessions.get(id) + if (!session) return + s.sessions.delete(id) + log.info("removing session", { id }) + teardown(session) + yield* bus.publish(Event.Deleted, { id: session.info.id }) + }) + + const list = Effect.fn("Pty.list")(function* () { + const s = yield* InstanceState.get(state) + return Array.from(s.sessions.values()).map((session) => session.info) + }) + + const get = Effect.fn("Pty.get")(function* (id: PtyID) { + const s = yield* InstanceState.get(state) + return s.sessions.get(id)?.info + }) + + const create = Effect.fn("Pty.create")(function* (input: CreateInput) { + const s = yield* InstanceState.get(state) + const bridge = yield* EffectBridge.make() + const id = PtyID.ascending() + const command = input.command || Shell.preferred() + const args = input.args || [] + if (Shell.login(command)) { + args.push("-l") + } + + const cwd = input.cwd || s.dir + const shell = yield* plugin.trigger("shell.env", { cwd }, { env: {} }) + const env = { + ...process.env, + ...input.env, + ...shell.env, + TERM: "xterm-256color", + OPENCODE_TERMINAL: "1", + } as Record + + if (process.platform === "win32") { + env.LC_ALL = "C.UTF-8" + env.LC_CTYPE = "C.UTF-8" + env.LANG = "C.UTF-8" + } + log.info("creating session", { id, cmd: command, args, cwd }) + + const { spawn } = yield* Effect.promise(() => pty()) + const proc = yield* Effect.sync(() => + spawn(command, args, { + name: "xterm-256color", + cwd, + env, + }), + ) + + const info = { + id, + title: input.title || `Terminal ${id.slice(-4)}`, + command, + args, + cwd, + status: "running", + pid: proc.pid, + } as const + const session: Active = { + info, + process: proc, + buffer: "", + bufferCursor: 0, + cursor: 0, + subscribers: new Map(), + } + s.sessions.set(id, session) + proc.onData( + Instance.bind((chunk) => { + session.cursor += chunk.length + + for (const [key, ws] of session.subscribers.entries()) { + if (ws.readyState !== 1) { + session.subscribers.delete(key) + continue + } + if (sock(ws) !== key) { + session.subscribers.delete(key) + continue + } + try { + ws.send(chunk) + } catch { + session.subscribers.delete(key) + } + } + + session.buffer += chunk + if (session.buffer.length <= BUFFER_LIMIT) return + const excess = session.buffer.length - BUFFER_LIMIT + session.buffer = session.buffer.slice(excess) + session.bufferCursor += excess + }), + ) + proc.onExit( + Instance.bind(({ exitCode }) => { + if (session.info.status === "exited") return + log.info("session exited", { id, exitCode }) + session.info.status = "exited" + bridge.fork(bus.publish(Event.Exited, { id, exitCode })) + bridge.fork(remove(id)) + }), + ) + yield* bus.publish(Event.Created, { info }) + return info + }) + + const update = Effect.fn("Pty.update")(function* (id: PtyID, input: UpdateInput) { + const s = yield* InstanceState.get(state) + const session = s.sessions.get(id) + if (!session) return + if (input.title) { + session.info.title = input.title + } + if (input.size) { + session.process.resize(input.size.cols, input.size.rows) + } + yield* bus.publish(Event.Updated, { info: session.info }) + return session.info + }) + + const resize = Effect.fn("Pty.resize")(function* (id: PtyID, cols: number, rows: number) { + const s = yield* InstanceState.get(state) + const session = s.sessions.get(id) + if (session && session.info.status === "running") { + session.process.resize(cols, rows) + } + }) + + const write = Effect.fn("Pty.write")(function* (id: PtyID, data: string) { + const s = yield* InstanceState.get(state) + const session = s.sessions.get(id) + if (session && session.info.status === "running") { + session.process.write(data) + } + }) + + const connect = Effect.fn("Pty.connect")(function* (id: PtyID, ws: Socket, cursor?: number) { + const s = yield* InstanceState.get(state) + const session = s.sessions.get(id) + if (!session) { + ws.close() + return + } + log.info("client connected to session", { id }) + + const sub = sock(ws) + session.subscribers.delete(sub) + session.subscribers.set(sub, ws) + + const cleanup = () => { + session.subscribers.delete(sub) + } + + const start = session.bufferCursor + const end = session.cursor + const from = + cursor === -1 ? end : typeof cursor === "number" && Number.isSafeInteger(cursor) ? Math.max(0, cursor) : 0 + + const data = (() => { + if (!session.buffer) return "" + if (from >= end) return "" + const offset = Math.max(0, from - start) + if (offset >= session.buffer.length) return "" + return session.buffer.slice(offset) + })() + + if (data) { + try { + for (let i = 0; i < data.length; i += BUFFER_CHUNK) { + ws.send(data.slice(i, i + BUFFER_CHUNK)) + } + } catch { + cleanup() + ws.close() + return + } + } + + try { + ws.send(meta(end)) + } catch { + cleanup() + ws.close() + return + } + + return { + onMessage: (message: string | ArrayBuffer) => { + session.process.write(String(message)) + }, + onClose: () => { + log.info("client disconnected from session", { id }) + cleanup() + }, + } + }) + + return Service.of({ list, get, create, update, remove, resize, write, connect }) + }), +) + +export const defaultLayer = layer.pipe(Layer.provide(Bus.layer), Layer.provide(Plugin.defaultLayer)) From 48f88af9aa930e245321b2cec5765896b02c36ef Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Thu, 16 Apr 2026 02:39:40 +0000 Subject: [PATCH 202/300] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index a12e6f7e5e..6f0d2fbb27 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-PvIx2g1J5QIUIzkz2ABaAM4K/k/+xlBPDUExoOJNNuo=", - "aarch64-linux": "sha256-YTAL+P13L5hgNJdDSiBED/UNa5zdTntnUUYDYL+Jdzo=", - "aarch64-darwin": "sha256-y2VCJifYAp+H0lpDcJ0QfKNMG00Q/usFElaUIpdc8Vs=", - "x86_64-darwin": "sha256-yz8edIlqLp06Y95ad8YjKz5azP7YATPle4TcDx6lM+U=" + "x86_64-linux": "sha256-VIgTxIjmZ4Bfwwdj/YFmRJdBpPHYhJSY31kh06EXX+0=", + "aarch64-linux": "sha256-9118AS1ED0nrliURgZYBRuF/18RqXpUouhYJRlZ6jeA=", + "aarch64-darwin": "sha256-ppo3MfSIGKQHJCdYEZiLFRc61PtcJ9J0kAXH1pNIonA=", + "x86_64-darwin": "sha256-m+CZSOglBCTfNzbdBX6hXdDqqOzHNMzAddVp6BZVDtU=" } } From 6c7e9f6f3ac211454576e6e51cc5ff65718cd491 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 22:39:59 -0400 Subject: [PATCH 203/300] refactor: migrate Effect call sites from Flock to EffectFlock (#22688) --- packages/opencode/src/config/config.ts | 899 +++++++++---------- packages/opencode/test/config/config.test.ts | 26 +- packages/shared/src/npm.ts | 8 +- packages/shared/src/util/effect-flock.ts | 2 +- 4 files changed, 463 insertions(+), 472 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 58d9343ad9..43ec8d7099 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -34,7 +34,8 @@ import type { ConsoleState } from "./console-state" import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { InstanceState } from "@/effect/instance-state" import { Context, Duration, Effect, Exit, Fiber, Layer, Option } from "effect" -import { Flock } from "@opencode-ai/shared/util/flock" +import { EffectFlock } from "@opencode-ai/shared/util/effect-flock" + import { isPathPluginSpec, parsePluginSpecifier, resolvePathPluginTarget } from "@/plugin/shared" import { Npm } from "../npm" import { InstanceRef } from "@/effect/instance-ref" @@ -1144,497 +1145,483 @@ export const ConfigDirectoryTypoError = NamedError.create( }), ) -export const layer: Layer.Layer = - Layer.effect( - Service, - Effect.gen(function* () { - const fs = yield* AppFileSystem.Service - const authSvc = yield* Auth.Service - const accountSvc = yield* Account.Service - const env = yield* Env.Service +export const layer: Layer.Layer< + Service, + never, + AppFileSystem.Service | Auth.Service | Account.Service | Env.Service | EffectFlock.Service +> = Layer.effect( + Service, + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + const authSvc = yield* Auth.Service + const accountSvc = yield* Account.Service + const env = yield* Env.Service + const flock = yield* EffectFlock.Service - const readConfigFile = Effect.fnUntraced(function* (filepath: string) { - return yield* fs.readFileString(filepath).pipe( - Effect.catchIf( - (e) => e.reason._tag === "NotFound", - () => Effect.succeed(undefined), - ), - Effect.orDie, - ) - }) - - const loadConfig = Effect.fnUntraced(function* ( - text: string, - options: { path: string } | { dir: string; source: string }, - ) { - const original = text - const source = "path" in options ? options.path : options.source - const isFile = "path" in options - const data = yield* Effect.promise(() => - ConfigPaths.parseText(text, "path" in options ? options.path : { source: options.source, dir: options.dir }), - ) - - const normalized = (() => { - if (!data || typeof data !== "object" || Array.isArray(data)) return data - const copy = { ...(data as Record) } - const hadLegacy = "theme" in copy || "keybinds" in copy || "tui" in copy - if (!hadLegacy) return copy - delete copy.theme - delete copy.keybinds - delete copy.tui - log.warn("tui keys in opencode config are deprecated; move them to tui.json", { path: source }) - return copy - })() - - const parsed = Info.safeParse(normalized) - if (parsed.success) { - if (!parsed.data.$schema && isFile) { - parsed.data.$schema = "https://opencode.ai/config.json" - const updated = original.replace(/^\s*\{/, '{\n "$schema": "https://opencode.ai/config.json",') - yield* fs.writeFileString(options.path, updated).pipe(Effect.catch(() => Effect.void)) - } - const data = parsed.data - if (data.plugin && isFile) { - const list = data.plugin - for (let i = 0; i < list.length; i++) { - list[i] = yield* Effect.promise(() => resolvePluginSpec(list[i], options.path)) - } - } - return data - } - - throw new InvalidError({ - path: source, - issues: parsed.error.issues, - }) - }) - - const loadFile = Effect.fnUntraced(function* (filepath: string) { - log.info("loading", { path: filepath }) - const text = yield* readConfigFile(filepath) - if (!text) return {} as Info - return yield* loadConfig(text, { path: filepath }) - }) - - const loadGlobal = Effect.fnUntraced(function* () { - let result: Info = pipe( - {}, - mergeDeep(yield* loadFile(path.join(Global.Path.config, "config.json"))), - mergeDeep(yield* loadFile(path.join(Global.Path.config, "opencode.json"))), - mergeDeep(yield* loadFile(path.join(Global.Path.config, "opencode.jsonc"))), - ) - - const legacy = path.join(Global.Path.config, "config") - if (existsSync(legacy)) { - yield* Effect.promise(() => - import(pathToFileURL(legacy).href, { with: { type: "toml" } }) - .then(async (mod) => { - const { provider, model, ...rest } = mod.default - if (provider && model) result.model = `${provider}/${model}` - result["$schema"] = "https://opencode.ai/config.json" - result = mergeDeep(result, rest) - await fsNode.writeFile(path.join(Global.Path.config, "config.json"), JSON.stringify(result, null, 2)) - await fsNode.unlink(legacy) - }) - .catch(() => {}), - ) - } - - return result - }) - - const [cachedGlobal, invalidateGlobal] = yield* Effect.cachedInvalidateWithTTL( - loadGlobal().pipe( - Effect.tapError((error) => - Effect.sync(() => log.error("failed to load global config, using defaults", { error: String(error) })), - ), - Effect.orElseSucceed((): Info => ({})), + const readConfigFile = Effect.fnUntraced(function* (filepath: string) { + return yield* fs.readFileString(filepath).pipe( + Effect.catchIf( + (e) => e.reason._tag === "NotFound", + () => Effect.succeed(undefined), ), - Duration.infinity, + Effect.orDie, + ) + }) + + const loadConfig = Effect.fnUntraced(function* ( + text: string, + options: { path: string } | { dir: string; source: string }, + ) { + const original = text + const source = "path" in options ? options.path : options.source + const isFile = "path" in options + const data = yield* Effect.promise(() => + ConfigPaths.parseText(text, "path" in options ? options.path : { source: options.source, dir: options.dir }), ) - const getGlobal = Effect.fn("Config.getGlobal")(function* () { - return yield* cachedGlobal + const normalized = (() => { + if (!data || typeof data !== "object" || Array.isArray(data)) return data + const copy = { ...(data as Record) } + const hadLegacy = "theme" in copy || "keybinds" in copy || "tui" in copy + if (!hadLegacy) return copy + delete copy.theme + delete copy.keybinds + delete copy.tui + log.warn("tui keys in opencode config are deprecated; move them to tui.json", { path: source }) + return copy + })() + + const parsed = Info.safeParse(normalized) + if (parsed.success) { + if (!parsed.data.$schema && isFile) { + parsed.data.$schema = "https://opencode.ai/config.json" + const updated = original.replace(/^\s*\{/, '{\n "$schema": "https://opencode.ai/config.json",') + yield* fs.writeFileString(options.path, updated).pipe(Effect.catch(() => Effect.void)) + } + const data = parsed.data + if (data.plugin && isFile) { + const list = data.plugin + for (let i = 0; i < list.length; i++) { + list[i] = yield* Effect.promise(() => resolvePluginSpec(list[i], options.path)) + } + } + return data + } + + throw new InvalidError({ + path: source, + issues: parsed.error.issues, + }) + }) + + const loadFile = Effect.fnUntraced(function* (filepath: string) { + log.info("loading", { path: filepath }) + const text = yield* readConfigFile(filepath) + if (!text) return {} as Info + return yield* loadConfig(text, { path: filepath }) + }) + + const loadGlobal = Effect.fnUntraced(function* () { + let result: Info = pipe( + {}, + mergeDeep(yield* loadFile(path.join(Global.Path.config, "config.json"))), + mergeDeep(yield* loadFile(path.join(Global.Path.config, "opencode.json"))), + mergeDeep(yield* loadFile(path.join(Global.Path.config, "opencode.jsonc"))), + ) + + const legacy = path.join(Global.Path.config, "config") + if (existsSync(legacy)) { + yield* Effect.promise(() => + import(pathToFileURL(legacy).href, { with: { type: "toml" } }) + .then(async (mod) => { + const { provider, model, ...rest } = mod.default + if (provider && model) result.model = `${provider}/${model}` + result["$schema"] = "https://opencode.ai/config.json" + result = mergeDeep(result, rest) + await fsNode.writeFile(path.join(Global.Path.config, "config.json"), JSON.stringify(result, null, 2)) + await fsNode.unlink(legacy) + }) + .catch(() => {}), + ) + } + + return result + }) + + const [cachedGlobal, invalidateGlobal] = yield* Effect.cachedInvalidateWithTTL( + loadGlobal().pipe( + Effect.tapError((error) => + Effect.sync(() => log.error("failed to load global config, using defaults", { error: String(error) })), + ), + Effect.orElseSucceed((): Info => ({})), + ), + Duration.infinity, + ) + + const getGlobal = Effect.fn("Config.getGlobal")(function* () { + return yield* cachedGlobal + }) + + const install = Effect.fn("Config.install")(function* (dir: string) { + const pkg = path.join(dir, "package.json") + const gitignore = path.join(dir, ".gitignore") + const plugin = path.join(dir, "node_modules", "@opencode-ai", "plugin", "package.json") + const target = Installation.isLocal() ? "*" : Installation.VERSION + const json = yield* fs.readJson(pkg).pipe( + Effect.catch(() => Effect.succeed({} satisfies Package)), + Effect.map((x): Package => (isRecord(x) ? (x as Package) : {})), + ) + const hasDep = json.dependencies?.["@opencode-ai/plugin"] === target + const hasIgnore = yield* fs.existsSafe(gitignore) + const hasPkg = yield* fs.existsSafe(plugin) + + if (!hasDep) { + yield* fs.writeJson(pkg, { + ...json, + dependencies: { + ...json.dependencies, + "@opencode-ai/plugin": target, + }, + }) + } + + if (!hasIgnore) { + yield* fs.writeFileString( + gitignore, + ["node_modules", "package.json", "package-lock.json", "bun.lock", ".gitignore"].join("\n"), + ) + } + + if (hasDep && hasIgnore && hasPkg) return + + yield* Effect.promise(() => Npm.install(dir)) + }) + + const installDependencies = Effect.fn("Config.installDependencies")(function* (dir: string, input?: InstallInput) { + if ( + !(yield* fs.access(dir, { writable: true }).pipe( + Effect.as(true), + Effect.orElseSucceed(() => false), + )) + ) + return + + const key = process.platform === "win32" ? "config-install:win32" : `config-install:${AppFileSystem.resolve(dir)}` + + yield* flock.withLock(install(dir), key).pipe(Effect.orDie) + }) + + const loadInstanceState = Effect.fn("Config.loadInstanceState")(function* (ctx: InstanceContext) { + const auth = yield* authSvc.all().pipe(Effect.orDie) + + let result: Info = {} + const consoleManagedProviders = new Set() + let activeOrgName: string | undefined + + const scope = Effect.fnUntraced(function* (source: string) { + if (source.startsWith("http://") || source.startsWith("https://")) return "global" + if (source === "OPENCODE_CONFIG_CONTENT") return "local" + if (yield* InstanceRef.use((ctx) => Effect.succeed(Instance.containsPath(source, ctx)))) return "local" + return "global" }) - const install = Effect.fn("Config.install")(function* (dir: string) { - const pkg = path.join(dir, "package.json") - const gitignore = path.join(dir, ".gitignore") - const plugin = path.join(dir, "node_modules", "@opencode-ai", "plugin", "package.json") - const target = Installation.isLocal() ? "*" : Installation.VERSION - const json = yield* fs.readJson(pkg).pipe( - Effect.catch(() => Effect.succeed({} satisfies Package)), - Effect.map((x): Package => (isRecord(x) ? (x as Package) : {})), - ) - const hasDep = json.dependencies?.["@opencode-ai/plugin"] === target - const hasIgnore = yield* fs.existsSafe(gitignore) - const hasPkg = yield* fs.existsSafe(plugin) + const track = Effect.fnUntraced(function* (source: string, list: PluginSpec[] | undefined, kind?: PluginScope) { + if (!list?.length) return + const hit = kind ?? (yield* scope(source)) + const plugins = deduplicatePluginOrigins([ + ...(result.plugin_origins ?? []), + ...list.map((spec) => ({ spec, source, scope: hit })), + ]) + result.plugin = plugins.map((item) => item.spec) + result.plugin_origins = plugins + }) - if (!hasDep) { - yield* fs.writeJson(pkg, { - ...json, - dependencies: { - ...json.dependencies, - "@opencode-ai/plugin": target, - }, + const merge = (source: string, next: Info, kind?: PluginScope) => { + result = mergeConfigConcatArrays(result, next) + return track(source, next.plugin, kind) + } + + for (const [key, value] of Object.entries(auth)) { + if (value.type === "wellknown") { + const url = key.replace(/\/+$/, "") + process.env[value.key] = value.token + log.debug("fetching remote config", { url: `${url}/.well-known/opencode` }) + const response = yield* Effect.promise(() => fetch(`${url}/.well-known/opencode`)) + if (!response.ok) { + throw new Error(`failed to fetch remote config from ${url}: ${response.status}`) + } + const wellknown = (yield* Effect.promise(() => response.json())) as any + const remoteConfig = wellknown.config ?? {} + if (!remoteConfig.$schema) remoteConfig.$schema = "https://opencode.ai/config.json" + const source = `${url}/.well-known/opencode` + const next = yield* loadConfig(JSON.stringify(remoteConfig), { + dir: path.dirname(source), + source, }) + yield* merge(source, next, "global") + log.debug("loaded remote config from well-known", { url }) + } + } + + const global = yield* getGlobal() + yield* merge(Global.Path.config, global, "global") + + if (Flag.OPENCODE_CONFIG) { + yield* merge(Flag.OPENCODE_CONFIG, yield* loadFile(Flag.OPENCODE_CONFIG)) + log.debug("loaded custom config", { path: Flag.OPENCODE_CONFIG }) + } + + if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) { + for (const file of yield* Effect.promise(() => + ConfigPaths.projectFiles("opencode", ctx.directory, ctx.worktree), + )) { + yield* merge(file, yield* loadFile(file), "local") + } + } + + result.agent = result.agent || {} + result.mode = result.mode || {} + result.plugin = result.plugin || [] + + const directories = yield* Effect.promise(() => ConfigPaths.directories(ctx.directory, ctx.worktree)) + + if (Flag.OPENCODE_CONFIG_DIR) { + log.debug("loading config from OPENCODE_CONFIG_DIR", { path: Flag.OPENCODE_CONFIG_DIR }) + } + + const deps: Fiber.Fiber[] = [] + + for (const dir of unique(directories)) { + if (dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) { + for (const file of ["opencode.json", "opencode.jsonc"]) { + const source = path.join(dir, file) + log.debug(`loading config from ${source}`) + yield* merge(source, yield* loadFile(source)) + result.agent ??= {} + result.mode ??= {} + result.plugin ??= [] + } } - if (!hasIgnore) { - yield* fs.writeFileString( - gitignore, - ["node_modules", "package.json", "package-lock.json", "bun.lock", ".gitignore"].join("\n"), - ) - } - - if (hasDep && hasIgnore && hasPkg) return - - yield* Effect.promise(() => Npm.install(dir)) - }) - - const installDependencies = Effect.fn("Config.installDependencies")(function* ( - dir: string, - input?: InstallInput, - ) { - if ( - !(yield* fs.access(dir, { writable: true }).pipe( - Effect.as(true), - Effect.orElseSucceed(() => false), - )) - ) - return - - const key = - process.platform === "win32" ? "config-install:win32" : `config-install:${AppFileSystem.resolve(dir)}` - - yield* Effect.acquireUseRelease( - Effect.promise((signal) => - Flock.acquire(key, { - signal, - onWait: (tick) => - input?.waitTick?.({ - dir, - attempt: tick.attempt, - delay: tick.delay, - waited: tick.waited, - }), - }), + const dep = yield* installDependencies(dir).pipe( + Effect.exit, + Effect.tap((exit) => + Exit.isFailure(exit) + ? Effect.sync(() => { + log.warn("background dependency install failed", { dir, error: String(exit.cause) }) + }) + : Effect.void, ), - () => install(dir), - (lease) => Effect.promise(() => lease.release()), + Effect.asVoid, + Effect.forkScoped, ) - }) + deps.push(dep) - const loadInstanceState = Effect.fn("Config.loadInstanceState")(function* (ctx: InstanceContext) { - const auth = yield* authSvc.all().pipe(Effect.orDie) + result.command = mergeDeep(result.command ?? {}, yield* Effect.promise(() => loadCommand(dir))) + result.agent = mergeDeep(result.agent, yield* Effect.promise(() => loadAgent(dir))) + result.agent = mergeDeep(result.agent, yield* Effect.promise(() => loadMode(dir))) + const list = yield* Effect.promise(() => loadPlugin(dir)) + yield* track(dir, list) + } - let result: Info = {} - const consoleManagedProviders = new Set() - let activeOrgName: string | undefined - - const scope = Effect.fnUntraced(function* (source: string) { - if (source.startsWith("http://") || source.startsWith("https://")) return "global" - if (source === "OPENCODE_CONFIG_CONTENT") return "local" - if (yield* InstanceRef.use((ctx) => Effect.succeed(Instance.containsPath(source, ctx)))) return "local" - return "global" + if (process.env.OPENCODE_CONFIG_CONTENT) { + const source = "OPENCODE_CONFIG_CONTENT" + const next = yield* loadConfig(process.env.OPENCODE_CONFIG_CONTENT, { + dir: ctx.directory, + source, }) + yield* merge(source, next, "local") + log.debug("loaded custom config from OPENCODE_CONFIG_CONTENT") + } - const track = Effect.fnUntraced(function* (source: string, list: PluginSpec[] | undefined, kind?: PluginScope) { - if (!list?.length) return - const hit = kind ?? (yield* scope(source)) - const plugins = deduplicatePluginOrigins([ - ...(result.plugin_origins ?? []), - ...list.map((spec) => ({ spec, source, scope: hit })), - ]) - result.plugin = plugins.map((item) => item.spec) - result.plugin_origins = plugins - }) + const activeAccount = Option.getOrUndefined( + yield* accountSvc.active().pipe(Effect.catch(() => Effect.succeed(Option.none()))), + ) + if (activeAccount?.active_org_id) { + const accountID = activeAccount.id + const orgID = activeAccount.active_org_id + const url = activeAccount.url + yield* Effect.gen(function* () { + const [configOpt, tokenOpt] = yield* Effect.all( + [accountSvc.config(accountID, orgID), accountSvc.token(accountID)], + { concurrency: 2 }, + ) + if (Option.isSome(tokenOpt)) { + process.env["OPENCODE_CONSOLE_TOKEN"] = tokenOpt.value + yield* env.set("OPENCODE_CONSOLE_TOKEN", tokenOpt.value) + } - const merge = (source: string, next: Info, kind?: PluginScope) => { - result = mergeConfigConcatArrays(result, next) - return track(source, next.plugin, kind) - } - - for (const [key, value] of Object.entries(auth)) { - if (value.type === "wellknown") { - const url = key.replace(/\/+$/, "") - process.env[value.key] = value.token - log.debug("fetching remote config", { url: `${url}/.well-known/opencode` }) - const response = yield* Effect.promise(() => fetch(`${url}/.well-known/opencode`)) - if (!response.ok) { - throw new Error(`failed to fetch remote config from ${url}: ${response.status}`) - } - const wellknown = (yield* Effect.promise(() => response.json())) as any - const remoteConfig = wellknown.config ?? {} - if (!remoteConfig.$schema) remoteConfig.$schema = "https://opencode.ai/config.json" - const source = `${url}/.well-known/opencode` - const next = yield* loadConfig(JSON.stringify(remoteConfig), { + if (Option.isSome(configOpt)) { + const source = `${url}/api/config` + const next = yield* loadConfig(JSON.stringify(configOpt.value), { dir: path.dirname(source), source, }) + for (const providerID of Object.keys(next.provider ?? {})) { + consoleManagedProviders.add(providerID) + } yield* merge(source, next, "global") - log.debug("loaded remote config from well-known", { url }) } - } - - const global = yield* getGlobal() - yield* merge(Global.Path.config, global, "global") - - if (Flag.OPENCODE_CONFIG) { - yield* merge(Flag.OPENCODE_CONFIG, yield* loadFile(Flag.OPENCODE_CONFIG)) - log.debug("loaded custom config", { path: Flag.OPENCODE_CONFIG }) - } - - if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) { - for (const file of yield* Effect.promise(() => - ConfigPaths.projectFiles("opencode", ctx.directory, ctx.worktree), - )) { - yield* merge(file, yield* loadFile(file), "local") - } - } - - result.agent = result.agent || {} - result.mode = result.mode || {} - result.plugin = result.plugin || [] - - const directories = yield* Effect.promise(() => ConfigPaths.directories(ctx.directory, ctx.worktree)) - - if (Flag.OPENCODE_CONFIG_DIR) { - log.debug("loading config from OPENCODE_CONFIG_DIR", { path: Flag.OPENCODE_CONFIG_DIR }) - } - - const deps: Fiber.Fiber[] = [] - - for (const dir of unique(directories)) { - if (dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) { - for (const file of ["opencode.json", "opencode.jsonc"]) { - const source = path.join(dir, file) - log.debug(`loading config from ${source}`) - yield* merge(source, yield* loadFile(source)) - result.agent ??= {} - result.mode ??= {} - result.plugin ??= [] - } - } - - const dep = yield* installDependencies(dir).pipe( - Effect.exit, - Effect.tap((exit) => - Exit.isFailure(exit) - ? Effect.sync(() => { - log.warn("background dependency install failed", { dir, error: String(exit.cause) }) - }) - : Effect.void, - ), - Effect.asVoid, - Effect.forkScoped, - ) - deps.push(dep) - - result.command = mergeDeep(result.command ?? {}, yield* Effect.promise(() => loadCommand(dir))) - result.agent = mergeDeep(result.agent, yield* Effect.promise(() => loadAgent(dir))) - result.agent = mergeDeep(result.agent, yield* Effect.promise(() => loadMode(dir))) - const list = yield* Effect.promise(() => loadPlugin(dir)) - yield* track(dir, list) - } - - if (process.env.OPENCODE_CONFIG_CONTENT) { - const source = "OPENCODE_CONFIG_CONTENT" - const next = yield* loadConfig(process.env.OPENCODE_CONFIG_CONTENT, { - dir: ctx.directory, - source, - }) - yield* merge(source, next, "local") - log.debug("loaded custom config from OPENCODE_CONFIG_CONTENT") - } - - const activeAccount = Option.getOrUndefined( - yield* accountSvc.active().pipe(Effect.catch(() => Effect.succeed(Option.none()))), + }).pipe( + Effect.withSpan("Config.loadActiveOrgConfig"), + Effect.catch((err) => { + log.debug("failed to fetch remote account config", { + error: err instanceof Error ? err.message : String(err), + }) + return Effect.void + }), ) - if (activeAccount?.active_org_id) { - const accountID = activeAccount.id - const orgID = activeAccount.active_org_id - const url = activeAccount.url - yield* Effect.gen(function* () { - const [configOpt, tokenOpt] = yield* Effect.all( - [accountSvc.config(accountID, orgID), accountSvc.token(accountID)], - { concurrency: 2 }, - ) - if (Option.isSome(tokenOpt)) { - process.env["OPENCODE_CONSOLE_TOKEN"] = tokenOpt.value - yield* env.set("OPENCODE_CONSOLE_TOKEN", tokenOpt.value) - } + } - if (Option.isSome(configOpt)) { - const source = `${url}/api/config` - const next = yield* loadConfig(JSON.stringify(configOpt.value), { - dir: path.dirname(source), - source, - }) - for (const providerID of Object.keys(next.provider ?? {})) { - consoleManagedProviders.add(providerID) - } - yield* merge(source, next, "global") - } - }).pipe( - Effect.withSpan("Config.loadActiveOrgConfig"), - Effect.catch((err) => { - log.debug("failed to fetch remote account config", { - error: err instanceof Error ? err.message : String(err), - }) - return Effect.void - }), - ) + if (existsSync(managedDir)) { + for (const file of ["opencode.json", "opencode.jsonc"]) { + const source = path.join(managedDir, file) + yield* merge(source, yield* loadFile(source), "global") } + } - if (existsSync(managedDir)) { - for (const file of ["opencode.json", "opencode.jsonc"]) { - const source = path.join(managedDir, file) - yield* merge(source, yield* loadFile(source), "global") - } - } + // macOS managed preferences (.mobileconfig deployed via MDM) override everything + result = mergeConfigConcatArrays(result, yield* Effect.promise(() => readManagedPreferences())) - // macOS managed preferences (.mobileconfig deployed via MDM) override everything - result = mergeConfigConcatArrays(result, yield* Effect.promise(() => readManagedPreferences())) - - for (const [name, mode] of Object.entries(result.mode ?? {})) { - result.agent = mergeDeep(result.agent ?? {}, { - [name]: { - ...mode, - mode: "primary" as const, - }, - }) - } - - if (Flag.OPENCODE_PERMISSION) { - result.permission = mergeDeep(result.permission ?? {}, JSON.parse(Flag.OPENCODE_PERMISSION)) - } - - if (result.tools) { - const perms: Record = {} - for (const [tool, enabled] of Object.entries(result.tools)) { - const action: PermissionAction = enabled ? "allow" : "deny" - if (tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit") { - perms.edit = action - continue - } - perms[tool] = action - } - result.permission = mergeDeep(perms, result.permission ?? {}) - } - - if (!result.username) result.username = os.userInfo().username - - if (result.autoshare === true && !result.share) { - result.share = "auto" - } - - if (Flag.OPENCODE_DISABLE_AUTOCOMPACT) { - result.compaction = { ...result.compaction, auto: false } - } - if (Flag.OPENCODE_DISABLE_PRUNE) { - result.compaction = { ...result.compaction, prune: false } - } - - return { - config: result, - directories, - deps, - consoleState: { - consoleManagedProviders: Array.from(consoleManagedProviders), - activeOrgName, - switchableOrgCount: 0, + for (const [name, mode] of Object.entries(result.mode ?? {})) { + result.agent = mergeDeep(result.agent ?? {}, { + [name]: { + ...mode, + mode: "primary" as const, }, + }) + } + + if (Flag.OPENCODE_PERMISSION) { + result.permission = mergeDeep(result.permission ?? {}, JSON.parse(Flag.OPENCODE_PERMISSION)) + } + + if (result.tools) { + const perms: Record = {} + for (const [tool, enabled] of Object.entries(result.tools)) { + const action: PermissionAction = enabled ? "allow" : "deny" + if (tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit") { + perms.edit = action + continue + } + perms[tool] = action } - }) + result.permission = mergeDeep(perms, result.permission ?? {}) + } - const state = yield* InstanceState.make( - Effect.fn("Config.state")(function* (ctx) { - return yield* loadInstanceState(ctx) - }), - ) + if (!result.username) result.username = os.userInfo().username - const get = Effect.fn("Config.get")(function* () { - return yield* InstanceState.use(state, (s) => s.config) - }) + if (result.autoshare === true && !result.share) { + result.share = "auto" + } - const directories = Effect.fn("Config.directories")(function* () { - return yield* InstanceState.use(state, (s) => s.directories) - }) + if (Flag.OPENCODE_DISABLE_AUTOCOMPACT) { + result.compaction = { ...result.compaction, auto: false } + } + if (Flag.OPENCODE_DISABLE_PRUNE) { + result.compaction = { ...result.compaction, prune: false } + } - const getConsoleState = Effect.fn("Config.getConsoleState")(function* () { - return yield* InstanceState.use(state, (s) => s.consoleState) - }) - - const waitForDependencies = Effect.fn("Config.waitForDependencies")(function* () { - yield* InstanceState.useEffect(state, (s) => - Effect.forEach(s.deps, Fiber.join, { concurrency: "unbounded" }).pipe(Effect.asVoid), - ) - }) - - const update = Effect.fn("Config.update")(function* (config: Info) { - const dir = yield* InstanceState.directory - const file = path.join(dir, "config.json") - const existing = yield* loadFile(file) - yield* fs - .writeFileString(file, JSON.stringify(mergeDeep(writable(existing), writable(config)), null, 2)) - .pipe(Effect.orDie) - yield* Effect.promise(() => Instance.dispose()) - }) - - const invalidate = Effect.fn("Config.invalidate")(function* (wait?: boolean) { - yield* invalidateGlobal - const task = Instance.disposeAll() - .catch(() => undefined) - .finally(() => - GlobalBus.emit("event", { - directory: "global", - payload: { - type: Event.Disposed.type, - properties: {}, - }, - }), - ) - if (wait) yield* Effect.promise(() => task) - else void task - }) - - const updateGlobal = Effect.fn("Config.updateGlobal")(function* (config: Info) { - const file = globalConfigFile() - const before = (yield* readConfigFile(file)) ?? "{}" - const input = writable(config) - - let next: Info - if (!file.endsWith(".jsonc")) { - const existing = parseConfig(before, file) - const merged = mergeDeep(writable(existing), input) - yield* fs.writeFileString(file, JSON.stringify(merged, null, 2)).pipe(Effect.orDie) - next = merged - } else { - const updated = patchJsonc(before, input) - next = parseConfig(updated, file) - yield* fs.writeFileString(file, updated).pipe(Effect.orDie) - } - - yield* invalidate() - return next - }) - - return Service.of({ - get, - getGlobal, - getConsoleState, - installDependencies, - update, - updateGlobal, - invalidate, + return { + config: result, directories, - waitForDependencies, - }) - }), - ) + deps, + consoleState: { + consoleManagedProviders: Array.from(consoleManagedProviders), + activeOrgName, + switchableOrgCount: 0, + }, + } + }) + + const state = yield* InstanceState.make( + Effect.fn("Config.state")(function* (ctx) { + return yield* loadInstanceState(ctx) + }), + ) + + const get = Effect.fn("Config.get")(function* () { + return yield* InstanceState.use(state, (s) => s.config) + }) + + const directories = Effect.fn("Config.directories")(function* () { + return yield* InstanceState.use(state, (s) => s.directories) + }) + + const getConsoleState = Effect.fn("Config.getConsoleState")(function* () { + return yield* InstanceState.use(state, (s) => s.consoleState) + }) + + const waitForDependencies = Effect.fn("Config.waitForDependencies")(function* () { + yield* InstanceState.useEffect(state, (s) => + Effect.forEach(s.deps, Fiber.join, { concurrency: "unbounded" }).pipe(Effect.asVoid), + ) + }) + + const update = Effect.fn("Config.update")(function* (config: Info) { + const dir = yield* InstanceState.directory + const file = path.join(dir, "config.json") + const existing = yield* loadFile(file) + yield* fs + .writeFileString(file, JSON.stringify(mergeDeep(writable(existing), writable(config)), null, 2)) + .pipe(Effect.orDie) + yield* Effect.promise(() => Instance.dispose()) + }) + + const invalidate = Effect.fn("Config.invalidate")(function* (wait?: boolean) { + yield* invalidateGlobal + const task = Instance.disposeAll() + .catch(() => undefined) + .finally(() => + GlobalBus.emit("event", { + directory: "global", + payload: { + type: Event.Disposed.type, + properties: {}, + }, + }), + ) + if (wait) yield* Effect.promise(() => task) + else void task + }) + + const updateGlobal = Effect.fn("Config.updateGlobal")(function* (config: Info) { + const file = globalConfigFile() + const before = (yield* readConfigFile(file)) ?? "{}" + const input = writable(config) + + let next: Info + if (!file.endsWith(".jsonc")) { + const existing = parseConfig(before, file) + const merged = mergeDeep(writable(existing), input) + yield* fs.writeFileString(file, JSON.stringify(merged, null, 2)).pipe(Effect.orDie) + next = merged + } else { + const updated = patchJsonc(before, input) + next = parseConfig(updated, file) + yield* fs.writeFileString(file, updated).pipe(Effect.orDie) + } + + yield* invalidate() + return next + }) + + return Service.of({ + get, + getGlobal, + getConsoleState, + installDependencies, + update, + updateGlobal, + invalidate, + directories, + waitForDependencies, + }) + }), +) export const defaultLayer = layer.pipe( + Layer.provide(EffectFlock.defaultLayer), Layer.provide(AppFileSystem.defaultLayer), Layer.provide(Env.defaultLayer), Layer.provide(Auth.defaultLayer), diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 88957c6141..8cf410c3d2 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -2,6 +2,8 @@ import { test, expect, describe, mock, afterEach, beforeEach, spyOn } from "bun: import { Deferred, Effect, Fiber, Layer, Option } from "effect" import { NodeFileSystem, NodePath } from "@effect/platform-node" import { Config } from "../../src/config" +import { EffectFlock } from "@opencode-ai/shared/util/effect-flock" + import { Instance } from "../../src/project/instance" import { Auth } from "../../src/auth" import { AccessToken, Account, AccountID, OrgID } from "../../src/account" @@ -34,7 +36,10 @@ const emptyAuth = Layer.mock(Auth.Service)({ all: () => Effect.succeed({}), }) +const testFlock = EffectFlock.defaultLayer + const layer = Config.layer.pipe( + Layer.provide(testFlock), Layer.provide(AppFileSystem.defaultLayer), Layer.provide(Env.defaultLayer), Layer.provide(emptyAuth), @@ -333,6 +338,7 @@ test("resolves env templates in account config with account token", async () => }) const layer = Config.layer.pipe( + Layer.provide(testFlock), Layer.provide(AppFileSystem.defaultLayer), Layer.provide(Env.defaultLayer), Layer.provide(emptyAuth), @@ -879,11 +885,7 @@ it.live("dedupes concurrent config dependency installs for the same dir", () => yield* Deferred.await(ready) let done = false - const second = yield* installDeps(dir, { - waitTick: () => { - Deferred.doneUnsafe(blocked, Effect.void) - }, - }).pipe( + const second = yield* installDeps(dir).pipe( Effect.tap(() => Effect.sync(() => { done = true @@ -892,7 +894,8 @@ it.live("dedupes concurrent config dependency installs for the same dir", () => Effect.forkScoped, ) - yield* Deferred.await(blocked) + // Give the second fiber time to hit the lock retry loop + yield* Effect.sleep(500) expect(done).toBe(false) yield* Deferred.succeed(hold, void 0) @@ -955,12 +958,9 @@ it.live("serializes config dependency installs across dirs", () => const first = yield* installDeps(a).pipe(Effect.forkScoped) yield* Deferred.await(ready) - const second = yield* installDeps(b, { - waitTick: () => { - Deferred.doneUnsafe(blocked, Effect.void) - }, - }).pipe(Effect.forkScoped) - yield* Deferred.await(blocked) + const second = yield* installDeps(b).pipe(Effect.forkScoped) + // Give the second fiber time to hit the lock retry loop + yield* Effect.sleep(500) expect(peak).toBe(1) yield* Deferred.succeed(hold, void 0) @@ -1826,6 +1826,7 @@ test("project config overrides remote well-known config", async () => { }) const layer = Config.layer.pipe( + Layer.provide(testFlock), Layer.provide(AppFileSystem.defaultLayer), Layer.provide(Env.defaultLayer), Layer.provide(fakeAuth), @@ -1882,6 +1883,7 @@ test("wellknown URL with trailing slash is normalized", async () => { }) const layer = Config.layer.pipe( + Layer.provide(testFlock), Layer.provide(AppFileSystem.defaultLayer), Layer.provide(Env.defaultLayer), Layer.provide(fakeAuth), diff --git a/packages/shared/src/npm.ts b/packages/shared/src/npm.ts index 994ec04dae..8bd0cc468b 100644 --- a/packages/shared/src/npm.ts +++ b/packages/shared/src/npm.ts @@ -5,7 +5,7 @@ import { Effect, Schema, Context, Layer, Option, FileSystem } from "effect" import { NodeFileSystem } from "@effect/platform-node" import { AppFileSystem } from "./filesystem" import { Global } from "./global" -import { Flock } from "./util/flock" +import { EffectFlock } from "./util/effect-flock" export namespace Npm { export class InstallFailedError extends Schema.TaggedErrorClass()("NpmInstallFailedError", { @@ -62,6 +62,7 @@ export namespace Npm { const afs = yield* AppFileSystem.Service const global = yield* Global.Service const fs = yield* FileSystem.FileSystem + const flock = yield* EffectFlock.Service const directory = (pkg: string) => path.join(global.cache, "packages", sanitize(pkg)) const outdated = Effect.fn("Npm.outdated")(function* (pkg: string, cachedVersion: string) { @@ -92,7 +93,7 @@ export namespace Npm { const add = Effect.fn("Npm.add")(function* (pkg: string) { const dir = directory(pkg) - yield* Flock.effect(`npm-install:${dir}`) + yield* flock.acquire(`npm-install:${dir}`) const arborist = new Arborist({ path: dir, @@ -133,7 +134,7 @@ export namespace Npm { }, Effect.scoped) const install = Effect.fn("Npm.install")(function* (dir: string) { - yield* Flock.effect(`npm-install:${dir}`) + yield* flock.acquire(`npm-install:${dir}`) const reify = Effect.fnUntraced(function* () { const arb = new Arborist({ @@ -240,6 +241,7 @@ export namespace Npm { ) export const defaultLayer = layer.pipe( + Layer.provide(EffectFlock.layer), Layer.provide(AppFileSystem.layer), Layer.provide(Global.layer), Layer.provide(NodeFileSystem.layer), diff --git a/packages/shared/src/util/effect-flock.ts b/packages/shared/src/util/effect-flock.ts index d728c0ef15..3e00afc9e4 100644 --- a/packages/shared/src/util/effect-flock.ts +++ b/packages/shared/src/util/effect-flock.ts @@ -274,5 +274,5 @@ export namespace EffectFlock { }), ) - export const live = layer.pipe(Layer.provide(AppFileSystem.defaultLayer)) + export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer), Layer.provide(Global.layer)) } From 379e40d7720ab1caecfc750ca87ab3d039957ba3 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 22:45:45 -0400 Subject: [PATCH 204/300] feat: unwrap InstanceState + EffectBridge namespaces to flat exports + barrel (#22721) --- packages/opencode/src/agent/agent.ts | 2 +- packages/opencode/src/bus/bus.ts | 4 +- packages/opencode/src/command/command.ts | 4 +- packages/opencode/src/config/config.ts | 2 +- packages/opencode/src/config/tui.ts | 2 +- packages/opencode/src/effect/bridge.ts | 80 ++++++----- packages/opencode/src/effect/index.ts | 2 + .../opencode/src/effect/instance-state.ts | 132 +++++++++--------- packages/opencode/src/env/env.ts | 2 +- packages/opencode/src/file/file.ts | 2 +- packages/opencode/src/file/time.ts | 2 +- packages/opencode/src/file/watcher.ts | 2 +- packages/opencode/src/format/format.ts | 2 +- packages/opencode/src/lsp/index.ts | 2 +- packages/opencode/src/mcp/mcp.ts | 4 +- .../opencode/src/permission/permission.ts | 2 +- packages/opencode/src/plugin/plugin.ts | 4 +- packages/opencode/src/project/vcs.ts | 2 +- packages/opencode/src/provider/auth.ts | 2 +- packages/opencode/src/provider/provider.ts | 4 +- packages/opencode/src/pty/service.ts | 4 +- packages/opencode/src/question/index.ts | 2 +- packages/opencode/src/session/compaction.ts | 2 +- packages/opencode/src/session/instruction.ts | 2 +- packages/opencode/src/session/llm.ts | 2 +- packages/opencode/src/session/prompt.ts | 4 +- packages/opencode/src/session/run-state.ts | 2 +- packages/opencode/src/session/session.ts | 2 +- packages/opencode/src/session/status.ts | 2 +- packages/opencode/src/share/share-next.ts | 4 +- packages/opencode/src/skill/skill.ts | 2 +- packages/opencode/src/snapshot/snapshot.ts | 2 +- packages/opencode/src/storage/db.ts | 2 +- .../opencode/src/tool/external-directory.ts | 2 +- packages/opencode/src/tool/glob.ts | 2 +- packages/opencode/src/tool/grep.ts | 2 +- packages/opencode/src/tool/registry.ts | 2 +- packages/opencode/src/worktree/worktree.ts | 2 +- .../test/effect/app-runtime-logger.test.ts | 2 +- .../test/effect/instance-state.test.ts | 4 +- 40 files changed, 152 insertions(+), 154 deletions(-) create mode 100644 packages/opencode/src/effect/index.ts diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 8d11a93b39..b027c8c945 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -20,7 +20,7 @@ import path from "path" import { Plugin } from "@/plugin" import { Skill } from "../skill" import { Effect, Context, Layer } from "effect" -import { InstanceState } from "@/effect/instance-state" +import { InstanceState } from "@/effect" import * as Option from "effect/Option" import * as OtelTracer from "@effect/opentelemetry/Tracer" diff --git a/packages/opencode/src/bus/bus.ts b/packages/opencode/src/bus/bus.ts index fe9169171c..12d7f246cd 100644 --- a/packages/opencode/src/bus/bus.ts +++ b/packages/opencode/src/bus/bus.ts @@ -1,10 +1,10 @@ import z from "zod" import { Effect, Exit, Layer, PubSub, Scope, Context, Stream } from "effect" -import { EffectBridge } from "@/effect/bridge" +import { EffectBridge } from "@/effect" import { Log } from "../util/log" import { BusEvent } from "./bus-event" import { GlobalBus } from "./global" -import { InstanceState } from "@/effect/instance-state" +import { InstanceState } from "@/effect" import { makeRuntime } from "@/effect/run-service" const log = Log.create({ service: "bus" }) diff --git a/packages/opencode/src/command/command.ts b/packages/opencode/src/command/command.ts index fe9005edb2..4ea1325240 100644 --- a/packages/opencode/src/command/command.ts +++ b/packages/opencode/src/command/command.ts @@ -1,6 +1,6 @@ import { BusEvent } from "@/bus/bus-event" -import { InstanceState } from "@/effect/instance-state" -import { EffectBridge } from "@/effect/bridge" +import { InstanceState } from "@/effect" +import { EffectBridge } from "@/effect" import type { InstanceContext } from "@/project/instance" import { SessionID, MessageID } from "@/session/schema" import { Effect, Layer, Context } from "effect" diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 43ec8d7099..7eeacf1ffc 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -32,7 +32,7 @@ import { isRecord } from "@/util/record" import { ConfigPaths } from "./paths" import type { ConsoleState } from "./console-state" import { AppFileSystem } from "@opencode-ai/shared/filesystem" -import { InstanceState } from "@/effect/instance-state" +import { InstanceState } from "@/effect" import { Context, Duration, Effect, Exit, Fiber, Layer, Option } from "effect" import { EffectFlock } from "@opencode-ai/shared/util/effect-flock" diff --git a/packages/opencode/src/config/tui.ts b/packages/opencode/src/config/tui.ts index 163bd4d7d7..24ccefecdb 100644 --- a/packages/opencode/src/config/tui.ts +++ b/packages/opencode/src/config/tui.ts @@ -10,7 +10,7 @@ import { Flag } from "@/flag/flag" import { Log } from "@/util/log" import { isRecord } from "@/util/record" import { Global } from "@/global" -import { InstanceState } from "@/effect/instance-state" +import { InstanceState } from "@/effect" import { makeRuntime } from "@/effect/run-service" import { AppFileSystem } from "@opencode-ai/shared/filesystem" diff --git a/packages/opencode/src/effect/bridge.ts b/packages/opencode/src/effect/bridge.ts index bafa5a0ea6..9ca7b50ad9 100644 --- a/packages/opencode/src/effect/bridge.ts +++ b/packages/opencode/src/effect/bridge.ts @@ -5,45 +5,43 @@ import { LocalContext } from "@/util/local-context" import { InstanceRef, WorkspaceRef } from "./instance-ref" import { attachWith } from "./run-service" -export namespace EffectBridge { - export interface Shape { - readonly promise: (effect: Effect.Effect) => Promise - readonly fork: (effect: Effect.Effect) => Fiber.Fiber - } - - function restore(instance: InstanceContext | undefined, workspace: string | undefined, fn: () => R): R { - if (instance && workspace !== undefined) { - return WorkspaceContext.restore(workspace, () => Instance.restore(instance, fn)) - } - if (instance) return Instance.restore(instance, fn) - if (workspace !== undefined) return WorkspaceContext.restore(workspace, fn) - return fn() - } - - export function make(): Effect.Effect { - return Effect.gen(function* () { - const ctx = yield* Effect.context() - const value = yield* InstanceRef - const instance = - value ?? - (() => { - try { - return Instance.current - } catch (err) { - if (!(err instanceof LocalContext.NotFound)) throw err - } - })() - const workspace = (yield* WorkspaceRef) ?? WorkspaceContext.workspaceID - const attach = (effect: Effect.Effect) => attachWith(effect, { instance, workspace }) - const wrap = (effect: Effect.Effect) => - attach(effect).pipe(Effect.provide(ctx)) as Effect.Effect - - return { - promise: (effect: Effect.Effect) => - restore(instance, workspace, () => Effect.runPromise(wrap(effect))), - fork: (effect: Effect.Effect) => - restore(instance, workspace, () => Effect.runFork(wrap(effect))), - } satisfies Shape - }) - } +export interface Shape { + readonly promise: (effect: Effect.Effect) => Promise + readonly fork: (effect: Effect.Effect) => Fiber.Fiber +} + +function restore(instance: InstanceContext | undefined, workspace: string | undefined, fn: () => R): R { + if (instance && workspace !== undefined) { + return WorkspaceContext.restore(workspace, () => Instance.restore(instance, fn)) + } + if (instance) return Instance.restore(instance, fn) + if (workspace !== undefined) return WorkspaceContext.restore(workspace, fn) + return fn() +} + +export function make(): Effect.Effect { + return Effect.gen(function* () { + const ctx = yield* Effect.context() + const value = yield* InstanceRef + const instance = + value ?? + (() => { + try { + return Instance.current + } catch (err) { + if (!(err instanceof LocalContext.NotFound)) throw err + } + })() + const workspace = (yield* WorkspaceRef) ?? WorkspaceContext.workspaceID + const attach = (effect: Effect.Effect) => attachWith(effect, { instance, workspace }) + const wrap = (effect: Effect.Effect) => + attach(effect).pipe(Effect.provide(ctx)) as Effect.Effect + + return { + promise: (effect: Effect.Effect) => + restore(instance, workspace, () => Effect.runPromise(wrap(effect))), + fork: (effect: Effect.Effect) => + restore(instance, workspace, () => Effect.runFork(wrap(effect))), + } satisfies Shape + }) } diff --git a/packages/opencode/src/effect/index.ts b/packages/opencode/src/effect/index.ts new file mode 100644 index 0000000000..d10afdff2b --- /dev/null +++ b/packages/opencode/src/effect/index.ts @@ -0,0 +1,2 @@ +export * as InstanceState from "./instance-state" +export * as EffectBridge from "./bridge" diff --git a/packages/opencode/src/effect/instance-state.ts b/packages/opencode/src/effect/instance-state.ts index b3392d1563..2a51931ada 100644 --- a/packages/opencode/src/effect/instance-state.ts +++ b/packages/opencode/src/effect/instance-state.ts @@ -13,72 +13,70 @@ export interface InstanceState { readonly cache: ScopedCache.ScopedCache } -export namespace InstanceState { - export const bind = any>(fn: F): F => { - try { - return Instance.bind(fn) - } catch (err) { - if (!(err instanceof LocalContext.NotFound)) throw err - } - const fiber = Fiber.getCurrent() - const ctx = fiber ? Context.getReferenceUnsafe(fiber.context, InstanceRef) : undefined - if (!ctx) return fn - return ((...args: any[]) => Instance.restore(ctx, () => fn(...args))) as F +export const bind = any>(fn: F): F => { + try { + return Instance.bind(fn) + } catch (err) { + if (!(err instanceof LocalContext.NotFound)) throw err } - - export const context = Effect.gen(function* () { - return (yield* InstanceRef) ?? Instance.current - }) - - export const workspaceID = Effect.gen(function* () { - return (yield* WorkspaceRef) ?? WorkspaceContext.workspaceID - }) - - export const directory = Effect.map(context, (ctx) => ctx.directory) - - export const make = ( - init: (ctx: InstanceContext) => Effect.Effect, - ): Effect.Effect>, never, R | Scope.Scope> => - Effect.gen(function* () { - const cache = yield* ScopedCache.make({ - capacity: Number.POSITIVE_INFINITY, - lookup: () => - Effect.gen(function* () { - return yield* init(yield* context) - }), - }) - - const off = registerDisposer((directory) => - Effect.runPromise(ScopedCache.invalidate(cache, directory).pipe(Effect.provide(EffectLogger.layer))), - ) - yield* Effect.addFinalizer(() => Effect.sync(off)) - - return { - [TypeId]: TypeId, - cache, - } - }) - - export const get = (self: InstanceState) => - Effect.gen(function* () { - return yield* ScopedCache.get(self.cache, yield* directory) - }) - - export const use = (self: InstanceState, select: (value: A) => B) => - Effect.map(get(self), select) - - export const useEffect = ( - self: InstanceState, - select: (value: A) => Effect.Effect, - ) => Effect.flatMap(get(self), select) - - export const has = (self: InstanceState) => - Effect.gen(function* () { - return yield* ScopedCache.has(self.cache, yield* directory) - }) - - export const invalidate = (self: InstanceState) => - Effect.gen(function* () { - return yield* ScopedCache.invalidate(self.cache, yield* directory) - }) + const fiber = Fiber.getCurrent() + const ctx = fiber ? Context.getReferenceUnsafe(fiber.context, InstanceRef) : undefined + if (!ctx) return fn + return ((...args: any[]) => Instance.restore(ctx, () => fn(...args))) as F } + +export const context = Effect.gen(function* () { + return (yield* InstanceRef) ?? Instance.current +}) + +export const workspaceID = Effect.gen(function* () { + return (yield* WorkspaceRef) ?? WorkspaceContext.workspaceID +}) + +export const directory = Effect.map(context, (ctx) => ctx.directory) + +export const make = ( + init: (ctx: InstanceContext) => Effect.Effect, +): Effect.Effect>, never, R | Scope.Scope> => + Effect.gen(function* () { + const cache = yield* ScopedCache.make({ + capacity: Number.POSITIVE_INFINITY, + lookup: () => + Effect.gen(function* () { + return yield* init(yield* context) + }), + }) + + const off = registerDisposer((directory) => + Effect.runPromise(ScopedCache.invalidate(cache, directory).pipe(Effect.provide(EffectLogger.layer))), + ) + yield* Effect.addFinalizer(() => Effect.sync(off)) + + return { + [TypeId]: TypeId, + cache, + } + }) + +export const get = (self: InstanceState) => + Effect.gen(function* () { + return yield* ScopedCache.get(self.cache, yield* directory) + }) + +export const use = (self: InstanceState, select: (value: A) => B) => + Effect.map(get(self), select) + +export const useEffect = ( + self: InstanceState, + select: (value: A) => Effect.Effect, +) => Effect.flatMap(get(self), select) + +export const has = (self: InstanceState) => + Effect.gen(function* () { + return yield* ScopedCache.has(self.cache, yield* directory) + }) + +export const invalidate = (self: InstanceState) => + Effect.gen(function* () { + return yield* ScopedCache.invalidate(self.cache, yield* directory) + }) diff --git a/packages/opencode/src/env/env.ts b/packages/opencode/src/env/env.ts index 0ffd5ebdc3..618ae32684 100644 --- a/packages/opencode/src/env/env.ts +++ b/packages/opencode/src/env/env.ts @@ -1,5 +1,5 @@ import { Context, Effect, Layer } from "effect" -import { InstanceState } from "@/effect/instance-state" +import { InstanceState } from "@/effect" type State = Record diff --git a/packages/opencode/src/file/file.ts b/packages/opencode/src/file/file.ts index a101574f61..35f2a8740a 100644 --- a/packages/opencode/src/file/file.ts +++ b/packages/opencode/src/file/file.ts @@ -1,5 +1,5 @@ import { BusEvent } from "@/bus/bus-event" -import { InstanceState } from "@/effect/instance-state" +import { InstanceState } from "@/effect" import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { Git } from "@/git" diff --git a/packages/opencode/src/file/time.ts b/packages/opencode/src/file/time.ts index 853da3bd98..86b6b4116b 100644 --- a/packages/opencode/src/file/time.ts +++ b/packages/opencode/src/file/time.ts @@ -1,5 +1,5 @@ import { DateTime, Effect, Layer, Option, Semaphore, Context } from "effect" -import { InstanceState } from "@/effect/instance-state" +import { InstanceState } from "@/effect" import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { Flag } from "@/flag/flag" import type { SessionID } from "@/session/schema" diff --git a/packages/opencode/src/file/watcher.ts b/packages/opencode/src/file/watcher.ts index 4dcec5094c..ab5942547d 100644 --- a/packages/opencode/src/file/watcher.ts +++ b/packages/opencode/src/file/watcher.ts @@ -7,7 +7,7 @@ import path from "path" import z from "zod" import { Bus } from "@/bus" import { BusEvent } from "@/bus/bus-event" -import { InstanceState } from "@/effect/instance-state" +import { InstanceState } from "@/effect" import { Flag } from "@/flag/flag" import { Git } from "@/git" import { Instance } from "@/project/instance" diff --git a/packages/opencode/src/format/format.ts b/packages/opencode/src/format/format.ts index 6df00d3db3..2ce922495e 100644 --- a/packages/opencode/src/format/format.ts +++ b/packages/opencode/src/format/format.ts @@ -1,7 +1,7 @@ import { Effect, Layer, Context } from "effect" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" -import { InstanceState } from "@/effect/instance-state" +import { InstanceState } from "@/effect" import path from "path" import { mergeDeep } from "remeda" import z from "zod" diff --git a/packages/opencode/src/lsp/index.ts b/packages/opencode/src/lsp/index.ts index 4daacd30b8..f567868f68 100644 --- a/packages/opencode/src/lsp/index.ts +++ b/packages/opencode/src/lsp/index.ts @@ -12,7 +12,7 @@ import { Flag } from "@/flag/flag" import { Process } from "../util/process" import { spawn as lspspawn } from "./launch" import { Effect, Layer, Context } from "effect" -import { InstanceState } from "@/effect/instance-state" +import { InstanceState } from "@/effect" export namespace LSP { const log = Log.create({ service: "lsp" }) diff --git a/packages/opencode/src/mcp/mcp.ts b/packages/opencode/src/mcp/mcp.ts index 947f29c05b..f5179b224d 100644 --- a/packages/opencode/src/mcp/mcp.ts +++ b/packages/opencode/src/mcp/mcp.ts @@ -25,8 +25,8 @@ import { Bus } from "@/bus" import { TuiEvent } from "@/cli/cmd/tui/event" import open from "open" import { Effect, Exit, Layer, Option, Context, Stream } from "effect" -import { EffectBridge } from "@/effect/bridge" -import { InstanceState } from "@/effect/instance-state" +import { EffectBridge } from "@/effect" +import { InstanceState } from "@/effect" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" diff --git a/packages/opencode/src/permission/permission.ts b/packages/opencode/src/permission/permission.ts index a5f6ded329..e2dead8fe2 100644 --- a/packages/opencode/src/permission/permission.ts +++ b/packages/opencode/src/permission/permission.ts @@ -1,7 +1,7 @@ import { Bus } from "@/bus" import { BusEvent } from "@/bus/bus-event" import { Config } from "@/config" -import { InstanceState } from "@/effect/instance-state" +import { InstanceState } from "@/effect" import { ProjectID } from "@/project/schema" import { MessageID, SessionID } from "@/session/schema" import { PermissionTable } from "@/session/session.sql" diff --git a/packages/opencode/src/plugin/plugin.ts b/packages/opencode/src/plugin/plugin.ts index 537794138a..23c807ebe7 100644 --- a/packages/opencode/src/plugin/plugin.ts +++ b/packages/opencode/src/plugin/plugin.ts @@ -18,8 +18,8 @@ import { gitlabAuthPlugin as GitlabAuthPlugin } from "opencode-gitlab-auth" import { PoeAuthPlugin } from "opencode-poe-auth" import { CloudflareAIGatewayAuthPlugin, CloudflareWorkersAuthPlugin } from "./cloudflare" import { Effect, Layer, Context, Stream } from "effect" -import { EffectBridge } from "@/effect/bridge" -import { InstanceState } from "@/effect/instance-state" +import { EffectBridge } from "@/effect" +import { InstanceState } from "@/effect" import { errorMessage } from "@/util/error" import { PluginLoader } from "./loader" import { parsePluginSpecifier, readPluginId, readV1Plugin, resolvePluginId } from "./shared" diff --git a/packages/opencode/src/project/vcs.ts b/packages/opencode/src/project/vcs.ts index e4093fd456..187c616602 100644 --- a/packages/opencode/src/project/vcs.ts +++ b/packages/opencode/src/project/vcs.ts @@ -3,7 +3,7 @@ import { formatPatch, structuredPatch } from "diff" import path from "path" import { Bus } from "@/bus" import { BusEvent } from "@/bus/bus-event" -import { InstanceState } from "@/effect/instance-state" +import { InstanceState } from "@/effect" import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { FileWatcher } from "@/file/watcher" import { Git } from "@/git" diff --git a/packages/opencode/src/provider/auth.ts b/packages/opencode/src/provider/auth.ts index 0f2923a587..fd71f2f7a3 100644 --- a/packages/opencode/src/provider/auth.ts +++ b/packages/opencode/src/provider/auth.ts @@ -1,7 +1,7 @@ import type { AuthOAuthResult, Hooks } from "@opencode-ai/plugin" import { NamedError } from "@opencode-ai/shared/util/error" import { Auth } from "@/auth" -import { InstanceState } from "@/effect/instance-state" +import { InstanceState } from "@/effect" import { zod } from "@/util/effect-zod" import { withStatics } from "@/util/schema" import { Plugin } from "../plugin" diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index fed4d93583..ef6cbd61e7 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -19,8 +19,8 @@ import { iife } from "@/util/iife" import { Global } from "../global" import path from "path" import { Effect, Layer, Context } from "effect" -import { EffectBridge } from "@/effect/bridge" -import { InstanceState } from "@/effect/instance-state" +import { EffectBridge } from "@/effect" +import { InstanceState } from "@/effect" import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { isRecord } from "@/util/record" diff --git a/packages/opencode/src/pty/service.ts b/packages/opencode/src/pty/service.ts index 3359d0aabf..ff52095b4f 100644 --- a/packages/opencode/src/pty/service.ts +++ b/packages/opencode/src/pty/service.ts @@ -1,6 +1,6 @@ import { BusEvent } from "@/bus/bus-event" import { Bus } from "@/bus" -import { InstanceState } from "@/effect/instance-state" +import { InstanceState } from "@/effect" import { Instance } from "@/project/instance" import type { Proc } from "#pty" import z from "zod" @@ -10,7 +10,7 @@ import { Shell } from "@/shell/shell" import { Plugin } from "@/plugin" import { PtyID } from "./schema" import { Effect, Layer, Context } from "effect" -import { EffectBridge } from "@/effect/bridge" +import { EffectBridge } from "@/effect" const log = Log.create({ service: "pty" }) diff --git a/packages/opencode/src/question/index.ts b/packages/opencode/src/question/index.ts index ba76efa640..8d023c18bf 100644 --- a/packages/opencode/src/question/index.ts +++ b/packages/opencode/src/question/index.ts @@ -1,7 +1,7 @@ import { Deferred, Effect, Layer, Schema, Context } from "effect" import { Bus } from "@/bus" import { BusEvent } from "@/bus/bus-event" -import { InstanceState } from "@/effect/instance-state" +import { InstanceState } from "@/effect" import { SessionID, MessageID } from "@/session/schema" import { zod } from "@/util/effect-zod" import { Log } from "@/util/log" diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index 644a76752d..3d39a60555 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -14,7 +14,7 @@ import { Config } from "@/config" import { NotFoundError } from "@/storage/db" import { ModelID, ProviderID } from "@/provider/schema" import { Effect, Layer, Context } from "effect" -import { InstanceState } from "@/effect/instance-state" +import { InstanceState } from "@/effect" import { isOverflow as overflow } from "./overflow" export namespace SessionCompaction { diff --git a/packages/opencode/src/session/instruction.ts b/packages/opencode/src/session/instruction.ts index 23dd88ff5a..076c81ec75 100644 --- a/packages/opencode/src/session/instruction.ts +++ b/packages/opencode/src/session/instruction.ts @@ -3,7 +3,7 @@ import path from "path" import { Effect, Layer, Context } from "effect" import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http" import { Config } from "@/config" -import { InstanceState } from "@/effect/instance-state" +import { InstanceState } from "@/effect" import { Flag } from "@/flag/flag" import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { withTransientReadRetry } from "@/util/effect-http-client" diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index 3db1c99d6b..bde36d2638 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -20,7 +20,7 @@ import { Wildcard } from "@/util/wildcard" import { SessionID } from "@/session/schema" import { Auth } from "@/auth" import { Installation } from "@/installation" -import { EffectBridge } from "@/effect/bridge" +import { EffectBridge } from "@/effect" import * as Option from "effect/Option" import * as OtelTracer from "@effect/opentelemetry/Tracer" diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index b699676897..7a74939034 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -45,10 +45,10 @@ import { decodeDataUrl } from "@/util/data-url" import { Process } from "@/util/process" import { Cause, Effect, Exit, Layer, Option, Scope, Context } from "effect" import { EffectLogger } from "@/effect/logger" -import { InstanceState } from "@/effect/instance-state" +import { InstanceState } from "@/effect" import { TaskTool, type TaskPromptOps } from "@/tool/task" import { SessionRunState } from "./run-state" -import { EffectBridge } from "@/effect/bridge" +import { EffectBridge } from "@/effect" // @ts-ignore globalThis.AI_SDK_LOG_WARNINGS = false diff --git a/packages/opencode/src/session/run-state.ts b/packages/opencode/src/session/run-state.ts index f67c726ec7..922daf1178 100644 --- a/packages/opencode/src/session/run-state.ts +++ b/packages/opencode/src/session/run-state.ts @@ -1,4 +1,4 @@ -import { InstanceState } from "@/effect/instance-state" +import { InstanceState } from "@/effect" import { Runner } from "@/effect/runner" import { Effect, Layer, Scope, Context } from "effect" import { Session } from "." diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts index 12ecd85529..0b82d8b99f 100644 --- a/packages/opencode/src/session/session.ts +++ b/packages/opencode/src/session/session.ts @@ -18,7 +18,7 @@ import { Log } from "../util/log" import { updateSchema } from "../util/update-schema" import { MessageV2 } from "./message-v2" import { Instance } from "../project/instance" -import { InstanceState } from "@/effect/instance-state" +import { InstanceState } from "@/effect" import { Snapshot } from "@/snapshot" import { ProjectID } from "../project/schema" import { WorkspaceID } from "../control-plane/schema" diff --git a/packages/opencode/src/session/status.ts b/packages/opencode/src/session/status.ts index 5800cb7322..f0d4e6cf79 100644 --- a/packages/opencode/src/session/status.ts +++ b/packages/opencode/src/session/status.ts @@ -1,6 +1,6 @@ import { BusEvent } from "@/bus/bus-event" import { Bus } from "@/bus" -import { InstanceState } from "@/effect/instance-state" +import { InstanceState } from "@/effect" import { SessionID } from "./schema" import { Effect, Layer, Context } from "effect" import z from "zod" diff --git a/packages/opencode/src/share/share-next.ts b/packages/opencode/src/share/share-next.ts index c764c20b99..9b345ac8ef 100644 --- a/packages/opencode/src/share/share-next.ts +++ b/packages/opencode/src/share/share-next.ts @@ -3,7 +3,7 @@ import { Effect, Exit, Layer, Option, Schema, Scope, Context, Stream } from "eff import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" import { Account } from "@/account" import { Bus } from "@/bus" -import { InstanceState } from "@/effect/instance-state" +import { InstanceState } from "@/effect" import { Provider } from "@/provider" import { ModelID, ProviderID } from "@/provider/schema" import { Session } from "@/session" @@ -142,7 +142,7 @@ export namespace ShareNext { }) } - const state: InstanceState = yield* InstanceState.make( + const state: InstanceState.InstanceState = yield* InstanceState.make( Effect.fn("ShareNext.state")(function* (_ctx) { const cache: State = { queue: new Map(), scope: yield* Scope.make() } diff --git a/packages/opencode/src/skill/skill.ts b/packages/opencode/src/skill/skill.ts index afc6446678..3122115cd3 100644 --- a/packages/opencode/src/skill/skill.ts +++ b/packages/opencode/src/skill/skill.ts @@ -6,7 +6,7 @@ import { Effect, Layer, Context } from "effect" import { NamedError } from "@opencode-ai/shared/util/error" import type { Agent } from "@/agent/agent" import { Bus } from "@/bus" -import { InstanceState } from "@/effect/instance-state" +import { InstanceState } from "@/effect" import { Flag } from "@/flag/flag" import { Global } from "@/global" import { Permission } from "@/permission" diff --git a/packages/opencode/src/snapshot/snapshot.ts b/packages/opencode/src/snapshot/snapshot.ts index 6624dee986..7aa3a4debf 100644 --- a/packages/opencode/src/snapshot/snapshot.ts +++ b/packages/opencode/src/snapshot/snapshot.ts @@ -4,7 +4,7 @@ import { formatPatch, structuredPatch } from "diff" import path from "path" import z from "zod" import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" -import { InstanceState } from "@/effect/instance-state" +import { InstanceState } from "@/effect" import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { Hash } from "@opencode-ai/shared/util/hash" import { Config } from "../config" diff --git a/packages/opencode/src/storage/db.ts b/packages/opencode/src/storage/db.ts index 68a41e471f..247cb347cb 100644 --- a/packages/opencode/src/storage/db.ts +++ b/packages/opencode/src/storage/db.ts @@ -12,7 +12,7 @@ import path from "path" import { readFileSync, readdirSync, existsSync } from "fs" import { Flag } from "../flag/flag" import { CHANNEL } from "../installation/meta" -import { InstanceState } from "@/effect/instance-state" +import { InstanceState } from "@/effect" import { iife } from "@/util/iife" import { init } from "#db" diff --git a/packages/opencode/src/tool/external-directory.ts b/packages/opencode/src/tool/external-directory.ts index 352cc07390..c91b698038 100644 --- a/packages/opencode/src/tool/external-directory.ts +++ b/packages/opencode/src/tool/external-directory.ts @@ -1,7 +1,7 @@ import path from "path" import { Effect } from "effect" import { EffectLogger } from "@/effect/logger" -import { InstanceState } from "@/effect/instance-state" +import { InstanceState } from "@/effect" import type { Tool } from "./tool" import { Instance } from "../project/instance" import { AppFileSystem } from "@opencode-ai/shared/filesystem" diff --git a/packages/opencode/src/tool/glob.ts b/packages/opencode/src/tool/glob.ts index 778a74ddcf..0a0a8f1e25 100644 --- a/packages/opencode/src/tool/glob.ts +++ b/packages/opencode/src/tool/glob.ts @@ -2,7 +2,7 @@ import path from "path" import z from "zod" import { Effect, Option } from "effect" import * as Stream from "effect/Stream" -import { InstanceState } from "@/effect/instance-state" +import { InstanceState } from "@/effect" import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { Ripgrep } from "../file/ripgrep" import { assertExternalDirectoryEffect } from "./external-directory" diff --git a/packages/opencode/src/tool/grep.ts b/packages/opencode/src/tool/grep.ts index 9a2bab5b2d..b6b4a063f0 100644 --- a/packages/opencode/src/tool/grep.ts +++ b/packages/opencode/src/tool/grep.ts @@ -1,7 +1,7 @@ import path from "path" import z from "zod" import { Effect, Option } from "effect" -import { InstanceState } from "@/effect/instance-state" +import { InstanceState } from "@/effect" import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { Ripgrep } from "../file/ripgrep" import { assertExternalDirectoryEffect } from "./external-directory" diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index b9870d194d..6171e4366e 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -35,7 +35,7 @@ import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" import { Ripgrep } from "../file/ripgrep" import { Format } from "../format" -import { InstanceState } from "@/effect/instance-state" +import { InstanceState } from "@/effect" import { Question } from "../question" import { Todo } from "../session/todo" import { LSP } from "../lsp" diff --git a/packages/opencode/src/worktree/worktree.ts b/packages/opencode/src/worktree/worktree.ts index 674d4d7570..fab9ce57fa 100644 --- a/packages/opencode/src/worktree/worktree.ts +++ b/packages/opencode/src/worktree/worktree.ts @@ -19,7 +19,7 @@ import { NodePath } from "@effect/platform-node" import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { BootstrapRuntime } from "@/effect/bootstrap-runtime" import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" -import { InstanceState } from "@/effect/instance-state" +import { InstanceState } from "@/effect" const log = Log.create({ service: "worktree" }) diff --git a/packages/opencode/test/effect/app-runtime-logger.test.ts b/packages/opencode/test/effect/app-runtime-logger.test.ts index 7388748f92..91f367ff3e 100644 --- a/packages/opencode/test/effect/app-runtime-logger.test.ts +++ b/packages/opencode/test/effect/app-runtime-logger.test.ts @@ -1,7 +1,7 @@ import { expect, test } from "bun:test" import { Context, Effect, Layer, Logger } from "effect" import { AppRuntime } from "../../src/effect/app-runtime" -import { EffectBridge } from "../../src/effect/bridge" +import { EffectBridge } from "../../src/effect" import { InstanceRef } from "../../src/effect/instance-ref" import { EffectLogger } from "../../src/effect/logger" import { makeRuntime } from "../../src/effect/run-service" diff --git a/packages/opencode/test/effect/instance-state.test.ts b/packages/opencode/test/effect/instance-state.test.ts index ca74c915be..50206ba84f 100644 --- a/packages/opencode/test/effect/instance-state.test.ts +++ b/packages/opencode/test/effect/instance-state.test.ts @@ -1,11 +1,11 @@ import { afterEach, expect, test } from "bun:test" import { Deferred, Duration, Effect, Exit, Fiber, Layer, ManagedRuntime, Context } from "effect" -import { InstanceState } from "../../src/effect/instance-state" +import { InstanceState } from "../../src/effect" import { InstanceRef } from "../../src/effect/instance-ref" import { Instance } from "../../src/project/instance" import { tmpdir } from "../fixture/fixture" -async function access(state: InstanceState, dir: string) { +async function access(state: InstanceState.InstanceState, dir: string) { return Instance.provide({ directory: dir, fn: () => Effect.runPromise(InstanceState.get(state)), From f6243603f8e432361a0f75a3e4705e14e88c0d4a Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Thu, 16 Apr 2026 02:46:39 +0000 Subject: [PATCH 205/300] chore: generate --- packages/opencode/src/effect/instance-state.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/opencode/src/effect/instance-state.ts b/packages/opencode/src/effect/instance-state.ts index 2a51931ada..2681d5febf 100644 --- a/packages/opencode/src/effect/instance-state.ts +++ b/packages/opencode/src/effect/instance-state.ts @@ -63,8 +63,7 @@ export const get = (self: InstanceState) => return yield* ScopedCache.get(self.cache, yield* directory) }) -export const use = (self: InstanceState, select: (value: A) => B) => - Effect.map(get(self), select) +export const use = (self: InstanceState, select: (value: A) => B) => Effect.map(get(self), select) export const useEffect = ( self: InstanceState, From 1508196c0f4f4892325accddb5affeadbc4e8574 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 22:50:22 -0400 Subject: [PATCH 206/300] feat: bridge question routes from Hono to Effect HttpApi (#22718) --- packages/opencode/src/flag/flag.ts | 1 + .../src/server/instance/httpapi/question.ts | 25 ++++++++++--- .../src/server/instance/httpapi/server.ts | 36 +++++++++---------- .../opencode/src/server/instance/index.ts | 14 ++++++-- 4 files changed, 49 insertions(+), 27 deletions(-) diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index a63f8d1c66..21923f982f 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -84,6 +84,7 @@ export namespace Flag { export const OPENCODE_STRICT_CONFIG_DEPS = truthy("OPENCODE_STRICT_CONFIG_DEPS") export const OPENCODE_WORKSPACE_ID = process.env["OPENCODE_WORKSPACE_ID"] + export const OPENCODE_EXPERIMENTAL_HTTPAPI = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_HTTPAPI") export const OPENCODE_EXPERIMENTAL_WORKSPACES = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_WORKSPACES") function number(key: string) { diff --git a/packages/opencode/src/server/instance/httpapi/question.ts b/packages/opencode/src/server/instance/httpapi/question.ts index 686c6abb17..51966d13b9 100644 --- a/packages/opencode/src/server/instance/httpapi/question.ts +++ b/packages/opencode/src/server/instance/httpapi/question.ts @@ -3,7 +3,7 @@ import { QuestionID } from "@/question/schema" import { Effect, Layer, Schema } from "effect" import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" -const root = "/experimental/httpapi/question" +const root = "/question" export const QuestionApi = HttpApi.make("question") .add( @@ -29,19 +29,29 @@ export const QuestionApi = HttpApi.make("question") description: "Provide answers to a question request from the AI assistant.", }), ), + HttpApiEndpoint.post("reject", `${root}/:requestID/reject`, { + params: { requestID: QuestionID }, + success: Schema.Boolean, + }).annotateMerge( + OpenApi.annotations({ + identifier: "question.reject", + summary: "Reject question request", + description: "Reject a question request from the AI assistant.", + }), + ), ) .annotateMerge( OpenApi.annotations({ title: "question", - description: "Experimental HttpApi question routes.", + description: "Question routes.", }), ), ) .annotateMerge( OpenApi.annotations({ - title: "opencode experimental HttpApi", + title: "opencode HttpApi", version: "0.0.1", - description: "Experimental HttpApi surface for selected instance routes.", + description: "Effect HttpApi surface for instance routes.", }), ) @@ -64,8 +74,13 @@ export const QuestionLive = Layer.unwrap( return true }) + const reject = Effect.fn("QuestionHttpApi.reject")(function* (ctx: { params: { requestID: QuestionID } }) { + yield* svc.reject(ctx.params.requestID) + return true + }) + return HttpApiBuilder.group(QuestionApi, "question", (handlers) => - handlers.handle("list", list).handle("reply", reply), + handlers.handle("list", list).handle("reply", reply).handle("reject", reject), ) }), ).pipe(Layer.provide(Question.defaultLayer)) diff --git a/packages/opencode/src/server/instance/httpapi/server.ts b/packages/opencode/src/server/instance/httpapi/server.ts index 9894343c56..2ca692efbe 100644 --- a/packages/opencode/src/server/instance/httpapi/server.ts +++ b/packages/opencode/src/server/instance/httpapi/server.ts @@ -1,17 +1,15 @@ -import { NodeHttpServer } from "@effect/platform-node" import { Effect, Layer, Redacted, Schema } from "effect" import { HttpApiBuilder, HttpApiMiddleware, HttpApiSecurity } from "effect/unstable/httpapi" -import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http" -import { createServer } from "node:http" +import { HttpRouter, HttpServer, HttpServerRequest } from "effect/unstable/http" import { AppRuntime } from "@/effect/app-runtime" import { InstanceRef, WorkspaceRef } from "@/effect/instance-ref" +import { Observability } from "@/effect/observability" +import { memoMap } from "@/effect/run-service" import { Flag } from "@/flag/flag" import { InstanceBootstrap } from "@/project/bootstrap" import { Instance } from "@/project/instance" +import { lazy } from "@/util/lazy" import { Filesystem } from "@/util/filesystem" -import { Permission } from "@/permission" -import { ProviderAuth } from "@/provider/auth" -import { Question } from "@/question" import { PermissionApi, PermissionLive } from "./permission" import { ProviderApi, ProviderLive } from "./provider" import { QuestionApi, QuestionLive } from "./question" @@ -113,26 +111,24 @@ export namespace ExperimentalHttpApiServer { const ProviderSecured = ProviderApi.middleware(Authorization) export const routes = Layer.mergeAll( - HttpApiBuilder.layer(QuestionSecured, { openapiPath: "/experimental/httpapi/question/doc" }).pipe( - Layer.provide(QuestionLive), - ), + HttpApiBuilder.layer(QuestionSecured).pipe(Layer.provide(QuestionLive)), HttpApiBuilder.layer(PermissionSecured, { openapiPath: "/experimental/httpapi/permission/doc" }).pipe( Layer.provide(PermissionLive), ), HttpApiBuilder.layer(ProviderSecured, { openapiPath: "/experimental/httpapi/provider/doc" }).pipe( Layer.provide(ProviderLive), ), - ).pipe(Layer.provide(auth), Layer.provide(normalize), Layer.provide(instance)) + ).pipe( + Layer.provide(auth), + Layer.provide(normalize), + Layer.provide(instance), + Layer.provide(HttpServer.layerServices), + Layer.provideMerge(Observability.layer), + ) - export const layer = (opts: { hostname: string; port: number }) => - HttpRouter.serve(routes, { disableListenLog: true, disableLogger: true }).pipe( - Layer.provideMerge(NodeHttpServer.layer(createServer, { port: opts.port, host: opts.hostname })), - ) - - export const layerTest = HttpRouter.serve(routes, { disableListenLog: true, disableLogger: true }).pipe( - Layer.provideMerge(NodeHttpServer.layerTest), - Layer.provideMerge(Question.defaultLayer), - Layer.provideMerge(Permission.defaultLayer), - Layer.provideMerge(ProviderAuth.defaultLayer), + export const webHandler = lazy(() => + HttpRouter.toWebHandler(routes, { + memoMap, + }), ) } diff --git a/packages/opencode/src/server/instance/index.ts b/packages/opencode/src/server/instance/index.ts index 4a03b7b29c..950b9a8588 100644 --- a/packages/opencode/src/server/instance/index.ts +++ b/packages/opencode/src/server/instance/index.ts @@ -14,6 +14,8 @@ import { LSP } from "../../lsp" import { Command } from "../../command" import { QuestionRoutes } from "./question" import { PermissionRoutes } from "./permission" +import { Flag } from "@/flag/flag" +import { ExperimentalHttpApiServer } from "./httpapi/server" import { ProjectRoutes } from "./project" import { SessionRoutes } from "./session" import { PtyRoutes } from "./pty" @@ -27,8 +29,8 @@ import { SyncRoutes } from "./sync" import { WorkspaceRouterMiddleware } from "./middleware" import { AppRuntime } from "@/effect/app-runtime" -export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => - new Hono() +export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { + const app = new Hono() .use(WorkspaceRouterMiddleware(upgrade)) .route("/project", ProjectRoutes()) .route("/pty", PtyRoutes(upgrade)) @@ -36,6 +38,13 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => .route("/experimental", ExperimentalRoutes()) .route("/session", SessionRoutes()) .route("/permission", PermissionRoutes()) + + if (Flag.OPENCODE_EXPERIMENTAL_HTTPAPI) { + const handler = ExperimentalHttpApiServer.webHandler().handler + app.all("/question", (c) => handler(c.req.raw)).all("/question/*", (c) => handler(c.req.raw)) + } + + return app .route("/question", QuestionRoutes()) .route("/provider", ProviderRoutes()) .route("/sync", SyncRoutes()) @@ -283,3 +292,4 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => return c.json(await AppRuntime.runPromise(Format.Service.use((svc) => svc.status()))) }, ) +} From 665a8430864fb7a55dedea63a6cbdbe400218f80 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 22:52:34 -0400 Subject: [PATCH 207/300] feat: unwrap Archive namespace to flat exports + barrel (#22722) --- packages/opencode/src/lsp/server.ts | 2 +- packages/opencode/src/util/archive.ts | 22 ++++++++++------------ packages/opencode/src/util/index.ts | 1 + 3 files changed, 12 insertions(+), 13 deletions(-) create mode 100644 packages/opencode/src/util/index.ts diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index f4554ae3e6..769880ef03 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -8,7 +8,7 @@ import fs from "fs/promises" import { Filesystem } from "../util/filesystem" import { Instance } from "../project/instance" import { Flag } from "../flag/flag" -import { Archive } from "../util/archive" +import { Archive } from "../util" import { Process } from "../util/process" import { which } from "../util/which" import { Module } from "@opencode-ai/shared/util/module" diff --git a/packages/opencode/src/util/archive.ts b/packages/opencode/src/util/archive.ts index f65ceba547..cf25636841 100644 --- a/packages/opencode/src/util/archive.ts +++ b/packages/opencode/src/util/archive.ts @@ -1,17 +1,15 @@ import path from "path" import { Process } from "./process" -export namespace Archive { - export async function extractZip(zipPath: string, destDir: string) { - if (process.platform === "win32") { - const winZipPath = path.resolve(zipPath) - const winDestDir = path.resolve(destDir) - // $global:ProgressPreference suppresses PowerShell's blue progress bar popup - const cmd = `$global:ProgressPreference = 'SilentlyContinue'; Expand-Archive -Path '${winZipPath}' -DestinationPath '${winDestDir}' -Force` - await Process.run(["powershell", "-NoProfile", "-NonInteractive", "-Command", cmd]) - return - } - - await Process.run(["unzip", "-o", "-q", zipPath, "-d", destDir]) +export async function extractZip(zipPath: string, destDir: string) { + if (process.platform === "win32") { + const winZipPath = path.resolve(zipPath) + const winDestDir = path.resolve(destDir) + // $global:ProgressPreference suppresses PowerShell's blue progress bar popup + const cmd = `$global:ProgressPreference = 'SilentlyContinue'; Expand-Archive -Path '${winZipPath}' -DestinationPath '${winDestDir}' -Force` + await Process.run(["powershell", "-NoProfile", "-NonInteractive", "-Command", cmd]) + return } + + await Process.run(["unzip", "-o", "-q", zipPath, "-d", destDir]) } diff --git a/packages/opencode/src/util/index.ts b/packages/opencode/src/util/index.ts new file mode 100644 index 0000000000..157bb8e521 --- /dev/null +++ b/packages/opencode/src/util/index.ts @@ -0,0 +1 @@ +export * as Archive from "./archive" From 702f7412676deae8317213a56a3b32095dba5aa4 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 22:53:10 -0400 Subject: [PATCH 208/300] feat: enable oxlint suspicious category, fix 24 violations (#22727) --- .oxlintrc.json | 22 ++++++++++++++++++- github/index.ts | 4 ++-- packages/app/src/context/global-sdk.tsx | 1 + .../app/src/utils/runtime-adapters.test.ts | 2 ++ .../workspace/[id]/billing/reload-section.tsx | 2 +- packages/desktop-electron/src/main/apps.ts | 2 +- packages/function/src/api.ts | 1 + packages/opencode/script/postinstall.mjs | 2 +- packages/opencode/src/bus/bus-event.ts | 2 +- packages/opencode/src/cli/cmd/debug/agent.ts | 1 + packages/opencode/src/cli/cmd/github.ts | 3 ++- packages/opencode/src/cli/cmd/web.ts | 2 +- packages/opencode/src/patch/patch.ts | 2 +- packages/opencode/src/server/instance/tui.ts | 2 +- packages/opencode/src/session/prompt.ts | 3 +-- packages/opencode/src/sync/sync-event.ts | 2 +- packages/opencode/src/tool/read.ts | 2 +- packages/opencode/src/tool/tool.ts | 2 +- packages/opencode/test/mcp/lifecycle.test.ts | 3 +++ packages/opencode/test/session/prompt.test.ts | 11 +++++----- 20 files changed, 49 insertions(+), 22 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index c366084ee7..37d91f4254 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -1,5 +1,8 @@ { "$schema": "https://raw.githubusercontent.com/nicolo-ribaudo/oxc-project.github.io/refs/heads/json-schema/src/public/.oxlintrc.schema.json", + "categories": { + "suspicious": "warn" + }, "rules": { // Effect uses `function*` with Effect.gen/Effect.fnUntraced that don't always yield "require-yield": "off", @@ -10,7 +13,24 @@ // Intentional control char matching (ANSI escapes, null byte sanitization) "no-control-regex": "off", // SST and plugin tools require triple-slash references - "triple-slash-reference": "off" + "triple-slash-reference": "off", + + // Suspicious category: suppress noisy rules + // Effect's nested function* closures inherently shadow outer scope + "no-shadow": "off", + // Namespace-heavy codebase makes this too noisy + "unicorn/consistent-function-scoping": "off", + // Opinionated — .sort()/.reverse() mutation is fine in this codebase + "unicorn/no-array-sort": "off", + "unicorn/no-array-reverse": "off", + // Not relevant — this isn't a DOM event handler codebase + "unicorn/prefer-add-event-listener": "off", + // Bundler handles module resolution + "unicorn/require-module-specifiers": "off", + // postMessage target origin not relevant for this codebase + "unicorn/require-post-message-target-origin": "off", + // Side-effectful constructors are intentional in some places + "no-new": "off" }, "ignorePatterns": ["**/node_modules", "**/dist", "**/.build", "**/.sst", "**/*.d.ts"] } diff --git a/github/index.ts b/github/index.ts index be8e5aafcd..4463aa2002 100644 --- a/github/index.ts +++ b/github/index.ts @@ -542,7 +542,7 @@ async function subscribeSessionEvents() { ? JSON.stringify(part.state.input) : "Unknown" console.log() - console.log(color + `|`, "\x1b[0m\x1b[2m" + ` ${tool.padEnd(7, " ")}`, "", "\x1b[0m" + title) + console.log(`${color}|`, `\x1b[0m\x1b[2m ${tool.padEnd(7, " ")}`, "", `\x1b[0m${title}`) } if (part.type === "text") { @@ -776,7 +776,7 @@ async function assertPermissions() { console.log(` permission: ${permission}`) } catch (error) { console.error(`Failed to check permissions: ${error}`) - throw new Error(`Failed to check permissions for user ${actor}: ${error}`) + throw new Error(`Failed to check permissions for user ${actor}: ${error}`, { cause: error }) } if (!["admin", "write"].includes(permission)) throw new Error(`User ${actor} does not have write permissions`) diff --git a/packages/app/src/context/global-sdk.tsx b/packages/app/src/context/global-sdk.tsx index 172b5c9664..e53d60d5a0 100644 --- a/packages/app/src/context/global-sdk.tsx +++ b/packages/app/src/context/global-sdk.tsx @@ -128,6 +128,7 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo if (started) return run started = true run = (async () => { + // oxlint-disable-next-line no-unmodified-loop-condition -- `started` is set to false by stop() which also aborts; both flags are checked to allow graceful exit while (!abort.signal.aborted && started) { attempt = new AbortController() lastEventAt = Date.now() diff --git a/packages/app/src/utils/runtime-adapters.test.ts b/packages/app/src/utils/runtime-adapters.test.ts index 9f408b8eb7..49552e179c 100644 --- a/packages/app/src/utils/runtime-adapters.test.ts +++ b/packages/app/src/utils/runtime-adapters.test.ts @@ -46,7 +46,9 @@ describe("runtime adapters", () => { }) test("resolves speech recognition constructor with webkit precedence", () => { + // oxlint-disable-next-line no-extraneous-class class SpeechCtor {} + // oxlint-disable-next-line no-extraneous-class class WebkitCtor {} const ctor = getSpeechRecognitionCtor({ SpeechRecognition: SpeechCtor, diff --git a/packages/console/app/src/routes/workspace/[id]/billing/reload-section.tsx b/packages/console/app/src/routes/workspace/[id]/billing/reload-section.tsx index 90c9d7a2e4..a25963ab07 100644 --- a/packages/console/app/src/routes/workspace/[id]/billing/reload-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/billing/reload-section.tsx @@ -90,7 +90,7 @@ export function ReloadSection() { } const info = billingInfo()! setStore("show", true) - setStore("reload", info.reload ? true : true) + setStore("reload", true) setStore("reloadAmount", info.reloadAmount.toString()) setStore("reloadTrigger", info.reloadTrigger.toString()) } diff --git a/packages/desktop-electron/src/main/apps.ts b/packages/desktop-electron/src/main/apps.ts index d21b6cc9e3..174da94a5d 100644 --- a/packages/desktop-electron/src/main/apps.ts +++ b/packages/desktop-electron/src/main/apps.ts @@ -28,7 +28,7 @@ export function wslPath(path: string, mode: "windows" | "linux" | null): string const output = execFileSync("wsl", ["-e", "wslpath", flag, path]) return output.toString().trim() } catch (error) { - throw new Error(`Failed to run wslpath: ${String(error)}`) + throw new Error(`Failed to run wslpath: ${String(error)}`, { cause: error }) } } diff --git a/packages/function/src/api.ts b/packages/function/src/api.ts index 68b2d450bb..58c74fe322 100644 --- a/packages/function/src/api.ts +++ b/packages/function/src/api.ts @@ -13,6 +13,7 @@ type Env = { } export class SyncServer extends DurableObject { + // oxlint-disable-next-line no-useless-constructor constructor(ctx: DurableObjectState, env: Env) { super(ctx, env) } diff --git a/packages/opencode/script/postinstall.mjs b/packages/opencode/script/postinstall.mjs index 2b990251ce..7dcf3958a9 100644 --- a/packages/opencode/script/postinstall.mjs +++ b/packages/opencode/script/postinstall.mjs @@ -64,7 +64,7 @@ function findBinary() { return { binaryPath, binaryName } } catch (error) { - throw new Error(`Could not find package ${packageName}: ${error.message}`) + throw new Error(`Could not find package ${packageName}: ${error.message}`, { cause: error }) } } diff --git a/packages/opencode/src/bus/bus-event.ts b/packages/opencode/src/bus/bus-event.ts index aad5f398e0..369a40ed88 100644 --- a/packages/opencode/src/bus/bus-event.ts +++ b/packages/opencode/src/bus/bus-event.ts @@ -25,7 +25,7 @@ export namespace BusEvent { properties: def.properties, }) .meta({ - ref: "Event" + "." + def.type, + ref: `Event.${def.type}`, }) }) .toArray() diff --git a/packages/opencode/src/cli/cmd/debug/agent.ts b/packages/opencode/src/cli/cmd/debug/agent.ts index 6c7ad39c1a..29d6ace598 100644 --- a/packages/opencode/src/cli/cmd/debug/agent.ts +++ b/packages/opencode/src/cli/cmd/debug/agent.ts @@ -111,6 +111,7 @@ function parseToolParams(input?: string) { } catch (evalError) { throw new Error( `Failed to parse --params. Use JSON or a JS object literal. JSON error: ${jsonError}. Eval error: ${evalError}.`, + { cause: evalError }, ) } } diff --git a/packages/opencode/src/cli/cmd/github.ts b/packages/opencode/src/cli/cmd/github.ts index 191aa2dfdf..46d091642f 100644 --- a/packages/opencode/src/cli/cmd/github.ts +++ b/packages/opencode/src/cli/cmd/github.ts @@ -1031,6 +1031,7 @@ export const GithubRunCommand = cmd({ console.error("Failed to get OIDC token:", error instanceof Error ? error.message : error) throw new Error( "Could not fetch an OIDC token. Make sure to add `id-token: write` to your workflow permissions.", + { cause: error }, ) } } @@ -1221,7 +1222,7 @@ export const GithubRunCommand = cmd({ console.log(` permission: ${permission}`) } catch (error) { console.error(`Failed to check permissions: ${error}`) - throw new Error(`Failed to check permissions for user ${actor}: ${error}`) + throw new Error(`Failed to check permissions for user ${actor}: ${error}`, { cause: error }) } if (!["admin", "write"].includes(permission)) throw new Error(`User ${actor} does not have write permissions`) diff --git a/packages/opencode/src/cli/cmd/web.ts b/packages/opencode/src/cli/cmd/web.ts index e656c83d9a..9dd8796d6e 100644 --- a/packages/opencode/src/cli/cmd/web.ts +++ b/packages/opencode/src/cli/cmd/web.ts @@ -34,7 +34,7 @@ export const WebCommand = cmd({ describe: "start opencode server and open web interface", handler: async (args) => { if (!Flag.OPENCODE_SERVER_PASSWORD) { - UI.println(UI.Style.TEXT_WARNING_BOLD + "! " + "OPENCODE_SERVER_PASSWORD is not set; server is unsecured.") + UI.println(UI.Style.TEXT_WARNING_BOLD + "! OPENCODE_SERVER_PASSWORD is not set; server is unsecured.") } const opts = await resolveNetworkOptions(args) const server = await Server.listen(opts) diff --git a/packages/opencode/src/patch/patch.ts b/packages/opencode/src/patch/patch.ts index d36ec72c72..749efd911c 100644 --- a/packages/opencode/src/patch/patch.ts +++ b/packages/opencode/src/patch/patch.ts @@ -313,7 +313,7 @@ export function deriveNewContentsFromChunks(filePath: string, chunks: UpdateFile try { originalContent = readFileSync(filePath, "utf-8") } catch (error) { - throw new Error(`Failed to read file ${filePath}: ${error}`) + throw new Error(`Failed to read file ${filePath}: ${error}`, { cause: error }) } let originalLines = originalContent.split("\n") diff --git a/packages/opencode/src/server/instance/tui.ts b/packages/opencode/src/server/instance/tui.ts index 13f150655b..0073ef98c9 100644 --- a/packages/opencode/src/server/instance/tui.ts +++ b/packages/opencode/src/server/instance/tui.ts @@ -339,7 +339,7 @@ export const TuiRoutes = lazy(() => properties: def.properties, }) .meta({ - ref: "Event" + "." + def.type, + ref: `Event.${def.type}`, }) }), ), diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 7a74939034..f04ea8cdeb 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -260,8 +260,7 @@ export namespace SessionPrompt { messageID: userMessage.info.id, sessionID: userMessage.info.sessionID, type: "text", - text: - BUILD_SWITCH + "\n\n" + `A plan file exists at ${plan}. You should execute on the plan defined within it`, + text: `${BUILD_SWITCH}\n\nA plan file exists at ${plan}. You should execute on the plan defined within it`, synthetic: true, }) userMessage.parts.push(part) diff --git a/packages/opencode/src/sync/sync-event.ts b/packages/opencode/src/sync/sync-event.ts index 2b1eb09810..bee7e3c4cf 100644 --- a/packages/opencode/src/sync/sync-event.ts +++ b/packages/opencode/src/sync/sync-event.ts @@ -273,7 +273,7 @@ export function payloads() { data: def.schema, }) .meta({ - ref: "SyncEvent" + "." + def.type, + ref: `SyncEvent.${def.type}`, }) }) .toArray() diff --git a/packages/opencode/src/tool/read.ts b/packages/opencode/src/tool/read.ts index 701bfc4b9d..4dc984d0ee 100644 --- a/packages/opencode/src/tool/read.ts +++ b/packages/opencode/src/tool/read.ts @@ -181,7 +181,7 @@ export const ReadTool = Tool.define( ) } - let output = [`${filepath}`, `file`, "" + "\n"].join("\n") + let output = [`${filepath}`, `file`, "\n"].join("\n") output += file.raw.map((line, i) => `${i + file.offset}: ${line}`).join("\n") const last = file.offset + file.raw.length - 1 diff --git a/packages/opencode/src/tool/tool.ts b/packages/opencode/src/tool/tool.ts index 30be63a320..ca25862349 100644 --- a/packages/opencode/src/tool/tool.ts +++ b/packages/opencode/src/tool/tool.ts @@ -78,7 +78,7 @@ export namespace Tool { ) { return () => Effect.gen(function* () { - const toolInfo = init instanceof Function ? { ...(yield* init()) } : { ...init } + const toolInfo = typeof init === "function" ? { ...(yield* init()) } : { ...init } const execute = toolInfo.execute toolInfo.execute = (args, ctx) => { const attrs = { diff --git a/packages/opencode/test/mcp/lifecycle.test.ts b/packages/opencode/test/mcp/lifecycle.test.ts index 09caa1cd8a..add7c66d94 100644 --- a/packages/opencode/test/mcp/lifecycle.test.ts +++ b/packages/opencode/test/mcp/lifecycle.test.ts @@ -53,6 +53,7 @@ function getOrCreateClientState(name?: string): MockClientState { class MockStdioTransport { stderr: null = null pid = 12345 + // oxlint-disable-next-line no-useless-constructor constructor(_opts: any) {} async start() { if (connectShouldHang) return new Promise(() => {}) // never resolves @@ -64,6 +65,7 @@ class MockStdioTransport { } class MockStreamableHTTP { + // oxlint-disable-next-line no-useless-constructor constructor(_url: URL, _opts?: any) {} async start() { if (connectShouldHang) return new Promise(() => {}) // never resolves @@ -76,6 +78,7 @@ class MockStreamableHTTP { } class MockSSE { + // oxlint-disable-next-line no-useless-constructor constructor(_url: URL, _opts?: any) {} async start() { if (connectShouldHang) return new Promise(() => {}) // never resolves diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index 1290570b81..4f5b19bca0 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -60,12 +60,11 @@ function chat(text: string) { function hanging(ready: () => void) { const encoder = new TextEncoder() let timer: ReturnType | undefined - const first = - `data: ${JSON.stringify({ - id: "chatcmpl-1", - object: "chat.completion.chunk", - choices: [{ delta: { role: "assistant" } }], - })}` + "\n\n" + const first = `data: ${JSON.stringify({ + id: "chatcmpl-1", + object: "chat.completion.chunk", + choices: [{ delta: { role: "assistant" } }], + })}\n\n` const rest = [ `data: ${JSON.stringify({ From b0eae5e12f271ee53e6ac269c12c8eeee7f65496 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 23:02:48 -0400 Subject: [PATCH 209/300] feat: bridge permission and provider auth routes behind OPENCODE_EXPERIMENTAL_HTTPAPI (#22736) --- packages/opencode/specs/effect/http-api.md | 82 ++++++++++++++----- .../src/server/instance/httpapi/permission.ts | 4 +- .../src/server/instance/httpapi/provider.ts | 4 +- .../src/server/instance/httpapi/question.ts | 2 +- .../src/server/instance/httpapi/server.ts | 16 ++-- .../opencode/src/server/instance/index.ts | 7 +- 6 files changed, 79 insertions(+), 36 deletions(-) diff --git a/packages/opencode/specs/effect/http-api.md b/packages/opencode/specs/effect/http-api.md index bd1213bb6d..71b50250ed 100644 --- a/packages/opencode/specs/effect/http-api.md +++ b/packages/opencode/specs/effect/http-api.md @@ -121,17 +121,46 @@ Why `question` first: Do not re-architect business logic during the HTTP migration. `HttpApi` handlers should call the same Effect services already used by the Hono handlers. -### 4. Build in parallel, do not bridge into Hono +### 4. Bridge into Hono behind a feature flag -The `HttpApi` implementation lives under `src/server/instance/httpapi/` as a standalone Effect HTTP server. It is **not mounted into the Hono app**. There is no `toWebHandler` bridge, no Hono `Handler` export, and no `.route()` call wiring it into `experimental.ts`. +The `HttpApi` routes are bridged into the Hono server via `HttpRouter.toWebHandler` with a shared `memoMap`. This means: -The standalone server (`httpapi/server.ts`) can be started independently and proves the routes work. Tests exercise it via `HttpRouter.serve` with `NodeHttpServer.layerTest`. +- one process, one port — no separate server +- the Effect handler shares layer instances with `AppRuntime` (same `Question.Service`, etc.) +- Effect middleware handles auth and instance lookup independently from Hono middleware +- Hono's `.all()` catch-all intercepts matching paths before the Hono route handlers -The goal is to build enough route coverage in the Effect server that the Hono server can eventually be replaced entirely. Until then, the two implementations exist side by side but are completely separate processes. +The bridge is gated behind `OPENCODE_EXPERIMENTAL_HTTPAPI` (or `OPENCODE_EXPERIMENTAL`). When the flag is off (default), all requests go through the original Hono handlers unchanged. -### 5. Migrate JSON route groups gradually +```ts +// in instance/index.ts +if (Flag.OPENCODE_EXPERIMENTAL_HTTPAPI) { + const handler = ExperimentalHttpApiServer.webHandler().handler + app.all("/question", (c) => handler(c.req.raw)).all("/question/*", (c) => handler(c.req.raw)) +} +``` -If the parallel slice works well, migrate additional JSON route groups one at a time. Leave streaming-style endpoints on Hono until there is a clear reason to move them. +The Hono route handlers are always registered (after the bridge) so `hono-openapi` generates the OpenAPI spec entries that feed SDK codegen. When the flag is on, these handlers are dead code — the `.all()` bridge matches first. + +### 5. Observability + +The `webHandler` provides `Observability.layer` via `Layer.provideMerge`. Since the `memoMap` is shared with `AppRuntime`, the tracing provider is deduplicated — no extra initialization cost. + +This gives: + +- **spans**: `Effect.fn("QuestionHttpApi.list")` etc. appear in traces alongside service-layer spans +- **HTTP logs**: `HttpMiddleware.logger` emits structured `Effect.log` entries with `http.method`, `http.url`, `http.status` annotations, flowing to motel via `OtlpLogger` + +### 6. Migrate JSON route groups gradually + +As each route group is ported to `HttpApi`: + +1. change its `root` path from `/experimental/httpapi/` to `/` +2. add `.all("/", handler)` / `.all("//*", handler)` to the flag block in `instance/index.ts` +3. for partial ports (e.g. only `GET /provider/auth`), bridge only the specific path +4. verify SDK output is unchanged + +Leave streaming-style endpoints on Hono until there is a clear reason to move them. ## Schema rule for HttpApi work @@ -302,36 +331,43 @@ The first slice is successful if: - OpenAPI is generated from the `HttpApi` contract - the tests are straightforward enough that the next slice feels mechanical -## Learnings from the question slice +## Learnings -The first parallel `question` spike gave us a concrete pattern to reuse. +### Schema - `Schema.Class` works well for route DTOs such as `Question.Request`, `Question.Info`, and `Question.Reply`. - scalar or collection schemas such as `Question.Answer` should stay as schemas and use helpers like `withStatics(...)` instead of being forced into classes. - if an `HttpApi` success schema uses `Schema.Class`, the handler or underlying service needs to return real schema instances rather than plain objects. - internal event payloads can stay anonymous when we want to avoid adding extra named OpenAPI component churn for non-route shapes. -- the experimental slice should stay as a standalone Effect server and keep calling the existing service layer unchanged. -- compare generated OpenAPI semantically at the route and schema level. +- `Schema.Class` emits named `$ref` in OpenAPI — only use it for types that already had `.meta({ ref })` in the old Zod schema. Inner/nested types should stay as `Schema.Struct` to avoid SDK shape changes. + +### Integration + +- `HttpRouter.toWebHandler` with the shared `memoMap` from `run-service.ts` cleanly bridges Effect routes into Hono — one process, one port, shared layer instances. +- `Observability.layer` must be explicitly provided via `Layer.provideMerge` in the routes layer for OTEL spans and HTTP logs to flow. The `memoMap` deduplicates it with `AppRuntime` — no extra cost. +- `HttpMiddleware.logger` (enabled by default when `disableLogger` is not set) emits structured `Effect.log` entries with `http.method`, `http.url`, `http.status` — these flow through `OtlpLogger` to motel. +- Hono OpenAPI stubs must remain registered for SDK codegen until the SDK pipeline reads from the Effect OpenAPI spec instead. +- the `OPENCODE_EXPERIMENTAL_HTTPAPI` flag gates the bridge at the Hono router level — default off, no behavior change unless opted in. ## Route inventory Status legend: -- `done` - parallel `HttpApi` slice exists +- `bridged` - Effect HttpApi slice exists and is bridged into Hono behind the flag +- `done` - Effect HttpApi slice exists but not yet bridged - `next` - good near-term candidate - `later` - possible, but not first wave - `defer` - not a good early `HttpApi` target Current instance route inventory: -- `question` - `done` - endpoints in slice: `GET /question`, `POST /question/:requestID/reply` -- `permission` - `done` - endpoints in slice: `GET /permission`, `POST /permission/:requestID/reply` -- `provider` - `next` - best next endpoint: `GET /provider/auth` - later endpoint: `GET /provider` - defer first-wave OAuth mutations +- `question` - `bridged` + endpoints: `GET /question`, `POST /question/:requestID/reply`, `POST /question/:requestID/reject` +- `permission` - `bridged` + endpoints: `GET /permission`, `POST /permission/:requestID/reply` +- `provider` - `bridged` (partial) + bridged endpoint: `GET /provider/auth` + not yet ported: `GET /provider`, OAuth mutations - `config` - `next` best next endpoint: `GET /config/providers` later endpoint: `GET /config` @@ -371,7 +407,13 @@ Recommended near-term sequence after the first spike: - [x] keep the underlying service calls identical to the current handlers - [x] compare generated OpenAPI against the current Hono/OpenAPI setup - [x] document how auth, instance lookup, and error mapping would compose in the new stack -- [ ] decide after the spike whether `HttpApi` should stay parallel, replace only some groups, or become the long-term default +- [x] bridge Effect routes into Hono via `toWebHandler` with shared `memoMap` +- [x] gate behind `OPENCODE_EXPERIMENTAL_HTTPAPI` flag +- [x] verify OTEL spans and HTTP logs flow to motel +- [x] bridge question, permission, and provider auth routes +- [ ] port remaining provider endpoints (`GET /provider`, OAuth mutations) +- [ ] port `config` read endpoints +- [ ] decide when to remove the flag and make Effect routes the default ## Rule of thumb diff --git a/packages/opencode/src/server/instance/httpapi/permission.ts b/packages/opencode/src/server/instance/httpapi/permission.ts index e3d152c5a4..ed8cb4e277 100644 --- a/packages/opencode/src/server/instance/httpapi/permission.ts +++ b/packages/opencode/src/server/instance/httpapi/permission.ts @@ -3,7 +3,7 @@ import { PermissionID } from "@/permission/schema" import { Effect, Layer, Schema } from "effect" import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" -const root = "/experimental/httpapi/permission" +const root = "/permission" export const PermissionApi = HttpApi.make("permission") .add( @@ -45,7 +45,7 @@ export const PermissionApi = HttpApi.make("permission") }), ) -export const PermissionLive = Layer.unwrap( +export const permissionHandlers = Layer.unwrap( Effect.gen(function* () { const svc = yield* Permission.Service diff --git a/packages/opencode/src/server/instance/httpapi/provider.ts b/packages/opencode/src/server/instance/httpapi/provider.ts index 23e2d1ea73..e59f23f123 100644 --- a/packages/opencode/src/server/instance/httpapi/provider.ts +++ b/packages/opencode/src/server/instance/httpapi/provider.ts @@ -2,7 +2,7 @@ import { ProviderAuth } from "@/provider/auth" import { Effect, Layer } from "effect" import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" -const root = "/experimental/httpapi/provider" +const root = "/provider" export const ProviderApi = HttpApi.make("provider") .add( @@ -33,7 +33,7 @@ export const ProviderApi = HttpApi.make("provider") }), ) -export const ProviderLive = Layer.unwrap( +export const providerHandlers = Layer.unwrap( Effect.gen(function* () { const svc = yield* ProviderAuth.Service diff --git a/packages/opencode/src/server/instance/httpapi/question.ts b/packages/opencode/src/server/instance/httpapi/question.ts index 51966d13b9..3192b530e9 100644 --- a/packages/opencode/src/server/instance/httpapi/question.ts +++ b/packages/opencode/src/server/instance/httpapi/question.ts @@ -55,7 +55,7 @@ export const QuestionApi = HttpApi.make("question") }), ) -export const QuestionLive = Layer.unwrap( +export const questionHandlers = Layer.unwrap( Effect.gen(function* () { const svc = yield* Question.Service diff --git a/packages/opencode/src/server/instance/httpapi/server.ts b/packages/opencode/src/server/instance/httpapi/server.ts index 2ca692efbe..9ecefe2915 100644 --- a/packages/opencode/src/server/instance/httpapi/server.ts +++ b/packages/opencode/src/server/instance/httpapi/server.ts @@ -10,9 +10,9 @@ import { InstanceBootstrap } from "@/project/bootstrap" import { Instance } from "@/project/instance" import { lazy } from "@/util/lazy" import { Filesystem } from "@/util/filesystem" -import { PermissionApi, PermissionLive } from "./permission" -import { ProviderApi, ProviderLive } from "./provider" -import { QuestionApi, QuestionLive } from "./question" +import { PermissionApi, permissionHandlers } from "./permission" +import { ProviderApi, providerHandlers } from "./provider" +import { QuestionApi, questionHandlers } from "./question" const Query = Schema.Struct({ directory: Schema.optional(Schema.String), @@ -111,13 +111,9 @@ export namespace ExperimentalHttpApiServer { const ProviderSecured = ProviderApi.middleware(Authorization) export const routes = Layer.mergeAll( - HttpApiBuilder.layer(QuestionSecured).pipe(Layer.provide(QuestionLive)), - HttpApiBuilder.layer(PermissionSecured, { openapiPath: "/experimental/httpapi/permission/doc" }).pipe( - Layer.provide(PermissionLive), - ), - HttpApiBuilder.layer(ProviderSecured, { openapiPath: "/experimental/httpapi/provider/doc" }).pipe( - Layer.provide(ProviderLive), - ), + HttpApiBuilder.layer(QuestionSecured).pipe(Layer.provide(questionHandlers)), + HttpApiBuilder.layer(PermissionSecured).pipe(Layer.provide(permissionHandlers)), + HttpApiBuilder.layer(ProviderSecured).pipe(Layer.provide(providerHandlers)), ).pipe( Layer.provide(auth), Layer.provide(normalize), diff --git a/packages/opencode/src/server/instance/index.ts b/packages/opencode/src/server/instance/index.ts index 950b9a8588..874790f1cc 100644 --- a/packages/opencode/src/server/instance/index.ts +++ b/packages/opencode/src/server/instance/index.ts @@ -41,7 +41,12 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { if (Flag.OPENCODE_EXPERIMENTAL_HTTPAPI) { const handler = ExperimentalHttpApiServer.webHandler().handler - app.all("/question", (c) => handler(c.req.raw)).all("/question/*", (c) => handler(c.req.raw)) + app + .all("/question", (c) => handler(c.req.raw)) + .all("/question/*", (c) => handler(c.req.raw)) + .all("/permission", (c) => handler(c.req.raw)) + .all("/permission/*", (c) => handler(c.req.raw)) + .all("/provider/auth", (c) => handler(c.req.raw)) } return app From 343a564183d3c1aa3fc4f46896c2350bda2d2058 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 23:15:58 -0400 Subject: [PATCH 210/300] feat: unwrap 11 util namespaces to flat exports + barrel (#22739) --- packages/opencode/src/acp/agent.ts | 4 +- packages/opencode/src/acp/session.ts | 2 +- packages/opencode/src/bus/bus.ts | 2 +- packages/opencode/src/cli/cmd/acp.ts | 2 +- packages/opencode/src/cli/cmd/agent.ts | 2 +- packages/opencode/src/cli/cmd/debug/lsp.ts | 2 +- packages/opencode/src/cli/cmd/debug/scrap.ts | 2 +- packages/opencode/src/cli/cmd/github.ts | 4 +- packages/opencode/src/cli/cmd/import.ts | 2 +- packages/opencode/src/cli/cmd/mcp.ts | 2 +- packages/opencode/src/cli/cmd/plug.ts | 4 +- packages/opencode/src/cli/cmd/pr.ts | 2 +- packages/opencode/src/cli/cmd/providers.ts | 2 +- packages/opencode/src/cli/cmd/run.ts | 4 +- packages/opencode/src/cli/cmd/session.ts | 6 +- .../src/cli/cmd/tui/component/dialog-mcp.tsx | 2 +- .../cmd/tui/component/dialog-session-list.tsx | 4 +- .../cli/cmd/tui/component/dialog-stash.tsx | 2 +- .../cmd/tui/component/prompt/autocomplete.tsx | 2 +- .../cli/cmd/tui/component/prompt/frecency.tsx | 2 +- .../cli/cmd/tui/component/prompt/history.tsx | 2 +- .../cli/cmd/tui/component/prompt/index.tsx | 4 +- .../cli/cmd/tui/component/prompt/stash.tsx | 2 +- .../cmd/tui/component/textarea-keybindings.ts | 2 +- .../src/cli/cmd/tui/context/keybind.tsx | 2 +- .../opencode/src/cli/cmd/tui/context/kv.tsx | 2 +- .../src/cli/cmd/tui/context/local.tsx | 2 +- .../opencode/src/cli/cmd/tui/context/sync.tsx | 2 +- .../src/cli/cmd/tui/context/theme.tsx | 2 +- .../tui/feature-plugins/system/plugins.tsx | 2 +- .../src/cli/cmd/tui/plugin/runtime.ts | 6 +- .../session/dialog-fork-from-timeline.tsx | 2 +- .../tui/routes/session/dialog-timeline.tsx | 2 +- .../src/cli/cmd/tui/routes/session/index.tsx | 4 +- .../cli/cmd/tui/routes/session/permission.tsx | 4 +- .../tui/routes/session/subagent-footer.tsx | 2 +- packages/opencode/src/cli/cmd/tui/thread.ts | 6 +- .../src/cli/cmd/tui/ui/dialog-confirm.tsx | 2 +- .../src/cli/cmd/tui/ui/dialog-select.tsx | 4 +- .../src/cli/cmd/tui/util/clipboard.ts | 4 +- .../opencode/src/cli/cmd/tui/util/editor.ts | 4 +- .../opencode/src/cli/cmd/tui/util/sound.ts | 2 +- .../src/cli/cmd/tui/util/transcript.ts | 2 +- packages/opencode/src/cli/cmd/tui/worker.ts | 4 +- packages/opencode/src/cli/cmd/uninstall.ts | 4 +- packages/opencode/src/cli/heap.ts | 2 +- packages/opencode/src/config/config.ts | 4 +- packages/opencode/src/config/markdown.ts | 2 +- packages/opencode/src/config/paths.ts | 2 +- packages/opencode/src/config/tui-migrate.ts | 4 +- packages/opencode/src/config/tui.ts | 2 +- .../src/control-plane/workspace-context.ts | 2 +- .../opencode/src/control-plane/workspace.ts | 4 +- packages/opencode/src/effect/bridge.ts | 2 +- .../opencode/src/effect/instance-state.ts | 2 +- packages/opencode/src/effect/logger.ts | 2 +- packages/opencode/src/effect/run-service.ts | 2 +- packages/opencode/src/file/file.ts | 2 +- packages/opencode/src/file/ripgrep.ts | 4 +- packages/opencode/src/file/time.ts | 2 +- packages/opencode/src/file/watcher.ts | 2 +- packages/opencode/src/format/format.ts | 2 +- packages/opencode/src/format/formatter.ts | 4 +- packages/opencode/src/global/global.ts | 2 +- packages/opencode/src/ide/ide.ts | 4 +- packages/opencode/src/index.ts | 4 +- .../opencode/src/installation/installation.ts | 2 +- packages/opencode/src/lsp/client.ts | 6 +- packages/opencode/src/lsp/index.ts | 4 +- packages/opencode/src/lsp/launch.ts | 2 +- packages/opencode/src/lsp/server.ts | 6 +- packages/opencode/src/mcp/mcp.ts | 2 +- packages/opencode/src/mcp/oauth-callback.ts | 2 +- packages/opencode/src/mcp/oauth-provider.ts | 2 +- packages/opencode/src/node.ts | 2 +- packages/opencode/src/npm/npm.ts | 4 +- packages/opencode/src/patch/patch.ts | 2 +- packages/opencode/src/permission/evaluate.ts | 2 +- .../opencode/src/permission/permission.ts | 4 +- packages/opencode/src/plugin/codex.ts | 2 +- .../src/plugin/github-copilot/copilot.ts | 2 +- packages/opencode/src/plugin/install.ts | 2 +- packages/opencode/src/plugin/meta.ts | 2 +- packages/opencode/src/plugin/plugin.ts | 2 +- packages/opencode/src/plugin/shared.ts | 2 +- packages/opencode/src/project/bootstrap.ts | 2 +- packages/opencode/src/project/instance.ts | 4 +- packages/opencode/src/project/project.ts | 2 +- packages/opencode/src/project/vcs.ts | 2 +- packages/opencode/src/provider/models.ts | 4 +- packages/opencode/src/provider/provider.ts | 2 +- packages/opencode/src/pty/service.ts | 2 +- packages/opencode/src/question/index.ts | 2 +- packages/opencode/src/server/control/index.ts | 2 +- packages/opencode/src/server/fence.ts | 2 +- .../opencode/src/server/instance/event.ts | 2 +- .../opencode/src/server/instance/global.ts | 2 +- .../src/server/instance/httpapi/server.ts | 2 +- .../src/server/instance/middleware.ts | 2 +- .../opencode/src/server/instance/session.ts | 2 +- packages/opencode/src/server/instance/sync.ts | 2 +- .../opencode/src/server/instance/workspace.ts | 2 +- packages/opencode/src/server/mdns.ts | 2 +- packages/opencode/src/server/middleware.ts | 2 +- packages/opencode/src/server/proxy.ts | 2 +- packages/opencode/src/server/server.ts | 2 +- packages/opencode/src/session/compaction.ts | 4 +- packages/opencode/src/session/instruction.ts | 2 +- packages/opencode/src/session/llm.ts | 4 +- packages/opencode/src/session/processor.ts | 2 +- packages/opencode/src/session/projectors.ts | 2 +- packages/opencode/src/session/prompt.ts | 4 +- packages/opencode/src/session/revert.ts | 2 +- packages/opencode/src/session/session.ts | 2 +- packages/opencode/src/share/share-next.ts | 2 +- packages/opencode/src/shell/shell.ts | 2 +- packages/opencode/src/skill/discovery.ts | 2 +- packages/opencode/src/skill/skill.ts | 2 +- packages/opencode/src/snapshot/snapshot.ts | 2 +- packages/opencode/src/storage/db.ts | 4 +- .../opencode/src/storage/json-migration.ts | 4 +- packages/opencode/src/storage/storage.ts | 2 +- packages/opencode/src/tool/bash.ts | 2 +- packages/opencode/src/tool/registry.ts | 2 +- packages/opencode/src/tool/truncate.ts | 2 +- packages/opencode/src/util/archive.ts | 2 +- packages/opencode/src/util/color.ts | 34 +- packages/opencode/src/util/filesystem.ts | 450 +++++++++--------- packages/opencode/src/util/index.ts | 11 + packages/opencode/src/util/keybind.ts | 190 ++++---- packages/opencode/src/util/local-context.ts | 40 +- packages/opencode/src/util/locale.ts | 148 +++--- packages/opencode/src/util/lock.ts | 156 +++--- packages/opencode/src/util/log.ts | 346 +++++++------- packages/opencode/src/util/process.ts | 324 +++++++------ packages/opencode/src/util/rpc.ts | 122 +++-- packages/opencode/src/util/token.ts | 8 +- packages/opencode/src/util/wildcard.ts | 104 ++-- packages/opencode/src/worktree/worktree.ts | 2 +- .../test/cli/tui/plugin-loader.test.ts | 2 +- packages/opencode/test/cli/tui/thread.test.ts | 2 +- .../opencode/test/config/agent-color.test.ts | 2 +- packages/opencode/test/config/config.test.ts | 2 +- packages/opencode/test/config/tui.test.ts | 2 +- packages/opencode/test/file/index.test.ts | 2 +- .../opencode/test/file/path-traversal.test.ts | 2 +- packages/opencode/test/file/time.test.ts | 2 +- packages/opencode/test/fixture/plug-worker.ts | 2 +- packages/opencode/test/keybind.test.ts | 2 +- packages/opencode/test/lsp/client.test.ts | 2 +- .../test/plugin/install-concurrency.test.ts | 4 +- packages/opencode/test/plugin/install.test.ts | 2 +- .../test/plugin/loader-shared.test.ts | 2 +- packages/opencode/test/plugin/meta.test.ts | 4 +- packages/opencode/test/preload.ts | 2 +- .../test/project/migrate-global.test.ts | 2 +- .../opencode/test/project/project.test.ts | 2 +- .../test/provider/amazon-bedrock.test.ts | 2 +- .../opencode/test/provider/provider.test.ts | 2 +- .../test/server/global-session-list.test.ts | 2 +- .../test/server/project-init-git.test.ts | 4 +- .../test/server/session-actions.test.ts | 2 +- .../opencode/test/server/session-list.test.ts | 2 +- .../test/server/session-messages.test.ts | 2 +- .../test/server/session-select.test.ts | 2 +- .../opencode/test/session/compaction.test.ts | 4 +- packages/opencode/test/session/llm.test.ts | 2 +- .../test/session/messages-pagination.test.ts | 2 +- .../test/session/processor-effect.test.ts | 2 +- .../test/session/prompt-effect.test.ts | 2 +- packages/opencode/test/session/prompt.test.ts | 2 +- .../test/session/revert-compact.test.ts | 2 +- .../opencode/test/session/session.test.ts | 2 +- .../test/session/snapshot-tool-race.test.ts | 2 +- .../structured-output-integration.test.ts | 2 +- packages/opencode/test/shell/shell.test.ts | 2 +- .../opencode/test/skill/discovery.test.ts | 2 +- .../opencode/test/snapshot/snapshot.test.ts | 2 +- packages/opencode/test/tool/bash.test.ts | 2 +- .../test/tool/external-directory.test.ts | 2 +- packages/opencode/test/tool/read.test.ts | 2 +- .../opencode/test/tool/truncation.test.ts | 4 +- .../opencode/test/util/filesystem.test.ts | 2 +- packages/opencode/test/util/lock.test.ts | 2 +- packages/opencode/test/util/log.test.ts | 2 +- packages/opencode/test/util/module.test.ts | 2 +- packages/opencode/test/util/process.test.ts | 2 +- packages/opencode/test/util/wildcard.test.ts | 2 +- 188 files changed, 1182 insertions(+), 1193 deletions(-) diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index 5f0bcdc24b..669462772d 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -31,9 +31,9 @@ import { type Usage, } from "@agentclientprotocol/sdk" -import { Log } from "../util/log" +import { Log } from "../util" import { pathToFileURL } from "url" -import { Filesystem } from "../util/filesystem" +import { Filesystem } from "../util" import { Hash } from "@opencode-ai/shared/util/hash" import { ACPSessionManager } from "./session" import type { ACPConfig } from "./types" diff --git a/packages/opencode/src/acp/session.ts b/packages/opencode/src/acp/session.ts index b96ebc1c89..523b037374 100644 --- a/packages/opencode/src/acp/session.ts +++ b/packages/opencode/src/acp/session.ts @@ -1,6 +1,6 @@ import { RequestError, type McpServer } from "@agentclientprotocol/sdk" import type { ACPSessionState } from "./types" -import { Log } from "@/util/log" +import { Log } from "@/util" import type { OpencodeClient } from "@opencode-ai/sdk/v2" const log = Log.create({ service: "acp-session-manager" }) diff --git a/packages/opencode/src/bus/bus.ts b/packages/opencode/src/bus/bus.ts index 12d7f246cd..beac809925 100644 --- a/packages/opencode/src/bus/bus.ts +++ b/packages/opencode/src/bus/bus.ts @@ -1,7 +1,7 @@ import z from "zod" import { Effect, Exit, Layer, PubSub, Scope, Context, Stream } from "effect" import { EffectBridge } from "@/effect" -import { Log } from "../util/log" +import { Log } from "../util" import { BusEvent } from "./bus-event" import { GlobalBus } from "./global" import { InstanceState } from "@/effect" diff --git a/packages/opencode/src/cli/cmd/acp.ts b/packages/opencode/src/cli/cmd/acp.ts index 2fb9038b0f..8141adc4f7 100644 --- a/packages/opencode/src/cli/cmd/acp.ts +++ b/packages/opencode/src/cli/cmd/acp.ts @@ -1,4 +1,4 @@ -import { Log } from "@/util/log" +import { Log } from "@/util" import { bootstrap } from "../bootstrap" import { cmd } from "./cmd" import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk" diff --git a/packages/opencode/src/cli/cmd/agent.ts b/packages/opencode/src/cli/cmd/agent.ts index 0e93946a23..fd559935fc 100644 --- a/packages/opencode/src/cli/cmd/agent.ts +++ b/packages/opencode/src/cli/cmd/agent.ts @@ -7,7 +7,7 @@ import { Agent } from "../../agent/agent" import { Provider } from "../../provider" import path from "path" import fs from "fs/promises" -import { Filesystem } from "../../util/filesystem" +import { Filesystem } from "../../util" import matter from "gray-matter" import { Instance } from "../../project/instance" import { EOL } from "os" diff --git a/packages/opencode/src/cli/cmd/debug/lsp.ts b/packages/opencode/src/cli/cmd/debug/lsp.ts index 18f67b3917..185cab9c75 100644 --- a/packages/opencode/src/cli/cmd/debug/lsp.ts +++ b/packages/opencode/src/cli/cmd/debug/lsp.ts @@ -3,7 +3,7 @@ import { AppRuntime } from "../../../effect/app-runtime" import { Effect } from "effect" import { bootstrap } from "../../bootstrap" import { cmd } from "../cmd" -import { Log } from "../../../util/log" +import { Log } from "../../../util" import { EOL } from "os" export const LSPCommand = cmd({ diff --git a/packages/opencode/src/cli/cmd/debug/scrap.ts b/packages/opencode/src/cli/cmd/debug/scrap.ts index f4b96e883a..464b165d72 100644 --- a/packages/opencode/src/cli/cmd/debug/scrap.ts +++ b/packages/opencode/src/cli/cmd/debug/scrap.ts @@ -1,6 +1,6 @@ import { EOL } from "os" import { Project } from "../../../project/project" -import { Log } from "../../../util/log" +import { Log } from "../../../util" import { cmd } from "../cmd" export const ScrapCommand = cmd({ diff --git a/packages/opencode/src/cli/cmd/github.ts b/packages/opencode/src/cli/cmd/github.ts index 46d091642f..d7863c5486 100644 --- a/packages/opencode/src/cli/cmd/github.ts +++ b/packages/opencode/src/cli/cmd/github.ts @@ -1,6 +1,6 @@ import path from "path" import { exec } from "child_process" -import { Filesystem } from "../../util/filesystem" +import { Filesystem } from "../../util" import * as prompts from "@clack/prompts" import { map, pipe, sortBy, values } from "remeda" import { Octokit } from "@octokit/rest" @@ -32,7 +32,7 @@ import { SessionPrompt } from "@/session/prompt" import { AppRuntime } from "@/effect/app-runtime" import { Git } from "@/git" import { setTimeout as sleep } from "node:timers/promises" -import { Process } from "@/util/process" +import { Process } from "@/util" import { Effect } from "effect" type GitHubAuthor = { diff --git a/packages/opencode/src/cli/cmd/import.ts b/packages/opencode/src/cli/cmd/import.ts index 1232f07422..bb8a1f63f3 100644 --- a/packages/opencode/src/cli/cmd/import.ts +++ b/packages/opencode/src/cli/cmd/import.ts @@ -9,7 +9,7 @@ import { SessionTable, MessageTable, PartTable } from "../../session/session.sql import { Instance } from "../../project/instance" import { ShareNext } from "../../share/share-next" import { EOL } from "os" -import { Filesystem } from "../../util/filesystem" +import { Filesystem } from "../../util" import { AppRuntime } from "@/effect/app-runtime" /** Discriminated union returned by the ShareNext API (GET /api/shares/:id/data) */ diff --git a/packages/opencode/src/cli/cmd/mcp.ts b/packages/opencode/src/cli/cmd/mcp.ts index b9e4b04219..06c03d9f49 100644 --- a/packages/opencode/src/cli/cmd/mcp.ts +++ b/packages/opencode/src/cli/cmd/mcp.ts @@ -13,7 +13,7 @@ import { Installation } from "../../installation" import path from "path" import { Global } from "../../global" import { modify, applyEdits } from "jsonc-parser" -import { Filesystem } from "../../util/filesystem" +import { Filesystem } from "../../util" import { Bus } from "../../bus" import { AppRuntime } from "../../effect/app-runtime" import { Effect } from "effect" diff --git a/packages/opencode/src/cli/cmd/plug.ts b/packages/opencode/src/cli/cmd/plug.ts index 692c556b24..42d06ff47f 100644 --- a/packages/opencode/src/cli/cmd/plug.ts +++ b/packages/opencode/src/cli/cmd/plug.ts @@ -7,8 +7,8 @@ import { installPlugin, patchPluginConfig, readPluginManifest } from "../../plug import { resolvePluginTarget } from "../../plugin/shared" import { Instance } from "../../project/instance" import { errorMessage } from "../../util/error" -import { Filesystem } from "../../util/filesystem" -import { Process } from "../../util/process" +import { Filesystem } from "../../util" +import { Process } from "../../util" import { UI } from "../ui" import { cmd } from "./cmd" diff --git a/packages/opencode/src/cli/cmd/pr.ts b/packages/opencode/src/cli/cmd/pr.ts index f392bab4c8..6141ef90a8 100644 --- a/packages/opencode/src/cli/cmd/pr.ts +++ b/packages/opencode/src/cli/cmd/pr.ts @@ -3,7 +3,7 @@ import { cmd } from "./cmd" import { AppRuntime } from "@/effect/app-runtime" import { Git } from "@/git" import { Instance } from "@/project/instance" -import { Process } from "@/util/process" +import { Process } from "@/util" export const PrCommand = cmd({ command: "pr ", diff --git a/packages/opencode/src/cli/cmd/providers.ts b/packages/opencode/src/cli/cmd/providers.ts index 5b7f5a1a0d..47a5c37e85 100644 --- a/packages/opencode/src/cli/cmd/providers.ts +++ b/packages/opencode/src/cli/cmd/providers.ts @@ -12,7 +12,7 @@ import { Global } from "../../global" import { Plugin } from "../../plugin" import { Instance } from "../../project/instance" import type { Hooks } from "@opencode-ai/plugin" -import { Process } from "../../util/process" +import { Process } from "../../util" import { text } from "node:stream/consumers" import { Effect } from "effect" diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index e94ba5d119..da72372370 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -6,7 +6,7 @@ import { cmd } from "./cmd" import { Flag } from "../../flag/flag" import { bootstrap } from "../bootstrap" import { EOL } from "os" -import { Filesystem } from "../../util/filesystem" +import { Filesystem } from "../../util" import { createOpencodeClient, type OpencodeClient, type ToolPart } from "@opencode-ai/sdk/v2" import { Server } from "../../server/server" import { Provider } from "../../provider" @@ -25,7 +25,7 @@ import { TaskTool } from "../../tool/task" import { SkillTool } from "../../tool/skill" import { BashTool } from "../../tool/bash" import { TodoWriteTool } from "../../tool/todo" -import { Locale } from "../../util/locale" +import { Locale } from "../../util" import { AppRuntime } from "@/effect/app-runtime" type ToolProps = { diff --git a/packages/opencode/src/cli/cmd/session.ts b/packages/opencode/src/cli/cmd/session.ts index 6f79e726fa..8537a74d45 100644 --- a/packages/opencode/src/cli/cmd/session.ts +++ b/packages/opencode/src/cli/cmd/session.ts @@ -4,10 +4,10 @@ import { Session } from "../../session" import { SessionID } from "../../session/schema" import { bootstrap } from "../bootstrap" import { UI } from "../ui" -import { Locale } from "../../util/locale" +import { Locale } from "../../util" import { Flag } from "../../flag/flag" -import { Filesystem } from "../../util/filesystem" -import { Process } from "../../util/process" +import { Filesystem } from "../../util" +import { Process } from "../../util" import { EOL } from "os" import path from "path" import { which } from "../../util/which" diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-mcp.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-mcp.tsx index 173c5ff60c..e3e80c0fda 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-mcp.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-mcp.tsx @@ -4,7 +4,7 @@ import { useSync } from "@tui/context/sync" import { map, pipe, entries, sortBy } from "remeda" import { DialogSelect, type DialogSelectRef, type DialogSelectOption } from "@tui/ui/dialog-select" import { useTheme } from "../context/theme" -import { Keybind } from "@/util/keybind" +import { Keybind } from "@/util" import { TextAttributes } from "@opentui/core" import { useSDK } from "@tui/context/sdk" diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx index 9ecb21e82a..a42755bee7 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx @@ -3,14 +3,14 @@ import { DialogSelect } from "@tui/ui/dialog-select" import { useRoute } from "@tui/context/route" import { useSync } from "@tui/context/sync" import { createMemo, createResource, createSignal, onMount } from "solid-js" -import { Locale } from "@/util/locale" +import { Locale } from "@/util" import { useProject } from "@tui/context/project" import { useKeybind } from "../context/keybind" import { useTheme } from "../context/theme" import { useSDK } from "../context/sdk" import { Flag } from "@/flag/flag" import { DialogSessionRename } from "./dialog-session-rename" -import { Keybind } from "@/util/keybind" +import { Keybind } from "@/util" import { createDebouncedSignal } from "../util/signal" import { useToast } from "../ui/toast" import { DialogWorkspaceCreate, openWorkspaceSession } from "./dialog-workspace-create" diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-stash.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-stash.tsx index e8664f6289..8a6e69145d 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-stash.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-stash.tsx @@ -1,7 +1,7 @@ import { useDialog } from "@tui/ui/dialog" import { DialogSelect } from "@tui/ui/dialog-select" import { createMemo, createSignal } from "solid-js" -import { Locale } from "@/util/locale" +import { Locale } from "@/util" import { useTheme } from "../context/theme" import { useKeybind } from "../context/keybind" import { usePromptStash, type StashEntry } from "./prompt/stash" diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index 7ca73310bc..305d076223 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -12,7 +12,7 @@ import { useTheme, selectedForeground } from "@tui/context/theme" import { SplitBorder } from "@tui/component/border" import { useCommandDialog } from "@tui/component/dialog-command" import { useTerminalDimensions } from "@opentui/solid" -import { Locale } from "@/util/locale" +import { Locale } from "@/util" import type { PromptInfo } from "./history" import { useFrecency } from "./frecency" diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/frecency.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/frecency.tsx index 3ea8826ef8..929f3a07da 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/frecency.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/frecency.tsx @@ -1,6 +1,6 @@ import path from "path" import { Global } from "@/global" -import { Filesystem } from "@/util/filesystem" +import { Filesystem } from "@/util" import { onMount } from "solid-js" import { createStore } from "solid-js/store" import { createSimpleContext } from "../../context/helper" diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx index d49dd5c7b6..03db74de94 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx @@ -1,6 +1,6 @@ import path from "path" import { Global } from "@/global" -import { Filesystem } from "@/util/filesystem" +import { Filesystem } from "@/util" import { onMount } from "solid-js" import { createStore, produce, unwrap } from "solid-js/store" import { createSimpleContext } from "../../context/helper" diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 87440d0e24..c361e48c9e 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -3,7 +3,7 @@ import { createEffect, createMemo, onMount, createSignal, onCleanup, on, Show, S import "opentui-spinner/solid" import path from "path" import { fileURLToPath } from "url" -import { Filesystem } from "@/util/filesystem" +import { Filesystem } from "@/util" import { useLocal } from "@tui/context/local" import { useTheme } from "@tui/context/theme" import { EmptyBorder, SplitBorder } from "@tui/component/border" @@ -27,7 +27,7 @@ import { Clipboard } from "../../util/clipboard" import type { AssistantMessage, FilePart, UserMessage } from "@opencode-ai/sdk/v2" import { TuiEvent } from "../../event" import { iife } from "@/util/iife" -import { Locale } from "@/util/locale" +import { Locale } from "@/util" import { formatDuration } from "@/util/format" import { createColors, createFrames } from "../../ui/spinner.ts" import { useDialog } from "@tui/ui/dialog" diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/stash.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/stash.tsx index ef3eb329a9..84ba62338a 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/stash.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/stash.tsx @@ -1,6 +1,6 @@ import path from "path" import { Global } from "@/global" -import { Filesystem } from "@/util/filesystem" +import { Filesystem } from "@/util" import { onMount } from "solid-js" import { createStore, produce, unwrap } from "solid-js/store" import { createSimpleContext } from "../../context/helper" diff --git a/packages/opencode/src/cli/cmd/tui/component/textarea-keybindings.ts b/packages/opencode/src/cli/cmd/tui/component/textarea-keybindings.ts index 36ab03de54..eb7b622c6f 100644 --- a/packages/opencode/src/cli/cmd/tui/component/textarea-keybindings.ts +++ b/packages/opencode/src/cli/cmd/tui/component/textarea-keybindings.ts @@ -1,7 +1,7 @@ import { createMemo } from "solid-js" import type { KeyBinding } from "@opentui/core" import { useKeybind } from "../context/keybind" -import { Keybind } from "@/util/keybind" +import { Keybind } from "@/util" const TEXTAREA_ACTIONS = [ "submit", diff --git a/packages/opencode/src/cli/cmd/tui/context/keybind.tsx b/packages/opencode/src/cli/cmd/tui/context/keybind.tsx index 8d3fe487d1..9c883aa205 100644 --- a/packages/opencode/src/cli/cmd/tui/context/keybind.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/keybind.tsx @@ -1,5 +1,5 @@ import { createMemo } from "solid-js" -import { Keybind } from "@/util/keybind" +import { Keybind } from "@/util" import { pipe, mapValues } from "remeda" import type { TuiConfig } from "@/config/tui" import type { ParsedKey, Renderable } from "@opentui/core" diff --git a/packages/opencode/src/cli/cmd/tui/context/kv.tsx b/packages/opencode/src/cli/cmd/tui/context/kv.tsx index 7a52156f88..dc0b96c62a 100644 --- a/packages/opencode/src/cli/cmd/tui/context/kv.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/kv.tsx @@ -1,5 +1,5 @@ import { Global } from "@/global" -import { Filesystem } from "@/util/filesystem" +import { Filesystem } from "@/util" import { createSignal, type Setter } from "solid-js" import { createStore } from "solid-js/store" import { createSimpleContext } from "./helper" diff --git a/packages/opencode/src/cli/cmd/tui/context/local.tsx b/packages/opencode/src/cli/cmd/tui/context/local.tsx index 29f95141c9..612f2b7177 100644 --- a/packages/opencode/src/cli/cmd/tui/context/local.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/local.tsx @@ -12,7 +12,7 @@ import { Provider } from "@/provider" import { useArgs } from "./args" import { useSDK } from "./sdk" import { RGBA } from "@opentui/core" -import { Filesystem } from "@/util/filesystem" +import { Filesystem } from "@/util" export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ name: "Local", diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index cab162f8f0..a0a59199bb 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -28,7 +28,7 @@ import type { Snapshot } from "@/snapshot" import { useExit } from "./exit" import { useArgs } from "./args" import { batch, createEffect, on } from "solid-js" -import { Log } from "@/util/log" +import { Log } from "@/util" import { ConsoleState, emptyConsoleState, type ConsoleState as ConsoleStateType } from "@/config/console-state" export const { use: useSync, provider: SyncProvider } = createSimpleContext({ diff --git a/packages/opencode/src/cli/cmd/tui/context/theme.tsx b/packages/opencode/src/cli/cmd/tui/context/theme.tsx index 0c0658e743..179dc93700 100644 --- a/packages/opencode/src/cli/cmd/tui/context/theme.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/theme.tsx @@ -40,7 +40,7 @@ import { useKV } from "./kv" import { useRenderer } from "@opentui/solid" import { createStore, produce } from "solid-js/store" import { Global } from "@/global" -import { Filesystem } from "@/util/filesystem" +import { Filesystem } from "@/util" import { useTuiConfig } from "./tui-config" import { isRecord } from "@/util/record" import type { TuiThemeCurrent } from "@opencode-ai/plugin/tui" diff --git a/packages/opencode/src/cli/cmd/tui/feature-plugins/system/plugins.tsx b/packages/opencode/src/cli/cmd/tui/feature-plugins/system/plugins.tsx index f2fd25ffb6..f391eb24a7 100644 --- a/packages/opencode/src/cli/cmd/tui/feature-plugins/system/plugins.tsx +++ b/packages/opencode/src/cli/cmd/tui/feature-plugins/system/plugins.tsx @@ -1,4 +1,4 @@ -import { Keybind } from "@/util/keybind" +import { Keybind } from "@/util" import type { TuiPlugin, TuiPluginApi, TuiPluginModule, TuiPluginStatus } from "@opencode-ai/plugin/tui" import { useKeyboard, useTerminalDimensions } from "@opentui/solid" import { fileURLToPath } from "url" diff --git a/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts b/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts index bd7eac7713..dd873b753a 100644 --- a/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts +++ b/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts @@ -15,7 +15,7 @@ import { fileURLToPath } from "url" import { Config } from "@/config" import { TuiConfig } from "@/config/tui" -import { Log } from "@/util/log" +import { Log } from "@/util" import { errorData, errorMessage } from "@/util/error" import { isRecord } from "@/util/record" import { Instance } from "@/project/instance" @@ -32,8 +32,8 @@ import { PluginMeta } from "@/plugin/meta" import { installPlugin as installModulePlugin, patchPluginConfig, readPluginManifest } from "@/plugin/install" import { hasTheme, upsertTheme } from "../context/theme" import { Global } from "@/global" -import { Filesystem } from "@/util/filesystem" -import { Process } from "@/util/process" +import { Filesystem } from "@/util" +import { Process } from "@/util" import { Flock } from "@opencode-ai/shared/util/flock" import { Flag } from "@/flag/flag" import { INTERNAL_TUI_PLUGINS, type InternalTuiPlugin } from "./internal" diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx index 742d51be22..0ce33a59a9 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx @@ -2,7 +2,7 @@ import { createMemo, onMount } from "solid-js" import { useSync } from "@tui/context/sync" import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select" import type { TextPart } from "@opencode-ai/sdk/v2" -import { Locale } from "@/util/locale" +import { Locale } from "@/util" import { useSDK } from "@tui/context/sdk" import { useRoute } from "@tui/context/route" import { useDialog } from "../../ui/dialog" diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-timeline.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-timeline.tsx index 87248a6a8b..c0052f25fb 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-timeline.tsx @@ -2,7 +2,7 @@ import { createMemo, onMount } from "solid-js" import { useSync } from "@tui/context/sync" import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select" import type { TextPart } from "@opencode-ai/sdk/v2" -import { Locale } from "@/util/locale" +import { Locale } from "@/util" import { DialogMessage } from "./dialog-message" import { useDialog } from "../../ui/dialog" import type { PromptInfo } from "../../component/prompt/history" diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 9f0dfa6038..58b5d6626c 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -33,7 +33,7 @@ import type { ReasoningPart, } from "@opencode-ai/sdk/v2" import { useLocal } from "@tui/context/local" -import { Locale } from "@/util/locale" +import { Locale } from "@/util" import type { Tool } from "@/tool/tool" import type { ReadTool } from "@/tool/read" import type { WriteTool } from "@/tool/write" @@ -73,7 +73,7 @@ import { Editor } from "../../util/editor" import stripAnsi from "strip-ansi" import { usePromptRef } from "../../context/prompt" import { useExit } from "../../context/exit" -import { Filesystem } from "@/util/filesystem" +import { Filesystem } from "@/util" import { Global } from "@/global" import { PermissionPrompt } from "./permission" import { QuestionPrompt } from "./question" diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx index ad824fe48f..3554ab44ca 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx @@ -11,8 +11,8 @@ import { useSync } from "../../context/sync" import { useTextareaKeybindings } from "../../component/textarea-keybindings" import path from "path" import { LANGUAGE_EXTENSIONS } from "@/lsp/language" -import { Keybind } from "@/util/keybind" -import { Locale } from "@/util/locale" +import { Keybind } from "@/util" +import { Locale } from "@/util" import { Global } from "@/global" import { useDialog } from "../../ui/dialog" import { getScrollAcceleration } from "../../util/scroll" diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/subagent-footer.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/subagent-footer.tsx index c857937d4a..5569599607 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/subagent-footer.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/subagent-footer.tsx @@ -6,7 +6,7 @@ import { SplitBorder } from "@tui/component/border" import type { AssistantMessage } from "@opencode-ai/sdk/v2" import { useCommandDialog } from "@tui/component/dialog-command" import { useKeybind } from "../../context/keybind" -import { Locale } from "@/util/locale" +import { Locale } from "@/util" import { useTerminalDimensions } from "@opentui/solid" export function SubagentFooter() { diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index 972e67d103..3aaa5a54f8 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -1,15 +1,15 @@ import { cmd } from "@/cli/cmd/cmd" import { tui } from "./app" -import { Rpc } from "@/util/rpc" +import { Rpc } from "@/util" import { type rpc } from "./worker" import path from "path" import { fileURLToPath } from "url" import { UI } from "@/cli/ui" -import { Log } from "@/util/log" +import { Log } from "@/util" import { errorMessage } from "@/util/error" import { withTimeout } from "@/util/timeout" import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network" -import { Filesystem } from "@/util/filesystem" +import { Filesystem } from "@/util" import type { GlobalEvent } from "@opencode-ai/sdk/v2" import type { EventSource } from "./context/sdk" import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32" diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx index 48adddaedc..526f3a61b5 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx @@ -4,7 +4,7 @@ import { useDialog, type DialogContext } from "./dialog" import { createStore } from "solid-js/store" import { For } from "solid-js" import { useKeyboard } from "@opentui/solid" -import { Locale } from "@/util/locale" +import { Locale } from "@/util" export type DialogConfirmProps = { title: string diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx index b6c937f411..dda9a5a8ed 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx @@ -8,8 +8,8 @@ import * as fuzzysort from "fuzzysort" import { isDeepEqual } from "remeda" import { useDialog, type DialogContext } from "@tui/ui/dialog" import { useKeybind } from "@tui/context/keybind" -import { Keybind } from "@/util/keybind" -import { Locale } from "@/util/locale" +import { Keybind } from "@/util" +import { Locale } from "@/util" import { getScrollAcceleration } from "../util/scroll" import { useTuiConfig } from "../context/tui-config" diff --git a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts index 87c0a63abc..a67eb04f69 100644 --- a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts +++ b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts @@ -4,8 +4,8 @@ import { lazy } from "../../../../util/lazy.js" import { tmpdir } from "os" import path from "path" import fs from "fs/promises" -import { Filesystem } from "../../../../util/filesystem" -import { Process } from "../../../../util/process" +import { Filesystem } from "../../../../util" +import { Process } from "../../../../util" import { which } from "../../../../util/which" /** diff --git a/packages/opencode/src/cli/cmd/tui/util/editor.ts b/packages/opencode/src/cli/cmd/tui/util/editor.ts index 9eaae99fce..540cf6f497 100644 --- a/packages/opencode/src/cli/cmd/tui/util/editor.ts +++ b/packages/opencode/src/cli/cmd/tui/util/editor.ts @@ -3,8 +3,8 @@ import { rm } from "node:fs/promises" import { tmpdir } from "node:os" import { join } from "node:path" import { CliRenderer } from "@opentui/core" -import { Filesystem } from "@/util/filesystem" -import { Process } from "@/util/process" +import { Filesystem } from "@/util" +import { Process } from "@/util" export namespace Editor { export async function open(opts: { value: string; renderer: CliRenderer }): Promise { diff --git a/packages/opencode/src/cli/cmd/tui/util/sound.ts b/packages/opencode/src/cli/cmd/tui/util/sound.ts index d3a8db8b4f..1be35eecbf 100644 --- a/packages/opencode/src/cli/cmd/tui/util/sound.ts +++ b/packages/opencode/src/cli/cmd/tui/util/sound.ts @@ -2,7 +2,7 @@ import { Player } from "cli-sound" import { mkdirSync } from "node:fs" import { tmpdir } from "node:os" import { basename, join } from "node:path" -import { Process } from "@/util/process" +import { Process } from "@/util" import { which } from "@/util/which" import pulseA from "../asset/pulse-a.wav" with { type: "file" } import pulseB from "../asset/pulse-b.wav" with { type: "file" } diff --git a/packages/opencode/src/cli/cmd/tui/util/transcript.ts b/packages/opencode/src/cli/cmd/tui/util/transcript.ts index a89559c953..8fa0bc426e 100644 --- a/packages/opencode/src/cli/cmd/tui/util/transcript.ts +++ b/packages/opencode/src/cli/cmd/tui/util/transcript.ts @@ -1,5 +1,5 @@ import type { AssistantMessage, Part, Provider, UserMessage } from "@opencode-ai/sdk/v2" -import { Locale } from "@/util/locale" +import { Locale } from "@/util" import * as Model from "./model" export type TranscriptOptions = { diff --git a/packages/opencode/src/cli/cmd/tui/worker.ts b/packages/opencode/src/cli/cmd/tui/worker.ts index da9e3985b5..393a407eb0 100644 --- a/packages/opencode/src/cli/cmd/tui/worker.ts +++ b/packages/opencode/src/cli/cmd/tui/worker.ts @@ -1,9 +1,9 @@ import { Installation } from "@/installation" import { Server } from "@/server/server" -import { Log } from "@/util/log" +import { Log } from "@/util" import { Instance } from "@/project/instance" import { InstanceBootstrap } from "@/project/bootstrap" -import { Rpc } from "@/util/rpc" +import { Rpc } from "@/util" import { upgrade } from "@/cli/upgrade" import { Config } from "@/config" import { GlobalBus } from "@/bus/global" diff --git a/packages/opencode/src/cli/cmd/uninstall.ts b/packages/opencode/src/cli/cmd/uninstall.ts index 31830f0859..c0517d491d 100644 --- a/packages/opencode/src/cli/cmd/uninstall.ts +++ b/packages/opencode/src/cli/cmd/uninstall.ts @@ -7,8 +7,8 @@ import { Global } from "../../global" import fs from "fs/promises" import path from "path" import os from "os" -import { Filesystem } from "../../util/filesystem" -import { Process } from "../../util/process" +import { Filesystem } from "../../util" +import { Process } from "../../util" interface UninstallArgs { keepConfig: boolean diff --git a/packages/opencode/src/cli/heap.ts b/packages/opencode/src/cli/heap.ts index bb5a3d0937..cf1cffa800 100644 --- a/packages/opencode/src/cli/heap.ts +++ b/packages/opencode/src/cli/heap.ts @@ -2,7 +2,7 @@ import path from "path" import { writeHeapSnapshot } from "node:v8" import { Flag } from "@/flag/flag" import { Global } from "@/global" -import { Log } from "@/util/log" +import { Log } from "@/util" const log = Log.create({ service: "heap" }) const MINUTE = 60_000 diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 7eeacf1ffc..ee1c755ebc 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -1,8 +1,8 @@ -import { Log } from "../util/log" +import { Log } from "../util" import path from "path" import { pathToFileURL } from "url" import os from "os" -import { Process } from "../util/process" +import { Process } from "../util" import z from "zod" import { mergeDeep, pipe, unique } from "remeda" import { Global } from "../global" diff --git a/packages/opencode/src/config/markdown.ts b/packages/opencode/src/config/markdown.ts index 2f1483dca3..8b5392be5e 100644 --- a/packages/opencode/src/config/markdown.ts +++ b/packages/opencode/src/config/markdown.ts @@ -1,7 +1,7 @@ import { NamedError } from "@opencode-ai/shared/util/error" import matter from "gray-matter" import { z } from "zod" -import { Filesystem } from "../util/filesystem" +import { Filesystem } from "../util" export namespace ConfigMarkdown { export const FILE_REGEX = /(? diff --git a/packages/opencode/src/effect/run-service.ts b/packages/opencode/src/effect/run-service.ts index 3de82e0d11..9553e7a3aa 100644 --- a/packages/opencode/src/effect/run-service.ts +++ b/packages/opencode/src/effect/run-service.ts @@ -1,7 +1,7 @@ import { Effect, Layer, ManagedRuntime } from "effect" import * as Context from "effect/Context" import { Instance } from "@/project/instance" -import { LocalContext } from "@/util/local-context" +import { LocalContext } from "@/util" import { InstanceRef, WorkspaceRef } from "./instance-ref" import { Observability } from "./observability" import { WorkspaceContext } from "@/control-plane/workspace-context" diff --git a/packages/opencode/src/file/file.ts b/packages/opencode/src/file/file.ts index 35f2a8740a..2269065913 100644 --- a/packages/opencode/src/file/file.ts +++ b/packages/opencode/src/file/file.ts @@ -12,7 +12,7 @@ import path from "path" import z from "zod" import { Global } from "../global" import { Instance } from "../project/instance" -import { Log } from "../util/log" +import { Log } from "../util" import { Protected } from "./protected" import { Ripgrep } from "./ripgrep" diff --git a/packages/opencode/src/file/ripgrep.ts b/packages/opencode/src/file/ripgrep.ts index fee9cf4430..9a78c5b7fb 100644 --- a/packages/opencode/src/file/ripgrep.ts +++ b/packages/opencode/src/file/ripgrep.ts @@ -5,8 +5,8 @@ import z from "zod" import { Cause, Context, Effect, Layer, Queue, Stream } from "effect" import { ripgrep } from "ripgrep" -import { Filesystem } from "@/util/filesystem" -import { Log } from "@/util/log" +import { Filesystem } from "@/util" +import { Log } from "@/util" export namespace Ripgrep { const log = Log.create({ service: "ripgrep" }) diff --git a/packages/opencode/src/file/time.ts b/packages/opencode/src/file/time.ts index 86b6b4116b..327eadbef5 100644 --- a/packages/opencode/src/file/time.ts +++ b/packages/opencode/src/file/time.ts @@ -3,7 +3,7 @@ import { InstanceState } from "@/effect" import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { Flag } from "@/flag/flag" import type { SessionID } from "@/session/schema" -import { Log } from "../util/log" +import { Log } from "../util" export namespace FileTime { const log = Log.create({ service: "file.time" }) diff --git a/packages/opencode/src/file/watcher.ts b/packages/opencode/src/file/watcher.ts index ab5942547d..f11cf88a65 100644 --- a/packages/opencode/src/file/watcher.ts +++ b/packages/opencode/src/file/watcher.ts @@ -15,7 +15,7 @@ import { lazy } from "@/util/lazy" import { Config } from "../config" import { FileIgnore } from "./ignore" import { Protected } from "./protected" -import { Log } from "../util/log" +import { Log } from "../util" declare const OPENCODE_LIBC: string | undefined diff --git a/packages/opencode/src/format/format.ts b/packages/opencode/src/format/format.ts index 2ce922495e..40855636f9 100644 --- a/packages/opencode/src/format/format.ts +++ b/packages/opencode/src/format/format.ts @@ -6,7 +6,7 @@ import path from "path" import { mergeDeep } from "remeda" import z from "zod" import { Config } from "../config" -import { Log } from "../util/log" +import { Log } from "../util" import * as Formatter from "./formatter" const log = Log.create({ service: "format" }) diff --git a/packages/opencode/src/format/formatter.ts b/packages/opencode/src/format/formatter.ts index 6c17310ff8..36249db7db 100644 --- a/packages/opencode/src/format/formatter.ts +++ b/packages/opencode/src/format/formatter.ts @@ -1,7 +1,7 @@ import { Npm } from "../npm" import { Instance } from "../project/instance" -import { Filesystem } from "../util/filesystem" -import { Process } from "../util/process" +import { Filesystem } from "../util" +import { Process } from "../util" import { which } from "../util/which" import { Flag } from "@/flag/flag" diff --git a/packages/opencode/src/global/global.ts b/packages/opencode/src/global/global.ts index 1bbb5968c9..3633e0855a 100644 --- a/packages/opencode/src/global/global.ts +++ b/packages/opencode/src/global/global.ts @@ -2,7 +2,7 @@ import fs from "fs/promises" import { xdgData, xdgCache, xdgConfig, xdgState } from "xdg-basedir" import path from "path" import os from "os" -import { Filesystem } from "../util/filesystem" +import { Filesystem } from "../util" import { Flock } from "@opencode-ai/shared/util/flock" const app = "opencode" diff --git a/packages/opencode/src/ide/ide.ts b/packages/opencode/src/ide/ide.ts index cbced9c3d8..65e80d7f28 100644 --- a/packages/opencode/src/ide/ide.ts +++ b/packages/opencode/src/ide/ide.ts @@ -1,8 +1,8 @@ import { BusEvent } from "@/bus/bus-event" import z from "zod" import { NamedError } from "@opencode-ai/shared/util/error" -import { Log } from "../util/log" -import { Process } from "@/util/process" +import { Log } from "../util" +import { Process } from "@/util" const SUPPORTED_IDES = [ { name: "Windsurf" as const, cmd: "windsurf" }, diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 641411461d..ab3ccb712a 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -2,7 +2,7 @@ import yargs from "yargs" import { hideBin } from "yargs/helpers" import { RunCommand } from "./cli/cmd/run" import { GenerateCommand } from "./cli/cmd/generate" -import { Log } from "./util/log" +import { Log } from "./util" import { ConsoleCommand } from "./cli/cmd/account" import { ProvidersCommand } from "./cli/cmd/providers" import { AgentCommand } from "./cli/cmd/agent" @@ -14,7 +14,7 @@ import { Installation } from "./installation" import { NamedError } from "@opencode-ai/shared/util/error" import { FormatError } from "./cli/error" import { ServeCommand } from "./cli/cmd/serve" -import { Filesystem } from "./util/filesystem" +import { Filesystem } from "./util" import { DebugCommand } from "./cli/cmd/debug" import { StatsCommand } from "./cli/cmd/stats" import { McpCommand } from "./cli/cmd/mcp" diff --git a/packages/opencode/src/installation/installation.ts b/packages/opencode/src/installation/installation.ts index 898af9269c..dcaa0cd723 100644 --- a/packages/opencode/src/installation/installation.ts +++ b/packages/opencode/src/installation/installation.ts @@ -7,7 +7,7 @@ import path from "path" import z from "zod" import { BusEvent } from "@/bus/bus-event" import { Flag } from "../flag/flag" -import { Log } from "../util/log" +import { Log } from "../util" import { CHANNEL as channel, VERSION as version } from "./meta" import semver from "semver" diff --git a/packages/opencode/src/lsp/client.ts b/packages/opencode/src/lsp/client.ts index fe5a9ab182..50051b3901 100644 --- a/packages/opencode/src/lsp/client.ts +++ b/packages/opencode/src/lsp/client.ts @@ -4,15 +4,15 @@ import path from "path" import { pathToFileURL, fileURLToPath } from "url" import { createMessageConnection, StreamMessageReader, StreamMessageWriter } from "vscode-jsonrpc/node" import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types" -import { Log } from "../util/log" -import { Process } from "../util/process" +import { Log } from "../util" +import { Process } from "../util" import { LANGUAGE_EXTENSIONS } from "./language" import z from "zod" import type { LSPServer } from "./server" import { NamedError } from "@opencode-ai/shared/util/error" import { withTimeout } from "../util/timeout" import { Instance } from "../project/instance" -import { Filesystem } from "../util/filesystem" +import { Filesystem } from "../util" const DIAGNOSTICS_DEBOUNCE_MS = 150 diff --git a/packages/opencode/src/lsp/index.ts b/packages/opencode/src/lsp/index.ts index f567868f68..a55ac18402 100644 --- a/packages/opencode/src/lsp/index.ts +++ b/packages/opencode/src/lsp/index.ts @@ -1,6 +1,6 @@ import { BusEvent } from "@/bus/bus-event" import { Bus } from "@/bus" -import { Log } from "../util/log" +import { Log } from "../util" import { LSPClient } from "./client" import path from "path" import { pathToFileURL, fileURLToPath } from "url" @@ -9,7 +9,7 @@ import z from "zod" import { Config } from "../config" import { Instance } from "../project/instance" import { Flag } from "@/flag/flag" -import { Process } from "../util/process" +import { Process } from "../util" import { spawn as lspspawn } from "./launch" import { Effect, Layer, Context } from "effect" import { InstanceState } from "@/effect" diff --git a/packages/opencode/src/lsp/launch.ts b/packages/opencode/src/lsp/launch.ts index 51a7c209b4..fb84666b01 100644 --- a/packages/opencode/src/lsp/launch.ts +++ b/packages/opencode/src/lsp/launch.ts @@ -1,5 +1,5 @@ import type { ChildProcessWithoutNullStreams } from "child_process" -import { Process } from "../util/process" +import { Process } from "../util" type Child = Process.Child & ChildProcessWithoutNullStreams diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index 769880ef03..8110e86082 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -2,14 +2,14 @@ import type { ChildProcessWithoutNullStreams } from "child_process" import path from "path" import os from "os" import { Global } from "../global" -import { Log } from "../util/log" +import { Log } from "../util" import { text } from "node:stream/consumers" import fs from "fs/promises" -import { Filesystem } from "../util/filesystem" +import { Filesystem } from "../util" import { Instance } from "../project/instance" import { Flag } from "../flag/flag" import { Archive } from "../util" -import { Process } from "../util/process" +import { Process } from "../util" import { which } from "../util/which" import { Module } from "@opencode-ai/shared/util/module" import { spawn } from "./launch" diff --git a/packages/opencode/src/mcp/mcp.ts b/packages/opencode/src/mcp/mcp.ts index f5179b224d..8a57bcff73 100644 --- a/packages/opencode/src/mcp/mcp.ts +++ b/packages/opencode/src/mcp/mcp.ts @@ -10,7 +10,7 @@ import { ToolListChangedNotificationSchema, } from "@modelcontextprotocol/sdk/types.js" import { Config } from "../config" -import { Log } from "../util/log" +import { Log } from "../util" import { NamedError } from "@opencode-ai/shared/util/error" import z from "zod/v4" import { Instance } from "../project/instance" diff --git a/packages/opencode/src/mcp/oauth-callback.ts b/packages/opencode/src/mcp/oauth-callback.ts index 6babccd779..3e6169517f 100644 --- a/packages/opencode/src/mcp/oauth-callback.ts +++ b/packages/opencode/src/mcp/oauth-callback.ts @@ -1,6 +1,6 @@ import { createConnection } from "net" import { createServer } from "http" -import { Log } from "../util/log" +import { Log } from "../util" import { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH, parseRedirectUri } from "./oauth-provider" const log = Log.create({ service: "mcp.oauth-callback" }) diff --git a/packages/opencode/src/mcp/oauth-provider.ts b/packages/opencode/src/mcp/oauth-provider.ts index 4fdc192df7..fe09e14a58 100644 --- a/packages/opencode/src/mcp/oauth-provider.ts +++ b/packages/opencode/src/mcp/oauth-provider.ts @@ -7,7 +7,7 @@ import type { } from "@modelcontextprotocol/sdk/shared/auth.js" import { Effect } from "effect" import { McpAuth } from "./auth" -import { Log } from "../util/log" +import { Log } from "../util" const log = Log.create({ service: "mcp.oauth" }) diff --git a/packages/opencode/src/node.ts b/packages/opencode/src/node.ts index 6f020576d9..a30783fb21 100644 --- a/packages/opencode/src/node.ts +++ b/packages/opencode/src/node.ts @@ -1,6 +1,6 @@ export { Config } from "./config" export { Server } from "./server/server" export { bootstrap } from "./cli/bootstrap" -export { Log } from "./util/log" +export { Log } from "./util" export { Database } from "./storage/db" export { JsonMigration } from "./storage/json-migration" diff --git a/packages/opencode/src/npm/npm.ts b/packages/opencode/src/npm/npm.ts index f905130719..7f17446057 100644 --- a/packages/opencode/src/npm/npm.ts +++ b/packages/opencode/src/npm/npm.ts @@ -2,10 +2,10 @@ import semver from "semver" import z from "zod" import { NamedError } from "@opencode-ai/shared/util/error" import { Global } from "../global" -import { Log } from "../util/log" +import { Log } from "../util" import path from "path" import { readdir, rm } from "fs/promises" -import { Filesystem } from "@/util/filesystem" +import { Filesystem } from "@/util" import { Flock } from "@opencode-ai/shared/util/flock" import { Arborist } from "@npmcli/arborist" diff --git a/packages/opencode/src/patch/patch.ts b/packages/opencode/src/patch/patch.ts index 749efd911c..1dc99b4da9 100644 --- a/packages/opencode/src/patch/patch.ts +++ b/packages/opencode/src/patch/patch.ts @@ -2,7 +2,7 @@ import z from "zod" import * as path from "path" import * as fs from "fs/promises" import { readFileSync } from "fs" -import { Log } from "../util/log" +import { Log } from "../util" const log = Log.create({ service: "patch" }) diff --git a/packages/opencode/src/permission/evaluate.ts b/packages/opencode/src/permission/evaluate.ts index 2b0604f4ba..bcc4e58118 100644 --- a/packages/opencode/src/permission/evaluate.ts +++ b/packages/opencode/src/permission/evaluate.ts @@ -1,4 +1,4 @@ -import { Wildcard } from "@/util/wildcard" +import { Wildcard } from "@/util" type Rule = { permission: string diff --git a/packages/opencode/src/permission/permission.ts b/packages/opencode/src/permission/permission.ts index e2dead8fe2..a8463510c4 100644 --- a/packages/opencode/src/permission/permission.ts +++ b/packages/opencode/src/permission/permission.ts @@ -7,9 +7,9 @@ import { MessageID, SessionID } from "@/session/schema" import { PermissionTable } from "@/session/session.sql" import { Database, eq } from "@/storage/db" import { zod } from "@/util/effect-zod" -import { Log } from "@/util/log" +import { Log } from "@/util" import { withStatics } from "@/util/schema" -import { Wildcard } from "@/util/wildcard" +import { Wildcard } from "@/util" import { Deferred, Effect, Layer, Schema, Context } from "effect" import os from "os" import { evaluate as evalRule } from "./evaluate" diff --git a/packages/opencode/src/plugin/codex.ts b/packages/opencode/src/plugin/codex.ts index ea356d55d2..e0f1afa63f 100644 --- a/packages/opencode/src/plugin/codex.ts +++ b/packages/opencode/src/plugin/codex.ts @@ -1,5 +1,5 @@ import type { Hooks, PluginInput } from "@opencode-ai/plugin" -import { Log } from "../util/log" +import { Log } from "../util" import { Installation } from "../installation" import { OAUTH_DUMMY_KEY } from "../auth" import os from "os" diff --git a/packages/opencode/src/plugin/github-copilot/copilot.ts b/packages/opencode/src/plugin/github-copilot/copilot.ts index e12d182e4f..eeea219241 100644 --- a/packages/opencode/src/plugin/github-copilot/copilot.ts +++ b/packages/opencode/src/plugin/github-copilot/copilot.ts @@ -2,7 +2,7 @@ import type { Hooks, PluginInput } from "@opencode-ai/plugin" import type { Model } from "@opencode-ai/sdk/v2" import { Installation } from "@/installation" import { iife } from "@/util/iife" -import { Log } from "../../util/log" +import { Log } from "../../util" import { setTimeout as sleep } from "node:timers/promises" import { CopilotModels } from "./models" import { MessageV2 } from "@/session/message-v2" diff --git a/packages/opencode/src/plugin/install.ts b/packages/opencode/src/plugin/install.ts index 8dd8212965..0a6256d6f2 100644 --- a/packages/opencode/src/plugin/install.ts +++ b/packages/opencode/src/plugin/install.ts @@ -9,7 +9,7 @@ import { import { ConfigPaths } from "@/config/paths" import { Global } from "@/global" -import { Filesystem } from "@/util/filesystem" +import { Filesystem } from "@/util" import { Flock } from "@opencode-ai/shared/util/flock" import { isRecord } from "@/util/record" diff --git a/packages/opencode/src/plugin/meta.ts b/packages/opencode/src/plugin/meta.ts index 3f02f543ef..89955d1dfb 100644 --- a/packages/opencode/src/plugin/meta.ts +++ b/packages/opencode/src/plugin/meta.ts @@ -3,7 +3,7 @@ import { fileURLToPath } from "url" import { Flag } from "@/flag/flag" import { Global } from "@/global" -import { Filesystem } from "@/util/filesystem" +import { Filesystem } from "@/util" import { Flock } from "@opencode-ai/shared/util/flock" import { parsePluginSpecifier, pluginSource } from "./shared" diff --git a/packages/opencode/src/plugin/plugin.ts b/packages/opencode/src/plugin/plugin.ts index 23c807ebe7..ec1cf1e313 100644 --- a/packages/opencode/src/plugin/plugin.ts +++ b/packages/opencode/src/plugin/plugin.ts @@ -7,7 +7,7 @@ import type { } from "@opencode-ai/plugin" import { Config } from "../config" import { Bus } from "../bus" -import { Log } from "../util/log" +import { Log } from "../util" import { createOpencodeClient } from "@opencode-ai/sdk" import { Flag } from "../flag/flag" import { CodexAuthPlugin } from "./codex" diff --git a/packages/opencode/src/plugin/shared.ts b/packages/opencode/src/plugin/shared.ts index 54cc32af5b..11f36c41ae 100644 --- a/packages/opencode/src/plugin/shared.ts +++ b/packages/opencode/src/plugin/shared.ts @@ -3,7 +3,7 @@ import { fileURLToPath, pathToFileURL } from "url" import npa from "npm-package-arg" import semver from "semver" import { Npm } from "../npm" -import { Filesystem } from "@/util/filesystem" +import { Filesystem } from "@/util" import { isRecord } from "@/util/record" // Old npm package names for plugins that are now built-in diff --git a/packages/opencode/src/project/bootstrap.ts b/packages/opencode/src/project/bootstrap.ts index a1f2a8cb02..f00d8ffd9b 100644 --- a/packages/opencode/src/project/bootstrap.ts +++ b/packages/opencode/src/project/bootstrap.ts @@ -8,7 +8,7 @@ import { Vcs } from "./vcs" import { Bus } from "../bus" import { Command } from "../command" import { Instance } from "./instance" -import { Log } from "@/util/log" +import { Log } from "@/util" import { FileWatcher } from "@/file/watcher" import { ShareNext } from "@/share/share-next" import * as Effect from "effect/Effect" diff --git a/packages/opencode/src/project/instance.ts b/packages/opencode/src/project/instance.ts index 2a20ecac97..a8a5218751 100644 --- a/packages/opencode/src/project/instance.ts +++ b/packages/opencode/src/project/instance.ts @@ -3,8 +3,8 @@ import { disposeInstance } from "@/effect/instance-registry" import { makeRuntime } from "@/effect/run-service" import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { iife } from "@/util/iife" -import { Log } from "@/util/log" -import { LocalContext } from "../util/local-context" +import { Log } from "@/util" +import { LocalContext } from "../util" import { Project } from "./project" import { WorkspaceContext } from "@/control-plane/workspace-context" diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index d20bf42494..9c4ed58ce8 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -2,7 +2,7 @@ import z from "zod" import { and, Database, eq } from "../storage/db" import { ProjectTable } from "./project.sql" import { SessionTable } from "../session/session.sql" -import { Log } from "../util/log" +import { Log } from "../util" import { Flag } from "@/flag/flag" import { BusEvent } from "@/bus/bus-event" import { GlobalBus } from "@/bus/global" diff --git a/packages/opencode/src/project/vcs.ts b/packages/opencode/src/project/vcs.ts index 187c616602..cb0b46adcb 100644 --- a/packages/opencode/src/project/vcs.ts +++ b/packages/opencode/src/project/vcs.ts @@ -7,7 +7,7 @@ import { InstanceState } from "@/effect" import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { FileWatcher } from "@/file/watcher" import { Git } from "@/git" -import { Log } from "@/util/log" +import { Log } from "@/util" import { Instance } from "./instance" import z from "zod" diff --git a/packages/opencode/src/provider/models.ts b/packages/opencode/src/provider/models.ts index 55f137aa0b..59d629a379 100644 --- a/packages/opencode/src/provider/models.ts +++ b/packages/opencode/src/provider/models.ts @@ -1,11 +1,11 @@ import { Global } from "../global" -import { Log } from "../util/log" +import { Log } from "../util" import path from "path" import z from "zod" import { Installation } from "../installation" import { Flag } from "../flag/flag" import { lazy } from "@/util/lazy" -import { Filesystem } from "../util/filesystem" +import { Filesystem } from "../util" import { Flock } from "@opencode-ai/shared/util/flock" import { Hash } from "@opencode-ai/shared/util/hash" diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index ef6cbd61e7..432dbab34a 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -4,7 +4,7 @@ import fuzzysort from "fuzzysort" import { Config } from "../config" import { mapValues, mergeDeep, omit, pickBy, sortBy } from "remeda" import { NoSuchModelError, type Provider as SDK } from "ai" -import { Log } from "../util/log" +import { Log } from "../util" import { Npm } from "../npm" import { Hash } from "@opencode-ai/shared/util/hash" import { Plugin } from "../plugin" diff --git a/packages/opencode/src/pty/service.ts b/packages/opencode/src/pty/service.ts index ff52095b4f..a4ebd0696b 100644 --- a/packages/opencode/src/pty/service.ts +++ b/packages/opencode/src/pty/service.ts @@ -4,7 +4,7 @@ import { InstanceState } from "@/effect" import { Instance } from "@/project/instance" import type { Proc } from "#pty" import z from "zod" -import { Log } from "../util/log" +import { Log } from "../util" import { lazy } from "@opencode-ai/shared/util/lazy" import { Shell } from "@/shell/shell" import { Plugin } from "@/plugin" diff --git a/packages/opencode/src/question/index.ts b/packages/opencode/src/question/index.ts index 8d023c18bf..627d04564d 100644 --- a/packages/opencode/src/question/index.ts +++ b/packages/opencode/src/question/index.ts @@ -4,7 +4,7 @@ import { BusEvent } from "@/bus/bus-event" import { InstanceState } from "@/effect" import { SessionID, MessageID } from "@/session/schema" import { zod } from "@/util/effect-zod" -import { Log } from "@/util/log" +import { Log } from "@/util" import { withStatics } from "@/util/schema" import { QuestionID } from "./schema" diff --git a/packages/opencode/src/server/control/index.ts b/packages/opencode/src/server/control/index.ts index cf8949c954..737f958d6b 100644 --- a/packages/opencode/src/server/control/index.ts +++ b/packages/opencode/src/server/control/index.ts @@ -1,6 +1,6 @@ import { Auth } from "@/auth" import { AppRuntime } from "@/effect/app-runtime" -import { Log } from "@/util/log" +import { Log } from "@/util" import { Effect } from "effect" import { ProviderID } from "@/provider/schema" import { Hono } from "hono" diff --git a/packages/opencode/src/server/fence.ts b/packages/opencode/src/server/fence.ts index b6dbde0081..87771745c8 100644 --- a/packages/opencode/src/server/fence.ts +++ b/packages/opencode/src/server/fence.ts @@ -3,7 +3,7 @@ import { Database, inArray } from "@/storage/db" import { EventSequenceTable } from "@/sync/event.sql" import { Workspace } from "@/control-plane/workspace" import type { WorkspaceID } from "@/control-plane/schema" -import { Log } from "@/util/log" +import { Log } from "@/util" const HEADER = "x-opencode-sync" type State = Record diff --git a/packages/opencode/src/server/instance/event.ts b/packages/opencode/src/server/instance/event.ts index f13ed035e0..103d3d7cfb 100644 --- a/packages/opencode/src/server/instance/event.ts +++ b/packages/opencode/src/server/instance/event.ts @@ -2,7 +2,7 @@ import z from "zod" import { Hono } from "hono" import { describeRoute, resolver } from "hono-openapi" import { streamSSE } from "hono/streaming" -import { Log } from "@/util/log" +import { Log } from "@/util" import { BusEvent } from "@/bus/bus-event" import { Bus } from "@/bus" import { AsyncQueue } from "../../util/queue" diff --git a/packages/opencode/src/server/instance/global.ts b/packages/opencode/src/server/instance/global.ts index b69f35a649..ac73bb64d8 100644 --- a/packages/opencode/src/server/instance/global.ts +++ b/packages/opencode/src/server/instance/global.ts @@ -10,7 +10,7 @@ import { AppRuntime } from "@/effect/app-runtime" import { AsyncQueue } from "@/util/queue" import { Instance } from "../../project/instance" import { Installation } from "@/installation" -import { Log } from "../../util/log" +import { Log } from "../../util" import { lazy } from "../../util/lazy" import { Config } from "../../config" import { errors } from "../error" diff --git a/packages/opencode/src/server/instance/httpapi/server.ts b/packages/opencode/src/server/instance/httpapi/server.ts index 9ecefe2915..62ffb5940d 100644 --- a/packages/opencode/src/server/instance/httpapi/server.ts +++ b/packages/opencode/src/server/instance/httpapi/server.ts @@ -9,7 +9,7 @@ import { Flag } from "@/flag/flag" import { InstanceBootstrap } from "@/project/bootstrap" import { Instance } from "@/project/instance" import { lazy } from "@/util/lazy" -import { Filesystem } from "@/util/filesystem" +import { Filesystem } from "@/util" import { PermissionApi, permissionHandlers } from "./permission" import { ProviderApi, providerHandlers } from "./provider" import { QuestionApi, questionHandlers } from "./question" diff --git a/packages/opencode/src/server/instance/middleware.ts b/packages/opencode/src/server/instance/middleware.ts index 5fd1fc25e8..7b66072c23 100644 --- a/packages/opencode/src/server/instance/middleware.ts +++ b/packages/opencode/src/server/instance/middleware.ts @@ -11,7 +11,7 @@ import { Session } from "@/session" import { SessionID } from "@/session/schema" import { WorkspaceContext } from "@/control-plane/workspace-context" import { AppRuntime } from "@/effect/app-runtime" -import { Log } from "@/util/log" +import { Log } from "@/util" import { AppFileSystem } from "@opencode-ai/shared/filesystem" type Rule = { method?: string; path: string; exact?: boolean; action: "local" | "forward" } diff --git a/packages/opencode/src/server/instance/session.ts b/packages/opencode/src/server/instance/session.ts index c606af8544..1b2755fb8a 100644 --- a/packages/opencode/src/server/instance/session.ts +++ b/packages/opencode/src/server/instance/session.ts @@ -18,7 +18,7 @@ import { AppRuntime } from "../../effect/app-runtime" import { Agent } from "../../agent/agent" import { Snapshot } from "@/snapshot" import { Command } from "../../command" -import { Log } from "../../util/log" +import { Log } from "../../util" import { Permission } from "@/permission" import { PermissionID } from "@/permission/schema" import { ModelID, ProviderID } from "@/provider/schema" diff --git a/packages/opencode/src/server/instance/sync.ts b/packages/opencode/src/server/instance/sync.ts index c22969130a..2513e519ee 100644 --- a/packages/opencode/src/server/instance/sync.ts +++ b/packages/opencode/src/server/instance/sync.ts @@ -5,7 +5,7 @@ import { SyncEvent } from "@/sync" import { Database, asc, and, not, or, lte, eq } from "@/storage/db" import { EventTable } from "@/sync/event.sql" import { lazy } from "@/util/lazy" -import { Log } from "@/util/log" +import { Log } from "@/util" import { errors } from "../error" const ReplayEvent = z.object({ diff --git a/packages/opencode/src/server/instance/workspace.ts b/packages/opencode/src/server/instance/workspace.ts index a4ff4eda8d..59369ef8e7 100644 --- a/packages/opencode/src/server/instance/workspace.ts +++ b/packages/opencode/src/server/instance/workspace.ts @@ -6,7 +6,7 @@ import { Workspace } from "../../control-plane/workspace" import { Instance } from "../../project/instance" import { errors } from "../error" import { lazy } from "../../util/lazy" -import { Log } from "@/util/log" +import { Log } from "@/util" import { errorData } from "@/util/error" const log = Log.create({ service: "server.workspace" }) diff --git a/packages/opencode/src/server/mdns.ts b/packages/opencode/src/server/mdns.ts index 778afa26ac..2011771a20 100644 --- a/packages/opencode/src/server/mdns.ts +++ b/packages/opencode/src/server/mdns.ts @@ -1,4 +1,4 @@ -import { Log } from "@/util/log" +import { Log } from "@/util" import { Bonjour } from "bonjour-service" const log = Log.create({ service: "mdns" }) diff --git a/packages/opencode/src/server/middleware.ts b/packages/opencode/src/server/middleware.ts index 880c432c7c..e0958196a5 100644 --- a/packages/opencode/src/server/middleware.ts +++ b/packages/opencode/src/server/middleware.ts @@ -5,7 +5,7 @@ import { Session } from "../session" import type { ContentfulStatusCode } from "hono/utils/http-status" import type { ErrorHandler, MiddlewareHandler } from "hono" import { HTTPException } from "hono/http-exception" -import { Log } from "../util/log" +import { Log } from "../util" import { Flag } from "@/flag/flag" import { basicAuth } from "hono/basic-auth" import { cors } from "hono/cors" diff --git a/packages/opencode/src/server/proxy.ts b/packages/opencode/src/server/proxy.ts index 5effa5d05f..07edcc2bb2 100644 --- a/packages/opencode/src/server/proxy.ts +++ b/packages/opencode/src/server/proxy.ts @@ -1,6 +1,6 @@ import { Hono } from "hono" import type { UpgradeWebSocket } from "hono/ws" -import { Log } from "@/util/log" +import { Log } from "@/util" import * as Fence from "./fence" import type { WorkspaceID } from "@/control-plane/schema" import { Workspace } from "@/control-plane/workspace" diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index c6c37ee438..fc3b399f79 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -7,7 +7,7 @@ import { AuthMiddleware, CompressionMiddleware, CorsMiddleware, ErrorMiddleware, import { FenceMiddleware } from "./fence" import { InstanceRoutes } from "./instance" import { initProjectors } from "./projectors" -import { Log } from "@/util/log" +import { Log } from "@/util" import { Flag } from "@/flag/flag" import { ControlPlaneRoutes } from "./control" import { UIRoutes } from "./ui" diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index 3d39a60555..72b9963215 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -5,8 +5,8 @@ import { SessionID, MessageID, PartID } from "./schema" import { Provider } from "../provider" import { MessageV2 } from "./message-v2" import z from "zod" -import { Token } from "../util/token" -import { Log } from "../util/log" +import { Token } from "../util" +import { Log } from "../util" import { SessionProcessor } from "./processor" import { Agent } from "@/agent/agent" import { Plugin } from "@/plugin" diff --git a/packages/opencode/src/session/instruction.ts b/packages/opencode/src/session/instruction.ts index 076c81ec75..cd2050adf5 100644 --- a/packages/opencode/src/session/instruction.ts +++ b/packages/opencode/src/session/instruction.ts @@ -9,7 +9,7 @@ import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { withTransientReadRetry } from "@/util/effect-http-client" import { Global } from "../global" import { Instance } from "../project/instance" -import { Log } from "../util/log" +import { Log } from "../util" import type { MessageV2 } from "./message-v2" import type { MessageID } from "./schema" diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index bde36d2638..8f93bd5e15 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -1,5 +1,5 @@ import { Provider } from "@/provider" -import { Log } from "@/util/log" +import { Log } from "@/util" import { Context, Effect, Layer, Record } from "effect" import * as Stream from "effect/Stream" import { streamText, wrapLanguageModel, type ModelMessage, type Tool, tool, jsonSchema } from "ai" @@ -16,7 +16,7 @@ import { Flag } from "@/flag/flag" import { Permission } from "@/permission" import { PermissionID } from "@/permission/schema" import { Bus } from "@/bus" -import { Wildcard } from "@/util/wildcard" +import { Wildcard } from "@/util" import { SessionID } from "@/session/schema" import { Auth } from "@/auth" import { Installation } from "@/installation" diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 1ae70c3c6e..72b27403bd 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -18,7 +18,7 @@ import { SessionSummary } from "./summary" import type { Provider } from "@/provider" import { Question } from "@/question" import { errorMessage } from "@/util/error" -import { Log } from "@/util/log" +import { Log } from "@/util" import { isRecord } from "@/util/record" export namespace SessionProcessor { diff --git a/packages/opencode/src/session/projectors.ts b/packages/opencode/src/session/projectors.ts index bc083105c2..1e092b07e0 100644 --- a/packages/opencode/src/session/projectors.ts +++ b/packages/opencode/src/session/projectors.ts @@ -3,7 +3,7 @@ import { SyncEvent } from "@/sync" import { Session } from "." import { MessageV2 } from "./message-v2" import { SessionTable, MessageTable, PartTable } from "./session.sql" -import { Log } from "../util/log" +import { Log } from "../util" const log = Log.create({ service: "session.projector" }) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index f04ea8cdeb..a072633aa7 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -3,7 +3,7 @@ import os from "os" import z from "zod" import { SessionID, MessageID, PartID } from "./schema" import { MessageV2 } from "./message-v2" -import { Log } from "../util/log" +import { Log } from "../util" import { SessionRevert } from "./revert" import { Session } from "." import { Agent } from "../agent/agent" @@ -42,7 +42,7 @@ import { Shell } from "@/shell/shell" import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { Truncate } from "@/tool/truncate" import { decodeDataUrl } from "@/util/data-url" -import { Process } from "@/util/process" +import { Process } from "@/util" import { Cause, Effect, Exit, Layer, Option, Scope, Context } from "effect" import { EffectLogger } from "@/effect/logger" import { InstanceState } from "@/effect" diff --git a/packages/opencode/src/session/revert.ts b/packages/opencode/src/session/revert.ts index 7a7f847ad1..383fe08e87 100644 --- a/packages/opencode/src/session/revert.ts +++ b/packages/opencode/src/session/revert.ts @@ -4,7 +4,7 @@ import { Bus } from "../bus" import { Snapshot } from "../snapshot" import { Storage } from "@/storage/storage" import { SyncEvent } from "../sync" -import { Log } from "../util/log" +import { Log } from "../util" import { Session } from "." import { MessageV2 } from "./message-v2" import { SessionID, MessageID, PartID } from "./schema" diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts index 0b82d8b99f..a4bf446a1a 100644 --- a/packages/opencode/src/session/session.ts +++ b/packages/opencode/src/session/session.ts @@ -14,7 +14,7 @@ import type { SQL } from "../storage/db" import { PartTable, SessionTable } from "./session.sql" import { ProjectTable } from "../project/project.sql" import { Storage } from "@/storage/storage" -import { Log } from "../util/log" +import { Log } from "../util" import { updateSchema } from "../util/update-schema" import { MessageV2 } from "./message-v2" import { Instance } from "../project/instance" diff --git a/packages/opencode/src/share/share-next.ts b/packages/opencode/src/share/share-next.ts index 9b345ac8ef..bcb1fcc962 100644 --- a/packages/opencode/src/share/share-next.ts +++ b/packages/opencode/src/share/share-next.ts @@ -11,7 +11,7 @@ import { MessageV2 } from "@/session/message-v2" import type { SessionID } from "@/session/schema" import { Database, eq } from "@/storage/db" import { Config } from "@/config" -import { Log } from "@/util/log" +import { Log } from "@/util" import { SessionShareTable } from "./share.sql" export namespace ShareNext { diff --git a/packages/opencode/src/shell/shell.ts b/packages/opencode/src/shell/shell.ts index 0044dda89c..056a794dc8 100644 --- a/packages/opencode/src/shell/shell.ts +++ b/packages/opencode/src/shell/shell.ts @@ -1,6 +1,6 @@ import { Flag } from "@/flag/flag" import { lazy } from "@/util/lazy" -import { Filesystem } from "@/util/filesystem" +import { Filesystem } from "@/util" import { which } from "@/util/which" import path from "path" import { spawn, type ChildProcess } from "child_process" diff --git a/packages/opencode/src/skill/discovery.ts b/packages/opencode/src/skill/discovery.ts index 0323f250f6..eff64ed2bb 100644 --- a/packages/opencode/src/skill/discovery.ts +++ b/packages/opencode/src/skill/discovery.ts @@ -4,7 +4,7 @@ import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } fr import { withTransientReadRetry } from "@/util/effect-http-client" import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { Global } from "../global" -import { Log } from "../util/log" +import { Log } from "../util" export namespace Discovery { const skillConcurrency = 4 diff --git a/packages/opencode/src/skill/skill.ts b/packages/opencode/src/skill/skill.ts index 3122115cd3..ef9f661cb5 100644 --- a/packages/opencode/src/skill/skill.ts +++ b/packages/opencode/src/skill/skill.ts @@ -14,7 +14,7 @@ import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { Config } from "../config" import { ConfigMarkdown } from "../config/markdown" import { Glob } from "@opencode-ai/shared/util/glob" -import { Log } from "../util/log" +import { Log } from "../util" import { Discovery } from "./discovery" const log = Log.create({ service: "skill" }) diff --git a/packages/opencode/src/snapshot/snapshot.ts b/packages/opencode/src/snapshot/snapshot.ts index 7aa3a4debf..8d8118131e 100644 --- a/packages/opencode/src/snapshot/snapshot.ts +++ b/packages/opencode/src/snapshot/snapshot.ts @@ -9,7 +9,7 @@ import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { Hash } from "@opencode-ai/shared/util/hash" import { Config } from "../config" import { Global } from "../global" -import { Log } from "../util/log" +import { Log } from "../util" export const Patch = z.object({ hash: z.string(), diff --git a/packages/opencode/src/storage/db.ts b/packages/opencode/src/storage/db.ts index 247cb347cb..ee53182f36 100644 --- a/packages/opencode/src/storage/db.ts +++ b/packages/opencode/src/storage/db.ts @@ -2,10 +2,10 @@ import { type SQLiteBunDatabase } from "drizzle-orm/bun-sqlite" import { migrate } from "drizzle-orm/bun-sqlite/migrator" import { type SQLiteTransaction } from "drizzle-orm/sqlite-core" export * from "drizzle-orm" -import { LocalContext } from "../util/local-context" +import { LocalContext } from "../util" import { lazy } from "../util/lazy" import { Global } from "../global" -import { Log } from "../util/log" +import { Log } from "../util" import { NamedError } from "@opencode-ai/shared/util/error" import z from "zod" import path from "path" diff --git a/packages/opencode/src/storage/json-migration.ts b/packages/opencode/src/storage/json-migration.ts index c13a005ca6..4bf75f5a1c 100644 --- a/packages/opencode/src/storage/json-migration.ts +++ b/packages/opencode/src/storage/json-migration.ts @@ -1,13 +1,13 @@ import type { SQLiteBunDatabase } from "drizzle-orm/bun-sqlite" import type { NodeSQLiteDatabase } from "drizzle-orm/node-sqlite" import { Global } from "../global" -import { Log } from "../util/log" +import { Log } from "../util" import { ProjectTable } from "../project/project.sql" import { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } from "../session/session.sql" import { SessionShareTable } from "../share/share.sql" import path from "path" import { existsSync } from "fs" -import { Filesystem } from "../util/filesystem" +import { Filesystem } from "../util" import { Glob } from "@opencode-ai/shared/util/glob" export namespace JsonMigration { diff --git a/packages/opencode/src/storage/storage.ts b/packages/opencode/src/storage/storage.ts index 359c750ced..f4793c6204 100644 --- a/packages/opencode/src/storage/storage.ts +++ b/packages/opencode/src/storage/storage.ts @@ -1,4 +1,4 @@ -import { Log } from "../util/log" +import { Log } from "../util" import path from "path" import { Global } from "../global" import { NamedError } from "@opencode-ai/shared/util/error" diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index 0ab1301305..1edd754143 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -4,7 +4,7 @@ import { createWriteStream } from "node:fs" import { Tool } from "./tool" import path from "path" import DESCRIPTION from "./bash.txt" -import { Log } from "../util/log" +import { Log } from "../util" import { Instance } from "../project/instance" import { lazy } from "@/util/lazy" import { Language, type Node } from "web-tree-sitter" diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 6171e4366e..80115884d9 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -22,7 +22,7 @@ import { ProviderID, type ModelID } from "../provider/schema" import { WebSearchTool } from "./websearch" import { CodeSearchTool } from "./codesearch" import { Flag } from "@/flag/flag" -import { Log } from "@/util/log" +import { Log } from "@/util" import { LspTool } from "./lsp" import { Truncate } from "./truncate" import { ApplyPatchTool } from "./apply_patch" diff --git a/packages/opencode/src/tool/truncate.ts b/packages/opencode/src/tool/truncate.ts index d607e22f28..d2aa944a85 100644 --- a/packages/opencode/src/tool/truncate.ts +++ b/packages/opencode/src/tool/truncate.ts @@ -5,7 +5,7 @@ import type { Agent } from "../agent/agent" import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { evaluate } from "@/permission/evaluate" import { Identifier } from "../id/id" -import { Log } from "../util/log" +import { Log } from "../util" import { ToolID } from "./schema" import { TRUNCATION_DIR } from "./truncation-dir" diff --git a/packages/opencode/src/util/archive.ts b/packages/opencode/src/util/archive.ts index cf25636841..21d014c6a8 100644 --- a/packages/opencode/src/util/archive.ts +++ b/packages/opencode/src/util/archive.ts @@ -1,5 +1,5 @@ import path from "path" -import { Process } from "./process" +import { Process } from "." export async function extractZip(zipPath: string, destDir: string) { if (process.platform === "win32") { diff --git a/packages/opencode/src/util/color.ts b/packages/opencode/src/util/color.ts index b96deaec47..43408295fa 100644 --- a/packages/opencode/src/util/color.ts +++ b/packages/opencode/src/util/color.ts @@ -1,19 +1,17 @@ -export namespace Color { - export function isValidHex(hex?: string): hex is string { - if (!hex) return false - return /^#[0-9a-fA-F]{6}$/.test(hex) - } - - export function hexToRgb(hex: string): { r: number; g: number; b: number } { - const r = parseInt(hex.slice(1, 3), 16) - const g = parseInt(hex.slice(3, 5), 16) - const b = parseInt(hex.slice(5, 7), 16) - return { r, g, b } - } - - export function hexToAnsiBold(hex?: string): string | undefined { - if (!isValidHex(hex)) return undefined - const { r, g, b } = hexToRgb(hex) - return `\x1b[38;2;${r};${g};${b}m\x1b[1m` - } +export function isValidHex(hex?: string): hex is string { + if (!hex) return false + return /^#[0-9a-fA-F]{6}$/.test(hex) +} + +export function hexToRgb(hex: string): { r: number; g: number; b: number } { + const r = parseInt(hex.slice(1, 3), 16) + const g = parseInt(hex.slice(3, 5), 16) + const b = parseInt(hex.slice(5, 7), 16) + return { r, g, b } +} + +export function hexToAnsiBold(hex?: string): string | undefined { + if (!isValidHex(hex)) return undefined + const { r, g, b } = hexToRgb(hex) + return `\x1b[38;2;${r};${g};${b}m\x1b[1m` } diff --git a/packages/opencode/src/util/filesystem.ts b/packages/opencode/src/util/filesystem.ts index b4aef05456..c3f59d3297 100644 --- a/packages/opencode/src/util/filesystem.ts +++ b/packages/opencode/src/util/filesystem.ts @@ -7,239 +7,237 @@ import { Readable } from "stream" import { pipeline } from "stream/promises" import { Glob } from "@opencode-ai/shared/util/glob" -export namespace Filesystem { - // Fast sync version for metadata checks - export async function exists(p: string): Promise { - return existsSync(p) - } +// Fast sync version for metadata checks +export async function exists(p: string): Promise { + return existsSync(p) +} - export async function isDir(p: string): Promise { - try { - return statSync(p).isDirectory() - } catch { - return false +export async function isDir(p: string): Promise { + try { + return statSync(p).isDirectory() + } catch { + return false + } +} + +export function stat(p: string): ReturnType | undefined { + return statSync(p, { throwIfNoEntry: false }) ?? undefined +} + +export async function statAsync(p: string): Promise | undefined> { + return statFile(p).catch((e) => { + if (isEnoent(e)) return undefined + throw e + }) +} + +export async function size(p: string): Promise { + const s = stat(p)?.size ?? 0 + return typeof s === "bigint" ? Number(s) : s +} + +export async function readText(p: string): Promise { + return readFile(p, "utf-8") +} + +export async function readJson(p: string): Promise { + return JSON.parse(await readFile(p, "utf-8")) +} + +export async function readBytes(p: string): Promise { + return readFile(p) +} + +export async function readArrayBuffer(p: string): Promise { + const buf = await readFile(p) + return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) as ArrayBuffer +} + +function isEnoent(e: unknown): e is { code: "ENOENT" } { + return typeof e === "object" && e !== null && "code" in e && (e as { code: string }).code === "ENOENT" +} + +export async function write(p: string, content: string | Buffer | Uint8Array, mode?: number): Promise { + try { + if (mode) { + await writeFile(p, content, { mode }) + } else { + await writeFile(p, content) } - } - - export function stat(p: string): ReturnType | undefined { - return statSync(p, { throwIfNoEntry: false }) ?? undefined - } - - export async function statAsync(p: string): Promise | undefined> { - return statFile(p).catch((e) => { - if (isEnoent(e)) return undefined - throw e - }) - } - - export async function size(p: string): Promise { - const s = stat(p)?.size ?? 0 - return typeof s === "bigint" ? Number(s) : s - } - - export async function readText(p: string): Promise { - return readFile(p, "utf-8") - } - - export async function readJson(p: string): Promise { - return JSON.parse(await readFile(p, "utf-8")) - } - - export async function readBytes(p: string): Promise { - return readFile(p) - } - - export async function readArrayBuffer(p: string): Promise { - const buf = await readFile(p) - return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) as ArrayBuffer - } - - function isEnoent(e: unknown): e is { code: "ENOENT" } { - return typeof e === "object" && e !== null && "code" in e && (e as { code: string }).code === "ENOENT" - } - - export async function write(p: string, content: string | Buffer | Uint8Array, mode?: number): Promise { - try { + } catch (e) { + if (isEnoent(e)) { + await mkdir(dirname(p), { recursive: true }) if (mode) { await writeFile(p, content, { mode }) } else { await writeFile(p, content) } - } catch (e) { - if (isEnoent(e)) { - await mkdir(dirname(p), { recursive: true }) - if (mode) { - await writeFile(p, content, { mode }) - } else { - await writeFile(p, content) - } - return - } - throw e + return } - } - - export async function writeJson(p: string, data: unknown, mode?: number): Promise { - return write(p, JSON.stringify(data, null, 2), mode) - } - - export async function writeStream( - p: string, - stream: ReadableStream | Readable, - mode?: number, - ): Promise { - const dir = dirname(p) - if (!existsSync(dir)) { - await mkdir(dir, { recursive: true }) - } - - const nodeStream = stream instanceof ReadableStream ? Readable.fromWeb(stream as any) : stream - const writeStream = createWriteStream(p) - await pipeline(nodeStream, writeStream) - - if (mode) { - await chmod(p, mode) - } - } - - export function mimeType(p: string): string { - return lookup(p) || "application/octet-stream" - } - - /** - * On Windows, normalize a path to its canonical casing using the filesystem. - * This is needed because Windows paths are case-insensitive but LSP servers - * may return paths with different casing than what we send them. - */ - export function normalizePath(p: string): string { - if (process.platform !== "win32") return p - const resolved = win32.normalize(win32.resolve(windowsPath(p))) - try { - return realpathSync.native(resolved) - } catch { - return resolved - } - } - - export function normalizePathPattern(p: string): string { - if (process.platform !== "win32") return p - if (p === "*") return p - const match = p.match(/^(.*)[\\/]\*$/) - if (!match) return normalizePath(p) - const dir = /^[A-Za-z]:$/.test(match[1]) ? match[1] + "\\" : match[1] - return join(normalizePath(dir), "*") - } - - // We cannot rely on path.resolve() here because git.exe may come from Git Bash, Cygwin, or MSYS2, so we need to translate these paths at the boundary. - // Also resolves symlinks so that callers using the result as a cache key - // always get the same canonical path for a given physical directory. - export function resolve(p: string): string { - const resolved = pathResolve(windowsPath(p)) - try { - return normalizePath(realpathSync(resolved)) - } catch (e) { - if (isEnoent(e)) return normalizePath(resolved) - throw e - } - } - - export function windowsPath(p: string): string { - if (process.platform !== "win32") return p - return ( - p - .replace(/^\/([a-zA-Z]):(?:[\\/]|$)/, (_, drive) => `${drive.toUpperCase()}:/`) - // Git Bash for Windows paths are typically //... - .replace(/^\/([a-zA-Z])(?:\/|$)/, (_, drive) => `${drive.toUpperCase()}:/`) - // Cygwin git paths are typically /cygdrive//... - .replace(/^\/cygdrive\/([a-zA-Z])(?:\/|$)/, (_, drive) => `${drive.toUpperCase()}:/`) - // WSL paths are typically /mnt//... - .replace(/^\/mnt\/([a-zA-Z])(?:\/|$)/, (_, drive) => `${drive.toUpperCase()}:/`) - ) - } - export function overlaps(a: string, b: string) { - const relA = relative(a, b) - const relB = relative(b, a) - return !relA || !relA.startsWith("..") || !relB || !relB.startsWith("..") - } - - export function contains(parent: string, child: string) { - return !relative(parent, child).startsWith("..") - } - - export async function findUp( - target: string, - start: string, - stop?: string, - options?: { rootFirst?: boolean }, - ): Promise - export async function findUp( - target: string[], - start: string, - stop?: string, - options?: { rootFirst?: boolean }, - ): Promise - export async function findUp( - target: string | string[], - start: string, - stop?: string, - options?: { rootFirst?: boolean }, - ) { - const dirs = [start] - let current = start - while (true) { - if (stop === current) break - const parent = dirname(current) - if (parent === current) break - dirs.push(parent) - current = parent - } - - const targets = Array.isArray(target) ? target : [target] - const result = [] - for (const dir of options?.rootFirst ? dirs.toReversed() : dirs) { - for (const item of targets) { - const search = join(dir, item) - if (await exists(search)) result.push(search) - } - } - return result - } - - export async function* up(options: { targets: string[]; start: string; stop?: string }) { - const { targets, start, stop } = options - let current = start - while (true) { - for (const target of targets) { - const search = join(current, target) - if (await exists(search)) yield search - } - if (stop === current) break - const parent = dirname(current) - if (parent === current) break - current = parent - } - } - - export async function globUp(pattern: string, start: string, stop?: string) { - let current = start - const result = [] - while (true) { - try { - const matches = await Glob.scan(pattern, { - cwd: current, - absolute: true, - include: "file", - dot: true, - }) - result.push(...matches) - } catch { - // Skip invalid glob patterns - } - if (stop === current) break - const parent = dirname(current) - if (parent === current) break - current = parent - } - return result + throw e } } + +export async function writeJson(p: string, data: unknown, mode?: number): Promise { + return write(p, JSON.stringify(data, null, 2), mode) +} + +export async function writeStream( + p: string, + stream: ReadableStream | Readable, + mode?: number, +): Promise { + const dir = dirname(p) + if (!existsSync(dir)) { + await mkdir(dir, { recursive: true }) + } + + const nodeStream = stream instanceof ReadableStream ? Readable.fromWeb(stream as any) : stream + const writeStream = createWriteStream(p) + await pipeline(nodeStream, writeStream) + + if (mode) { + await chmod(p, mode) + } +} + +export function mimeType(p: string): string { + return lookup(p) || "application/octet-stream" +} + +/** + * On Windows, normalize a path to its canonical casing using the filesystem. + * This is needed because Windows paths are case-insensitive but LSP servers + * may return paths with different casing than what we send them. + */ +export function normalizePath(p: string): string { + if (process.platform !== "win32") return p + const resolved = win32.normalize(win32.resolve(windowsPath(p))) + try { + return realpathSync.native(resolved) + } catch { + return resolved + } +} + +export function normalizePathPattern(p: string): string { + if (process.platform !== "win32") return p + if (p === "*") return p + const match = p.match(/^(.*)[\\/]\*$/) + if (!match) return normalizePath(p) + const dir = /^[A-Za-z]:$/.test(match[1]) ? match[1] + "\\" : match[1] + return join(normalizePath(dir), "*") +} + +// We cannot rely on path.resolve() here because git.exe may come from Git Bash, Cygwin, or MSYS2, so we need to translate these paths at the boundary. +// Also resolves symlinks so that callers using the result as a cache key +// always get the same canonical path for a given physical directory. +export function resolve(p: string): string { + const resolved = pathResolve(windowsPath(p)) + try { + return normalizePath(realpathSync(resolved)) + } catch (e) { + if (isEnoent(e)) return normalizePath(resolved) + throw e + } +} + +export function windowsPath(p: string): string { + if (process.platform !== "win32") return p + return ( + p + .replace(/^\/([a-zA-Z]):(?:[\\/]|$)/, (_, drive) => `${drive.toUpperCase()}:/`) + // Git Bash for Windows paths are typically //... + .replace(/^\/([a-zA-Z])(?:\/|$)/, (_, drive) => `${drive.toUpperCase()}:/`) + // Cygwin git paths are typically /cygdrive//... + .replace(/^\/cygdrive\/([a-zA-Z])(?:\/|$)/, (_, drive) => `${drive.toUpperCase()}:/`) + // WSL paths are typically /mnt//... + .replace(/^\/mnt\/([a-zA-Z])(?:\/|$)/, (_, drive) => `${drive.toUpperCase()}:/`) + ) +} +export function overlaps(a: string, b: string) { + const relA = relative(a, b) + const relB = relative(b, a) + return !relA || !relA.startsWith("..") || !relB || !relB.startsWith("..") +} + +export function contains(parent: string, child: string) { + return !relative(parent, child).startsWith("..") +} + +export async function findUp( + target: string, + start: string, + stop?: string, + options?: { rootFirst?: boolean }, +): Promise +export async function findUp( + target: string[], + start: string, + stop?: string, + options?: { rootFirst?: boolean }, +): Promise +export async function findUp( + target: string | string[], + start: string, + stop?: string, + options?: { rootFirst?: boolean }, +) { + const dirs = [start] + let current = start + while (true) { + if (stop === current) break + const parent = dirname(current) + if (parent === current) break + dirs.push(parent) + current = parent + } + + const targets = Array.isArray(target) ? target : [target] + const result = [] + for (const dir of options?.rootFirst ? dirs.toReversed() : dirs) { + for (const item of targets) { + const search = join(dir, item) + if (await exists(search)) result.push(search) + } + } + return result +} + +export async function* up(options: { targets: string[]; start: string; stop?: string }) { + const { targets, start, stop } = options + let current = start + while (true) { + for (const target of targets) { + const search = join(current, target) + if (await exists(search)) yield search + } + if (stop === current) break + const parent = dirname(current) + if (parent === current) break + current = parent + } +} + +export async function globUp(pattern: string, start: string, stop?: string) { + let current = start + const result = [] + while (true) { + try { + const matches = await Glob.scan(pattern, { + cwd: current, + absolute: true, + include: "file", + dot: true, + }) + result.push(...matches) + } catch { + // Skip invalid glob patterns + } + if (stop === current) break + const parent = dirname(current) + if (parent === current) break + current = parent + } + return result +} diff --git a/packages/opencode/src/util/index.ts b/packages/opencode/src/util/index.ts index 157bb8e521..f051ad9649 100644 --- a/packages/opencode/src/util/index.ts +++ b/packages/opencode/src/util/index.ts @@ -1 +1,12 @@ export * as Archive from "./archive" +export * as Color from "./color" +export * as Filesystem from "./filesystem" +export * as Keybind from "./keybind" +export * as LocalContext from "./local-context" +export * as Locale from "./locale" +export * as Lock from "./lock" +export * as Log from "./log" +export * as Process from "./process" +export * as Rpc from "./rpc" +export * as Token from "./token" +export * as Wildcard from "./wildcard" diff --git a/packages/opencode/src/util/keybind.ts b/packages/opencode/src/util/keybind.ts index 83c7945ae1..10a68c4b2a 100644 --- a/packages/opencode/src/util/keybind.ts +++ b/packages/opencode/src/util/keybind.ts @@ -1,103 +1,101 @@ import { isDeepEqual } from "remeda" import type { ParsedKey } from "@opentui/core" -export namespace Keybind { - /** - * Keybind info derived from OpenTUI's ParsedKey with our custom `leader` field. - * This ensures type compatibility and catches missing fields at compile time. - */ - export type Info = Pick & { - leader: boolean // our custom field - } +/** + * Keybind info derived from OpenTUI's ParsedKey with our custom `leader` field. + * This ensures type compatibility and catches missing fields at compile time. + */ +export type Info = Pick & { + leader: boolean // our custom field +} - export function match(a: Info | undefined, b: Info): boolean { - if (!a) return false - const normalizedA = { ...a, super: a.super ?? false } - const normalizedB = { ...b, super: b.super ?? false } - return isDeepEqual(normalizedA, normalizedB) - } +export function match(a: Info | undefined, b: Info): boolean { + if (!a) return false + const normalizedA = { ...a, super: a.super ?? false } + const normalizedB = { ...b, super: b.super ?? false } + return isDeepEqual(normalizedA, normalizedB) +} - /** - * Convert OpenTUI's ParsedKey to our Keybind.Info format. - * This helper ensures all required fields are present and avoids manual object creation. - */ - export function fromParsedKey(key: ParsedKey, leader = false): Info { - return { - name: key.name === " " ? "space" : key.name, - ctrl: key.ctrl, - meta: key.meta, - shift: key.shift, - super: key.super ?? false, - leader, - } - } - - export function toString(info: Info | undefined): string { - if (!info) return "" - const parts: string[] = [] - - if (info.ctrl) parts.push("ctrl") - if (info.meta) parts.push("alt") - if (info.super) parts.push("super") - if (info.shift) parts.push("shift") - if (info.name) { - if (info.name === "delete") parts.push("del") - else parts.push(info.name) - } - - let result = parts.join("+") - - if (info.leader) { - result = result ? ` ${result}` : `` - } - - return result - } - - export function parse(key: string): Info[] { - if (key === "none") return [] - - return key.split(",").map((combo) => { - // Handle syntax by replacing with leader+ - const normalized = combo.replace(//g, "leader+") - const parts = normalized.toLowerCase().split("+") - const info: Info = { - ctrl: false, - meta: false, - shift: false, - leader: false, - name: "", - } - - for (const part of parts) { - switch (part) { - case "ctrl": - info.ctrl = true - break - case "alt": - case "meta": - case "option": - info.meta = true - break - case "super": - info.super = true - break - case "shift": - info.shift = true - break - case "leader": - info.leader = true - break - case "esc": - info.name = "escape" - break - default: - info.name = part - break - } - } - - return info - }) +/** + * Convert OpenTUI's ParsedKey to our Keybind.Info format. + * This helper ensures all required fields are present and avoids manual object creation. + */ +export function fromParsedKey(key: ParsedKey, leader = false): Info { + return { + name: key.name === " " ? "space" : key.name, + ctrl: key.ctrl, + meta: key.meta, + shift: key.shift, + super: key.super ?? false, + leader, } } + +export function toString(info: Info | undefined): string { + if (!info) return "" + const parts: string[] = [] + + if (info.ctrl) parts.push("ctrl") + if (info.meta) parts.push("alt") + if (info.super) parts.push("super") + if (info.shift) parts.push("shift") + if (info.name) { + if (info.name === "delete") parts.push("del") + else parts.push(info.name) + } + + let result = parts.join("+") + + if (info.leader) { + result = result ? ` ${result}` : `` + } + + return result +} + +export function parse(key: string): Info[] { + if (key === "none") return [] + + return key.split(",").map((combo) => { + // Handle syntax by replacing with leader+ + const normalized = combo.replace(//g, "leader+") + const parts = normalized.toLowerCase().split("+") + const info: Info = { + ctrl: false, + meta: false, + shift: false, + leader: false, + name: "", + } + + for (const part of parts) { + switch (part) { + case "ctrl": + info.ctrl = true + break + case "alt": + case "meta": + case "option": + info.meta = true + break + case "super": + info.super = true + break + case "shift": + info.shift = true + break + case "leader": + info.leader = true + break + case "esc": + info.name = "escape" + break + default: + info.name = part + break + } + } + + return info + }) +} diff --git a/packages/opencode/src/util/local-context.ts b/packages/opencode/src/util/local-context.ts index 26f88ab09e..c1aef946f4 100644 --- a/packages/opencode/src/util/local-context.ts +++ b/packages/opencode/src/util/local-context.ts @@ -1,25 +1,23 @@ import { AsyncLocalStorage } from "async_hooks" -export namespace LocalContext { - export class NotFound extends Error { - constructor(public override readonly name: string) { - super(`No context found for ${name}`) - } - } - - export function create(name: string) { - const storage = new AsyncLocalStorage() - return { - use() { - const result = storage.getStore() - if (!result) { - throw new NotFound(name) - } - return result - }, - provide(value: T, fn: () => R) { - return storage.run(value, fn) - }, - } +export class NotFound extends Error { + constructor(public override readonly name: string) { + super(`No context found for ${name}`) + } +} + +export function create(name: string) { + const storage = new AsyncLocalStorage() + return { + use() { + const result = storage.getStore() + if (!result) { + throw new NotFound(name) + } + return result + }, + provide(value: T, fn: () => R) { + return storage.run(value, fn) + }, } } diff --git a/packages/opencode/src/util/locale.ts b/packages/opencode/src/util/locale.ts index 653da09a0b..202e856b2e 100644 --- a/packages/opencode/src/util/locale.ts +++ b/packages/opencode/src/util/locale.ts @@ -1,81 +1,79 @@ -export namespace Locale { - export function titlecase(str: string) { - return str.replace(/\b\w/g, (c) => c.toUpperCase()) - } +export function titlecase(str: string) { + return str.replace(/\b\w/g, (c) => c.toUpperCase()) +} - export function time(input: number): string { - const date = new Date(input) - return date.toLocaleTimeString(undefined, { timeStyle: "short" }) - } +export function time(input: number): string { + const date = new Date(input) + return date.toLocaleTimeString(undefined, { timeStyle: "short" }) +} - export function datetime(input: number): string { - const date = new Date(input) - const localTime = time(input) - const localDate = date.toLocaleDateString() - return `${localTime} · ${localDate}` - } +export function datetime(input: number): string { + const date = new Date(input) + const localTime = time(input) + const localDate = date.toLocaleDateString() + return `${localTime} · ${localDate}` +} - export function todayTimeOrDateTime(input: number): string { - const date = new Date(input) - const now = new Date() - const isToday = - date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth() && date.getDate() === now.getDate() +export function todayTimeOrDateTime(input: number): string { + const date = new Date(input) + const now = new Date() + const isToday = + date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth() && date.getDate() === now.getDate() - if (isToday) { - return time(input) - } else { - return datetime(input) - } - } - - export function number(num: number): string { - if (num >= 1000000) { - return (num / 1000000).toFixed(1) + "M" - } else if (num >= 1000) { - return (num / 1000).toFixed(1) + "K" - } - return num.toString() - } - - export function duration(input: number) { - if (input < 1000) { - return `${input}ms` - } - if (input < 60000) { - return `${(input / 1000).toFixed(1)}s` - } - if (input < 3600000) { - const minutes = Math.floor(input / 60000) - const seconds = Math.floor((input % 60000) / 1000) - return `${minutes}m ${seconds}s` - } - if (input < 86400000) { - const hours = Math.floor(input / 3600000) - const minutes = Math.floor((input % 3600000) / 60000) - return `${hours}h ${minutes}m` - } - const hours = Math.floor(input / 3600000) - const days = Math.floor((input % 3600000) / 86400000) - return `${days}d ${hours}h` - } - - export function truncate(str: string, len: number): string { - if (str.length <= len) return str - return str.slice(0, len - 1) + "…" - } - - export function truncateMiddle(str: string, maxLength: number = 35): string { - if (str.length <= maxLength) return str - - const ellipsis = "…" - const keepStart = Math.ceil((maxLength - ellipsis.length) / 2) - const keepEnd = Math.floor((maxLength - ellipsis.length) / 2) - - return str.slice(0, keepStart) + ellipsis + str.slice(-keepEnd) - } - - export function pluralize(count: number, singular: string, plural: string): string { - const template = count === 1 ? singular : plural - return template.replace("{}", count.toString()) + if (isToday) { + return time(input) + } else { + return datetime(input) } } + +export function number(num: number): string { + if (num >= 1000000) { + return (num / 1000000).toFixed(1) + "M" + } else if (num >= 1000) { + return (num / 1000).toFixed(1) + "K" + } + return num.toString() +} + +export function duration(input: number) { + if (input < 1000) { + return `${input}ms` + } + if (input < 60000) { + return `${(input / 1000).toFixed(1)}s` + } + if (input < 3600000) { + const minutes = Math.floor(input / 60000) + const seconds = Math.floor((input % 60000) / 1000) + return `${minutes}m ${seconds}s` + } + if (input < 86400000) { + const hours = Math.floor(input / 3600000) + const minutes = Math.floor((input % 3600000) / 60000) + return `${hours}h ${minutes}m` + } + const hours = Math.floor(input / 3600000) + const days = Math.floor((input % 3600000) / 86400000) + return `${days}d ${hours}h` +} + +export function truncate(str: string, len: number): string { + if (str.length <= len) return str + return str.slice(0, len - 1) + "…" +} + +export function truncateMiddle(str: string, maxLength: number = 35): string { + if (str.length <= maxLength) return str + + const ellipsis = "…" + const keepStart = Math.ceil((maxLength - ellipsis.length) / 2) + const keepEnd = Math.floor((maxLength - ellipsis.length) / 2) + + return str.slice(0, keepStart) + ellipsis + str.slice(-keepEnd) +} + +export function pluralize(count: number, singular: string, plural: string): string { + const template = count === 1 ? singular : plural + return template.replace("{}", count.toString()) +} diff --git a/packages/opencode/src/util/lock.ts b/packages/opencode/src/util/lock.ts index 3aea64394f..3f8e609378 100644 --- a/packages/opencode/src/util/lock.ts +++ b/packages/opencode/src/util/lock.ts @@ -1,54 +1,62 @@ -export namespace Lock { - const locks = new Map< - string, - { - readers: number - writer: boolean - waitingReaders: (() => void)[] - waitingWriters: (() => void)[] - } - >() +const locks = new Map< + string, + { + readers: number + writer: boolean + waitingReaders: (() => void)[] + waitingWriters: (() => void)[] + } +>() - function get(key: string) { - if (!locks.has(key)) { - locks.set(key, { - readers: 0, - writer: false, - waitingReaders: [], - waitingWriters: [], +function get(key: string) { + if (!locks.has(key)) { + locks.set(key, { + readers: 0, + writer: false, + waitingReaders: [], + waitingWriters: [], + }) + } + return locks.get(key)! +} + +function process(key: string) { + const lock = locks.get(key) + if (!lock || lock.writer || lock.readers > 0) return + + // Prioritize writers to prevent starvation + if (lock.waitingWriters.length > 0) { + const nextWriter = lock.waitingWriters.shift()! + nextWriter() + return + } + + // Wake up all waiting readers + while (lock.waitingReaders.length > 0) { + const nextReader = lock.waitingReaders.shift()! + nextReader() + } + + // Clean up empty locks + if (lock.readers === 0 && !lock.writer && lock.waitingReaders.length === 0 && lock.waitingWriters.length === 0) { + locks.delete(key) + } +} + +export async function read(key: string): Promise { + const lock = get(key) + + return new Promise((resolve) => { + if (!lock.writer && lock.waitingWriters.length === 0) { + lock.readers++ + resolve({ + [Symbol.dispose]: () => { + lock.readers-- + process(key) + }, }) - } - return locks.get(key)! - } - - function process(key: string) { - const lock = locks.get(key) - if (!lock || lock.writer || lock.readers > 0) return - - // Prioritize writers to prevent starvation - if (lock.waitingWriters.length > 0) { - const nextWriter = lock.waitingWriters.shift()! - nextWriter() - return - } - - // Wake up all waiting readers - while (lock.waitingReaders.length > 0) { - const nextReader = lock.waitingReaders.shift()! - nextReader() - } - - // Clean up empty locks - if (lock.readers === 0 && !lock.writer && lock.waitingReaders.length === 0 && lock.waitingWriters.length === 0) { - locks.delete(key) - } - } - - export async function read(key: string): Promise { - const lock = get(key) - - return new Promise((resolve) => { - if (!lock.writer && lock.waitingWriters.length === 0) { + } else { + lock.waitingReaders.push(() => { lock.readers++ resolve({ [Symbol.dispose]: () => { @@ -56,25 +64,25 @@ export namespace Lock { process(key) }, }) - } else { - lock.waitingReaders.push(() => { - lock.readers++ - resolve({ - [Symbol.dispose]: () => { - lock.readers-- - process(key) - }, - }) - }) - } - }) - } + }) + } + }) +} - export async function write(key: string): Promise { - const lock = get(key) +export async function write(key: string): Promise { + const lock = get(key) - return new Promise((resolve) => { - if (!lock.writer && lock.readers === 0) { + return new Promise((resolve) => { + if (!lock.writer && lock.readers === 0) { + lock.writer = true + resolve({ + [Symbol.dispose]: () => { + lock.writer = false + process(key) + }, + }) + } else { + lock.waitingWriters.push(() => { lock.writer = true resolve({ [Symbol.dispose]: () => { @@ -82,17 +90,7 @@ export namespace Lock { process(key) }, }) - } else { - lock.waitingWriters.push(() => { - lock.writer = true - resolve({ - [Symbol.dispose]: () => { - lock.writer = false - process(key) - }, - }) - }) - } - }) - } + }) + } + }) } diff --git a/packages/opencode/src/util/log.ts b/packages/opencode/src/util/log.ts index 7812632768..6be9816a87 100644 --- a/packages/opencode/src/util/log.ts +++ b/packages/opencode/src/util/log.ts @@ -5,183 +5,181 @@ import { Global } from "../global" import z from "zod" import { Glob } from "@opencode-ai/shared/util/glob" -export namespace Log { - export const Level = z.enum(["DEBUG", "INFO", "WARN", "ERROR"]).meta({ ref: "LogLevel", description: "Log level" }) - export type Level = z.infer +export const Level = z.enum(["DEBUG", "INFO", "WARN", "ERROR"]).meta({ ref: "LogLevel", description: "Log level" }) +export type Level = z.infer - const levelPriority: Record = { - DEBUG: 0, - INFO: 1, - WARN: 2, - ERROR: 3, - } - const keep = 10 +const levelPriority: Record = { + DEBUG: 0, + INFO: 1, + WARN: 2, + ERROR: 3, +} +const keep = 10 - let level: Level = "INFO" +let level: Level = "INFO" - function shouldLog(input: Level): boolean { - return levelPriority[input] >= levelPriority[level] - } +function shouldLog(input: Level): boolean { + return levelPriority[input] >= levelPriority[level] +} - export type Logger = { - debug(message?: any, extra?: Record): void - info(message?: any, extra?: Record): void - error(message?: any, extra?: Record): void - warn(message?: any, extra?: Record): void - tag(key: string, value: string): Logger - clone(): Logger - time( - message: string, - extra?: Record, - ): { - stop(): void - [Symbol.dispose](): void - } - } - - const loggers = new Map() - - export const Default = create({ service: "default" }) - - export interface Options { - print: boolean - dev?: boolean - level?: Level - } - - let logpath = "" - export function file() { - return logpath - } - let write = (msg: any) => { - process.stderr.write(msg) - return msg.length - } - - export async function init(options: Options) { - if (options.level) level = options.level - cleanup(Global.Path.log) - if (options.print) return - logpath = path.join( - Global.Path.log, - options.dev ? "dev.log" : new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log", - ) - await fs.truncate(logpath).catch(() => {}) - const stream = createWriteStream(logpath, { flags: "a" }) - write = async (msg: any) => { - return new Promise((resolve, reject) => { - stream.write(msg, (err) => { - if (err) reject(err) - else resolve(msg.length) - }) - }) - } - } - - async function cleanup(dir: string) { - const files = ( - await Glob.scan("????-??-??T??????.log", { - cwd: dir, - absolute: false, - include: "file", - }).catch(() => []) - ) - .filter((file) => path.basename(file) === file) - .sort() - if (files.length <= keep) return - - const doomed = files.slice(0, -keep) - await Promise.all(doomed.map((file) => fs.unlink(path.join(dir, file)).catch(() => {}))) - } - - function formatError(error: Error, depth = 0): string { - const result = error.message - return error.cause instanceof Error && depth < 10 - ? result + " Caused by: " + formatError(error.cause, depth + 1) - : result - } - - let last = Date.now() - export function create(tags?: Record) { - tags = tags || {} - - const service = tags["service"] - if (service && typeof service === "string") { - const cached = loggers.get(service) - if (cached) { - return cached - } - } - - function build(message: any, extra?: Record) { - const prefix = Object.entries({ - ...tags, - ...extra, - }) - .filter(([_, value]) => value !== undefined && value !== null) - .map(([key, value]) => { - const prefix = `${key}=` - if (value instanceof Error) return prefix + formatError(value) - if (typeof value === "object") return prefix + JSON.stringify(value) - return prefix + value - }) - .join(" ") - const next = new Date() - const diff = next.getTime() - last - last = next.getTime() - return [next.toISOString().split(".")[0], "+" + diff + "ms", prefix, message].filter(Boolean).join(" ") + "\n" - } - const result: Logger = { - debug(message?: any, extra?: Record) { - if (shouldLog("DEBUG")) { - write("DEBUG " + build(message, extra)) - } - }, - info(message?: any, extra?: Record) { - if (shouldLog("INFO")) { - write("INFO " + build(message, extra)) - } - }, - error(message?: any, extra?: Record) { - if (shouldLog("ERROR")) { - write("ERROR " + build(message, extra)) - } - }, - warn(message?: any, extra?: Record) { - if (shouldLog("WARN")) { - write("WARN " + build(message, extra)) - } - }, - tag(key: string, value: string) { - if (tags) tags[key] = value - return result - }, - clone() { - return Log.create({ ...tags }) - }, - time(message: string, extra?: Record) { - const now = Date.now() - result.info(message, { status: "started", ...extra }) - function stop() { - result.info(message, { - status: "completed", - duration: Date.now() - now, - ...extra, - }) - } - return { - stop, - [Symbol.dispose]() { - stop() - }, - } - }, - } - - if (service && typeof service === "string") { - loggers.set(service, result) - } - - return result +export type Logger = { + debug(message?: any, extra?: Record): void + info(message?: any, extra?: Record): void + error(message?: any, extra?: Record): void + warn(message?: any, extra?: Record): void + tag(key: string, value: string): Logger + clone(): Logger + time( + message: string, + extra?: Record, + ): { + stop(): void + [Symbol.dispose](): void } } + +const loggers = new Map() + +export const Default = create({ service: "default" }) + +export interface Options { + print: boolean + dev?: boolean + level?: Level +} + +let logpath = "" +export function file() { + return logpath +} +let write = (msg: any) => { + process.stderr.write(msg) + return msg.length +} + +export async function init(options: Options) { + if (options.level) level = options.level + cleanup(Global.Path.log) + if (options.print) return + logpath = path.join( + Global.Path.log, + options.dev ? "dev.log" : new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log", + ) + await fs.truncate(logpath).catch(() => {}) + const stream = createWriteStream(logpath, { flags: "a" }) + write = async (msg: any) => { + return new Promise((resolve, reject) => { + stream.write(msg, (err) => { + if (err) reject(err) + else resolve(msg.length) + }) + }) + } +} + +async function cleanup(dir: string) { + const files = ( + await Glob.scan("????-??-??T??????.log", { + cwd: dir, + absolute: false, + include: "file", + }).catch(() => []) + ) + .filter((file) => path.basename(file) === file) + .sort() + if (files.length <= keep) return + + const doomed = files.slice(0, -keep) + await Promise.all(doomed.map((file) => fs.unlink(path.join(dir, file)).catch(() => {}))) +} + +function formatError(error: Error, depth = 0): string { + const result = error.message + return error.cause instanceof Error && depth < 10 + ? result + " Caused by: " + formatError(error.cause, depth + 1) + : result +} + +let last = Date.now() +export function create(tags?: Record) { + tags = tags || {} + + const service = tags["service"] + if (service && typeof service === "string") { + const cached = loggers.get(service) + if (cached) { + return cached + } + } + + function build(message: any, extra?: Record) { + const prefix = Object.entries({ + ...tags, + ...extra, + }) + .filter(([_, value]) => value !== undefined && value !== null) + .map(([key, value]) => { + const prefix = `${key}=` + if (value instanceof Error) return prefix + formatError(value) + if (typeof value === "object") return prefix + JSON.stringify(value) + return prefix + value + }) + .join(" ") + const next = new Date() + const diff = next.getTime() - last + last = next.getTime() + return [next.toISOString().split(".")[0], "+" + diff + "ms", prefix, message].filter(Boolean).join(" ") + "\n" + } + const result: Logger = { + debug(message?: any, extra?: Record) { + if (shouldLog("DEBUG")) { + write("DEBUG " + build(message, extra)) + } + }, + info(message?: any, extra?: Record) { + if (shouldLog("INFO")) { + write("INFO " + build(message, extra)) + } + }, + error(message?: any, extra?: Record) { + if (shouldLog("ERROR")) { + write("ERROR " + build(message, extra)) + } + }, + warn(message?: any, extra?: Record) { + if (shouldLog("WARN")) { + write("WARN " + build(message, extra)) + } + }, + tag(key: string, value: string) { + if (tags) tags[key] = value + return result + }, + clone() { + return create({ ...tags }) + }, + time(message: string, extra?: Record) { + const now = Date.now() + result.info(message, { status: "started", ...extra }) + function stop() { + result.info(message, { + status: "completed", + duration: Date.now() - now, + ...extra, + }) + } + return { + stop, + [Symbol.dispose]() { + stop() + }, + } + }, + } + + if (service && typeof service === "string") { + loggers.set(service, result) + } + + return result +} diff --git a/packages/opencode/src/util/process.ts b/packages/opencode/src/util/process.ts index e45ceb4710..96c35e5d6a 100644 --- a/packages/opencode/src/util/process.ts +++ b/packages/opencode/src/util/process.ts @@ -3,174 +3,172 @@ import launch from "cross-spawn" import { buffer } from "node:stream/consumers" import { errorMessage } from "./error" -export namespace Process { - export type Stdio = "inherit" | "pipe" | "ignore" - export type Shell = boolean | string +export type Stdio = "inherit" | "pipe" | "ignore" +export type Shell = boolean | string - export interface Options { - cwd?: string - env?: NodeJS.ProcessEnv | null - stdin?: Stdio - stdout?: Stdio - stderr?: Stdio - shell?: Shell - abort?: AbortSignal - kill?: NodeJS.Signals | number - timeout?: number - } +export interface Options { + cwd?: string + env?: NodeJS.ProcessEnv | null + stdin?: Stdio + stdout?: Stdio + stderr?: Stdio + shell?: Shell + abort?: AbortSignal + kill?: NodeJS.Signals | number + timeout?: number +} - export interface RunOptions extends Omit { - nothrow?: boolean - } +export interface RunOptions extends Omit { + nothrow?: boolean +} - export interface Result { - code: number - stdout: Buffer - stderr: Buffer - } +export interface Result { + code: number + stdout: Buffer + stderr: Buffer +} - export interface TextResult extends Result { - text: string - } +export interface TextResult extends Result { + text: string +} - export class RunFailedError extends Error { - readonly cmd: string[] - readonly code: number - readonly stdout: Buffer - readonly stderr: Buffer +export class RunFailedError extends Error { + readonly cmd: string[] + readonly code: number + readonly stdout: Buffer + readonly stderr: Buffer - constructor(cmd: string[], code: number, stdout: Buffer, stderr: Buffer) { - const text = stderr.toString().trim() - super( - text - ? `Command failed with code ${code}: ${cmd.join(" ")}\n${text}` - : `Command failed with code ${code}: ${cmd.join(" ")}`, - ) - this.name = "ProcessRunFailedError" - this.cmd = [...cmd] - this.code = code - this.stdout = stdout - this.stderr = stderr - } - } - - export type Child = ChildProcess & { exited: Promise } - - export function spawn(cmd: string[], opts: Options = {}): Child { - if (cmd.length === 0) throw new Error("Command is required") - opts.abort?.throwIfAborted() - - const proc = launch(cmd[0], cmd.slice(1), { - cwd: opts.cwd, - shell: opts.shell, - env: opts.env === null ? {} : opts.env ? { ...process.env, ...opts.env } : undefined, - stdio: [opts.stdin ?? "ignore", opts.stdout ?? "ignore", opts.stderr ?? "ignore"], - windowsHide: process.platform === "win32", - }) - - let closed = false - let timer: ReturnType | undefined - - const abort = () => { - if (closed) return - if (proc.exitCode !== null || proc.signalCode !== null) return - closed = true - - proc.kill(opts.kill ?? "SIGTERM") - - const ms = opts.timeout ?? 5_000 - if (ms <= 0) return - timer = setTimeout(() => proc.kill("SIGKILL"), ms) - } - - const exited = new Promise((resolve, reject) => { - const done = () => { - opts.abort?.removeEventListener("abort", abort) - if (timer) clearTimeout(timer) - } - - proc.once("exit", (code, signal) => { - done() - resolve(code ?? (signal ? 1 : 0)) - }) - - proc.once("error", (error) => { - done() - reject(error) - }) - }) - void exited.catch(() => undefined) - - if (opts.abort) { - opts.abort.addEventListener("abort", abort, { once: true }) - if (opts.abort.aborted) abort() - } - - const child = proc as Child - child.exited = exited - return child - } - - export async function run(cmd: string[], opts: RunOptions = {}): Promise { - const proc = spawn(cmd, { - cwd: opts.cwd, - env: opts.env, - stdin: opts.stdin, - shell: opts.shell, - abort: opts.abort, - kill: opts.kill, - timeout: opts.timeout, - stdout: "pipe", - stderr: "pipe", - }) - - if (!proc.stdout || !proc.stderr) throw new Error("Process output not available") - - const out = await Promise.all([proc.exited, buffer(proc.stdout), buffer(proc.stderr)]) - .then(([code, stdout, stderr]) => ({ - code, - stdout, - stderr, - })) - .catch((err: unknown) => { - if (!opts.nothrow) throw err - return { - code: 1, - stdout: Buffer.alloc(0), - stderr: Buffer.from(errorMessage(err)), - } - }) - if (out.code === 0 || opts.nothrow) return out - throw new RunFailedError(cmd, out.code, out.stdout, out.stderr) - } - - // Duplicated in `packages/sdk/js/src/process.ts` because the SDK cannot import - // `opencode` without creating a cycle. Keep both copies in sync. - export async function stop(proc: ChildProcess) { - if (proc.exitCode !== null || proc.signalCode !== null) return - - if (process.platform !== "win32" || !proc.pid) { - proc.kill() - return - } - - const out = await run(["taskkill", "/pid", String(proc.pid), "/T", "/F"], { - nothrow: true, - }) - - if (out.code === 0) return - proc.kill() - } - - export async function text(cmd: string[], opts: RunOptions = {}): Promise { - const out = await run(cmd, opts) - return { - ...out, - text: out.stdout.toString(), - } - } - - export async function lines(cmd: string[], opts: RunOptions = {}): Promise { - return (await text(cmd, opts)).text.split(/\r?\n/).filter(Boolean) + constructor(cmd: string[], code: number, stdout: Buffer, stderr: Buffer) { + const text = stderr.toString().trim() + super( + text + ? `Command failed with code ${code}: ${cmd.join(" ")}\n${text}` + : `Command failed with code ${code}: ${cmd.join(" ")}`, + ) + this.name = "ProcessRunFailedError" + this.cmd = [...cmd] + this.code = code + this.stdout = stdout + this.stderr = stderr } } + +export type Child = ChildProcess & { exited: Promise } + +export function spawn(cmd: string[], opts: Options = {}): Child { + if (cmd.length === 0) throw new Error("Command is required") + opts.abort?.throwIfAborted() + + const proc = launch(cmd[0], cmd.slice(1), { + cwd: opts.cwd, + shell: opts.shell, + env: opts.env === null ? {} : opts.env ? { ...process.env, ...opts.env } : undefined, + stdio: [opts.stdin ?? "ignore", opts.stdout ?? "ignore", opts.stderr ?? "ignore"], + windowsHide: process.platform === "win32", + }) + + let closed = false + let timer: ReturnType | undefined + + const abort = () => { + if (closed) return + if (proc.exitCode !== null || proc.signalCode !== null) return + closed = true + + proc.kill(opts.kill ?? "SIGTERM") + + const ms = opts.timeout ?? 5_000 + if (ms <= 0) return + timer = setTimeout(() => proc.kill("SIGKILL"), ms) + } + + const exited = new Promise((resolve, reject) => { + const done = () => { + opts.abort?.removeEventListener("abort", abort) + if (timer) clearTimeout(timer) + } + + proc.once("exit", (code, signal) => { + done() + resolve(code ?? (signal ? 1 : 0)) + }) + + proc.once("error", (error) => { + done() + reject(error) + }) + }) + void exited.catch(() => undefined) + + if (opts.abort) { + opts.abort.addEventListener("abort", abort, { once: true }) + if (opts.abort.aborted) abort() + } + + const child = proc as Child + child.exited = exited + return child +} + +export async function run(cmd: string[], opts: RunOptions = {}): Promise { + const proc = spawn(cmd, { + cwd: opts.cwd, + env: opts.env, + stdin: opts.stdin, + shell: opts.shell, + abort: opts.abort, + kill: opts.kill, + timeout: opts.timeout, + stdout: "pipe", + stderr: "pipe", + }) + + if (!proc.stdout || !proc.stderr) throw new Error("Process output not available") + + const out = await Promise.all([proc.exited, buffer(proc.stdout), buffer(proc.stderr)]) + .then(([code, stdout, stderr]) => ({ + code, + stdout, + stderr, + })) + .catch((err: unknown) => { + if (!opts.nothrow) throw err + return { + code: 1, + stdout: Buffer.alloc(0), + stderr: Buffer.from(errorMessage(err)), + } + }) + if (out.code === 0 || opts.nothrow) return out + throw new RunFailedError(cmd, out.code, out.stdout, out.stderr) +} + +// Duplicated in `packages/sdk/js/src/process.ts` because the SDK cannot import +// `opencode` without creating a cycle. Keep both copies in sync. +export async function stop(proc: ChildProcess) { + if (proc.exitCode !== null || proc.signalCode !== null) return + + if (process.platform !== "win32" || !proc.pid) { + proc.kill() + return + } + + const out = await run(["taskkill", "/pid", String(proc.pid), "/T", "/F"], { + nothrow: true, + }) + + if (out.code === 0) return + proc.kill() +} + +export async function text(cmd: string[], opts: RunOptions = {}): Promise { + const out = await run(cmd, opts) + return { + ...out, + text: out.stdout.toString(), + } +} + +export async function lines(cmd: string[], opts: RunOptions = {}): Promise { + return (await text(cmd, opts)).text.split(/\r?\n/).filter(Boolean) +} diff --git a/packages/opencode/src/util/rpc.ts b/packages/opencode/src/util/rpc.ts index ebd8be40e4..98f3a09d46 100644 --- a/packages/opencode/src/util/rpc.ts +++ b/packages/opencode/src/util/rpc.ts @@ -1,66 +1,64 @@ -export namespace Rpc { - type Definition = { - [method: string]: (input: any) => any - } +type Definition = { + [method: string]: (input: any) => any +} - export function listen(rpc: Definition) { - onmessage = async (evt) => { - const parsed = JSON.parse(evt.data) - if (parsed.type === "rpc.request") { - const result = await rpc[parsed.method](parsed.input) - postMessage(JSON.stringify({ type: "rpc.result", result, id: parsed.id })) - } - } - } - - export function emit(event: string, data: unknown) { - postMessage(JSON.stringify({ type: "rpc.event", event, data })) - } - - export function client(target: { - postMessage: (data: string) => void | null - onmessage: ((this: Worker, ev: MessageEvent) => any) | null - }) { - const pending = new Map void>() - const listeners = new Map void>>() - let id = 0 - target.onmessage = async (evt) => { - const parsed = JSON.parse(evt.data) - if (parsed.type === "rpc.result") { - const resolve = pending.get(parsed.id) - if (resolve) { - resolve(parsed.result) - pending.delete(parsed.id) - } - } - if (parsed.type === "rpc.event") { - const handlers = listeners.get(parsed.event) - if (handlers) { - for (const handler of handlers) { - handler(parsed.data) - } - } - } - } - return { - call(method: Method, input: Parameters[0]): Promise> { - const requestId = id++ - return new Promise((resolve) => { - pending.set(requestId, resolve) - target.postMessage(JSON.stringify({ type: "rpc.request", method, input, id: requestId })) - }) - }, - on(event: string, handler: (data: Data) => void) { - let handlers = listeners.get(event) - if (!handlers) { - handlers = new Set() - listeners.set(event, handlers) - } - handlers.add(handler) - return () => { - handlers!.delete(handler) - } - }, +export function listen(rpc: Definition) { + onmessage = async (evt) => { + const parsed = JSON.parse(evt.data) + if (parsed.type === "rpc.request") { + const result = await rpc[parsed.method](parsed.input) + postMessage(JSON.stringify({ type: "rpc.result", result, id: parsed.id })) } } } + +export function emit(event: string, data: unknown) { + postMessage(JSON.stringify({ type: "rpc.event", event, data })) +} + +export function client(target: { + postMessage: (data: string) => void | null + onmessage: ((this: Worker, ev: MessageEvent) => any) | null +}) { + const pending = new Map void>() + const listeners = new Map void>>() + let id = 0 + target.onmessage = async (evt) => { + const parsed = JSON.parse(evt.data) + if (parsed.type === "rpc.result") { + const resolve = pending.get(parsed.id) + if (resolve) { + resolve(parsed.result) + pending.delete(parsed.id) + } + } + if (parsed.type === "rpc.event") { + const handlers = listeners.get(parsed.event) + if (handlers) { + for (const handler of handlers) { + handler(parsed.data) + } + } + } + } + return { + call(method: Method, input: Parameters[0]): Promise> { + const requestId = id++ + return new Promise((resolve) => { + pending.set(requestId, resolve) + target.postMessage(JSON.stringify({ type: "rpc.request", method, input, id: requestId })) + }) + }, + on(event: string, handler: (data: Data) => void) { + let handlers = listeners.get(event) + if (!handlers) { + handlers = new Set() + listeners.set(event, handlers) + } + handlers.add(handler) + return () => { + handlers!.delete(handler) + } + }, + } +} diff --git a/packages/opencode/src/util/token.ts b/packages/opencode/src/util/token.ts index cee5adc377..52951c4cf2 100644 --- a/packages/opencode/src/util/token.ts +++ b/packages/opencode/src/util/token.ts @@ -1,7 +1,5 @@ -export namespace Token { - const CHARS_PER_TOKEN = 4 +const CHARS_PER_TOKEN = 4 - export function estimate(input: string) { - return Math.max(0, Math.round((input || "").length / CHARS_PER_TOKEN)) - } +export function estimate(input: string) { + return Math.max(0, Math.round((input || "").length / CHARS_PER_TOKEN)) } diff --git a/packages/opencode/src/util/wildcard.ts b/packages/opencode/src/util/wildcard.ts index f54b6c85fd..0efb94e915 100644 --- a/packages/opencode/src/util/wildcard.ts +++ b/packages/opencode/src/util/wildcard.ts @@ -1,59 +1,57 @@ import { sortBy, pipe } from "remeda" -export namespace Wildcard { - export function match(str: string, pattern: string) { - if (str) str = str.replaceAll("\\", "/") - if (pattern) pattern = pattern.replaceAll("\\", "/") - let escaped = pattern - .replace(/[.+^${}()|[\]\\]/g, "\\$&") // escape special regex chars - .replace(/\*/g, ".*") // * becomes .* - .replace(/\?/g, ".") // ? becomes . +export function match(str: string, pattern: string) { + if (str) str = str.replaceAll("\\", "/") + if (pattern) pattern = pattern.replaceAll("\\", "/") + let escaped = pattern + .replace(/[.+^${}()|[\]\\]/g, "\\$&") // escape special regex chars + .replace(/\*/g, ".*") // * becomes .* + .replace(/\?/g, ".") // ? becomes . - // If pattern ends with " *" (space + wildcard), make the trailing part optional - // This allows "ls *" to match both "ls" and "ls -la" - if (escaped.endsWith(" .*")) { - escaped = escaped.slice(0, -3) + "( .*)?" - } - - const flags = process.platform === "win32" ? "si" : "s" - return new RegExp("^" + escaped + "$", flags).test(str) + // If pattern ends with " *" (space + wildcard), make the trailing part optional + // This allows "ls *" to match both "ls" and "ls -la" + if (escaped.endsWith(" .*")) { + escaped = escaped.slice(0, -3) + "( .*)?" } - export function all(input: string, patterns: Record) { - const sorted = pipe(patterns, Object.entries, sortBy([([key]) => key.length, "asc"], [([key]) => key, "asc"])) - let result = undefined - for (const [pattern, value] of sorted) { - if (match(input, pattern)) { - result = value - continue - } - } - return result - } - - export function allStructured(input: { head: string; tail: string[] }, patterns: Record) { - const sorted = pipe(patterns, Object.entries, sortBy([([key]) => key.length, "asc"], [([key]) => key, "asc"])) - let result = undefined - for (const [pattern, value] of sorted) { - const parts = pattern.split(/\s+/) - if (!match(input.head, parts[0])) continue - if (parts.length === 1 || matchSequence(input.tail, parts.slice(1))) { - result = value - continue - } - } - return result - } - - function matchSequence(items: string[], patterns: string[]): boolean { - if (patterns.length === 0) return true - const [pattern, ...rest] = patterns - if (pattern === "*") return matchSequence(items, rest) - for (let i = 0; i < items.length; i++) { - if (match(items[i], pattern) && matchSequence(items.slice(i + 1), rest)) { - return true - } - } - return false - } + const flags = process.platform === "win32" ? "si" : "s" + return new RegExp("^" + escaped + "$", flags).test(str) +} + +export function all(input: string, patterns: Record) { + const sorted = pipe(patterns, Object.entries, sortBy([([key]) => key.length, "asc"], [([key]) => key, "asc"])) + let result = undefined + for (const [pattern, value] of sorted) { + if (match(input, pattern)) { + result = value + continue + } + } + return result +} + +export function allStructured(input: { head: string; tail: string[] }, patterns: Record) { + const sorted = pipe(patterns, Object.entries, sortBy([([key]) => key.length, "asc"], [([key]) => key, "asc"])) + let result = undefined + for (const [pattern, value] of sorted) { + const parts = pattern.split(/\s+/) + if (!match(input.head, parts[0])) continue + if (parts.length === 1 || matchSequence(input.tail, parts.slice(1))) { + result = value + continue + } + } + return result +} + +function matchSequence(items: string[], patterns: string[]): boolean { + if (patterns.length === 0) return true + const [pattern, ...rest] = patterns + if (pattern === "*") return matchSequence(items, rest) + for (let i = 0; i < items.length; i++) { + if (match(items[i], pattern) && matchSequence(items.slice(i + 1), rest)) { + return true + } + } + return false } diff --git a/packages/opencode/src/worktree/worktree.ts b/packages/opencode/src/worktree/worktree.ts index fab9ce57fa..86ef95f0e6 100644 --- a/packages/opencode/src/worktree/worktree.ts +++ b/packages/opencode/src/worktree/worktree.ts @@ -7,7 +7,7 @@ import { Project } from "../project/project" import { Database, eq } from "../storage/db" import { ProjectTable } from "../project/project.sql" import type { ProjectID } from "../project/schema" -import { Log } from "../util/log" +import { Log } from "../util" import { Slug } from "@opencode-ai/shared/util/slug" import { errorMessage } from "../util/error" import { BusEvent } from "@/bus/bus-event" diff --git a/packages/opencode/test/cli/tui/plugin-loader.test.ts b/packages/opencode/test/cli/tui/plugin-loader.test.ts index 119517b10c..8446570cc3 100644 --- a/packages/opencode/test/cli/tui/plugin-loader.test.ts +++ b/packages/opencode/test/cli/tui/plugin-loader.test.ts @@ -6,7 +6,7 @@ import { tmpdir } from "../../fixture/fixture" import { createTuiPluginApi } from "../../fixture/tui-plugin" import { Global } from "../../../src/global" import { TuiConfig } from "../../../src/config/tui" -import { Filesystem } from "../../../src/util/filesystem" +import { Filesystem } from "../../../src/util" const { allThemes, addTheme } = await import("../../../src/cli/cmd/tui/context/theme") const { TuiPluginRuntime } = await import("../../../src/cli/cmd/tui/plugin/runtime") diff --git a/packages/opencode/test/cli/tui/thread.test.ts b/packages/opencode/test/cli/tui/thread.test.ts index 176c2575a3..1c5c7e65e4 100644 --- a/packages/opencode/test/cli/tui/thread.test.ts +++ b/packages/opencode/test/cli/tui/thread.test.ts @@ -3,7 +3,7 @@ import fs from "fs/promises" import path from "path" import { tmpdir } from "../../fixture/fixture" import * as App from "../../../src/cli/cmd/tui/app" -import { Rpc } from "../../../src/util/rpc" +import { Rpc } from "../../../src/util" import { UI } from "../../../src/cli/ui" import * as Timeout from "../../../src/util/timeout" import * as Network from "../../../src/cli/network" diff --git a/packages/opencode/test/config/agent-color.test.ts b/packages/opencode/test/config/agent-color.test.ts index d77782354c..bfa948619b 100644 --- a/packages/opencode/test/config/agent-color.test.ts +++ b/packages/opencode/test/config/agent-color.test.ts @@ -5,7 +5,7 @@ import { provideInstance, tmpdir } from "../fixture/fixture" import { Instance } from "../../src/project/instance" import { Config } from "../../src/config" import { Agent as AgentSvc } from "../../src/agent/agent" -import { Color } from "../../src/util/color" +import { Color } from "../../src/util" import { AppRuntime } from "../../src/effect/app-runtime" const load = () => AppRuntime.runPromise(Config.Service.use((svc) => svc.get())) diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 8cf410c3d2..bc9fe5b015 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -23,7 +23,7 @@ import fs from "fs/promises" import { pathToFileURL } from "url" import { Global } from "../../src/global" import { ProjectID } from "../../src/project/schema" -import { Filesystem } from "../../src/util/filesystem" +import { Filesystem } from "../../src/util" import * as Network from "../../src/util/network" import { Npm } from "../../src/npm" diff --git a/packages/opencode/test/config/tui.test.ts b/packages/opencode/test/config/tui.test.ts index 4767e94b01..c80905cd1d 100644 --- a/packages/opencode/test/config/tui.test.ts +++ b/packages/opencode/test/config/tui.test.ts @@ -6,7 +6,7 @@ import { Instance } from "../../src/project/instance" import { Config } from "../../src/config" import { TuiConfig } from "../../src/config/tui" import { Global } from "../../src/global" -import { Filesystem } from "../../src/util/filesystem" +import { Filesystem } from "../../src/util" import { AppRuntime } from "../../src/effect/app-runtime" const managedConfigDir = process.env.OPENCODE_TEST_MANAGED_CONFIG_DIR! diff --git a/packages/opencode/test/file/index.test.ts b/packages/opencode/test/file/index.test.ts index 877e2ae0a3..28fd2c8384 100644 --- a/packages/opencode/test/file/index.test.ts +++ b/packages/opencode/test/file/index.test.ts @@ -5,7 +5,7 @@ import path from "path" import fs from "fs/promises" import { File } from "../../src/file" import { Instance } from "../../src/project/instance" -import { Filesystem } from "../../src/util/filesystem" +import { Filesystem } from "../../src/util" import { provideInstance, tmpdir } from "../fixture/fixture" afterEach(async () => { diff --git a/packages/opencode/test/file/path-traversal.test.ts b/packages/opencode/test/file/path-traversal.test.ts index 1f2e45a6ad..1190053949 100644 --- a/packages/opencode/test/file/path-traversal.test.ts +++ b/packages/opencode/test/file/path-traversal.test.ts @@ -2,7 +2,7 @@ import { test, expect, describe } from "bun:test" import { Effect } from "effect" import path from "path" import fs from "fs/promises" -import { Filesystem } from "../../src/util/filesystem" +import { Filesystem } from "../../src/util" import { File } from "../../src/file" import { Instance } from "../../src/project/instance" import { provideInstance, tmpdir } from "../fixture/fixture" diff --git a/packages/opencode/test/file/time.test.ts b/packages/opencode/test/file/time.test.ts index 7f65d05ead..cb6390df87 100644 --- a/packages/opencode/test/file/time.test.ts +++ b/packages/opencode/test/file/time.test.ts @@ -6,7 +6,7 @@ import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" import { FileTime } from "../../src/file/time" import { Instance } from "../../src/project/instance" import { SessionID } from "../../src/session/schema" -import { Filesystem } from "../../src/util/filesystem" +import { Filesystem } from "../../src/util" import { provideInstance, provideTmpdirInstance, tmpdirScoped } from "../fixture/fixture" import { testEffect } from "../lib/effect" diff --git a/packages/opencode/test/fixture/plug-worker.ts b/packages/opencode/test/fixture/plug-worker.ts index e4b80c5dc5..c9afcd39f9 100644 --- a/packages/opencode/test/fixture/plug-worker.ts +++ b/packages/opencode/test/fixture/plug-worker.ts @@ -1,7 +1,7 @@ import path from "path" import { createPlugTask, type PlugCtx, type PlugDeps } from "../../src/cli/cmd/plug" -import { Filesystem } from "../../src/util/filesystem" +import { Filesystem } from "../../src/util" type Msg = { dir: string diff --git a/packages/opencode/test/keybind.test.ts b/packages/opencode/test/keybind.test.ts index 4ca1f1697e..1e900a6020 100644 --- a/packages/opencode/test/keybind.test.ts +++ b/packages/opencode/test/keybind.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect } from "bun:test" -import { Keybind } from "../src/util/keybind" +import { Keybind } from "../src/util" describe("Keybind.toString", () => { test("should convert simple key to string", () => { diff --git a/packages/opencode/test/lsp/client.test.ts b/packages/opencode/test/lsp/client.test.ts index c2ba3ac5b0..414d11f8e7 100644 --- a/packages/opencode/test/lsp/client.test.ts +++ b/packages/opencode/test/lsp/client.test.ts @@ -3,7 +3,7 @@ import path from "path" import { LSPClient } from "../../src/lsp/client" import { LSPServer } from "../../src/lsp/server" import { Instance } from "../../src/project/instance" -import { Log } from "../../src/util/log" +import { Log } from "../../src/util" // Minimal fake LSP server that speaks JSON-RPC over stdio function spawnFakeServer() { diff --git a/packages/opencode/test/plugin/install-concurrency.test.ts b/packages/opencode/test/plugin/install-concurrency.test.ts index cf3e8692e1..06ec2fc996 100644 --- a/packages/opencode/test/plugin/install-concurrency.test.ts +++ b/packages/opencode/test/plugin/install-concurrency.test.ts @@ -2,8 +2,8 @@ import { describe, expect, test } from "bun:test" import fs from "fs/promises" import path from "path" -import { Process } from "../../src/util/process" -import { Filesystem } from "../../src/util/filesystem" +import { Process } from "../../src/util" +import { Filesystem } from "../../src/util" import { tmpdir } from "../fixture/fixture" const root = path.join(import.meta.dir, "../..") diff --git a/packages/opencode/test/plugin/install.test.ts b/packages/opencode/test/plugin/install.test.ts index 5ce21c4cf4..f125f188a7 100644 --- a/packages/opencode/test/plugin/install.test.ts +++ b/packages/opencode/test/plugin/install.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test" import fs from "fs/promises" import path from "path" import { parse as parseJsonc } from "jsonc-parser" -import { Filesystem } from "../../src/util/filesystem" +import { Filesystem } from "../../src/util" import { createPlugTask, type PlugCtx, type PlugDeps } from "../../src/cli/cmd/plug" import { tmpdir } from "../fixture/fixture" diff --git a/packages/opencode/test/plugin/loader-shared.test.ts b/packages/opencode/test/plugin/loader-shared.test.ts index 4265e83c55..5072c1e748 100644 --- a/packages/opencode/test/plugin/loader-shared.test.ts +++ b/packages/opencode/test/plugin/loader-shared.test.ts @@ -4,7 +4,7 @@ import fs from "fs/promises" import path from "path" import { pathToFileURL } from "url" import { tmpdir } from "../fixture/fixture" -import { Filesystem } from "../../src/util/filesystem" +import { Filesystem } from "../../src/util" const disableDefault = process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS = "1" diff --git a/packages/opencode/test/plugin/meta.test.ts b/packages/opencode/test/plugin/meta.test.ts index 0571740667..3e2d4c6177 100644 --- a/packages/opencode/test/plugin/meta.test.ts +++ b/packages/opencode/test/plugin/meta.test.ts @@ -4,8 +4,8 @@ import path from "path" import { pathToFileURL } from "url" import { tmpdir } from "../fixture/fixture" -import { Process } from "../../src/util/process" -import { Filesystem } from "../../src/util/filesystem" +import { Process } from "../../src/util" +import { Filesystem } from "../../src/util" const { PluginMeta } = await import("../../src/plugin/meta") const root = path.join(import.meta.dir, "../..") diff --git a/packages/opencode/test/preload.ts b/packages/opencode/test/preload.ts index 0ddc797faf..ba5df4f1ea 100644 --- a/packages/opencode/test/preload.ts +++ b/packages/opencode/test/preload.ts @@ -78,7 +78,7 @@ delete process.env["OPENCODE_SERVER_USERNAME"] process.env["OPENCODE_DB"] = ":memory:" // Now safe to import from src/ -const { Log } = await import("../src/util/log") +const { Log } = await import("../src/util") const { initProjectors } = await import("../src/server/projectors") Log.init({ diff --git a/packages/opencode/test/project/migrate-global.test.ts b/packages/opencode/test/project/migrate-global.test.ts index d4313c12f1..d645fb25b8 100644 --- a/packages/opencode/test/project/migrate-global.test.ts +++ b/packages/opencode/test/project/migrate-global.test.ts @@ -5,7 +5,7 @@ import { SessionTable } from "../../src/session/session.sql" import { ProjectTable } from "../../src/project/project.sql" import { ProjectID } from "../../src/project/schema" import { SessionID } from "../../src/session/schema" -import { Log } from "../../src/util/log" +import { Log } from "../../src/util" import { $ } from "bun" import { tmpdir } from "../fixture/fixture" import { Effect } from "effect" diff --git a/packages/opencode/test/project/project.test.ts b/packages/opencode/test/project/project.test.ts index ba253a9205..a579a2335d 100644 --- a/packages/opencode/test/project/project.test.ts +++ b/packages/opencode/test/project/project.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" import { Project } from "../../src/project/project" -import { Log } from "../../src/util/log" +import { Log } from "../../src/util" import { $ } from "bun" import path from "path" import { tmpdir } from "../fixture/fixture" diff --git a/packages/opencode/test/provider/amazon-bedrock.test.ts b/packages/opencode/test/provider/amazon-bedrock.test.ts index 6809e4d17e..03f83601dd 100644 --- a/packages/opencode/test/provider/amazon-bedrock.test.ts +++ b/packages/opencode/test/provider/amazon-bedrock.test.ts @@ -8,7 +8,7 @@ import { Instance } from "../../src/project/instance" import { Provider } from "../../src/provider" import { Env } from "../../src/env" import { Global } from "../../src/global" -import { Filesystem } from "../../src/util/filesystem" +import { Filesystem } from "../../src/util" import { Effect } from "effect" import { AppRuntime } from "../../src/effect/app-runtime" import { makeRuntime } from "../../src/effect/run-service" diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index a6a93e8091..300a5b9031 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -9,7 +9,7 @@ import { Plugin } from "../../src/plugin/index" import { ModelsDev } from "../../src/provider/models" import { Provider } from "../../src/provider" import { ProviderID, ModelID } from "../../src/provider/schema" -import { Filesystem } from "../../src/util/filesystem" +import { Filesystem } from "../../src/util" import { Env } from "../../src/env" import { Effect } from "effect" import { AppRuntime } from "../../src/effect/app-runtime" diff --git a/packages/opencode/test/server/global-session-list.test.ts b/packages/opencode/test/server/global-session-list.test.ts index a1e374b4f7..c029fd9336 100644 --- a/packages/opencode/test/server/global-session-list.test.ts +++ b/packages/opencode/test/server/global-session-list.test.ts @@ -4,7 +4,7 @@ import z from "zod" import { Instance } from "../../src/project/instance" import { Project } from "../../src/project/project" import { Session as SessionNs } from "../../src/session" -import { Log } from "../../src/util/log" +import { Log } from "../../src/util" import { tmpdir } from "../fixture/fixture" Log.init({ print: false }) diff --git a/packages/opencode/test/server/project-init-git.test.ts b/packages/opencode/test/server/project-init-git.test.ts index 406b3d6d89..c3ee18e73a 100644 --- a/packages/opencode/test/server/project-init-git.test.ts +++ b/packages/opencode/test/server/project-init-git.test.ts @@ -5,8 +5,8 @@ import { GlobalBus } from "../../src/bus/global" import { Snapshot } from "../../src/snapshot" import { Instance } from "../../src/project/instance" import { Server } from "../../src/server/server" -import { Filesystem } from "../../src/util/filesystem" -import { Log } from "../../src/util/log" +import { Filesystem } from "../../src/util" +import { Log } from "../../src/util" import { resetDatabase } from "../fixture/db" import { provideInstance, tmpdir } from "../fixture/fixture" diff --git a/packages/opencode/test/server/session-actions.test.ts b/packages/opencode/test/server/session-actions.test.ts index 301691ae2f..3209ebff35 100644 --- a/packages/opencode/test/server/session-actions.test.ts +++ b/packages/opencode/test/server/session-actions.test.ts @@ -4,7 +4,7 @@ import { Instance } from "../../src/project/instance" import { Server } from "../../src/server/server" import { Session as SessionNs } from "../../src/session" import type { SessionID } from "../../src/session/schema" -import { Log } from "../../src/util/log" +import { Log } from "../../src/util" import { tmpdir } from "../fixture/fixture" Log.init({ print: false }) diff --git a/packages/opencode/test/server/session-list.test.ts b/packages/opencode/test/server/session-list.test.ts index 75adb7f9f3..9af60b9bdd 100644 --- a/packages/opencode/test/server/session-list.test.ts +++ b/packages/opencode/test/server/session-list.test.ts @@ -2,7 +2,7 @@ import { afterEach, describe, expect, test } from "bun:test" import { Effect } from "effect" import { Instance } from "../../src/project/instance" import { Session as SessionNs } from "../../src/session" -import { Log } from "../../src/util/log" +import { Log } from "../../src/util" import { tmpdir } from "../fixture/fixture" Log.init({ print: false }) diff --git a/packages/opencode/test/server/session-messages.test.ts b/packages/opencode/test/server/session-messages.test.ts index 24ee6a1b43..d558d4324f 100644 --- a/packages/opencode/test/server/session-messages.test.ts +++ b/packages/opencode/test/server/session-messages.test.ts @@ -5,7 +5,7 @@ import { Server } from "../../src/server/server" import { Session as SessionNs } from "../../src/session" import { MessageV2 } from "../../src/session/message-v2" import { MessageID, PartID, type SessionID } from "../../src/session/schema" -import { Log } from "../../src/util/log" +import { Log } from "../../src/util" import { tmpdir } from "../fixture/fixture" Log.init({ print: false }) diff --git a/packages/opencode/test/server/session-select.test.ts b/packages/opencode/test/server/session-select.test.ts index 12552538da..c53448dfd4 100644 --- a/packages/opencode/test/server/session-select.test.ts +++ b/packages/opencode/test/server/session-select.test.ts @@ -2,7 +2,7 @@ import { afterEach, describe, expect, test } from "bun:test" import { Effect } from "effect" import { Session as SessionNs } from "../../src/session" import type { SessionID } from "../../src/session/schema" -import { Log } from "../../src/util/log" +import { Log } from "../../src/util" import { Instance } from "../../src/project/instance" import { Server } from "../../src/server/server" import { tmpdir } from "../fixture/fixture" diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index 7711d31931..ee01932210 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/packages/opencode/test/session/compaction.test.ts @@ -8,9 +8,9 @@ import { Config } from "../../src/config" import { Agent } from "../../src/agent/agent" import { LLM } from "../../src/session/llm" import { SessionCompaction } from "../../src/session/compaction" -import { Token } from "../../src/util/token" +import { Token } from "../../src/util" import { Instance } from "../../src/project/instance" -import { Log } from "../../src/util/log" +import { Log } from "../../src/util" import { Permission } from "../../src/permission" import { Plugin } from "../../src/plugin" import { provideTmpdirInstance, tmpdir } from "../fixture/fixture" diff --git a/packages/opencode/test/session/llm.test.ts b/packages/opencode/test/session/llm.test.ts index f25ecc356a..d1d53f605b 100644 --- a/packages/opencode/test/session/llm.test.ts +++ b/packages/opencode/test/session/llm.test.ts @@ -10,7 +10,7 @@ import { Provider } from "../../src/provider" import { ProviderTransform } from "../../src/provider/transform" import { ModelsDev } from "../../src/provider/models" import { ProviderID, ModelID } from "../../src/provider/schema" -import { Filesystem } from "../../src/util/filesystem" +import { Filesystem } from "../../src/util" import { tmpdir } from "../fixture/fixture" import type { Agent } from "../../src/agent/agent" import { MessageV2 } from "../../src/session/message-v2" diff --git a/packages/opencode/test/session/messages-pagination.test.ts b/packages/opencode/test/session/messages-pagination.test.ts index f728bd3646..804076dd48 100644 --- a/packages/opencode/test/session/messages-pagination.test.ts +++ b/packages/opencode/test/session/messages-pagination.test.ts @@ -6,7 +6,7 @@ import { Session as SessionNs } from "../../src/session" import { MessageV2 } from "../../src/session/message-v2" import { MessageID, PartID, type SessionID } from "../../src/session/schema" import { ModelID, ProviderID } from "../../src/provider/schema" -import { Log } from "../../src/util/log" +import { Log } from "../../src/util" const root = path.join(__dirname, "../..") Log.init({ print: false }) diff --git a/packages/opencode/test/session/processor-effect.test.ts b/packages/opencode/test/session/processor-effect.test.ts index 982399d6d1..87ff40c707 100644 --- a/packages/opencode/test/session/processor-effect.test.ts +++ b/packages/opencode/test/session/processor-effect.test.ts @@ -18,7 +18,7 @@ import { MessageID, PartID, SessionID } from "../../src/session/schema" import { SessionStatus } from "../../src/session/status" import { SessionSummary } from "../../src/session/summary" import { Snapshot } from "../../src/snapshot" -import { Log } from "../../src/util/log" +import { Log } from "../../src/util" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" import { provideTmpdirServer } from "../fixture/fixture" import { testEffect } from "../lib/effect" diff --git a/packages/opencode/test/session/prompt-effect.test.ts b/packages/opencode/test/session/prompt-effect.test.ts index 5ff8bf3424..0a750352a7 100644 --- a/packages/opencode/test/session/prompt-effect.test.ts +++ b/packages/opencode/test/session/prompt-effect.test.ts @@ -36,7 +36,7 @@ import { Shell } from "../../src/shell/shell" import { Snapshot } from "../../src/snapshot" import { ToolRegistry } from "../../src/tool/registry" import { Truncate } from "../../src/tool/truncate" -import { Log } from "../../src/util/log" +import { Log } from "../../src/util" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" import { Ripgrep } from "../../src/file/ripgrep" import { Format } from "../../src/format" diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index 4f5b19bca0..acf305f3f9 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -8,7 +8,7 @@ import { ModelID, ProviderID } from "../../src/provider/schema" import { Session } from "../../src/session" import { MessageV2 } from "../../src/session/message-v2" import { SessionPrompt } from "../../src/session/prompt" -import { Log } from "../../src/util/log" +import { Log } from "../../src/util" import { tmpdir } from "../fixture/fixture" Log.init({ print: false }) diff --git a/packages/opencode/test/session/revert-compact.test.ts b/packages/opencode/test/session/revert-compact.test.ts index 679f6166ff..211fcde9a8 100644 --- a/packages/opencode/test/session/revert-compact.test.ts +++ b/packages/opencode/test/session/revert-compact.test.ts @@ -7,7 +7,7 @@ import { ModelID, ProviderID } from "../../src/provider/schema" import { SessionRevert } from "../../src/session/revert" import { MessageV2 } from "../../src/session/message-v2" import { Snapshot } from "../../src/snapshot" -import { Log } from "../../src/util/log" +import { Log } from "../../src/util" import { MessageID, PartID, SessionID } from "../../src/session/schema" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" import { provideTmpdirInstance } from "../fixture/fixture" diff --git a/packages/opencode/test/session/session.test.ts b/packages/opencode/test/session/session.test.ts index 15132a2701..9c4686cba6 100644 --- a/packages/opencode/test/session/session.test.ts +++ b/packages/opencode/test/session/session.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test" import path from "path" import { Session as SessionNs } from "../../src/session" import { Bus } from "../../src/bus" -import { Log } from "../../src/util/log" +import { Log } from "../../src/util" import { Instance } from "../../src/project/instance" import { MessageV2 } from "../../src/session/message-v2" import { MessageID, PartID, type SessionID } from "../../src/session/schema" diff --git a/packages/opencode/test/session/snapshot-tool-race.test.ts b/packages/opencode/test/session/snapshot-tool-race.test.ts index 3681b14f7a..cb7fe4568e 100644 --- a/packages/opencode/test/session/snapshot-tool-race.test.ts +++ b/packages/opencode/test/session/snapshot-tool-race.test.ts @@ -22,7 +22,7 @@ import { SessionPrompt } from "../../src/session/prompt" import { SessionRevert } from "../../src/session/revert" import { SessionSummary } from "../../src/session/summary" import { MessageV2 } from "../../src/session/message-v2" -import { Log } from "../../src/util/log" +import { Log } from "../../src/util" import { provideTmpdirServer } from "../fixture/fixture" import { testEffect } from "../lib/effect" import { TestLLMServer } from "../lib/llm-server" diff --git a/packages/opencode/test/session/structured-output-integration.test.ts b/packages/opencode/test/session/structured-output-integration.test.ts index 64266de47a..346705bf22 100644 --- a/packages/opencode/test/session/structured-output-integration.test.ts +++ b/packages/opencode/test/session/structured-output-integration.test.ts @@ -3,7 +3,7 @@ import path from "path" import { Effect, Layer } from "effect" import { Session } from "../../src/session" import { SessionPrompt } from "../../src/session/prompt" -import { Log } from "../../src/util/log" +import { Log } from "../../src/util" import { Instance } from "../../src/project/instance" import { MessageV2 } from "../../src/session/message-v2" diff --git a/packages/opencode/test/shell/shell.test.ts b/packages/opencode/test/shell/shell.test.ts index 760d6dc05a..6d7a77d72d 100644 --- a/packages/opencode/test/shell/shell.test.ts +++ b/packages/opencode/test/shell/shell.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test" import path from "path" import { Shell } from "../../src/shell/shell" -import { Filesystem } from "../../src/util/filesystem" +import { Filesystem } from "../../src/util" const withShell = async (shell: string | undefined, fn: () => void | Promise) => { const prev = process.env.SHELL diff --git a/packages/opencode/test/skill/discovery.test.ts b/packages/opencode/test/skill/discovery.test.ts index de356ef154..175500862d 100644 --- a/packages/opencode/test/skill/discovery.test.ts +++ b/packages/opencode/test/skill/discovery.test.ts @@ -2,7 +2,7 @@ import { describe, test, expect, beforeAll, afterAll } from "bun:test" import { Effect } from "effect" import { Discovery } from "../../src/skill/discovery" import { Global } from "../../src/global" -import { Filesystem } from "../../src/util/filesystem" +import { Filesystem } from "../../src/util" import { rm } from "fs/promises" import path from "path" diff --git a/packages/opencode/test/snapshot/snapshot.test.ts b/packages/opencode/test/snapshot/snapshot.test.ts index 0c480a97c2..3330b497c3 100644 --- a/packages/opencode/test/snapshot/snapshot.test.ts +++ b/packages/opencode/test/snapshot/snapshot.test.ts @@ -5,7 +5,7 @@ import path from "path" import { Effect } from "effect" import { Snapshot } from "../../src/snapshot" import { Instance } from "../../src/project/instance" -import { Filesystem } from "../../src/util/filesystem" +import { Filesystem } from "../../src/util" import { provideInstance, tmpdir } from "../fixture/fixture" // Git always outputs /-separated paths internally. Snapshot.patch() joins them diff --git a/packages/opencode/test/tool/bash.test.ts b/packages/opencode/test/tool/bash.test.ts index 19135ba98b..6a3eac15e0 100644 --- a/packages/opencode/test/tool/bash.test.ts +++ b/packages/opencode/test/tool/bash.test.ts @@ -5,7 +5,7 @@ import path from "path" import { Shell } from "../../src/shell/shell" import { BashTool } from "../../src/tool/bash" import { Instance } from "../../src/project/instance" -import { Filesystem } from "../../src/util/filesystem" +import { Filesystem } from "../../src/util" import { tmpdir } from "../fixture/fixture" import type { Permission } from "../../src/permission" import { Agent } from "../../src/agent/agent" diff --git a/packages/opencode/test/tool/external-directory.test.ts b/packages/opencode/test/tool/external-directory.test.ts index 727ab74f18..ee8cb53963 100644 --- a/packages/opencode/test/tool/external-directory.test.ts +++ b/packages/opencode/test/tool/external-directory.test.ts @@ -4,7 +4,7 @@ import { Effect } from "effect" import type { Tool } from "../../src/tool/tool" import { Instance } from "../../src/project/instance" import { assertExternalDirectory } from "../../src/tool/external-directory" -import { Filesystem } from "../../src/util/filesystem" +import { Filesystem } from "../../src/util" import { tmpdir } from "../fixture/fixture" import type { Permission } from "../../src/permission" import { SessionID, MessageID } from "../../src/session/schema" diff --git a/packages/opencode/test/tool/read.test.ts b/packages/opencode/test/tool/read.test.ts index f14ec33105..fa65068f86 100644 --- a/packages/opencode/test/tool/read.test.ts +++ b/packages/opencode/test/tool/read.test.ts @@ -13,7 +13,7 @@ import { Instruction } from "../../src/session/instruction" import { ReadTool } from "../../src/tool/read" import { Truncate } from "../../src/tool/truncate" import { Tool } from "../../src/tool/tool" -import { Filesystem } from "../../src/util/filesystem" +import { Filesystem } from "../../src/util" import { provideInstance, tmpdirScoped } from "../fixture/fixture" import { testEffect } from "../lib/effect" diff --git a/packages/opencode/test/tool/truncation.test.ts b/packages/opencode/test/tool/truncation.test.ts index c9ef0d82a3..d0873046d6 100644 --- a/packages/opencode/test/tool/truncation.test.ts +++ b/packages/opencode/test/tool/truncation.test.ts @@ -3,8 +3,8 @@ import { NodeFileSystem } from "@effect/platform-node" import { Effect, FileSystem, Layer } from "effect" import { Truncate } from "../../src/tool/truncate" import { Identifier } from "../../src/id/id" -import { Process } from "../../src/util/process" -import { Filesystem } from "../../src/util/filesystem" +import { Process } from "../../src/util" +import { Filesystem } from "../../src/util" import path from "path" import { testEffect } from "../lib/effect" import { writeFileStringScoped } from "../lib/filesystem" diff --git a/packages/opencode/test/util/filesystem.test.ts b/packages/opencode/test/util/filesystem.test.ts index 3abcf011bc..1f3a66b950 100644 --- a/packages/opencode/test/util/filesystem.test.ts +++ b/packages/opencode/test/util/filesystem.test.ts @@ -1,7 +1,7 @@ import { describe, test, expect } from "bun:test" import path from "path" import fs from "fs/promises" -import { Filesystem } from "../../src/util/filesystem" +import { Filesystem } from "../../src/util" import { tmpdir } from "../fixture/fixture" describe("filesystem", () => { diff --git a/packages/opencode/test/util/lock.test.ts b/packages/opencode/test/util/lock.test.ts index b877311e39..d51b936484 100644 --- a/packages/opencode/test/util/lock.test.ts +++ b/packages/opencode/test/util/lock.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { Lock } from "../../src/util/lock" +import { Lock } from "../../src/util" function tick() { return new Promise((r) => queueMicrotask(r)) diff --git a/packages/opencode/test/util/log.test.ts b/packages/opencode/test/util/log.test.ts index 33e64fcd01..336b16a17b 100644 --- a/packages/opencode/test/util/log.test.ts +++ b/packages/opencode/test/util/log.test.ts @@ -2,7 +2,7 @@ import { afterEach, expect, test } from "bun:test" import fs from "fs/promises" import path from "path" import { Global } from "../../src/global" -import { Log } from "../../src/util/log" +import { Log } from "../../src/util" import { tmpdir } from "../fixture/fixture" const log = Global.Path.log diff --git a/packages/opencode/test/util/module.test.ts b/packages/opencode/test/util/module.test.ts index 6f8539bfb7..6725149c74 100644 --- a/packages/opencode/test/util/module.test.ts +++ b/packages/opencode/test/util/module.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test" import path from "path" import { Module } from "@opencode-ai/shared/util/module" -import { Filesystem } from "../../src/util/filesystem" +import { Filesystem } from "../../src/util" import { tmpdir } from "../fixture/fixture" describe("util.module", () => { diff --git a/packages/opencode/test/util/process.test.ts b/packages/opencode/test/util/process.test.ts index 1d08cba6b7..5442025700 100644 --- a/packages/opencode/test/util/process.test.ts +++ b/packages/opencode/test/util/process.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test" import fs from "fs/promises" import path from "path" -import { Process } from "../../src/util/process" +import { Process } from "../../src/util" import { tmpdir } from "../fixture/fixture" function node(script: string) { diff --git a/packages/opencode/test/util/wildcard.test.ts b/packages/opencode/test/util/wildcard.test.ts index 56e753d12a..7c9b1e4ac1 100644 --- a/packages/opencode/test/util/wildcard.test.ts +++ b/packages/opencode/test/util/wildcard.test.ts @@ -1,5 +1,5 @@ import { test, expect } from "bun:test" -import { Wildcard } from "../../src/util/wildcard" +import { Wildcard } from "../../src/util" test("match handles glob tokens", () => { expect(Wildcard.match("file1.txt", "file?.txt")).toBe(true) From 80f1f1b5b8535b6008af54621665738115346cde Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 23:27:32 -0400 Subject: [PATCH 211/300] feat: enable type-aware no-floating-promises rule, fix all 177 violations (#22741) --- .oxlintrc.json | 8 +++++++- bun.lock | 15 ++++++++++++++ github/index.ts | 2 +- package.json | 1 + packages/app/src/app.tsx | 4 ++-- .../components/dialog-connect-provider.tsx | 4 ++-- .../app/src/components/dialog-select-file.tsx | 4 ++-- .../src/components/dialog-select-server.tsx | 6 +++--- packages/app/src/components/prompt-input.tsx | 10 +++++----- .../app/src/components/prompt-input/submit.ts | 2 +- .../src/components/session-context-usage.tsx | 2 +- .../session/session-sortable-terminal-tab.tsx | 2 +- packages/app/src/components/terminal.tsx | 2 +- packages/app/src/context/global-sync.tsx | 10 +++++----- packages/app/src/context/layout.tsx | 2 +- packages/app/src/context/terminal.tsx | 4 ++-- packages/app/src/pages/layout.tsx | 20 +++++++++---------- .../src/pages/layout/sidebar-workspace.tsx | 2 +- packages/app/src/pages/session.tsx | 2 +- packages/app/src/pages/session/helpers.ts | 2 +- .../console/app/script/generate-sitemap.ts | 2 +- .../console/app/src/component/spotlight.tsx | 2 +- .../app/src/routes/black/subscribe/[plan].tsx | 2 +- .../console/app/src/routes/download/index.tsx | 2 +- packages/console/app/src/routes/index.tsx | 2 +- packages/console/app/src/routes/temp.tsx | 2 +- .../app/src/routes/zen/util/dataDumper.ts | 4 ++-- packages/desktop/src/entry.tsx | 4 ++-- packages/desktop/src/index.tsx | 2 +- packages/desktop/src/loading.tsx | 2 +- packages/desktop/src/menu.ts | 2 +- packages/desktop/src/webview-zoom.ts | 2 +- packages/enterprise/test-debug.ts | 2 +- packages/opencode/script/postinstall.mjs | 2 +- packages/opencode/src/acp/agent.ts | 4 ++-- packages/opencode/src/cli/cmd/tui/app.tsx | 6 +++--- .../cmd/tui/component/dialog-session-list.tsx | 2 +- .../tui/component/dialog-session-rename.tsx | 2 +- .../cli/cmd/tui/component/error-component.tsx | 4 ++-- .../cli/cmd/tui/component/prompt/index.tsx | 16 +++++++-------- .../opencode/src/cli/cmd/tui/context/kv.tsx | 2 +- .../src/cli/cmd/tui/context/local.tsx | 2 +- .../opencode/src/cli/cmd/tui/context/sync.tsx | 6 +++--- .../src/cli/cmd/tui/context/theme.tsx | 4 ++-- .../tui/feature-plugins/system/plugins.tsx | 4 ++-- .../cmd/tui/routes/session/dialog-message.tsx | 2 +- .../src/cli/cmd/tui/routes/session/index.tsx | 14 ++++++------- .../cli/cmd/tui/routes/session/permission.tsx | 8 ++++---- .../cli/cmd/tui/routes/session/question.tsx | 6 +++--- packages/opencode/src/config/config.ts | 6 +++--- .../opencode/src/control-plane/workspace.ts | 6 +++--- packages/opencode/src/file/watcher.ts | 6 +++--- packages/opencode/src/lsp/client.ts | 2 +- packages/opencode/src/lsp/index.ts | 4 ++-- packages/opencode/src/plugin/plugin.ts | 2 +- packages/opencode/src/provider/models.ts | 2 +- .../opencode/src/server/instance/session.ts | 18 +++++++++-------- packages/opencode/src/server/proxy.ts | 2 +- packages/opencode/src/storage/db.ts | 6 +++--- packages/opencode/src/sync/sync-event.ts | 6 +++--- packages/opencode/src/util/defer.ts | 2 +- packages/opencode/src/util/log.ts | 2 +- .../test/cli/tui/plugin-lifecycle.test.ts | 2 +- packages/opencode/test/mcp/headers.test.ts | 4 ++-- packages/opencode/test/mcp/lifecycle.test.ts | 10 +++++----- .../test/mcp/oauth-auto-connect.test.ts | 8 ++++---- .../opencode/test/mcp/oauth-browser.test.ts | 10 +++++----- .../test/memory/abort-leak-webfetch.ts | 2 +- .../opencode/test/permission/next.test.ts | 2 +- packages/opencode/test/preload.ts | 2 +- .../test/project/migrate-global.test.ts | 2 +- .../opencode/test/project/project.test.ts | 2 +- .../test/server/global-session-list.test.ts | 2 +- .../test/server/project-init-git.test.ts | 2 +- .../test/server/session-actions.test.ts | 2 +- .../opencode/test/server/session-list.test.ts | 2 +- .../test/server/session-messages.test.ts | 2 +- .../test/server/session-select.test.ts | 2 +- .../opencode/test/session/compaction.test.ts | 2 +- packages/opencode/test/session/llm.test.ts | 2 +- .../test/session/messages-pagination.test.ts | 2 +- .../test/session/processor-effect.test.ts | 2 +- .../test/session/prompt-effect.test.ts | 2 +- packages/opencode/test/session/prompt.test.ts | 6 +++--- .../test/session/revert-compact.test.ts | 2 +- .../opencode/test/session/session.test.ts | 2 +- .../test/session/snapshot-tool-race.test.ts | 2 +- .../structured-output-integration.test.ts | 2 +- .../opencode/test/skill/discovery.test.ts | 2 +- .../js/src/gen/core/serverSentEvents.gen.ts | 2 +- .../src/v2/gen/core/serverSentEvents.gen.ts | 2 +- packages/slack/src/index.ts | 4 ++-- packages/ui/src/components/basic-tool.tsx | 2 +- packages/ui/src/components/list.tsx | 2 +- packages/ui/src/components/message-part.tsx | 2 +- packages/ui/src/components/text-field.tsx | 5 +++-- packages/ui/src/components/text-reveal.tsx | 2 +- .../components/thinking-heading.stories.tsx | 2 +- .../ui/src/components/tool-error-card.tsx | 2 +- .../ui/src/components/tool-status-title.tsx | 2 +- packages/ui/src/pierre/worker.ts | 2 +- packages/ui/vite.config.ts | 4 ++-- script/duplicate-pr.ts | 2 +- 103 files changed, 212 insertions(+), 187 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index 37d91f4254..e16c8408d6 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -30,7 +30,13 @@ // postMessage target origin not relevant for this codebase "unicorn/require-post-message-target-origin": "off", // Side-effectful constructors are intentional in some places - "no-new": "off" + "no-new": "off", + + // Type-aware: catch unhandled promises + "typescript/no-floating-promises": "warn" + }, + "options": { + "typeAware": true }, "ignorePatterns": ["**/node_modules", "**/dist", "**/.build", "**/.sst", "**/*.d.ts"] } diff --git a/bun.lock b/bun.lock index 705181160a..a011a648fe 100644 --- a/bun.lock +++ b/bun.lock @@ -20,6 +20,7 @@ "glob": "13.0.5", "husky": "9.1.7", "oxlint": "1.60.0", + "oxlint-tsgolint": "0.21.0", "prettier": "3.6.2", "semver": "^7.6.0", "sst": "3.18.10", @@ -1680,6 +1681,18 @@ "@oxc-transform/binding-win32-x64-msvc": ["@oxc-transform/binding-win32-x64-msvc@0.96.0", "", { "os": "win32", "cpu": "x64" }, "sha512-0fI0P0W7bSO/GCP/N5dkmtB9vBqCA4ggo1WmXTnxNJVmFFOtcA1vYm1I9jl8fxo+sucW2WnlpnI4fjKdo3JKxA=="], + "@oxlint-tsgolint/darwin-arm64": ["@oxlint-tsgolint/darwin-arm64@0.21.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-P20j3MLqfwIT+94qGU3htC7dWp4pXGZW1p1p7FRUzu1aopq7c9nPCgf0W/WjktqQ57+iuTq9mbSlwWinl6+H1A=="], + + "@oxlint-tsgolint/darwin-x64": ["@oxlint-tsgolint/darwin-x64@0.21.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-81TmmuBcPedEA0MwRmObuQuXnCprS1UiHQWGe7pseqNAJzUWXeAPrayqKTACX92VpruJI+yvY0XJrFp11PpcTA=="], + + "@oxlint-tsgolint/linux-arm64": ["@oxlint-tsgolint/linux-arm64@0.21.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-sbjBr6zDduX8rNO0PTjhf7VYLCPWqdijWiMPp8e10qu6Tam1GdaVLaLlX8QrNupTgglO1GvqqgY/jcacWL8a6g=="], + + "@oxlint-tsgolint/linux-x64": ["@oxlint-tsgolint/linux-x64@0.21.0", "", { "os": "linux", "cpu": "x64" }, "sha512-jNrOcy53R5TJQfrK444Cm60bW9437xDoxPbm3AdvFSo/fhdFMllawc7uZC2Wzr+EAjTkW13K8R4QHzsUdBG9fQ=="], + + "@oxlint-tsgolint/win32-arm64": ["@oxlint-tsgolint/win32-arm64@0.21.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-xWeRxJJILDE4b9UqHEWGBxcBc1TUS6zWHhxcyxTZMwf4q3wdKeu0OHYAcwLGJzoSjEIf6FTjyfPiRNil2oqsdg=="], + + "@oxlint-tsgolint/win32-x64": ["@oxlint-tsgolint/win32-x64@0.21.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Ob9AA9teI8ckPo1whV1smLr5NrqwgBv/8boDbK0YZG+fKgNGRwr1hBj1ORgFWOQaUBv+5njp5A0RAfJJjQ95QQ=="], + "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.60.0", "", { "os": "android", "cpu": "arm" }, "sha512-YdeJKaZckDQL1qa62a1aKq/goyq48aX3yOxaaWqWb4sau4Ee4IiLbamftNLU3zbePky6QsDj6thnSSzHRBjDfA=="], "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.60.0", "", { "os": "android", "cpu": "arm64" }, "sha512-7ANS7PpXCfq84xZQ8E5WPs14gwcuPcl+/8TFNXfpSu0CQBXz3cUo2fDpHT8v8HJN+Ut02eacvMAzTnc9s6X4tw=="], @@ -4100,6 +4113,8 @@ "oxlint": ["oxlint@1.60.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.60.0", "@oxlint/binding-android-arm64": "1.60.0", "@oxlint/binding-darwin-arm64": "1.60.0", "@oxlint/binding-darwin-x64": "1.60.0", "@oxlint/binding-freebsd-x64": "1.60.0", "@oxlint/binding-linux-arm-gnueabihf": "1.60.0", "@oxlint/binding-linux-arm-musleabihf": "1.60.0", "@oxlint/binding-linux-arm64-gnu": "1.60.0", "@oxlint/binding-linux-arm64-musl": "1.60.0", "@oxlint/binding-linux-ppc64-gnu": "1.60.0", "@oxlint/binding-linux-riscv64-gnu": "1.60.0", "@oxlint/binding-linux-riscv64-musl": "1.60.0", "@oxlint/binding-linux-s390x-gnu": "1.60.0", "@oxlint/binding-linux-x64-gnu": "1.60.0", "@oxlint/binding-linux-x64-musl": "1.60.0", "@oxlint/binding-openharmony-arm64": "1.60.0", "@oxlint/binding-win32-arm64-msvc": "1.60.0", "@oxlint/binding-win32-ia32-msvc": "1.60.0", "@oxlint/binding-win32-x64-msvc": "1.60.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.18.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-tnRzTWiWJ9pg3ftRWnD0+Oqh78L6ZSwcEudvCZaER0PIqiAnNyXj5N1dPwjmNpDalkKS9m/WMLN1CTPUBPmsgw=="], + "oxlint-tsgolint": ["oxlint-tsgolint@0.21.0", "", { "optionalDependencies": { "@oxlint-tsgolint/darwin-arm64": "0.21.0", "@oxlint-tsgolint/darwin-x64": "0.21.0", "@oxlint-tsgolint/linux-arm64": "0.21.0", "@oxlint-tsgolint/linux-x64": "0.21.0", "@oxlint-tsgolint/win32-arm64": "0.21.0", "@oxlint-tsgolint/win32-x64": "0.21.0" }, "bin": { "tsgolint": "bin/tsgolint.js" } }, "sha512-HiWPhANwRnN1pZJQ2SgNB3WRR+1etLJHmRzQ/MJhyINsEIaOUCjxhlXJKbEaVUwdnyXwRWqo/P9Fx21lz0/mSg=="], + "p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="], "p-defer": ["p-defer@3.0.0", "", {}, "sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw=="], diff --git a/github/index.ts b/github/index.ts index 4463aa2002..51ee2a46a5 100644 --- a/github/index.ts +++ b/github/index.ts @@ -513,7 +513,7 @@ async function subscribeSessionEvents() { const decoder = new TextDecoder() let text = "" - ;(async () => { + void (async () => { while (true) { try { const { done, value } = await reader.read() diff --git a/package.json b/package.json index 8c5ae91955..5fecc09922 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "glob": "13.0.5", "husky": "9.1.7", "oxlint": "1.60.0", + "oxlint-tsgolint": "0.21.0", "prettier": "3.6.2", "semver": "^7.6.0", "sst": "3.18.10", diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index 9983548ba0..a2a746c05b 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -197,12 +197,12 @@ function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) { fallback={ { - if (checkMode() === "background") healthCheckActions.refetch() + if (checkMode() === "background") void healthCheckActions.refetch() }} onServerSelected={(key) => { setCheckMode("blocking") server.setActive(key) - healthCheckActions.refetch() + void healthCheckActions.refetch() }} /> } diff --git a/packages/app/src/components/dialog-connect-provider.tsx b/packages/app/src/components/dialog-connect-provider.tsx index 41225d02aa..e305743799 100644 --- a/packages/app/src/components/dialog-connect-provider.tsx +++ b/packages/app/src/components/dialog-connect-provider.tsx @@ -327,7 +327,7 @@ export function DialogConnectProvider(props: { provider: string }) { if (loading()) return if (methods().length === 1) { auto = true - selectMethod(0) + void selectMethod(0) } }) @@ -373,7 +373,7 @@ export function DialogConnectProvider(props: { provider: string }) { key={(m) => m?.label} onSelect={async (selected, index) => { if (!selected) return - selectMethod(index) + void selectMethod(index) }} > {(i) => ( diff --git a/packages/app/src/components/dialog-select-file.tsx b/packages/app/src/components/dialog-select-file.tsx index a0347a0399..186906f920 100644 --- a/packages/app/src/components/dialog-select-file.tsx +++ b/packages/app/src/components/dialog-select-file.tsx @@ -348,8 +348,8 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil const open = (path: string) => { const value = file.tab(path) - tabs().open(value) - file.load(path) + void tabs().open(value) + void file.load(path) if (!view().reviewPanel.opened()) view().reviewPanel.open() layout.fileTree.setTab("all") props.onOpenFile?.(path) diff --git a/packages/app/src/components/dialog-select-server.tsx b/packages/app/src/components/dialog-select-server.tsx index ca4c42a376..dd92edec3e 100644 --- a/packages/app/src/components/dialog-select-server.tsx +++ b/packages/app/src/components/dialog-select-server.tsx @@ -344,7 +344,7 @@ export function DialogSelectServer() { createEffect(() => { items() - refreshHealth() + void refreshHealth() const interval = setInterval(refreshHealth, 10_000) onCleanup(() => clearInterval(interval)) }) @@ -498,7 +498,7 @@ export function DialogSelectServer() { async function handleRemove(url: ServerConnection.Key) { server.remove(url) if ((await platform.getDefaultServer?.()) === url) { - platform.setDefaultServer?.(null) + void platform.setDefaultServer?.(null) } } @@ -536,7 +536,7 @@ export function DialogSelectServer() { items={sortedItems} key={(x) => x.http.url} onSelect={(x) => { - if (x) select(x) + if (x) void select(x) }} divider={true} class="px-5 [&_[data-slot=list-search-wrapper]]:w-full [&_[data-slot=list-scroll]]h-[300px] [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-base [&_[data-slot=list-items]]:rounded-md [&_[data-slot=list-item]]:min-h-14 [&_[data-slot=list-item]]:p-3 [&_[data-slot=list-item]]:!bg-transparent" diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 8ddb10a906..534215022a 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -212,9 +212,9 @@ export const PromptInput: Component = (props) => { if (!view().reviewPanel.opened()) view().reviewPanel.open() layout.fileTree.setTab("all") const tab = files.tab(item.path) - tabs().open(tab) + void tabs().open(tab) tabs().setActive(tab) - Promise.resolve(files.load(item.path)).finally(() => queueCommentFocus()) + void Promise.resolve(files.load(item.path)).finally(() => queueCommentFocus()) } const recent = createMemo(() => { @@ -1139,7 +1139,7 @@ export const PromptInput: Component = (props) => { } if (working()) { - abort() + void abort() event.preventDefault() event.stopPropagation() return @@ -1205,7 +1205,7 @@ export const PromptInput: Component = (props) => { return } if (working()) { - abort() + void abort() event.preventDefault() } return @@ -1245,7 +1245,7 @@ export const PromptInput: Component = (props) => { ) { return } - handleSubmit(event) + void handleSubmit(event) } } diff --git a/packages/app/src/components/prompt-input/submit.ts b/packages/app/src/components/prompt-input/submit.ts index 27e8980431..6805f619c1 100644 --- a/packages/app/src/components/prompt-input/submit.ts +++ b/packages/app/src/components/prompt-input/submit.ts @@ -295,7 +295,7 @@ export function createPromptSubmit(input: PromptSubmitInput) { const mode = input.mode() if (text.trim().length === 0 && images.length === 0 && input.commentCount() === 0) { - if (input.working()) abort() + if (input.working()) void abort() return } diff --git a/packages/app/src/components/session-context-usage.tsx b/packages/app/src/components/session-context-usage.tsx index d7c249ab03..6b7fe4ef7d 100644 --- a/packages/app/src/components/session-context-usage.tsx +++ b/packages/app/src/components/session-context-usage.tsx @@ -24,7 +24,7 @@ function openSessionContext(args: { }) { if (!args.view.reviewPanel.opened()) args.view.reviewPanel.open() if (args.layout.fileTree.opened() && args.layout.fileTree.tab() !== "all") args.layout.fileTree.setTab("all") - args.tabs.open("context") + void args.tabs.open("context") args.tabs.setActive("context") } diff --git a/packages/app/src/components/session/session-sortable-terminal-tab.tsx b/packages/app/src/components/session/session-sortable-terminal-tab.tsx index ba697f91af..2d88ed1806 100644 --- a/packages/app/src/components/session/session-sortable-terminal-tab.tsx +++ b/packages/app/src/components/session/session-sortable-terminal-tab.tsx @@ -44,7 +44,7 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () => const close = () => { const count = terminal.all().length - terminal.close(props.terminal.id) + void terminal.close(props.terminal.id) if (count === 1) { props.onClose?.() } diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx index 9b7ef83b28..db7d53f2b6 100644 --- a/packages/app/src/components/terminal.tsx +++ b/packages/app/src/components/terminal.tsx @@ -415,7 +415,7 @@ export const Terminal = (props: TerminalProps) => { if (local.autoFocus !== false) focusTerminal() if (typeof document !== "undefined" && document.fonts) { - document.fonts.ready.then(scheduleFit) + void document.fonts.ready.then(scheduleFit) } const onResize = t.onResize((size) => { diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index fe5f2f1301..57b76a96f7 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -237,7 +237,7 @@ function createGlobalSync() { }) sessionLoads.set(directory, promise) - promise.finally(() => { + void promise.finally(() => { sessionLoads.delete(directory) children.unpin(directory) }) @@ -273,7 +273,7 @@ function createGlobalSync() { })() booting.set(directory, promise) - promise.finally(() => { + void promise.finally(() => { booting.delete(directory) children.unpin(directory) }) @@ -317,7 +317,7 @@ function createGlobalSync() { setSessionTodo, vcsCache: children.vcsCache.get(directory), loadLsp: () => { - sdkFor(directory) + void sdkFor(directory) .lsp.status() .then((x) => { setStore("lsp", x.data ?? []) @@ -359,13 +359,13 @@ function createGlobalSync() { eventFrame = undefined eventTimer = setTimeout(() => { eventTimer = undefined - globalSDK.event.start() + void globalSDK.event.start() }, 0) }) } else { eventTimer = setTimeout(() => { eventTimer = undefined - globalSDK.event.start() + void globalSDK.event.start() }, 0) } void bootstrap() diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx index 87f11d2b64..74ea285310 100644 --- a/packages/app/src/context/layout.tsx +++ b/packages/app/src/context/layout.tsx @@ -582,7 +582,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( open(directory: string) { const root = rootFor(directory) if (server.projects.list().find((x) => x.worktree === root)) return - globalSync.project.loadSessions(root) + void globalSync.project.loadSessions(root) server.projects.open(root) }, close(directory: string) { diff --git a/packages/app/src/context/terminal.tsx b/packages/app/src/context/terminal.tsx index 17355aab9a..31d2d6e04c 100644 --- a/packages/app/src/context/terminal.tsx +++ b/packages/app/src/context/terminal.tsx @@ -117,7 +117,7 @@ export function clearWorkspaceTerminals(dir: string, sessionIDs?: string[], plat entry?.value.clear() } - removePersisted(Persist.workspace(dir, "terminal"), platform) + void removePersisted(Persist.workspace(dir, "terminal"), platform) const legacy = new Set(getLegacyTerminalStorageKeys(dir)) for (const id of sessionIDs ?? []) { @@ -126,7 +126,7 @@ export function clearWorkspaceTerminals(dir: string, sessionIDs?: string[], plat } } for (const key of legacy) { - removePersisted({ key }, platform) + void removePersisted({ key }, platform) } } diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 3ba2659a3b..8fad0bafe3 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -956,7 +956,7 @@ export default function Layout(props: ParentProps) { // warm up child store to prevent flicker globalSync.child(target.worktree) - openProject(target.worktree) + void openProject(target.worktree) } function navigateSessionByUnseen(offset: number) { @@ -1094,7 +1094,7 @@ export default function Layout(props: ParentProps) { disabled: !params.dir || !params.id, onSelect: () => { const session = currentSessions().find((s) => s.id === params.id) - if (session) archiveSession(session) + if (session) void archiveSession(session) }, }, { @@ -1360,11 +1360,11 @@ export default function Layout(props: ParentProps) { if (!server.isLocal()) return for (const directory of collectOpenProjectDeepLinks(urls)) { - openProject(directory) + void openProject(directory) } for (const link of collectNewSessionDeepLinks(urls)) { - openProject(link.directory, false) + void openProject(link.directory, false) const slug = base64Encode(link.directory) if (link.prompt) { setSessionHandoff(slug, { prompt: link.prompt }) @@ -1453,11 +1453,11 @@ export default function Layout(props: ParentProps) { function resolve(result: string | string[] | null) { if (Array.isArray(result)) { for (const directory of result) { - openProject(directory, false) + void openProject(directory, false) } - navigateToProject(result[0]) + void navigateToProject(result[0]) } else if (result) { - openProject(result) + void openProject(result) } } @@ -1825,7 +1825,7 @@ export default function Layout(props: ParentProps) { const next = new Set(dirs) for (const directory of next) { if (loadedSessionDirs.has(directory)) continue - globalSync.project.loadSessions(directory) + void globalSync.project.loadSessions(directory) } loadedSessionDirs.clear() @@ -2110,7 +2110,7 @@ export default function Layout(props: ParentProps) { onSave={(next) => { const item = project() if (!item) return - renameProject(item, next) + void renameProject(item, next) }} class="text-14-medium text-text-strong truncate" displayClass="text-14-medium text-text-strong truncate" @@ -2242,7 +2242,7 @@ export default function Layout(props: ParentProps) { onClick={() => { const item = project() if (!item) return - createWorkspace(item) + void createWorkspace(item) }} > {language.t("workspace.new")} diff --git a/packages/app/src/pages/layout/sidebar-workspace.tsx b/packages/app/src/pages/layout/sidebar-workspace.tsx index 9e00691471..9d74651b94 100644 --- a/packages/app/src/pages/layout/sidebar-workspace.tsx +++ b/packages/app/src/pages/layout/sidebar-workspace.tsx @@ -277,7 +277,7 @@ const WorkspaceSessionList = (props: { class="flex w-full text-left justify-start text-14-regular text-text-weak pl-2 pr-10" size="large" onClick={(e: MouseEvent) => { - props.loadMore() + void props.loadMore() ;(e.currentTarget as HTMLButtonElement).blur() }} > diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 32df997f7f..c63bbc4f93 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -484,7 +484,7 @@ export default function Page() { if (!tab) return const path = file.pathFromTab(tab) - if (path) file.load(path) + if (path) void file.load(path) }) createEffect( diff --git a/packages/app/src/pages/session/helpers.ts b/packages/app/src/pages/session/helpers.ts index f3215f6850..e136ba9991 100644 --- a/packages/app/src/pages/session/helpers.ts +++ b/packages/app/src/pages/session/helpers.ts @@ -117,7 +117,7 @@ export const createOpenReviewFile = (input: { input.openTab(tab) input.setActive(tab) } - if (maybePromise instanceof Promise) maybePromise.then(open) + if (maybePromise instanceof Promise) void maybePromise.then(open) else open() }) } diff --git a/packages/console/app/script/generate-sitemap.ts b/packages/console/app/script/generate-sitemap.ts index 9fd3ba0f0f..1cf64d6e89 100755 --- a/packages/console/app/script/generate-sitemap.ts +++ b/packages/console/app/script/generate-sitemap.ts @@ -105,4 +105,4 @@ async function main() { console.log(`✓ Sitemap generated at ${outputPath}`) } -main() +void main() diff --git a/packages/console/app/src/component/spotlight.tsx b/packages/console/app/src/component/spotlight.tsx index 7043069905..19accb88a6 100644 --- a/packages/console/app/src/component/spotlight.tsx +++ b/packages/console/app/src/component/spotlight.tsx @@ -766,7 +766,7 @@ export default function Spotlight(props: SpotlightProps) { } } - initializeWebGPU() + void initializeWebGPU() onCleanup(() => { if (cleanupFunctionRef) { diff --git a/packages/console/app/src/routes/black/subscribe/[plan].tsx b/packages/console/app/src/routes/black/subscribe/[plan].tsx index 19b56eabe6..52e6408761 100644 --- a/packages/console/app/src/routes/black/subscribe/[plan].tsx +++ b/packages/console/app/src/routes/black/subscribe/[plan].tsx @@ -298,7 +298,7 @@ export default function BlackSubscribe() { // Resolve stripe promise once createEffect(() => { - stripePromise.then((s) => { + void stripePromise.then((s) => { if (s) setStripe(s) }) }) diff --git a/packages/console/app/src/routes/download/index.tsx b/packages/console/app/src/routes/download/index.tsx index 0278d8622b..b5c202a5ec 100644 --- a/packages/console/app/src/routes/download/index.tsx +++ b/packages/console/app/src/routes/download/index.tsx @@ -77,7 +77,7 @@ export default function Download() { const handleCopyClick = (command: string) => (event: Event) => { const button = event.currentTarget as HTMLButtonElement - navigator.clipboard.writeText(command) + void navigator.clipboard.writeText(command) button.setAttribute("data-copied", "") setTimeout(() => { button.removeAttribute("data-copied") diff --git a/packages/console/app/src/routes/index.tsx b/packages/console/app/src/routes/index.tsx index b5b12a84bd..ee40ded87b 100644 --- a/packages/console/app/src/routes/index.tsx +++ b/packages/console/app/src/routes/index.tsx @@ -35,7 +35,7 @@ export default function Home() { const button = event.currentTarget as HTMLButtonElement const text = button.textContent if (text) { - navigator.clipboard.writeText(text) + void navigator.clipboard.writeText(text) button.setAttribute("data-copied", "") setTimeout(() => { button.removeAttribute("data-copied") diff --git a/packages/console/app/src/routes/temp.tsx b/packages/console/app/src/routes/temp.tsx index 4eed47857a..6bbabc9ea1 100644 --- a/packages/console/app/src/routes/temp.tsx +++ b/packages/console/app/src/routes/temp.tsx @@ -27,7 +27,7 @@ export default function Home() { const callback = () => { const text = button.textContent if (text) { - navigator.clipboard.writeText(text) + void navigator.clipboard.writeText(text) button.setAttribute("data-copied", "") setTimeout(() => { button.removeAttribute("data-copied") diff --git a/packages/console/app/src/routes/zen/util/dataDumper.ts b/packages/console/app/src/routes/zen/util/dataDumper.ts index b852ca0b5c..bc88c3813d 100644 --- a/packages/console/app/src/routes/zen/util/dataDumper.ts +++ b/packages/console/app/src/routes/zen/util/dataDumper.ts @@ -26,14 +26,14 @@ export function createDataDumper(sessionId: string, requestId: string, projectId const minute = timestamp.substring(10, 12) const second = timestamp.substring(12, 14) - waitUntil( + void waitUntil( Resource.ZenDataNew.put( `data/${data.modelName}/${year}/${month}/${day}/${hour}/${minute}/${second}/${requestId}.json`, JSON.stringify({ timestamp, ...data }), ), ) - waitUntil( + void waitUntil( Resource.ZenDataNew.put( `meta/${data.modelName}/${sessionId}/${requestId}.json`, JSON.stringify({ timestamp, ...metadata }), diff --git a/packages/desktop/src/entry.tsx b/packages/desktop/src/entry.tsx index b1c9f13f9c..0e43d85fae 100644 --- a/packages/desktop/src/entry.tsx +++ b/packages/desktop/src/entry.tsx @@ -1,5 +1,5 @@ if (location.pathname === "/loading") { - import("./loading") + void import("./loading") } else { - import("./") + void import("./") } diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx index 5fe88d501b..d6a0ad74f8 100644 --- a/packages/desktop/src/index.tsx +++ b/packages/desktop/src/index.tsx @@ -410,7 +410,7 @@ const createPlatform = (): Platform => { } let menuTrigger = null as null | ((id: string) => void) -createMenu((id) => { +void createMenu((id) => { menuTrigger?.(id) }) void listenForDeepLinks() diff --git a/packages/desktop/src/loading.tsx b/packages/desktop/src/loading.tsx index a02f1a95e5..bcea016be4 100644 --- a/packages/desktop/src/loading.tsx +++ b/packages/desktop/src/loading.tsx @@ -48,7 +48,7 @@ render(() => { }) onCleanup(() => { - listener.then((cb) => cb()) + void listener.then((cb) => cb()) timers.forEach(clearTimeout) }) }) diff --git a/packages/desktop/src/menu.ts b/packages/desktop/src/menu.ts index 9005dd702f..837c8c017f 100644 --- a/packages/desktop/src/menu.ts +++ b/packages/desktop/src/menu.ts @@ -186,5 +186,5 @@ export async function createMenu(trigger: (id: string) => void) { }), ], }) - menu.setAsAppMenu() + void menu.setAsAppMenu() } diff --git a/packages/desktop/src/webview-zoom.ts b/packages/desktop/src/webview-zoom.ts index 06f46a3afd..46de208b0e 100644 --- a/packages/desktop/src/webview-zoom.ts +++ b/packages/desktop/src/webview-zoom.ts @@ -17,7 +17,7 @@ const clamp = (value: number) => Math.min(Math.max(value, MIN_ZOOM_LEVEL), MAX_Z const applyZoom = (next: number) => { setWebviewZoom(next) - invoke("plugin:webview|set_webview_zoom", { + void invoke("plugin:webview|set_webview_zoom", { value: next, }) } diff --git a/packages/enterprise/test-debug.ts b/packages/enterprise/test-debug.ts index a2ec4d8cdf..28558dec19 100644 --- a/packages/enterprise/test-debug.ts +++ b/packages/enterprise/test-debug.ts @@ -37,4 +37,4 @@ async function test() { await Share.remove({ id: shareInfo.id, secret: shareInfo.secret }) } -test() +void test() diff --git a/packages/opencode/script/postinstall.mjs b/packages/opencode/script/postinstall.mjs index 7dcf3958a9..99f8bf4321 100644 --- a/packages/opencode/script/postinstall.mjs +++ b/packages/opencode/script/postinstall.mjs @@ -112,7 +112,7 @@ async function main() { } try { - main() + void main() } catch (error) { console.error("Postinstall script error:", error.message) process.exit(0) diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index 669462772d..57cce66680 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -242,7 +242,7 @@ export namespace ACP { const newContent = getNewContent(content, diff) if (newContent) { - this.connection.writeTextFile({ + void this.connection.writeTextFile({ sessionId: session.id, path: filepath, content: newContent, @@ -1253,7 +1253,7 @@ export namespace ACP { ) setTimeout(() => { - this.connection.sessionUpdate({ + void this.connection.sessionUpdate({ sessionId, update: { sessionUpdate: "available_commands_update", diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 3d5350cb69..e7e9fd9cd2 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -350,7 +350,7 @@ function App(props: { onSnapshot?: () => Promise }) { if (match) { continued = true if (args.fork) { - sdk.client.session.fork({ sessionID: match }).then((result) => { + void sdk.client.session.fork({ sessionID: match }).then((result) => { if (result.data?.id) { route.navigate({ type: "session", sessionID: result.data.id }) } else { @@ -370,7 +370,7 @@ function App(props: { onSnapshot?: () => Promise }) { createEffect(() => { if (forked || sync.status !== "complete" || !args.sessionID || !args.fork) return forked = true - sdk.client.session.fork({ sessionID: args.sessionID }).then((result) => { + void sdk.client.session.fork({ sessionID: args.sessionID }).then((result) => { if (result.data?.id) { route.navigate({ type: "session", sessionID: result.data.id }) } else { @@ -818,7 +818,7 @@ function App(props: { onSnapshot?: () => Promise }) { `Successfully updated to OpenCode v${result.data.version}. Please restart the application.`, ) - exit() + void exit() }) const plugin = createMemo(() => { diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx index a42755bee7..f58b73c9a7 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx @@ -145,7 +145,7 @@ export function DialogSessionList() { title: "delete", onTrigger: async (option) => { if (toDelete() === option.value) { - sdk.client.session.delete({ + void sdk.client.session.delete({ sessionID: option.value, }) setToDelete(undefined) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-session-rename.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-session-rename.tsx index 141340d556..a079941c11 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-session-rename.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-session-rename.tsx @@ -19,7 +19,7 @@ export function DialogSessionRename(props: DialogSessionRenameProps) { title="Rename Session" value={session()?.title} onConfirm={(value) => { - sdk.client.session.update({ + void sdk.client.session.update({ sessionID: props.session, title: value, }) diff --git a/packages/opencode/src/cli/cmd/tui/component/error-component.tsx b/packages/opencode/src/cli/cmd/tui/component/error-component.tsx index b22163902e..e8758b3d7f 100644 --- a/packages/opencode/src/cli/cmd/tui/component/error-component.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/error-component.tsx @@ -26,7 +26,7 @@ export function ErrorComponent(props: { useKeyboard((evt) => { if (evt.ctrl && evt.name === "c") { - handleExit() + void handleExit() } }) const [copied, setCopied] = createSignal(false) @@ -56,7 +56,7 @@ export function ErrorComponent(props: { issueURL.searchParams.set("opencode-version", Installation.VERSION) const copyIssueURL = () => { - Clipboard.copy(issueURL.toString()).then(() => { + void Clipboard.copy(issueURL.toString()).then(() => { setCopied(true) }) } diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index c361e48c9e..b80c32243f 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -235,7 +235,7 @@ export function Prompt(props: PromptProps) { hidden: true, onSelect: (dialog) => { if (!input.focused) return - submit() + void submit() dialog.clear() }, }, @@ -280,7 +280,7 @@ export function Prompt(props: PromptProps) { }, 5000) if (store.interrupt >= 2) { - sdk.client.session.abort({ + void sdk.client.session.abort({ sessionID: props.sessionID, }) setStore("interrupt", 0) @@ -429,7 +429,7 @@ export function Prompt(props: PromptProps) { setStore("extmarkToPartIndex", new Map()) }, submit() { - submit() + void submit() }, } @@ -604,12 +604,12 @@ export function Prompt(props: PromptProps) { if (!store.prompt.input) return const trimmed = store.prompt.input.trim() if (trimmed === "exit" || trimmed === "quit" || trimmed === ":q") { - exit() + void exit() return } const selectedModel = local.model.current() if (!selectedModel) { - promptModelWarning() + void promptModelWarning() return } @@ -660,7 +660,7 @@ export function Prompt(props: PromptProps) { const variant = local.model.variant.current() if (store.mode === "shell") { - sdk.client.session.shell({ + void sdk.client.session.shell({ sessionID, agent: local.agent.current().name, model: { @@ -685,7 +685,7 @@ export function Prompt(props: PromptProps) { const restOfInput = firstLineEnd === -1 ? "" : inputText.slice(firstLineEnd + 1) const args = firstLineArgs.join(" ") + (restOfInput ? "\n" + restOfInput : "") - sdk.client.session.command({ + void sdk.client.session.command({ sessionID, command: command.slice(1), arguments: args, @@ -1208,7 +1208,7 @@ export function Prompt(props: PromptProps) { const r = retry() if (!r) return if (isTruncated()) { - DialogAlert.show(dialog, "Retry Error", r.message) + void DialogAlert.show(dialog, "Retry Error", r.message) } } diff --git a/packages/opencode/src/cli/cmd/tui/context/kv.tsx b/packages/opencode/src/cli/cmd/tui/context/kv.tsx index dc0b96c62a..39e976b0e5 100644 --- a/packages/opencode/src/cli/cmd/tui/context/kv.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/kv.tsx @@ -44,7 +44,7 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({ }, set(key: string, value: any) { setStore(key, value) - Filesystem.writeJson(filePath, store) + void Filesystem.writeJson(filePath, store) }, } return result diff --git a/packages/opencode/src/cli/cmd/tui/context/local.tsx b/packages/opencode/src/cli/cmd/tui/context/local.tsx index 612f2b7177..4c298ec113 100644 --- a/packages/opencode/src/cli/cmd/tui/context/local.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/local.tsx @@ -131,7 +131,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ return } state.pending = false - Filesystem.writeJson(filePath, { + void Filesystem.writeJson(filePath, { recent: modelStore.recent, favorite: modelStore.favorite, variant: modelStore.variant, diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index a0a59199bb..2558f9751f 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -111,7 +111,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ event.subscribe((event) => { switch (event.type) { case "server.instance.disposed": - bootstrap() + void bootstrap() break case "permission.replied": { const requests = store.permission[event.properties.sessionID] @@ -336,7 +336,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ case "lsp.updated": { const workspace = project.workspace.current() - sdk.client.lsp.status({ workspace }).then((x) => setStore("lsp", x.data ?? [])) + void sdk.client.lsp.status({ workspace }).then((x) => setStore("lsp", x.data ?? [])) break } @@ -415,7 +415,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ .then(() => { if (store.status !== "complete") setStore("status", "partial") // non-blocking - Promise.all([ + void Promise.all([ ...(args.continue ? [] : [sessionListPromise.then((sessions) => setStore("session", reconcile(sessions)))]), consoleStatePromise.then((consoleState) => setStore("console_state", reconcile(consoleState))), sdk.client.command.list({ workspace }).then((x) => setStore("command", reconcile(x.data ?? []))), diff --git a/packages/opencode/src/cli/cmd/tui/context/theme.tsx b/packages/opencode/src/cli/cmd/tui/context/theme.tsx index 179dc93700..679be8f254 100644 --- a/packages/opencode/src/cli/cmd/tui/context/theme.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/theme.tsx @@ -329,7 +329,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ }) function init() { - Promise.allSettled([ + void Promise.allSettled([ resolveSystemTheme(store.mode), getCustomThemes() .then((custom) => { @@ -377,7 +377,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ if (store.mode === mode) return setStore("mode", mode) renderer.clearPaletteCache() - resolveSystemTheme(mode) + void resolveSystemTheme(mode) } function pin(mode: "dark" | "light" = store.mode) { diff --git a/packages/opencode/src/cli/cmd/tui/feature-plugins/system/plugins.tsx b/packages/opencode/src/cli/cmd/tui/feature-plugins/system/plugins.tsx index f391eb24a7..b5edabcf0e 100644 --- a/packages/opencode/src/cli/cmd/tui/feature-plugins/system/plugins.tsx +++ b/packages/opencode/src/cli/cmd/tui/feature-plugins/system/plugins.tsx @@ -78,7 +78,7 @@ function Install(props: { api: TuiPluginApi }) { } setBusy(true) - props.api.plugins + void props.api.plugins .install(mod, { global: global() }) .then((out) => { if (!out.ok) { @@ -188,7 +188,7 @@ function View(props: { api: TuiPluginApi }) { if (!item) return setLock(true) const task = item.active ? props.api.plugins.deactivate(x) : props.api.plugins.activate(x) - task + void task .then((ok) => { if (!ok) { props.api.ui.toast({ diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx index a51a6cfe58..835ac8f5d5 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx @@ -29,7 +29,7 @@ export function DialogMessage(props: { const msg = message() if (!msg) return - sdk.client.session.revert({ + void sdk.client.session.revert({ sessionID: props.sessionID, messageID: msg.id, }) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 58b5d6626c..2ea936c898 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -241,7 +241,7 @@ export function Session() { if (kv.get(GO_UPSELL_DONT_SHOW)) return - DialogGoUpsell.show(dialog).then((dontShowAgain) => { + void DialogGoUpsell.show(dialog).then((dontShowAgain) => { if (dontShowAgain) kv.set(GO_UPSELL_DONT_SHOW, true) kv.set(GO_UPSELL_LAST_SEEN_AT, Date.now()) }) @@ -272,7 +272,7 @@ export function Session() { useKeyboard((evt) => { if (!session()?.parentID) return if (keybind.match("app_exit", evt)) { - exit() + void exit() } }) @@ -483,7 +483,7 @@ export function Session() { }) return } - sdk.client.session.summarize({ + void sdk.client.session.summarize({ sessionID: route.sessionID, modelID: selectedModel.modelID, providerID: selectedModel.providerID, @@ -529,7 +529,7 @@ export function Session() { const revert = session()?.revert?.messageID const message = messages().findLast((x) => (!revert || x.id < revert) && x.role === "user") if (!message) return - sdk.client.session + void sdk.client.session .revert({ sessionID: route.sessionID, messageID: message.id, @@ -568,13 +568,13 @@ export function Session() { if (!messageID) return const message = messages().find((x) => x.role === "user" && x.id > messageID) if (!message) { - sdk.client.session.unrevert({ + void sdk.client.session.unrevert({ sessionID: route.sessionID, }) prompt?.set({ input: "", parts: [] }) return } - sdk.client.session.revert({ + void sdk.client.session.revert({ sessionID: route.sessionID, messageID: message.id, }) @@ -1966,7 +1966,7 @@ function Task(props: ToolProps) { onMount(() => { if (props.metadata.sessionId && !sync.data.message[props.metadata.sessionId]?.length) - sync.session.sync(props.metadata.sessionId) + void sync.session.sync(props.metadata.sessionId) }) const messages = createMemo(() => sync.data.message[props.metadata.sessionId ?? ""] ?? []) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx index 3554ab44ca..54cc86a40d 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx @@ -184,7 +184,7 @@ export function PermissionPrompt(props: { request: PermissionRequest }) { onSelect={(option) => { setStore("stage", "permission") if (option === "cancel") return - sdk.client.permission.reply({ + void sdk.client.permission.reply({ reply: "always", requestID: props.request.id, }) @@ -194,7 +194,7 @@ export function PermissionPrompt(props: { request: PermissionRequest }) { { - sdk.client.permission.reply({ + void sdk.client.permission.reply({ reply: "reject", requestID: props.request.id, message: message || undefined, @@ -447,13 +447,13 @@ export function PermissionPrompt(props: { request: PermissionRequest }) { setStore("stage", "reject") return } - sdk.client.permission.reply({ + void sdk.client.permission.reply({ reply: "reject", requestID: props.request.id, }) return } - sdk.client.permission.reply({ + void sdk.client.permission.reply({ reply: "once", requestID: props.request.id, }) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/question.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/question.tsx index 65989b9f35..3ff95b4bb8 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/question.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/question.tsx @@ -45,14 +45,14 @@ export function QuestionPrompt(props: { request: QuestionRequest }) { function submit() { const answers = questions().map((_, i) => store.answers[i] ?? []) - sdk.client.question.reply({ + void sdk.client.question.reply({ requestID: props.request.id, answers, }) } function reject() { - sdk.client.question.reject({ + void sdk.client.question.reject({ requestID: props.request.id, }) } @@ -67,7 +67,7 @@ export function QuestionPrompt(props: { request: QuestionRequest }) { setStore("custom", inputs) } if (single()) { - sdk.client.question.reply({ + void sdk.client.question.reply({ requestID: props.request.id, answers: [[answer]], }) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index ee1c755ebc..3da2dd6bdb 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -171,7 +171,7 @@ async function loadCommand(dir: string) { ? err.data.message : `Failed to parse command ${item}` const { Session } = await import("@/session") - Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() }) + void Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() }) log.error("failed to load command", { command: item, err }) return undefined }) @@ -210,7 +210,7 @@ async function loadAgent(dir: string) { ? err.data.message : `Failed to parse agent ${item}` const { Session } = await import("@/session") - Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() }) + void Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() }) log.error("failed to load agent", { agent: item, err }) return undefined }) @@ -248,7 +248,7 @@ async function loadMode(dir: string) { ? err.data.message : `Failed to parse mode ${item}` const { Session } = await import("@/session") - Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() }) + void Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() }) log.error("failed to load mode", { mode: item, err }) return undefined }) diff --git a/packages/opencode/src/control-plane/workspace.ts b/packages/opencode/src/control-plane/workspace.ts index a0d4c16803..dfd018db7e 100644 --- a/packages/opencode/src/control-plane/workspace.ts +++ b/packages/opencode/src/control-plane/workspace.ts @@ -114,7 +114,7 @@ export namespace Workspace { await adaptor.create(config) - startSync(info) + void startSync(info) await waitEvent({ timeout: TIMEOUT, @@ -294,7 +294,7 @@ export namespace Workspace { ) const spaces = rows.map(fromRow).sort((a, b) => a.id.localeCompare(b.id)) - for (const space of spaces) startSync(space) + for (const space of spaces) void startSync(space) return spaces } @@ -307,7 +307,7 @@ export namespace Workspace { export const get = fn(WorkspaceID.zod, async (id) => { const space = lookup(id) if (!space) return - startSync(space) + void startSync(space) return space }) diff --git a/packages/opencode/src/file/watcher.ts b/packages/opencode/src/file/watcher.ts index f11cf88a65..3e3da444a5 100644 --- a/packages/opencode/src/file/watcher.ts +++ b/packages/opencode/src/file/watcher.ts @@ -98,9 +98,9 @@ export namespace FileWatcher { const cb: ParcelWatcher.SubscribeCallback = Instance.bind((err, evts) => { if (err) return for (const evt of evts) { - if (evt.type === "create") Bus.publish(Event.Updated, { file: evt.path, event: "add" }) - if (evt.type === "update") Bus.publish(Event.Updated, { file: evt.path, event: "change" }) - if (evt.type === "delete") Bus.publish(Event.Updated, { file: evt.path, event: "unlink" }) + if (evt.type === "create") void Bus.publish(Event.Updated, { file: evt.path, event: "add" }) + if (evt.type === "update") void Bus.publish(Event.Updated, { file: evt.path, event: "change" }) + if (evt.type === "delete") void Bus.publish(Event.Updated, { file: evt.path, event: "unlink" }) } }) diff --git a/packages/opencode/src/lsp/client.ts b/packages/opencode/src/lsp/client.ts index 50051b3901..27301e79a7 100644 --- a/packages/opencode/src/lsp/client.ts +++ b/packages/opencode/src/lsp/client.ts @@ -59,7 +59,7 @@ export namespace LSPClient { const exists = diagnostics.has(filePath) diagnostics.set(filePath, params.diagnostics) if (!exists && input.serverID === "typescript") return - Bus.publish(Event.Diagnostics, { path: filePath, serverID: input.serverID }) + void Bus.publish(Event.Diagnostics, { path: filePath, serverID: input.serverID }) }) connection.onRequest("window/workDoneProgress/create", (params) => { l.info("window/workDoneProgress/create", params) diff --git a/packages/opencode/src/lsp/index.ts b/packages/opencode/src/lsp/index.ts index a55ac18402..5146c40abe 100644 --- a/packages/opencode/src/lsp/index.ts +++ b/packages/opencode/src/lsp/index.ts @@ -293,7 +293,7 @@ export namespace LSP { const task = schedule(server, root, root + server.id) s.spawning.set(root + server.id, task) - task.finally(() => { + void task.finally(() => { if (s.spawning.get(root + server.id) === task) { s.spawning.delete(root + server.id) } @@ -303,7 +303,7 @@ export namespace LSP { if (!client) continue result.push(client) - Bus.publish(Event.Updated, {}) + void Bus.publish(Event.Updated, {}) } return result diff --git a/packages/opencode/src/plugin/plugin.ts b/packages/opencode/src/plugin/plugin.ts index ec1cf1e313..d1fc60d993 100644 --- a/packages/opencode/src/plugin/plugin.ts +++ b/packages/opencode/src/plugin/plugin.ts @@ -245,7 +245,7 @@ export const layer = Layer.effect( Stream.runForEach((input) => Effect.sync(() => { for (const hook of hooks) { - hook["event"]?.({ event: input as any }) + void hook["event"]?.({ event: input as any }) } }), ), diff --git a/packages/opencode/src/provider/models.ts b/packages/opencode/src/provider/models.ts index 59d629a379..245730e00f 100644 --- a/packages/opencode/src/provider/models.ts +++ b/packages/opencode/src/provider/models.ts @@ -172,7 +172,7 @@ export namespace ModelsDev { } if (!Flag.OPENCODE_DISABLE_MODELS_FETCH && !process.argv.includes("--get-yargs-completions")) { - ModelsDev.refresh() + void ModelsDev.refresh() setInterval( async () => { await ModelsDev.refresh() diff --git a/packages/opencode/src/server/instance/session.ts b/packages/opencode/src/server/instance/session.ts index 1b2755fb8a..06495b628c 100644 --- a/packages/opencode/src/server/instance/session.ts +++ b/packages/opencode/src/server/instance/session.ts @@ -898,7 +898,7 @@ export const SessionRoutes = lazy(() => const msg = await AppRuntime.runPromise( SessionPrompt.Service.use((svc) => svc.prompt({ ...body, sessionID })), ) - stream.write(JSON.stringify(msg)) + void stream.write(JSON.stringify(msg)) }) }, ) @@ -926,13 +926,15 @@ export const SessionRoutes = lazy(() => async (c) => { const sessionID = c.req.valid("param").sessionID const body = c.req.valid("json") - AppRuntime.runPromise(SessionPrompt.Service.use((svc) => svc.prompt({ ...body, sessionID }))).catch((err) => { - log.error("prompt_async failed", { sessionID, error: err }) - Bus.publish(Session.Event.Error, { - sessionID, - error: new NamedError.Unknown({ message: err instanceof Error ? err.message : String(err) }).toObject(), - }) - }) + void AppRuntime.runPromise(SessionPrompt.Service.use((svc) => svc.prompt({ ...body, sessionID }))).catch( + (err) => { + log.error("prompt_async failed", { sessionID, error: err }) + void Bus.publish(Session.Event.Error, { + sessionID, + error: new NamedError.Unknown({ message: err instanceof Error ? err.message : String(err) }).toObject(), + }) + }, + ) return c.body(null, 204) }, diff --git a/packages/opencode/src/server/proxy.ts b/packages/opencode/src/server/proxy.ts index 07edcc2bb2..5e36f2cff9 100644 --- a/packages/opencode/src/server/proxy.ts +++ b/packages/opencode/src/server/proxy.ts @@ -76,7 +76,7 @@ const app = (upgrade: UpgradeWebSocket) => queue.length = 0 } remote.onmessage = (event) => { - send(ws, event.data) + void send(ws, event.data) } remote.onerror = () => { ws.close(1011, "proxy error") diff --git a/packages/opencode/src/storage/db.ts b/packages/opencode/src/storage/db.ts index ee53182f36..7acd458dcd 100644 --- a/packages/opencode/src/storage/db.ts +++ b/packages/opencode/src/storage/db.ts @@ -134,7 +134,7 @@ export namespace Database { if (err instanceof LocalContext.NotFound) { const effects: (() => void | Promise)[] = [] const result = ctx.provide({ effects, tx: Client() }, () => callback(Client())) - for (const effect of effects) effect() + for (const effect of effects) void effect() return result } throw err @@ -146,7 +146,7 @@ export namespace Database { try { ctx.use().effects.push(bound) } catch { - bound() + void bound() } } @@ -165,7 +165,7 @@ export namespace Database { const effects: (() => void | Promise)[] = [] const txCallback = InstanceState.bind((tx: TxOrDb) => ctx.provide({ tx, effects }, () => callback(tx))) const result = Client().transaction(txCallback, { behavior: options?.behavior }) - for (const effect of effects) effect() + for (const effect of effects) void effect() return result as NotPromise } throw err diff --git a/packages/opencode/src/sync/sync-event.ts b/packages/opencode/src/sync/sync-event.ts index bee7e3c4cf..d4ad860409 100644 --- a/packages/opencode/src/sync/sync-event.ts +++ b/packages/opencode/src/sync/sync-event.ts @@ -142,11 +142,11 @@ function process(def: Def, event: Event, options: { if (options?.publish) { const result = convertEvent(def.type, event.data) if (result instanceof Promise) { - result.then((data) => { - ProjectBus.publish({ type: def.type, properties: def.schema }, data) + void result.then((data) => { + void ProjectBus.publish({ type: def.type, properties: def.schema }, data) }) } else { - ProjectBus.publish({ type: def.type, properties: def.schema }, result) + void ProjectBus.publish({ type: def.type, properties: def.schema }, result) } GlobalBus.emit("event", { diff --git a/packages/opencode/src/util/defer.ts b/packages/opencode/src/util/defer.ts index 8de21528cc..d1c9edc66a 100644 --- a/packages/opencode/src/util/defer.ts +++ b/packages/opencode/src/util/defer.ts @@ -3,7 +3,7 @@ export function defer void | Promise>( ): T extends () => Promise ? { [Symbol.asyncDispose]: () => Promise } : { [Symbol.dispose]: () => void } { return { [Symbol.dispose]() { - fn() + void fn() }, [Symbol.asyncDispose]() { return Promise.resolve(fn()) diff --git a/packages/opencode/src/util/log.ts b/packages/opencode/src/util/log.ts index 6be9816a87..7c1581bfc0 100644 --- a/packages/opencode/src/util/log.ts +++ b/packages/opencode/src/util/log.ts @@ -59,7 +59,7 @@ let write = (msg: any) => { export async function init(options: Options) { if (options.level) level = options.level - cleanup(Global.Path.log) + void cleanup(Global.Path.log) if (options.print) return logpath = path.join( Global.Path.log, diff --git a/packages/opencode/test/cli/tui/plugin-lifecycle.test.ts b/packages/opencode/test/cli/tui/plugin-lifecycle.test.ts index b22180ef31..078e4484db 100644 --- a/packages/opencode/test/cli/tui/plugin-lifecycle.test.ts +++ b/packages/opencode/test/cli/tui/plugin-lifecycle.test.ts @@ -209,7 +209,7 @@ test( const done = await new Promise((resolve) => { const timer = setTimeout(() => resolve("timeout"), 7000) - TuiPluginRuntime.dispose().then(() => { + void TuiPluginRuntime.dispose().then(() => { clearTimeout(timer) resolve("done") }) diff --git a/packages/opencode/test/mcp/headers.test.ts b/packages/opencode/test/mcp/headers.test.ts index 14c08e3036..175717d056 100644 --- a/packages/opencode/test/mcp/headers.test.ts +++ b/packages/opencode/test/mcp/headers.test.ts @@ -10,7 +10,7 @@ const transportCalls: Array<{ }> = [] // Mock the transport constructors to capture their arguments -mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({ +void mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({ StreamableHTTPClientTransport: class MockStreamableHTTP { constructor(url: URL, options?: { authProvider?: unknown; requestInit?: RequestInit }) { transportCalls.push({ @@ -25,7 +25,7 @@ mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({ }, })) -mock.module("@modelcontextprotocol/sdk/client/sse.js", () => ({ +void mock.module("@modelcontextprotocol/sdk/client/sse.js", () => ({ SSEClientTransport: class MockSSE { constructor(url: URL, options?: { authProvider?: unknown; requestInit?: RequestInit }) { transportCalls.push({ diff --git a/packages/opencode/test/mcp/lifecycle.test.ts b/packages/opencode/test/mcp/lifecycle.test.ts index add7c66d94..31712f1561 100644 --- a/packages/opencode/test/mcp/lifecycle.test.ts +++ b/packages/opencode/test/mcp/lifecycle.test.ts @@ -89,19 +89,19 @@ class MockSSE { } } -mock.module("@modelcontextprotocol/sdk/client/stdio.js", () => ({ +void mock.module("@modelcontextprotocol/sdk/client/stdio.js", () => ({ StdioClientTransport: MockStdioTransport, })) -mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({ +void mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({ StreamableHTTPClientTransport: MockStreamableHTTP, })) -mock.module("@modelcontextprotocol/sdk/client/sse.js", () => ({ +void mock.module("@modelcontextprotocol/sdk/client/sse.js", () => ({ SSEClientTransport: MockSSE, })) -mock.module("@modelcontextprotocol/sdk/client/auth.js", () => ({ +void mock.module("@modelcontextprotocol/sdk/client/auth.js", () => ({ UnauthorizedError: class extends Error { constructor() { super("Unauthorized") @@ -110,7 +110,7 @@ mock.module("@modelcontextprotocol/sdk/client/auth.js", () => ({ })) // Mock Client that delegates to per-name MockClientState -mock.module("@modelcontextprotocol/sdk/client/index.js", () => ({ +void mock.module("@modelcontextprotocol/sdk/client/index.js", () => ({ Client: class MockClient { _state!: MockClientState transport: any diff --git a/packages/opencode/test/mcp/oauth-auto-connect.test.ts b/packages/opencode/test/mcp/oauth-auto-connect.test.ts index 89edd09084..8b29f6d1e3 100644 --- a/packages/opencode/test/mcp/oauth-auto-connect.test.ts +++ b/packages/opencode/test/mcp/oauth-auto-connect.test.ts @@ -22,7 +22,7 @@ let simulateAuthFlow = true let connectSucceedsImmediately = false // Mock the transport constructors to simulate OAuth auto-auth on 401 -mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({ +void mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({ StreamableHTTPClientTransport: class MockStreamableHTTP { authProvider: | { @@ -66,7 +66,7 @@ mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({ }, })) -mock.module("@modelcontextprotocol/sdk/client/sse.js", () => ({ +void mock.module("@modelcontextprotocol/sdk/client/sse.js", () => ({ SSEClientTransport: class MockSSE { constructor(url: URL, options?: { authProvider?: unknown }) { transportCalls.push({ @@ -82,7 +82,7 @@ mock.module("@modelcontextprotocol/sdk/client/sse.js", () => ({ })) // Mock the MCP SDK Client -mock.module("@modelcontextprotocol/sdk/client/index.js", () => ({ +void mock.module("@modelcontextprotocol/sdk/client/index.js", () => ({ Client: class MockClient { async connect(transport: { start: () => Promise }) { await transport.start() @@ -99,7 +99,7 @@ mock.module("@modelcontextprotocol/sdk/client/index.js", () => ({ })) // Mock UnauthorizedError in the auth module so instanceof checks work -mock.module("@modelcontextprotocol/sdk/client/auth.js", () => ({ +void mock.module("@modelcontextprotocol/sdk/client/auth.js", () => ({ UnauthorizedError: MockUnauthorizedError, })) diff --git a/packages/opencode/test/mcp/oauth-browser.test.ts b/packages/opencode/test/mcp/oauth-browser.test.ts index b6a32b1e1b..3a6df02a15 100644 --- a/packages/opencode/test/mcp/oauth-browser.test.ts +++ b/packages/opencode/test/mcp/oauth-browser.test.ts @@ -7,7 +7,7 @@ import type { MCP as MCPNS } from "../../src/mcp/index" let openShouldFail = false let openCalledWith: string | undefined -mock.module("open", () => ({ +void mock.module("open", () => ({ default: async (url: string) => { openCalledWith = url @@ -39,7 +39,7 @@ const transportCalls: Array<{ }> = [] // Mock the transport constructors -mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({ +void mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({ StreamableHTTPClientTransport: class MockStreamableHTTP { url: string authProvider: { redirectToAuthorization?: (url: URL) => Promise } | undefined @@ -65,7 +65,7 @@ mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({ }, })) -mock.module("@modelcontextprotocol/sdk/client/sse.js", () => ({ +void mock.module("@modelcontextprotocol/sdk/client/sse.js", () => ({ SSEClientTransport: class MockSSE { constructor(url: URL) { transportCalls.push({ @@ -81,7 +81,7 @@ mock.module("@modelcontextprotocol/sdk/client/sse.js", () => ({ })) // Mock the MCP SDK Client to trigger OAuth flow -mock.module("@modelcontextprotocol/sdk/client/index.js", () => ({ +void mock.module("@modelcontextprotocol/sdk/client/index.js", () => ({ Client: class MockClient { async connect(transport: { start: () => Promise }) { await transport.start() @@ -90,7 +90,7 @@ mock.module("@modelcontextprotocol/sdk/client/index.js", () => ({ })) // Mock UnauthorizedError in the auth module -mock.module("@modelcontextprotocol/sdk/client/auth.js", () => ({ +void mock.module("@modelcontextprotocol/sdk/client/auth.js", () => ({ UnauthorizedError: MockUnauthorizedError, })) diff --git a/packages/opencode/test/memory/abort-leak-webfetch.ts b/packages/opencode/test/memory/abort-leak-webfetch.ts index 1286d5f0b3..c3197f8dd5 100644 --- a/packages/opencode/test/memory/abort-leak-webfetch.ts +++ b/packages/opencode/test/memory/abort-leak-webfetch.ts @@ -44,6 +44,6 @@ try { const after = heap() process.stdout.write(JSON.stringify({ baseline, after, growth: after - baseline })) } finally { - server.stop(true) + void server.stop(true) process.exit(0) } diff --git a/packages/opencode/test/permission/next.test.ts b/packages/opencode/test/permission/next.test.ts index 805c230f3e..d654d4b876 100644 --- a/packages/opencode/test/permission/next.test.ts +++ b/packages/opencode/test/permission/next.test.ts @@ -954,7 +954,7 @@ it.live("pending permission rejects on instance dispose", () => }).pipe(run, Effect.forkScoped) expect(yield* waitForPending(1).pipe(run)).toHaveLength(1) - yield* Effect.promise(() => Instance.provide({ directory: dir, fn: () => Instance.dispose() })) + yield* Effect.promise(() => Instance.provide({ directory: dir, fn: () => void Instance.dispose() })) const exit = yield* Fiber.await(fiber) expect(Exit.isFailure(exit)).toBe(true) diff --git a/packages/opencode/test/preload.ts b/packages/opencode/test/preload.ts index ba5df4f1ea..7c6f04c796 100644 --- a/packages/opencode/test/preload.ts +++ b/packages/opencode/test/preload.ts @@ -81,7 +81,7 @@ process.env["OPENCODE_DB"] = ":memory:" const { Log } = await import("../src/util") const { initProjectors } = await import("../src/server/projectors") -Log.init({ +void Log.init({ print: false, dev: true, level: "DEBUG", diff --git a/packages/opencode/test/project/migrate-global.test.ts b/packages/opencode/test/project/migrate-global.test.ts index d645fb25b8..c399d8872d 100644 --- a/packages/opencode/test/project/migrate-global.test.ts +++ b/packages/opencode/test/project/migrate-global.test.ts @@ -10,7 +10,7 @@ import { $ } from "bun" import { tmpdir } from "../fixture/fixture" import { Effect } from "effect" -Log.init({ print: false }) +void Log.init({ print: false }) function run(fn: (svc: Project.Interface) => Effect.Effect) { return Effect.runPromise( diff --git a/packages/opencode/test/project/project.test.ts b/packages/opencode/test/project/project.test.ts index a579a2335d..4c272b7949 100644 --- a/packages/opencode/test/project/project.test.ts +++ b/packages/opencode/test/project/project.test.ts @@ -12,7 +12,7 @@ import { NodePath } from "@effect/platform-node" import { AppFileSystem } from "@opencode-ai/shared/filesystem" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" -Log.init({ print: false }) +void Log.init({ print: false }) const encoder = new TextEncoder() diff --git a/packages/opencode/test/server/global-session-list.test.ts b/packages/opencode/test/server/global-session-list.test.ts index c029fd9336..0edabd8e65 100644 --- a/packages/opencode/test/server/global-session-list.test.ts +++ b/packages/opencode/test/server/global-session-list.test.ts @@ -7,7 +7,7 @@ import { Session as SessionNs } from "../../src/session" import { Log } from "../../src/util" import { tmpdir } from "../fixture/fixture" -Log.init({ print: false }) +void Log.init({ print: false }) function run(fx: Effect.Effect) { return Effect.runPromise(fx.pipe(Effect.provide(SessionNs.defaultLayer))) diff --git a/packages/opencode/test/server/project-init-git.test.ts b/packages/opencode/test/server/project-init-git.test.ts index c3ee18e73a..a29b4ebb35 100644 --- a/packages/opencode/test/server/project-init-git.test.ts +++ b/packages/opencode/test/server/project-init-git.test.ts @@ -10,7 +10,7 @@ import { Log } from "../../src/util" import { resetDatabase } from "../fixture/db" import { provideInstance, tmpdir } from "../fixture/fixture" -Log.init({ print: false }) +void Log.init({ print: false }) afterEach(async () => { await resetDatabase() diff --git a/packages/opencode/test/server/session-actions.test.ts b/packages/opencode/test/server/session-actions.test.ts index 3209ebff35..4be2344aab 100644 --- a/packages/opencode/test/server/session-actions.test.ts +++ b/packages/opencode/test/server/session-actions.test.ts @@ -7,7 +7,7 @@ import type { SessionID } from "../../src/session/schema" import { Log } from "../../src/util" import { tmpdir } from "../fixture/fixture" -Log.init({ print: false }) +void Log.init({ print: false }) function run(fx: Effect.Effect) { return Effect.runPromise(fx.pipe(Effect.provide(SessionNs.defaultLayer))) diff --git a/packages/opencode/test/server/session-list.test.ts b/packages/opencode/test/server/session-list.test.ts index 9af60b9bdd..602d0f2049 100644 --- a/packages/opencode/test/server/session-list.test.ts +++ b/packages/opencode/test/server/session-list.test.ts @@ -5,7 +5,7 @@ import { Session as SessionNs } from "../../src/session" import { Log } from "../../src/util" import { tmpdir } from "../fixture/fixture" -Log.init({ print: false }) +void Log.init({ print: false }) function run(fx: Effect.Effect) { return Effect.runPromise(fx.pipe(Effect.provide(SessionNs.defaultLayer))) diff --git a/packages/opencode/test/server/session-messages.test.ts b/packages/opencode/test/server/session-messages.test.ts index d558d4324f..50b7658969 100644 --- a/packages/opencode/test/server/session-messages.test.ts +++ b/packages/opencode/test/server/session-messages.test.ts @@ -8,7 +8,7 @@ import { MessageID, PartID, type SessionID } from "../../src/session/schema" import { Log } from "../../src/util" import { tmpdir } from "../fixture/fixture" -Log.init({ print: false }) +void Log.init({ print: false }) function run(fx: Effect.Effect) { return Effect.runPromise(fx.pipe(Effect.provide(SessionNs.defaultLayer))) diff --git a/packages/opencode/test/server/session-select.test.ts b/packages/opencode/test/server/session-select.test.ts index c53448dfd4..21e07f88a0 100644 --- a/packages/opencode/test/server/session-select.test.ts +++ b/packages/opencode/test/server/session-select.test.ts @@ -7,7 +7,7 @@ import { Instance } from "../../src/project/instance" import { Server } from "../../src/server/server" import { tmpdir } from "../fixture/fixture" -Log.init({ print: false }) +void Log.init({ print: false }) function run(fx: Effect.Effect) { return Effect.runPromise(fx.pipe(Effect.provide(SessionNs.defaultLayer))) diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index ee01932210..ee3f645c52 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/packages/opencode/test/session/compaction.test.ts @@ -27,7 +27,7 @@ import { ProviderTest } from "../fake/provider" import { testEffect } from "../lib/effect" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" -Log.init({ print: false }) +void Log.init({ print: false }) function run(fx: Effect.Effect) { return Effect.runPromise(fx.pipe(Effect.provide(SessionNs.defaultLayer))) diff --git a/packages/opencode/test/session/llm.test.ts b/packages/opencode/test/session/llm.test.ts index d1d53f605b..f26bef6052 100644 --- a/packages/opencode/test/session/llm.test.ts +++ b/packages/opencode/test/session/llm.test.ts @@ -229,7 +229,7 @@ beforeEach(() => { }) afterAll(() => { - state.server?.stop() + void state.server?.stop() }) function createChatStream(text: string) { diff --git a/packages/opencode/test/session/messages-pagination.test.ts b/packages/opencode/test/session/messages-pagination.test.ts index 804076dd48..40ccacc584 100644 --- a/packages/opencode/test/session/messages-pagination.test.ts +++ b/packages/opencode/test/session/messages-pagination.test.ts @@ -9,7 +9,7 @@ import { ModelID, ProviderID } from "../../src/provider/schema" import { Log } from "../../src/util" const root = path.join(__dirname, "../..") -Log.init({ print: false }) +void Log.init({ print: false }) function run(fx: Effect.Effect) { return Effect.runPromise(fx.pipe(Effect.provide(SessionNs.defaultLayer))) diff --git a/packages/opencode/test/session/processor-effect.test.ts b/packages/opencode/test/session/processor-effect.test.ts index 87ff40c707..74ce913077 100644 --- a/packages/opencode/test/session/processor-effect.test.ts +++ b/packages/opencode/test/session/processor-effect.test.ts @@ -24,7 +24,7 @@ import { provideTmpdirServer } from "../fixture/fixture" import { testEffect } from "../lib/effect" import { raw, reply, TestLLMServer } from "../lib/llm-server" -Log.init({ print: false }) +void Log.init({ print: false }) const summary = Layer.succeed( SessionSummary.Service, diff --git a/packages/opencode/test/session/prompt-effect.test.ts b/packages/opencode/test/session/prompt-effect.test.ts index 0a750352a7..6819da4817 100644 --- a/packages/opencode/test/session/prompt-effect.test.ts +++ b/packages/opencode/test/session/prompt-effect.test.ts @@ -44,7 +44,7 @@ import { provideTmpdirInstance, provideTmpdirServer } from "../fixture/fixture" import { testEffect } from "../lib/effect" import { reply, TestLLMServer } from "../lib/llm-server" -Log.init({ print: false }) +void Log.init({ print: false }) const summary = Layer.succeed( SessionSummary.Service, diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index acf305f3f9..2b489da9e9 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -11,7 +11,7 @@ import { SessionPrompt } from "../../src/session/prompt" import { Log } from "../../src/util" import { tmpdir } from "../fixture/fixture" -Log.init({ print: false }) +void Log.init({ print: false }) function run(fx: Effect.Effect) { return Effect.runPromise( @@ -316,7 +316,7 @@ describe("session.prompt regression", () => { ), }) } finally { - server.stop(true) + void server.stop(true) } }) @@ -409,7 +409,7 @@ describe("session.prompt regression", () => { ), }) } finally { - server.stop(true) + void server.stop(true) } }) }) diff --git a/packages/opencode/test/session/revert-compact.test.ts b/packages/opencode/test/session/revert-compact.test.ts index 211fcde9a8..f28fb94c0b 100644 --- a/packages/opencode/test/session/revert-compact.test.ts +++ b/packages/opencode/test/session/revert-compact.test.ts @@ -13,7 +13,7 @@ import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" import { provideTmpdirInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" -Log.init({ print: false }) +void Log.init({ print: false }) const env = Layer.mergeAll( Session.defaultLayer, diff --git a/packages/opencode/test/session/session.test.ts b/packages/opencode/test/session/session.test.ts index 9c4686cba6..f63ad9beed 100644 --- a/packages/opencode/test/session/session.test.ts +++ b/packages/opencode/test/session/session.test.ts @@ -10,7 +10,7 @@ import { AppRuntime } from "../../src/effect/app-runtime" import { tmpdir } from "../fixture/fixture" const projectRoot = path.join(__dirname, "../..") -Log.init({ print: false }) +void Log.init({ print: false }) function create(input?: SessionNs.CreateInput) { return AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create(input))) diff --git a/packages/opencode/test/session/snapshot-tool-race.test.ts b/packages/opencode/test/session/snapshot-tool-race.test.ts index cb7fe4568e..38aed43765 100644 --- a/packages/opencode/test/session/snapshot-tool-race.test.ts +++ b/packages/opencode/test/session/snapshot-tool-race.test.ts @@ -57,7 +57,7 @@ import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" import { Ripgrep } from "../../src/file/ripgrep" import { Format } from "../../src/format" -Log.init({ print: false }) +void Log.init({ print: false }) const mcp = Layer.succeed( MCP.Service, diff --git a/packages/opencode/test/session/structured-output-integration.test.ts b/packages/opencode/test/session/structured-output-integration.test.ts index 346705bf22..fb8d42f077 100644 --- a/packages/opencode/test/session/structured-output-integration.test.ts +++ b/packages/opencode/test/session/structured-output-integration.test.ts @@ -8,7 +8,7 @@ import { Instance } from "../../src/project/instance" import { MessageV2 } from "../../src/session/message-v2" const projectRoot = path.join(__dirname, "../..") -Log.init({ print: false }) +void Log.init({ print: false }) // Skip tests if no API key is available const hasApiKey = !!process.env.ANTHROPIC_API_KEY diff --git a/packages/opencode/test/skill/discovery.test.ts b/packages/opencode/test/skill/discovery.test.ts index 175500862d..3f82103293 100644 --- a/packages/opencode/test/skill/discovery.test.ts +++ b/packages/opencode/test/skill/discovery.test.ts @@ -42,7 +42,7 @@ beforeAll(async () => { }) afterAll(async () => { - server?.stop() + void server?.stop() await rm(cacheDir, { recursive: true, force: true }) }) diff --git a/packages/sdk/js/src/gen/core/serverSentEvents.gen.ts b/packages/sdk/js/src/gen/core/serverSentEvents.gen.ts index 8f7fac549d..ffc4f16dc1 100644 --- a/packages/sdk/js/src/gen/core/serverSentEvents.gen.ts +++ b/packages/sdk/js/src/gen/core/serverSentEvents.gen.ts @@ -111,7 +111,7 @@ export const createSseClient = ({ const abortHandler = () => { try { - reader.cancel() + void reader.cancel() } catch { // noop } diff --git a/packages/sdk/js/src/v2/gen/core/serverSentEvents.gen.ts b/packages/sdk/js/src/v2/gen/core/serverSentEvents.gen.ts index 056a812593..eecc3c37a0 100644 --- a/packages/sdk/js/src/v2/gen/core/serverSentEvents.gen.ts +++ b/packages/sdk/js/src/v2/gen/core/serverSentEvents.gen.ts @@ -138,7 +138,7 @@ export const createSseClient = ({ const abortHandler = () => { try { - reader.cancel() + void reader.cancel() } catch { // noop } diff --git a/packages/slack/src/index.ts b/packages/slack/src/index.ts index 85d6851296..bd5523a2a0 100644 --- a/packages/slack/src/index.ts +++ b/packages/slack/src/index.ts @@ -20,7 +20,7 @@ const opencode = await createOpencode({ console.log("✅ Opencode server ready") const sessions = new Map() -;(async () => { +void (async () => { const events = await opencode.client.event.subscribe() for await (const event of events.stream) { if (event.type === "message.part.updated") { @@ -29,7 +29,7 @@ const sessions = new Map { + void heightAnim.finished.then(() => { if (!contentRef || !open()) return contentRef.style.overflow = "visible" contentRef.style.height = "auto" diff --git a/packages/ui/src/components/list.tsx b/packages/ui/src/components/list.tsx index b5879624e0..cc5fc0ce5d 100644 --- a/packages/ui/src/components/list.tsx +++ b/packages/ui/src/components/list.tsx @@ -107,7 +107,7 @@ export function List(props: ListProps & { ref?: (ref: ListRef) => void }) // Force a refetch even if the value is unchanged. // This is important for programmatic changes like Tab completion. if (prev === value) { - refetch() + void refetch() return } queueMicrotask(() => refetch()) diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 81e6a52a26..a47ff18045 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -1158,7 +1158,7 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp onMouseDown={(e) => e.preventDefault()} onClick={(event) => { event.stopPropagation() - handleCopy() + void handleCopy() }} aria-label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copyMessage")} /> diff --git a/packages/ui/src/components/text-field.tsx b/packages/ui/src/components/text-field.tsx index d10f5d6ace..93b2663bad 100644 --- a/packages/ui/src/components/text-field.tsx +++ b/packages/ui/src/components/text-field.tsx @@ -6,7 +6,8 @@ import { IconButton } from "./icon-button" import { Tooltip } from "./tooltip" export interface TextFieldProps - extends ComponentProps, + extends + ComponentProps, Partial< Pick< ComponentProps, @@ -75,7 +76,7 @@ export function TextField(props: TextFieldProps) { } function handleClick() { - if (local.copyable) handleCopy() + if (local.copyable) void handleCopy() } return ( diff --git a/packages/ui/src/components/text-reveal.tsx b/packages/ui/src/components/text-reveal.tsx index 02bf8084ce..2d2a94e6a3 100644 --- a/packages/ui/src/components/text-reveal.tsx +++ b/packages/ui/src/components/text-reveal.tsx @@ -102,7 +102,7 @@ export function TextReveal(props: { requestAnimationFrame(() => setState("ready", true)) return } - fonts.ready.finally(() => { + void fonts.ready.finally(() => { widen(win()) requestAnimationFrame(() => setState("ready", true)) }) diff --git a/packages/ui/src/components/thinking-heading.stories.tsx b/packages/ui/src/components/thinking-heading.stories.tsx index 3a65619ce1..12a06b4d83 100644 --- a/packages/ui/src/components/thinking-heading.stories.tsx +++ b/packages/ui/src/components/thinking-heading.stories.tsx @@ -442,7 +442,7 @@ function AnimatedHeading(props) { onMount(() => { measure() - document.fonts?.ready.finally(() => { + void document.fonts?.ready.finally(() => { measure() requestAnimationFrame(() => setState("ready", true)) }) diff --git a/packages/ui/src/components/tool-error-card.tsx b/packages/ui/src/components/tool-error-card.tsx index 038870d384..9983e2fe79 100644 --- a/packages/ui/src/components/tool-error-card.tsx +++ b/packages/ui/src/components/tool-error-card.tsx @@ -128,7 +128,7 @@ export function ToolErrorCard(props: ToolErrorCardProps) { onMouseDown={(e) => e.preventDefault()} onClick={(e) => { e.stopPropagation() - copy() + void copy() }} aria-label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.toolErrorCard.copyError")} /> diff --git a/packages/ui/src/components/tool-status-title.tsx b/packages/ui/src/components/tool-status-title.tsx index 2a58e0e5bb..412d92e3db 100644 --- a/packages/ui/src/components/tool-status-title.tsx +++ b/packages/ui/src/components/tool-status-title.tsx @@ -86,7 +86,7 @@ export function ToolStatusTitle(props: { finish() return } - fonts.ready.finally(() => { + void fonts.ready.finally(() => { measure() finish() }) diff --git a/packages/ui/src/pierre/worker.ts b/packages/ui/src/pierre/worker.ts index 1993ad7aa6..d25dee4d9d 100644 --- a/packages/ui/src/pierre/worker.ts +++ b/packages/ui/src/pierre/worker.ts @@ -25,7 +25,7 @@ function createPool(lineDiffType: "none" | "word-alt") { }, ) - pool.initialize() + void pool.initialize() return pool } diff --git a/packages/ui/vite.config.ts b/packages/ui/vite.config.ts index 335084bd64..1e38adede2 100644 --- a/packages/ui/vite.config.ts +++ b/packages/ui/vite.config.ts @@ -36,10 +36,10 @@ function providerIconsPlugin() { return { name: "provider-icons-plugin", configureServer() { - fetchProviderIcons() + void fetchProviderIcons() }, buildStart() { - fetchProviderIcons() + void fetchProviderIcons() }, } } diff --git a/script/duplicate-pr.ts b/script/duplicate-pr.ts index b77737c1d4..2ef16cb653 100755 --- a/script/duplicate-pr.ts +++ b/script/duplicate-pr.ts @@ -76,4 +76,4 @@ Examples: } } -main() +void main() From 0beaf04df5d04e09edf58471244243098e34c324 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Thu, 16 Apr 2026 03:28:30 +0000 Subject: [PATCH 212/300] chore: generate --- packages/sdk/js/src/v2/gen/core/serverSentEvents.gen.ts | 2 +- packages/ui/src/components/text-field.tsx | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/sdk/js/src/v2/gen/core/serverSentEvents.gen.ts b/packages/sdk/js/src/v2/gen/core/serverSentEvents.gen.ts index eecc3c37a0..056a812593 100644 --- a/packages/sdk/js/src/v2/gen/core/serverSentEvents.gen.ts +++ b/packages/sdk/js/src/v2/gen/core/serverSentEvents.gen.ts @@ -138,7 +138,7 @@ export const createSseClient = ({ const abortHandler = () => { try { - void reader.cancel() + reader.cancel() } catch { // noop } diff --git a/packages/ui/src/components/text-field.tsx b/packages/ui/src/components/text-field.tsx index 93b2663bad..82be20f9ea 100644 --- a/packages/ui/src/components/text-field.tsx +++ b/packages/ui/src/components/text-field.tsx @@ -6,8 +6,7 @@ import { IconButton } from "./icon-button" import { Tooltip } from "./tooltip" export interface TextFieldProps - extends - ComponentProps, + extends ComponentProps, Partial< Pick< ComponentProps, From a427a28fa9750e5a9bcae3e72cfa582b071c640b Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 23:28:46 -0400 Subject: [PATCH 213/300] feat: unwrap project namespaces to flat exports + barrel (#22743) --- packages/opencode/src/cli/cmd/debug/scrap.ts | 2 +- packages/opencode/src/cli/cmd/stats.ts | 2 +- .../opencode/src/control-plane/workspace.ts | 2 +- packages/opencode/src/effect/app-runtime.ts | 4 +- .../opencode/src/effect/bootstrap-runtime.ts | 2 +- packages/opencode/src/project/bootstrap.ts | 4 +- packages/opencode/src/project/index.ts | 2 + packages/opencode/src/project/instance.ts | 2 +- packages/opencode/src/project/project.ts | 842 +++++++++--------- packages/opencode/src/project/vcs.ts | 434 +++++---- .../src/server/instance/experimental.ts | 2 +- .../opencode/src/server/instance/index.ts | 2 +- .../opencode/src/server/instance/project.ts | 2 +- packages/opencode/src/worktree/worktree.ts | 2 +- .../test/project/migrate-global.test.ts | 2 +- .../opencode/test/project/project.test.ts | 2 +- packages/opencode/test/project/vcs.test.ts | 2 +- .../test/server/global-session-list.test.ts | 2 +- 18 files changed, 655 insertions(+), 657 deletions(-) create mode 100644 packages/opencode/src/project/index.ts diff --git a/packages/opencode/src/cli/cmd/debug/scrap.ts b/packages/opencode/src/cli/cmd/debug/scrap.ts index 464b165d72..300a7b9656 100644 --- a/packages/opencode/src/cli/cmd/debug/scrap.ts +++ b/packages/opencode/src/cli/cmd/debug/scrap.ts @@ -1,5 +1,5 @@ import { EOL } from "os" -import { Project } from "../../../project/project" +import { Project } from "../../../project" import { Log } from "../../../util" import { cmd } from "../cmd" diff --git a/packages/opencode/src/cli/cmd/stats.ts b/packages/opencode/src/cli/cmd/stats.ts index 527a6ac952..d66ac252fa 100644 --- a/packages/opencode/src/cli/cmd/stats.ts +++ b/packages/opencode/src/cli/cmd/stats.ts @@ -4,7 +4,7 @@ import { Session } from "../../session" import { bootstrap } from "../bootstrap" import { Database } from "../../storage/db" import { SessionTable } from "../../session/session.sql" -import { Project } from "../../project/project" +import { Project } from "../../project" import { Instance } from "../../project/instance" import { AppRuntime } from "@/effect/app-runtime" diff --git a/packages/opencode/src/control-plane/workspace.ts b/packages/opencode/src/control-plane/workspace.ts index dfd018db7e..f38b27e6f8 100644 --- a/packages/opencode/src/control-plane/workspace.ts +++ b/packages/opencode/src/control-plane/workspace.ts @@ -2,7 +2,7 @@ import z from "zod" import { setTimeout as sleep } from "node:timers/promises" import { fn } from "@/util/fn" import { Database, asc, eq, inArray } from "@/storage/db" -import { Project } from "@/project/project" +import { Project } from "@/project" import { BusEvent } from "@/bus/bus-event" import { GlobalBus } from "@/bus/global" import { SyncEvent } from "@/sync" diff --git a/packages/opencode/src/effect/app-runtime.ts b/packages/opencode/src/effect/app-runtime.ts index f9f811e711..7608e9c701 100644 --- a/packages/opencode/src/effect/app-runtime.ts +++ b/packages/opencode/src/effect/app-runtime.ts @@ -40,8 +40,8 @@ import { Command } from "@/command" import { Truncate } from "@/tool/truncate" import { ToolRegistry } from "@/tool/registry" import { Format } from "@/format" -import { Project } from "@/project/project" -import { Vcs } from "@/project/vcs" +import { Project } from "@/project" +import { Vcs } from "@/project" import { Worktree } from "@/worktree" import { Pty } from "@/pty" import { Installation } from "@/installation" diff --git a/packages/opencode/src/effect/bootstrap-runtime.ts b/packages/opencode/src/effect/bootstrap-runtime.ts index d8400c52ae..7d34b4bd48 100644 --- a/packages/opencode/src/effect/bootstrap-runtime.ts +++ b/packages/opencode/src/effect/bootstrap-runtime.ts @@ -7,7 +7,7 @@ import { FileWatcher } from "@/file/watcher" import { Format } from "@/format" import { ShareNext } from "@/share/share-next" import { File } from "@/file" -import { Vcs } from "@/project/vcs" +import { Vcs } from "@/project" import { Snapshot } from "@/snapshot" import { Bus } from "@/bus" import { Observability } from "./observability" diff --git a/packages/opencode/src/project/bootstrap.ts b/packages/opencode/src/project/bootstrap.ts index f00d8ffd9b..c88eb8e039 100644 --- a/packages/opencode/src/project/bootstrap.ts +++ b/packages/opencode/src/project/bootstrap.ts @@ -3,8 +3,8 @@ import { Format } from "../format" import { LSP } from "../lsp" import { File } from "../file" import { Snapshot } from "../snapshot" -import { Project } from "./project" -import { Vcs } from "./vcs" +import { Project } from "." +import { Vcs } from "." import { Bus } from "../bus" import { Command } from "../command" import { Instance } from "./instance" diff --git a/packages/opencode/src/project/index.ts b/packages/opencode/src/project/index.ts new file mode 100644 index 0000000000..d9f168f6ff --- /dev/null +++ b/packages/opencode/src/project/index.ts @@ -0,0 +1,2 @@ +export * as Vcs from "./vcs" +export * as Project from "./project" diff --git a/packages/opencode/src/project/instance.ts b/packages/opencode/src/project/instance.ts index a8a5218751..b95962ae08 100644 --- a/packages/opencode/src/project/instance.ts +++ b/packages/opencode/src/project/instance.ts @@ -5,7 +5,7 @@ import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { iife } from "@/util/iife" import { Log } from "@/util" import { LocalContext } from "../util" -import { Project } from "./project" +import { Project } from "." import { WorkspaceContext } from "@/control-plane/workspace-context" export interface InstanceContext { diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index 9c4ed58ce8..99fe88ff16 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -14,474 +14,472 @@ import { NodePath } from "@effect/platform-node" import { AppFileSystem } from "@opencode-ai/shared/filesystem" import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" -export namespace Project { - const log = Log.create({ service: "project" }) +const log = Log.create({ service: "project" }) - export const Info = z - .object({ - id: ProjectID.zod, - worktree: z.string(), - vcs: z.literal("git").optional(), - name: z.string().optional(), - icon: z - .object({ - url: z.string().optional(), - override: z.string().optional(), - color: z.string().optional(), - }) - .optional(), - commands: z - .object({ - start: z.string().optional().describe("Startup script to run when creating a new workspace (worktree)"), - }) - .optional(), - time: z.object({ - created: z.number(), - updated: z.number(), - initialized: z.number().optional(), - }), - sandboxes: z.array(z.string()), - }) - .meta({ - ref: "Project", - }) - export type Info = z.infer - - export const Event = { - Updated: BusEvent.define("project.updated", Info), - } - - type Row = typeof ProjectTable.$inferSelect - - export function fromRow(row: Row): Info { - const icon = - row.icon_url || row.icon_color - ? { url: row.icon_url ?? undefined, color: row.icon_color ?? undefined } - : undefined - return { - id: row.id, - worktree: row.worktree, - vcs: row.vcs ? Info.shape.vcs.parse(row.vcs) : undefined, - name: row.name ?? undefined, - icon, - time: { - created: row.time_created, - updated: row.time_updated, - initialized: row.time_initialized ?? undefined, - }, - sandboxes: row.sandboxes, - commands: row.commands ?? undefined, - } - } - - export const UpdateInput = z.object({ - projectID: ProjectID.zod, +export const Info = z + .object({ + id: ProjectID.zod, + worktree: z.string(), + vcs: z.literal("git").optional(), name: z.string().optional(), - icon: Info.shape.icon.optional(), - commands: Info.shape.commands.optional(), + icon: z + .object({ + url: z.string().optional(), + override: z.string().optional(), + color: z.string().optional(), + }) + .optional(), + commands: z + .object({ + start: z.string().optional().describe("Startup script to run when creating a new workspace (worktree)"), + }) + .optional(), + time: z.object({ + created: z.number(), + updated: z.number(), + initialized: z.number().optional(), + }), + sandboxes: z.array(z.string()), }) - export type UpdateInput = z.infer + .meta({ + ref: "Project", + }) +export type Info = z.infer - // --------------------------------------------------------------------------- - // Effect service - // --------------------------------------------------------------------------- +export const Event = { + Updated: BusEvent.define("project.updated", Info), +} - export interface Interface { - readonly fromDirectory: (directory: string) => Effect.Effect<{ project: Info; sandbox: string }> - readonly discover: (input: Info) => Effect.Effect - readonly list: () => Effect.Effect - readonly get: (id: ProjectID) => Effect.Effect - readonly update: (input: UpdateInput) => Effect.Effect - readonly initGit: (input: { directory: string; project: Info }) => Effect.Effect - readonly setInitialized: (id: ProjectID) => Effect.Effect - readonly sandboxes: (id: ProjectID) => Effect.Effect - readonly addSandbox: (id: ProjectID, directory: string) => Effect.Effect - readonly removeSandbox: (id: ProjectID, directory: string) => Effect.Effect +type Row = typeof ProjectTable.$inferSelect + +export function fromRow(row: Row): Info { + const icon = + row.icon_url || row.icon_color + ? { url: row.icon_url ?? undefined, color: row.icon_color ?? undefined } + : undefined + return { + id: row.id, + worktree: row.worktree, + vcs: row.vcs ? Info.shape.vcs.parse(row.vcs) : undefined, + name: row.name ?? undefined, + icon, + time: { + created: row.time_created, + updated: row.time_updated, + initialized: row.time_initialized ?? undefined, + }, + sandboxes: row.sandboxes, + commands: row.commands ?? undefined, } +} - export class Service extends Context.Service()("@opencode/Project") {} +export const UpdateInput = z.object({ + projectID: ProjectID.zod, + name: z.string().optional(), + icon: Info.shape.icon.optional(), + commands: Info.shape.commands.optional(), +}) +export type UpdateInput = z.infer - type GitResult = { code: number; text: string; stderr: string } +// --------------------------------------------------------------------------- +// Effect service +// --------------------------------------------------------------------------- - export const layer: Layer.Layer< - Service, - never, - AppFileSystem.Service | Path.Path | ChildProcessSpawner.ChildProcessSpawner - > = Layer.effect( - Service, - Effect.gen(function* () { - const fs = yield* AppFileSystem.Service - const pathSvc = yield* Path.Path - const spawner = yield* ChildProcessSpawner.ChildProcessSpawner +export interface Interface { + readonly fromDirectory: (directory: string) => Effect.Effect<{ project: Info; sandbox: string }> + readonly discover: (input: Info) => Effect.Effect + readonly list: () => Effect.Effect + readonly get: (id: ProjectID) => Effect.Effect + readonly update: (input: UpdateInput) => Effect.Effect + readonly initGit: (input: { directory: string; project: Info }) => Effect.Effect + readonly setInitialized: (id: ProjectID) => Effect.Effect + readonly sandboxes: (id: ProjectID) => Effect.Effect + readonly addSandbox: (id: ProjectID, directory: string) => Effect.Effect + readonly removeSandbox: (id: ProjectID, directory: string) => Effect.Effect +} - const git = Effect.fnUntraced( - function* (args: string[], opts?: { cwd?: string }) { - const handle = yield* spawner.spawn( - ChildProcess.make("git", args, { cwd: opts?.cwd, extendEnv: true, stdin: "ignore" }), - ) - const [text, stderr] = yield* Effect.all( - [Stream.mkString(Stream.decodeText(handle.stdout)), Stream.mkString(Stream.decodeText(handle.stderr))], - { concurrency: 2 }, - ) - const code = yield* handle.exitCode - return { code, text, stderr } satisfies GitResult - }, - Effect.scoped, - Effect.catch(() => Effect.succeed({ code: 1, text: "", stderr: "" } satisfies GitResult)), +export class Service extends Context.Service()("@opencode/Project") {} + +type GitResult = { code: number; text: string; stderr: string } + +export const layer: Layer.Layer< + Service, + never, + AppFileSystem.Service | Path.Path | ChildProcessSpawner.ChildProcessSpawner +> = Layer.effect( + Service, + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + const pathSvc = yield* Path.Path + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner + + const git = Effect.fnUntraced( + function* (args: string[], opts?: { cwd?: string }) { + const handle = yield* spawner.spawn( + ChildProcess.make("git", args, { cwd: opts?.cwd, extendEnv: true, stdin: "ignore" }), + ) + const [text, stderr] = yield* Effect.all( + [Stream.mkString(Stream.decodeText(handle.stdout)), Stream.mkString(Stream.decodeText(handle.stderr))], + { concurrency: 2 }, + ) + const code = yield* handle.exitCode + return { code, text, stderr } satisfies GitResult + }, + Effect.scoped, + Effect.catch(() => Effect.succeed({ code: 1, text: "", stderr: "" } satisfies GitResult)), + ) + + const db = (fn: (d: Parameters[0] extends (trx: infer D) => any ? D : never) => T) => + Effect.sync(() => Database.use(fn)) + + const emitUpdated = (data: Info) => + Effect.sync(() => + GlobalBus.emit("event", { + directory: "global", + project: data.id, + payload: { type: Event.Updated.type, properties: data }, + }), ) - const db = (fn: (d: Parameters[0] extends (trx: infer D) => any ? D : never) => T) => - Effect.sync(() => Database.use(fn)) + const fakeVcs = Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS) - const emitUpdated = (data: Info) => - Effect.sync(() => - GlobalBus.emit("event", { - directory: "global", - project: data.id, - payload: { type: Event.Updated.type, properties: data }, - }), - ) + const resolveGitPath = (cwd: string, name: string) => { + if (!name) return cwd + name = name.replace(/[\r\n]+$/, "") + if (!name) return cwd + name = AppFileSystem.windowsPath(name) + if (pathSvc.isAbsolute(name)) return pathSvc.normalize(name) + return pathSvc.resolve(cwd, name) + } - const fakeVcs = Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS) + const scope = yield* Scope.Scope - const resolveGitPath = (cwd: string, name: string) => { - if (!name) return cwd - name = name.replace(/[\r\n]+$/, "") - if (!name) return cwd - name = AppFileSystem.windowsPath(name) - if (pathSvc.isAbsolute(name)) return pathSvc.normalize(name) - return pathSvc.resolve(cwd, name) - } + const readCachedProjectId = Effect.fnUntraced(function* (dir: string) { + return yield* fs.readFileString(pathSvc.join(dir, "opencode")).pipe( + Effect.map((x) => x.trim()), + Effect.map(ProjectID.make), + Effect.catch(() => Effect.void), + ) + }) - const scope = yield* Scope.Scope + const fromDirectory = Effect.fn("Project.fromDirectory")(function* (directory: string) { + log.info("fromDirectory", { directory }) - const readCachedProjectId = Effect.fnUntraced(function* (dir: string) { - return yield* fs.readFileString(pathSvc.join(dir, "opencode")).pipe( - Effect.map((x) => x.trim()), - Effect.map(ProjectID.make), - Effect.catch(() => Effect.void), - ) + // Phase 1: discover git info + type DiscoveryResult = { id: ProjectID; worktree: string; sandbox: string; vcs: Info["vcs"] } + + const data: DiscoveryResult = yield* Effect.gen(function* () { + const dotgitMatches = yield* fs.up({ targets: [".git"], start: directory }).pipe(Effect.orDie) + const dotgit = dotgitMatches[0] + + if (!dotgit) { + return { + id: ProjectID.global, + worktree: "/", + sandbox: "/", + vcs: fakeVcs, + } + } + + let sandbox = pathSvc.dirname(dotgit) + const gitBinary = yield* Effect.sync(() => which("git")) + let id = yield* readCachedProjectId(dotgit) + + if (!gitBinary) { + return { + id: id ?? ProjectID.global, + worktree: sandbox, + sandbox, + vcs: fakeVcs, + } + } + + const commonDir = yield* git(["rev-parse", "--git-common-dir"], { cwd: sandbox }) + if (commonDir.code !== 0) { + return { + id: id ?? ProjectID.global, + worktree: sandbox, + sandbox, + vcs: fakeVcs, + } + } + const worktree = (() => { + const common = resolveGitPath(sandbox, commonDir.text.trim()) + return common === sandbox ? sandbox : pathSvc.dirname(common) + })() + + if (id == null) { + id = yield* readCachedProjectId(pathSvc.join(worktree, ".git")) + } + + if (!id) { + const revList = yield* git(["rev-list", "--max-parents=0", "HEAD"], { cwd: sandbox }) + const roots = revList.text + .split("\n") + .filter(Boolean) + .map((x) => x.trim()) + .toSorted() + + id = roots[0] ? ProjectID.make(roots[0]) : undefined + if (id) { + yield* fs.writeFileString(pathSvc.join(worktree, ".git", "opencode"), id).pipe(Effect.ignore) + } + } + + if (!id) { + return { id: ProjectID.global, worktree: sandbox, sandbox, vcs: "git" as const } + } + + const topLevel = yield* git(["rev-parse", "--show-toplevel"], { cwd: sandbox }) + if (topLevel.code !== 0) { + return { + id, + worktree: sandbox, + sandbox, + vcs: fakeVcs, + } + } + sandbox = resolveGitPath(sandbox, topLevel.text.trim()) + + return { id, sandbox, worktree, vcs: "git" as const } }) - const fromDirectory = Effect.fn("Project.fromDirectory")(function* (directory: string) { - log.info("fromDirectory", { directory }) - - // Phase 1: discover git info - type DiscoveryResult = { id: ProjectID; worktree: string; sandbox: string; vcs: Info["vcs"] } - - const data: DiscoveryResult = yield* Effect.gen(function* () { - const dotgitMatches = yield* fs.up({ targets: [".git"], start: directory }).pipe(Effect.orDie) - const dotgit = dotgitMatches[0] - - if (!dotgit) { - return { - id: ProjectID.global, - worktree: "/", - sandbox: "/", - vcs: fakeVcs, - } + // Phase 2: upsert + const row = yield* db((d) => d.select().from(ProjectTable).where(eq(ProjectTable.id, data.id)).get()) + const existing = row + ? fromRow(row) + : { + id: data.id, + worktree: data.worktree, + vcs: data.vcs, + sandboxes: [] as string[], + time: { created: Date.now(), updated: Date.now() }, } - let sandbox = pathSvc.dirname(dotgit) - const gitBinary = yield* Effect.sync(() => which("git")) - let id = yield* readCachedProjectId(dotgit) + if (Flag.OPENCODE_EXPERIMENTAL_ICON_DISCOVERY) + yield* discover(existing).pipe(Effect.ignore, Effect.forkIn(scope)) - if (!gitBinary) { - return { - id: id ?? ProjectID.global, - worktree: sandbox, - sandbox, - vcs: fakeVcs, - } - } + const result: Info = { + ...existing, + worktree: data.worktree, + vcs: data.vcs, + time: { ...existing.time, updated: Date.now() }, + } + if (data.sandbox !== result.worktree && !result.sandboxes.includes(data.sandbox)) + result.sandboxes.push(data.sandbox) + result.sandboxes = yield* Effect.forEach( + result.sandboxes, + (s) => + fs.exists(s).pipe( + Effect.orDie, + Effect.map((exists) => (exists ? s : undefined)), + ), + { concurrency: "unbounded" }, + ).pipe(Effect.map((arr) => arr.filter((x): x is string => x !== undefined))) - const commonDir = yield* git(["rev-parse", "--git-common-dir"], { cwd: sandbox }) - if (commonDir.code !== 0) { - return { - id: id ?? ProjectID.global, - worktree: sandbox, - sandbox, - vcs: fakeVcs, - } - } - const worktree = (() => { - const common = resolveGitPath(sandbox, commonDir.text.trim()) - return common === sandbox ? sandbox : pathSvc.dirname(common) - })() - - if (id == null) { - id = yield* readCachedProjectId(pathSvc.join(worktree, ".git")) - } - - if (!id) { - const revList = yield* git(["rev-list", "--max-parents=0", "HEAD"], { cwd: sandbox }) - const roots = revList.text - .split("\n") - .filter(Boolean) - .map((x) => x.trim()) - .toSorted() - - id = roots[0] ? ProjectID.make(roots[0]) : undefined - if (id) { - yield* fs.writeFileString(pathSvc.join(worktree, ".git", "opencode"), id).pipe(Effect.ignore) - } - } - - if (!id) { - return { id: ProjectID.global, worktree: sandbox, sandbox, vcs: "git" as const } - } - - const topLevel = yield* git(["rev-parse", "--show-toplevel"], { cwd: sandbox }) - if (topLevel.code !== 0) { - return { - id, - worktree: sandbox, - sandbox, - vcs: fakeVcs, - } - } - sandbox = resolveGitPath(sandbox, topLevel.text.trim()) - - return { id, sandbox, worktree, vcs: "git" as const } - }) - - // Phase 2: upsert - const row = yield* db((d) => d.select().from(ProjectTable).where(eq(ProjectTable.id, data.id)).get()) - const existing = row - ? fromRow(row) - : { - id: data.id, - worktree: data.worktree, - vcs: data.vcs, - sandboxes: [] as string[], - time: { created: Date.now(), updated: Date.now() }, - } - - if (Flag.OPENCODE_EXPERIMENTAL_ICON_DISCOVERY) - yield* discover(existing).pipe(Effect.ignore, Effect.forkIn(scope)) - - const result: Info = { - ...existing, - worktree: data.worktree, - vcs: data.vcs, - time: { ...existing.time, updated: Date.now() }, - } - if (data.sandbox !== result.worktree && !result.sandboxes.includes(data.sandbox)) - result.sandboxes.push(data.sandbox) - result.sandboxes = yield* Effect.forEach( - result.sandboxes, - (s) => - fs.exists(s).pipe( - Effect.orDie, - Effect.map((exists) => (exists ? s : undefined)), - ), - { concurrency: "unbounded" }, - ).pipe(Effect.map((arr) => arr.filter((x): x is string => x !== undefined))) - - yield* db((d) => - d - .insert(ProjectTable) - .values({ - id: result.id, + yield* db((d) => + d + .insert(ProjectTable) + .values({ + id: result.id, + worktree: result.worktree, + vcs: result.vcs ?? null, + name: result.name, + icon_url: result.icon?.url, + icon_color: result.icon?.color, + time_created: result.time.created, + time_updated: result.time.updated, + time_initialized: result.time.initialized, + sandboxes: result.sandboxes, + commands: result.commands, + }) + .onConflictDoUpdate({ + target: ProjectTable.id, + set: { worktree: result.worktree, vcs: result.vcs ?? null, name: result.name, icon_url: result.icon?.url, icon_color: result.icon?.color, - time_created: result.time.created, time_updated: result.time.updated, time_initialized: result.time.initialized, sandboxes: result.sandboxes, commands: result.commands, - }) - .onConflictDoUpdate({ - target: ProjectTable.id, - set: { - worktree: result.worktree, - vcs: result.vcs ?? null, - name: result.name, - icon_url: result.icon?.url, - icon_color: result.icon?.color, - time_updated: result.time.updated, - time_initialized: result.time.initialized, - sandboxes: result.sandboxes, - commands: result.commands, - }, - }) + }, + }) + .run(), + ) + + if (data.id !== ProjectID.global) { + yield* db((d) => + d + .update(SessionTable) + .set({ project_id: data.id }) + .where(and(eq(SessionTable.project_id, ProjectID.global), eq(SessionTable.directory, data.worktree))) .run(), ) + } - if (data.id !== ProjectID.global) { - yield* db((d) => - d - .update(SessionTable) - .set({ project_id: data.id }) - .where(and(eq(SessionTable.project_id, ProjectID.global), eq(SessionTable.directory, data.worktree))) - .run(), - ) - } + yield* emitUpdated(result) + return { project: result, sandbox: data.sandbox } + }) - yield* emitUpdated(result) - return { project: result, sandbox: data.sandbox } - }) + const discover = Effect.fn("Project.discover")(function* (input: Info) { + if (input.vcs !== "git") return + if (input.icon?.override) return + if (input.icon?.url) return - const discover = Effect.fn("Project.discover")(function* (input: Info) { - if (input.vcs !== "git") return - if (input.icon?.override) return - if (input.icon?.url) return + const matches = yield* fs + .glob("**/favicon.{ico,png,svg,jpg,jpeg,webp}", { + cwd: input.worktree, + absolute: true, + include: "file", + }) + .pipe(Effect.orDie) + const shortest = matches.sort((a, b) => a.length - b.length)[0] + if (!shortest) return - const matches = yield* fs - .glob("**/favicon.{ico,png,svg,jpg,jpeg,webp}", { - cwd: input.worktree, - absolute: true, - include: "file", + const buffer = yield* fs.readFile(shortest).pipe(Effect.orDie) + const base64 = Buffer.from(buffer).toString("base64") + const mime = AppFileSystem.mimeType(shortest) + const url = `data:${mime};base64,${base64}` + yield* update({ projectID: input.id, icon: { url } }) + }) + + const list = Effect.fn("Project.list")(function* () { + return yield* db((d) => d.select().from(ProjectTable).all().map(fromRow)) + }) + + const get = Effect.fn("Project.get")(function* (id: ProjectID) { + const row = yield* db((d) => d.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get()) + return row ? fromRow(row) : undefined + }) + + const update = Effect.fn("Project.update")(function* (input: UpdateInput) { + const result = yield* db((d) => + d + .update(ProjectTable) + .set({ + name: input.name, + icon_url: input.icon?.url, + icon_color: input.icon?.color, + commands: input.commands, + time_updated: Date.now(), }) - .pipe(Effect.orDie) - const shortest = matches.sort((a, b) => a.length - b.length)[0] - if (!shortest) return + .where(eq(ProjectTable.id, input.projectID)) + .returning() + .get(), + ) + if (!result) throw new Error(`Project not found: ${input.projectID}`) + const data = fromRow(result) + yield* emitUpdated(data) + return data + }) - const buffer = yield* fs.readFile(shortest).pipe(Effect.orDie) - const base64 = Buffer.from(buffer).toString("base64") - const mime = AppFileSystem.mimeType(shortest) - const url = `data:${mime};base64,${base64}` - yield* update({ projectID: input.id, icon: { url } }) - }) + const initGit = Effect.fn("Project.initGit")(function* (input: { directory: string; project: Info }) { + if (input.project.vcs === "git") return input.project + if (!(yield* Effect.sync(() => which("git")))) throw new Error("Git is not installed") + const result = yield* git(["init", "--quiet"], { cwd: input.directory }) + if (result.code !== 0) { + throw new Error(result.stderr.trim() || result.text.trim() || "Failed to initialize git repository") + } + const { project } = yield* fromDirectory(input.directory) + return project + }) - const list = Effect.fn("Project.list")(function* () { - return yield* db((d) => d.select().from(ProjectTable).all().map(fromRow)) - }) + const setInitialized = Effect.fn("Project.setInitialized")(function* (id: ProjectID) { + yield* db((d) => + d.update(ProjectTable).set({ time_initialized: Date.now() }).where(eq(ProjectTable.id, id)).run(), + ) + }) - const get = Effect.fn("Project.get")(function* (id: ProjectID) { - const row = yield* db((d) => d.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get()) - return row ? fromRow(row) : undefined - }) + const sandboxes = Effect.fn("Project.sandboxes")(function* (id: ProjectID) { + const row = yield* db((d) => d.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get()) + if (!row) return [] + const data = fromRow(row) + return yield* Effect.forEach( + data.sandboxes, + (dir) => + fs.isDir(dir).pipe( + Effect.orDie, + Effect.map((ok) => (ok ? dir : undefined)), + ), + { concurrency: "unbounded" }, + ).pipe(Effect.map((arr) => arr.filter((x): x is string => x !== undefined))) + }) - const update = Effect.fn("Project.update")(function* (input: UpdateInput) { - const result = yield* db((d) => - d - .update(ProjectTable) - .set({ - name: input.name, - icon_url: input.icon?.url, - icon_color: input.icon?.color, - commands: input.commands, - time_updated: Date.now(), - }) - .where(eq(ProjectTable.id, input.projectID)) - .returning() - .get(), - ) - if (!result) throw new Error(`Project not found: ${input.projectID}`) - const data = fromRow(result) - yield* emitUpdated(data) - return data - }) + const addSandbox = Effect.fn("Project.addSandbox")(function* (id: ProjectID, directory: string) { + const row = yield* db((d) => d.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get()) + if (!row) throw new Error(`Project not found: ${id}`) + const sboxes = [...row.sandboxes] + if (!sboxes.includes(directory)) sboxes.push(directory) + const result = yield* db((d) => + d + .update(ProjectTable) + .set({ sandboxes: sboxes, time_updated: Date.now() }) + .where(eq(ProjectTable.id, id)) + .returning() + .get(), + ) + if (!result) throw new Error(`Project not found: ${id}`) + yield* emitUpdated(fromRow(result)) + }) - const initGit = Effect.fn("Project.initGit")(function* (input: { directory: string; project: Info }) { - if (input.project.vcs === "git") return input.project - if (!(yield* Effect.sync(() => which("git")))) throw new Error("Git is not installed") - const result = yield* git(["init", "--quiet"], { cwd: input.directory }) - if (result.code !== 0) { - throw new Error(result.stderr.trim() || result.text.trim() || "Failed to initialize git repository") - } - const { project } = yield* fromDirectory(input.directory) - return project - }) + const removeSandbox = Effect.fn("Project.removeSandbox")(function* (id: ProjectID, directory: string) { + const row = yield* db((d) => d.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get()) + if (!row) throw new Error(`Project not found: ${id}`) + const sboxes = row.sandboxes.filter((s) => s !== directory) + const result = yield* db((d) => + d + .update(ProjectTable) + .set({ sandboxes: sboxes, time_updated: Date.now() }) + .where(eq(ProjectTable.id, id)) + .returning() + .get(), + ) + if (!result) throw new Error(`Project not found: ${id}`) + yield* emitUpdated(fromRow(result)) + }) - const setInitialized = Effect.fn("Project.setInitialized")(function* (id: ProjectID) { - yield* db((d) => - d.update(ProjectTable).set({ time_initialized: Date.now() }).where(eq(ProjectTable.id, id)).run(), - ) - }) + return Service.of({ + fromDirectory, + discover, + list, + get, + update, + initGit, + setInitialized, + sandboxes, + addSandbox, + removeSandbox, + }) + }), +) - const sandboxes = Effect.fn("Project.sandboxes")(function* (id: ProjectID) { - const row = yield* db((d) => d.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get()) - if (!row) return [] - const data = fromRow(row) - return yield* Effect.forEach( - data.sandboxes, - (dir) => - fs.isDir(dir).pipe( - Effect.orDie, - Effect.map((ok) => (ok ? dir : undefined)), - ), - { concurrency: "unbounded" }, - ).pipe(Effect.map((arr) => arr.filter((x): x is string => x !== undefined))) - }) +export const defaultLayer = layer.pipe( + Layer.provide(CrossSpawnSpawner.defaultLayer), + Layer.provide(AppFileSystem.defaultLayer), + Layer.provide(NodePath.layer), +) - const addSandbox = Effect.fn("Project.addSandbox")(function* (id: ProjectID, directory: string) { - const row = yield* db((d) => d.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get()) - if (!row) throw new Error(`Project not found: ${id}`) - const sboxes = [...row.sandboxes] - if (!sboxes.includes(directory)) sboxes.push(directory) - const result = yield* db((d) => - d - .update(ProjectTable) - .set({ sandboxes: sboxes, time_updated: Date.now() }) - .where(eq(ProjectTable.id, id)) - .returning() - .get(), - ) - if (!result) throw new Error(`Project not found: ${id}`) - yield* emitUpdated(fromRow(result)) - }) - - const removeSandbox = Effect.fn("Project.removeSandbox")(function* (id: ProjectID, directory: string) { - const row = yield* db((d) => d.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get()) - if (!row) throw new Error(`Project not found: ${id}`) - const sboxes = row.sandboxes.filter((s) => s !== directory) - const result = yield* db((d) => - d - .update(ProjectTable) - .set({ sandboxes: sboxes, time_updated: Date.now() }) - .where(eq(ProjectTable.id, id)) - .returning() - .get(), - ) - if (!result) throw new Error(`Project not found: ${id}`) - yield* emitUpdated(fromRow(result)) - }) - - return Service.of({ - fromDirectory, - discover, - list, - get, - update, - initGit, - setInitialized, - sandboxes, - addSandbox, - removeSandbox, - }) - }), +export function list() { + return Database.use((db) => + db + .select() + .from(ProjectTable) + .all() + .map((row) => fromRow(row)), + ) +} + +export function get(id: ProjectID): Info | undefined { + const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get()) + if (!row) return undefined + return fromRow(row) +} + +export function setInitialized(id: ProjectID) { + Database.use((db) => + db.update(ProjectTable).set({ time_initialized: Date.now() }).where(eq(ProjectTable.id, id)).run(), ) - - export const defaultLayer = layer.pipe( - Layer.provide(CrossSpawnSpawner.defaultLayer), - Layer.provide(AppFileSystem.defaultLayer), - Layer.provide(NodePath.layer), - ) - - export function list() { - return Database.use((db) => - db - .select() - .from(ProjectTable) - .all() - .map((row) => fromRow(row)), - ) - } - - export function get(id: ProjectID): Info | undefined { - const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get()) - if (!row) return undefined - return fromRow(row) - } - - export function setInitialized(id: ProjectID) { - Database.use((db) => - db.update(ProjectTable).set({ time_initialized: Date.now() }).where(eq(ProjectTable.id, id)).run(), - ) - } } diff --git a/packages/opencode/src/project/vcs.ts b/packages/opencode/src/project/vcs.ts index cb0b46adcb..559371859f 100644 --- a/packages/opencode/src/project/vcs.ts +++ b/packages/opencode/src/project/vcs.ts @@ -11,223 +11,221 @@ import { Log } from "@/util" import { Instance } from "./instance" import z from "zod" -export namespace Vcs { - const log = Log.create({ service: "vcs" }) +const log = Log.create({ service: "vcs" }) - const count = (text: string) => { - if (!text) return 0 - if (!text.endsWith("\n")) return text.split("\n").length - return text.slice(0, -1).split("\n").length - } - - const work = Effect.fnUntraced(function* (fs: AppFileSystem.Interface, cwd: string, file: string) { - const full = path.join(cwd, file) - if (!(yield* fs.exists(full).pipe(Effect.orDie))) return "" - const buf = yield* fs.readFile(full).pipe(Effect.catch(() => Effect.succeed(new Uint8Array()))) - if (Buffer.from(buf).includes(0)) return "" - return Buffer.from(buf).toString("utf8") - }) - - const nums = (list: Git.Stat[]) => - new Map(list.map((item) => [item.file, { additions: item.additions, deletions: item.deletions }] as const)) - - const merge = (...lists: Git.Item[][]) => { - const out = new Map() - lists.flat().forEach((item) => { - if (!out.has(item.file)) out.set(item.file, item) - }) - return [...out.values()] - } - - const files = Effect.fnUntraced(function* ( - fs: AppFileSystem.Interface, - git: Git.Interface, - cwd: string, - ref: string | undefined, - list: Git.Item[], - map: Map, - ) { - const base = ref ? yield* git.prefix(cwd) : "" - const patch = (file: string, before: string, after: string) => - formatPatch(structuredPatch(file, file, before, after, "", "", { context: Number.MAX_SAFE_INTEGER })) - const next = yield* Effect.forEach( - list, - (item) => - Effect.gen(function* () { - const before = item.status === "added" || !ref ? "" : yield* git.show(cwd, ref, item.file, base) - const after = item.status === "deleted" ? "" : yield* work(fs, cwd, item.file) - const stat = map.get(item.file) - return { - file: item.file, - patch: patch(item.file, before, after), - additions: stat?.additions ?? (item.status === "added" ? count(after) : 0), - deletions: stat?.deletions ?? (item.status === "deleted" ? count(before) : 0), - status: item.status, - } satisfies FileDiff - }), - { concurrency: 8 }, - ) - return next.toSorted((a, b) => a.file.localeCompare(b.file)) - }) - - const track = Effect.fnUntraced(function* ( - fs: AppFileSystem.Interface, - git: Git.Interface, - cwd: string, - ref: string | undefined, - ) { - if (!ref) return yield* files(fs, git, cwd, ref, yield* git.status(cwd), new Map()) - const [list, stats] = yield* Effect.all([git.status(cwd), git.stats(cwd, ref)], { concurrency: 2 }) - return yield* files(fs, git, cwd, ref, list, nums(stats)) - }) - - const compare = Effect.fnUntraced(function* ( - fs: AppFileSystem.Interface, - git: Git.Interface, - cwd: string, - ref: string, - ) { - const [list, stats, extra] = yield* Effect.all([git.diff(cwd, ref), git.stats(cwd, ref), git.status(cwd)], { - concurrency: 3, - }) - return yield* files( - fs, - git, - cwd, - ref, - merge( - list, - extra.filter((item) => item.code === "??"), - ), - nums(stats), - ) - }) - - export const Mode = z.enum(["git", "branch"]) - export type Mode = z.infer - - export const Event = { - BranchUpdated: BusEvent.define( - "vcs.branch.updated", - z.object({ - branch: z.string().optional(), - }), - ), - } - - export const Info = z - .object({ - branch: z.string().optional(), - default_branch: z.string().optional(), - }) - .meta({ - ref: "VcsInfo", - }) - export type Info = z.infer - - export const FileDiff = z - .object({ - file: z.string(), - patch: z.string(), - additions: z.number(), - deletions: z.number(), - status: z.enum(["added", "deleted", "modified"]).optional(), - }) - .meta({ - ref: "VcsFileDiff", - }) - export type FileDiff = z.infer - - export interface Interface { - readonly init: () => Effect.Effect - readonly branch: () => Effect.Effect - readonly defaultBranch: () => Effect.Effect - readonly diff: (mode: Mode) => Effect.Effect - } - - interface State { - current: string | undefined - root: Git.Base | undefined - } - - export class Service extends Context.Service()("@opencode/Vcs") {} - - export const layer: Layer.Layer = Layer.effect( - Service, - Effect.gen(function* () { - const fs = yield* AppFileSystem.Service - const git = yield* Git.Service - const bus = yield* Bus.Service - - const state = yield* InstanceState.make( - Effect.fn("Vcs.state")(function* (ctx) { - if (ctx.project.vcs !== "git") { - return { current: undefined, root: undefined } - } - - const get = Effect.fnUntraced(function* () { - return yield* git.branch(ctx.directory) - }) - const [current, root] = yield* Effect.all([git.branch(ctx.directory), git.defaultBranch(ctx.directory)], { - concurrency: 2, - }) - const value = { current, root } - log.info("initialized", { branch: value.current, default_branch: value.root?.name }) - - yield* bus.subscribe(FileWatcher.Event.Updated).pipe( - Stream.filter((evt) => evt.properties.file.endsWith("HEAD")), - Stream.runForEach((_evt) => - Effect.gen(function* () { - const next = yield* get() - if (next !== value.current) { - log.info("branch changed", { from: value.current, to: next }) - value.current = next - yield* bus.publish(Event.BranchUpdated, { branch: next }) - } - }), - ), - Effect.forkScoped, - ) - - return value - }), - ) - - return Service.of({ - init: Effect.fn("Vcs.init")(function* () { - yield* InstanceState.get(state) - }), - branch: Effect.fn("Vcs.branch")(function* () { - return yield* InstanceState.use(state, (x) => x.current) - }), - defaultBranch: Effect.fn("Vcs.defaultBranch")(function* () { - return yield* InstanceState.use(state, (x) => x.root?.name) - }), - diff: Effect.fn("Vcs.diff")(function* (mode: Mode) { - const value = yield* InstanceState.get(state) - if (Instance.project.vcs !== "git") return [] - if (mode === "git") { - return yield* track( - fs, - git, - Instance.directory, - (yield* git.hasHead(Instance.directory)) ? "HEAD" : undefined, - ) - } - - if (!value.root) return [] - if (value.current && value.current === value.root.name) return [] - const ref = yield* git.mergeBase(Instance.directory, value.root.ref) - if (!ref) return [] - return yield* compare(fs, git, Instance.directory, ref) - }), - }) - }), - ) - - export const defaultLayer = layer.pipe( - Layer.provide(Git.defaultLayer), - Layer.provide(AppFileSystem.defaultLayer), - Layer.provide(Bus.layer), - ) +const count = (text: string) => { + if (!text) return 0 + if (!text.endsWith("\n")) return text.split("\n").length + return text.slice(0, -1).split("\n").length } + +const work = Effect.fnUntraced(function* (fs: AppFileSystem.Interface, cwd: string, file: string) { + const full = path.join(cwd, file) + if (!(yield* fs.exists(full).pipe(Effect.orDie))) return "" + const buf = yield* fs.readFile(full).pipe(Effect.catch(() => Effect.succeed(new Uint8Array()))) + if (Buffer.from(buf).includes(0)) return "" + return Buffer.from(buf).toString("utf8") +}) + +const nums = (list: Git.Stat[]) => + new Map(list.map((item) => [item.file, { additions: item.additions, deletions: item.deletions }] as const)) + +const merge = (...lists: Git.Item[][]) => { + const out = new Map() + lists.flat().forEach((item) => { + if (!out.has(item.file)) out.set(item.file, item) + }) + return [...out.values()] +} + +const files = Effect.fnUntraced(function* ( + fs: AppFileSystem.Interface, + git: Git.Interface, + cwd: string, + ref: string | undefined, + list: Git.Item[], + map: Map, +) { + const base = ref ? yield* git.prefix(cwd) : "" + const patch = (file: string, before: string, after: string) => + formatPatch(structuredPatch(file, file, before, after, "", "", { context: Number.MAX_SAFE_INTEGER })) + const next = yield* Effect.forEach( + list, + (item) => + Effect.gen(function* () { + const before = item.status === "added" || !ref ? "" : yield* git.show(cwd, ref, item.file, base) + const after = item.status === "deleted" ? "" : yield* work(fs, cwd, item.file) + const stat = map.get(item.file) + return { + file: item.file, + patch: patch(item.file, before, after), + additions: stat?.additions ?? (item.status === "added" ? count(after) : 0), + deletions: stat?.deletions ?? (item.status === "deleted" ? count(before) : 0), + status: item.status, + } satisfies FileDiff + }), + { concurrency: 8 }, + ) + return next.toSorted((a, b) => a.file.localeCompare(b.file)) +}) + +const track = Effect.fnUntraced(function* ( + fs: AppFileSystem.Interface, + git: Git.Interface, + cwd: string, + ref: string | undefined, +) { + if (!ref) return yield* files(fs, git, cwd, ref, yield* git.status(cwd), new Map()) + const [list, stats] = yield* Effect.all([git.status(cwd), git.stats(cwd, ref)], { concurrency: 2 }) + return yield* files(fs, git, cwd, ref, list, nums(stats)) +}) + +const compare = Effect.fnUntraced(function* ( + fs: AppFileSystem.Interface, + git: Git.Interface, + cwd: string, + ref: string, +) { + const [list, stats, extra] = yield* Effect.all([git.diff(cwd, ref), git.stats(cwd, ref), git.status(cwd)], { + concurrency: 3, + }) + return yield* files( + fs, + git, + cwd, + ref, + merge( + list, + extra.filter((item) => item.code === "??"), + ), + nums(stats), + ) +}) + +export const Mode = z.enum(["git", "branch"]) +export type Mode = z.infer + +export const Event = { + BranchUpdated: BusEvent.define( + "vcs.branch.updated", + z.object({ + branch: z.string().optional(), + }), + ), +} + +export const Info = z + .object({ + branch: z.string().optional(), + default_branch: z.string().optional(), + }) + .meta({ + ref: "VcsInfo", + }) +export type Info = z.infer + +export const FileDiff = z + .object({ + file: z.string(), + patch: z.string(), + additions: z.number(), + deletions: z.number(), + status: z.enum(["added", "deleted", "modified"]).optional(), + }) + .meta({ + ref: "VcsFileDiff", + }) +export type FileDiff = z.infer + +export interface Interface { + readonly init: () => Effect.Effect + readonly branch: () => Effect.Effect + readonly defaultBranch: () => Effect.Effect + readonly diff: (mode: Mode) => Effect.Effect +} + +interface State { + current: string | undefined + root: Git.Base | undefined +} + +export class Service extends Context.Service()("@opencode/Vcs") {} + +export const layer: Layer.Layer = Layer.effect( + Service, + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + const git = yield* Git.Service + const bus = yield* Bus.Service + + const state = yield* InstanceState.make( + Effect.fn("Vcs.state")(function* (ctx) { + if (ctx.project.vcs !== "git") { + return { current: undefined, root: undefined } + } + + const get = Effect.fnUntraced(function* () { + return yield* git.branch(ctx.directory) + }) + const [current, root] = yield* Effect.all([git.branch(ctx.directory), git.defaultBranch(ctx.directory)], { + concurrency: 2, + }) + const value = { current, root } + log.info("initialized", { branch: value.current, default_branch: value.root?.name }) + + yield* bus.subscribe(FileWatcher.Event.Updated).pipe( + Stream.filter((evt) => evt.properties.file.endsWith("HEAD")), + Stream.runForEach((_evt) => + Effect.gen(function* () { + const next = yield* get() + if (next !== value.current) { + log.info("branch changed", { from: value.current, to: next }) + value.current = next + yield* bus.publish(Event.BranchUpdated, { branch: next }) + } + }), + ), + Effect.forkScoped, + ) + + return value + }), + ) + + return Service.of({ + init: Effect.fn("Vcs.init")(function* () { + yield* InstanceState.get(state) + }), + branch: Effect.fn("Vcs.branch")(function* () { + return yield* InstanceState.use(state, (x) => x.current) + }), + defaultBranch: Effect.fn("Vcs.defaultBranch")(function* () { + return yield* InstanceState.use(state, (x) => x.root?.name) + }), + diff: Effect.fn("Vcs.diff")(function* (mode: Mode) { + const value = yield* InstanceState.get(state) + if (Instance.project.vcs !== "git") return [] + if (mode === "git") { + return yield* track( + fs, + git, + Instance.directory, + (yield* git.hasHead(Instance.directory)) ? "HEAD" : undefined, + ) + } + + if (!value.root) return [] + if (value.current && value.current === value.root.name) return [] + const ref = yield* git.mergeBase(Instance.directory, value.root.ref) + if (!ref) return [] + return yield* compare(fs, git, Instance.directory, ref) + }), + }) + }), +) + +export const defaultLayer = layer.pipe( + Layer.provide(Git.defaultLayer), + Layer.provide(AppFileSystem.defaultLayer), + Layer.provide(Bus.layer), +) diff --git a/packages/opencode/src/server/instance/experimental.ts b/packages/opencode/src/server/instance/experimental.ts index 6e1a47ed20..610d67df08 100644 --- a/packages/opencode/src/server/instance/experimental.ts +++ b/packages/opencode/src/server/instance/experimental.ts @@ -5,7 +5,7 @@ import { ProviderID, ModelID } from "../../provider/schema" import { ToolRegistry } from "../../tool/registry" import { Worktree } from "../../worktree" import { Instance } from "../../project/instance" -import { Project } from "../../project/project" +import { Project } from "../../project" import { MCP } from "../../mcp" import { Session } from "../../session" import { Config } from "../../config" diff --git a/packages/opencode/src/server/instance/index.ts b/packages/opencode/src/server/instance/index.ts index 874790f1cc..9ef6da63ac 100644 --- a/packages/opencode/src/server/instance/index.ts +++ b/packages/opencode/src/server/instance/index.ts @@ -6,7 +6,7 @@ import z from "zod" import { Format } from "../../format" import { TuiRoutes } from "./tui" import { Instance } from "../../project/instance" -import { Vcs } from "../../project/vcs" +import { Vcs } from "../../project" import { Agent } from "../../agent/agent" import { Skill } from "../../skill" import { Global } from "../../global" diff --git a/packages/opencode/src/server/instance/project.ts b/packages/opencode/src/server/instance/project.ts index 7a8e0353a2..eea741596d 100644 --- a/packages/opencode/src/server/instance/project.ts +++ b/packages/opencode/src/server/instance/project.ts @@ -2,7 +2,7 @@ import { Hono } from "hono" import { describeRoute, validator } from "hono-openapi" import { resolver } from "hono-openapi" import { Instance } from "../../project/instance" -import { Project } from "../../project/project" +import { Project } from "../../project" import z from "zod" import { ProjectID } from "../../project/schema" import { errors } from "../error" diff --git a/packages/opencode/src/worktree/worktree.ts b/packages/opencode/src/worktree/worktree.ts index 86ef95f0e6..8eea6445aa 100644 --- a/packages/opencode/src/worktree/worktree.ts +++ b/packages/opencode/src/worktree/worktree.ts @@ -3,7 +3,7 @@ import { NamedError } from "@opencode-ai/shared/util/error" import { Global } from "../global" import { Instance } from "../project/instance" import { InstanceBootstrap } from "../project/bootstrap" -import { Project } from "../project/project" +import { Project } from "../project" import { Database, eq } from "../storage/db" import { ProjectTable } from "../project/project.sql" import type { ProjectID } from "../project/schema" diff --git a/packages/opencode/test/project/migrate-global.test.ts b/packages/opencode/test/project/migrate-global.test.ts index c399d8872d..a63ac1cd98 100644 --- a/packages/opencode/test/project/migrate-global.test.ts +++ b/packages/opencode/test/project/migrate-global.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { Project } from "../../src/project/project" +import { Project } from "../../src/project" import { Database, eq } from "../../src/storage/db" import { SessionTable } from "../../src/session/session.sql" import { ProjectTable } from "../../src/project/project.sql" diff --git a/packages/opencode/test/project/project.test.ts b/packages/opencode/test/project/project.test.ts index 4c272b7949..4dc9ee5efa 100644 --- a/packages/opencode/test/project/project.test.ts +++ b/packages/opencode/test/project/project.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { Project } from "../../src/project/project" +import { Project } from "../../src/project" import { Log } from "../../src/util" import { $ } from "bun" import path from "path" diff --git a/packages/opencode/test/project/vcs.test.ts b/packages/opencode/test/project/vcs.test.ts index 5461de5c33..8f0eaecc27 100644 --- a/packages/opencode/test/project/vcs.test.ts +++ b/packages/opencode/test/project/vcs.test.ts @@ -8,7 +8,7 @@ import { AppRuntime } from "../../src/effect/app-runtime" import { FileWatcher } from "../../src/file/watcher" import { Instance } from "../../src/project/instance" import { GlobalBus } from "../../src/bus/global" -import { Vcs } from "../../src/project/vcs" +import { Vcs } from "../../src/project" // Skip in CI — native @parcel/watcher binding needed const describeVcs = FileWatcher.hasNativeBinding() && !process.env.CI ? describe : describe.skip diff --git a/packages/opencode/test/server/global-session-list.test.ts b/packages/opencode/test/server/global-session-list.test.ts index 0edabd8e65..d0f71b8fd3 100644 --- a/packages/opencode/test/server/global-session-list.test.ts +++ b/packages/opencode/test/server/global-session-list.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test" import { Effect } from "effect" import z from "zod" import { Instance } from "../../src/project/instance" -import { Project } from "../../src/project/project" +import { Project } from "../../src/project" import { Session as SessionNs } from "../../src/session" import { Log } from "../../src/util" import { tmpdir } from "../fixture/fixture" From 581d5208ca0317dd0f441bc50eeda8e1ad614529 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 23:28:46 -0400 Subject: [PATCH 214/300] feat: unwrap share namespaces to flat exports + barrel (#22744) --- packages/opencode/src/cli/cmd/github.ts | 2 +- packages/opencode/src/cli/cmd/import.ts | 2 +- packages/opencode/src/effect/app-runtime.ts | 4 +- .../opencode/src/effect/bootstrap-runtime.ts | 2 +- packages/opencode/src/project/bootstrap.ts | 2 +- .../opencode/src/server/instance/session.ts | 2 +- packages/opencode/src/share/index.ts | 2 + packages/opencode/src/share/session.ts | 100 ++- packages/opencode/src/share/share-next.ts | 614 +++++++++--------- .../opencode/test/share/share-next.test.ts | 2 +- 10 files changed, 365 insertions(+), 367 deletions(-) create mode 100644 packages/opencode/src/share/index.ts diff --git a/packages/opencode/src/cli/cmd/github.ts b/packages/opencode/src/cli/cmd/github.ts index d7863c5486..822d78770e 100644 --- a/packages/opencode/src/cli/cmd/github.ts +++ b/packages/opencode/src/cli/cmd/github.ts @@ -21,7 +21,7 @@ import { cmd } from "./cmd" import { ModelsDev } from "../../provider/models" import { Instance } from "@/project/instance" import { bootstrap } from "../bootstrap" -import { SessionShare } from "@/share/session" +import { SessionShare } from "@/share" import { Session } from "../../session" import type { SessionID } from "../../session/schema" import { MessageID, PartID } from "../../session/schema" diff --git a/packages/opencode/src/cli/cmd/import.ts b/packages/opencode/src/cli/cmd/import.ts index bb8a1f63f3..38d2376bc5 100644 --- a/packages/opencode/src/cli/cmd/import.ts +++ b/packages/opencode/src/cli/cmd/import.ts @@ -7,7 +7,7 @@ import { bootstrap } from "../bootstrap" import { Database } from "../../storage/db" import { SessionTable, MessageTable, PartTable } from "../../session/session.sql" import { Instance } from "../../project/instance" -import { ShareNext } from "../../share/share-next" +import { ShareNext } from "../../share" import { EOL } from "os" import { Filesystem } from "../../util" import { AppRuntime } from "@/effect/app-runtime" diff --git a/packages/opencode/src/effect/app-runtime.ts b/packages/opencode/src/effect/app-runtime.ts index 7608e9c701..495cf9eea8 100644 --- a/packages/opencode/src/effect/app-runtime.ts +++ b/packages/opencode/src/effect/app-runtime.ts @@ -45,8 +45,8 @@ import { Vcs } from "@/project" import { Worktree } from "@/worktree" import { Pty } from "@/pty" import { Installation } from "@/installation" -import { ShareNext } from "@/share/share-next" -import { SessionShare } from "@/share/session" +import { ShareNext } from "@/share" +import { SessionShare } from "@/share" export const AppLayer = Layer.mergeAll( AppFileSystem.defaultLayer, diff --git a/packages/opencode/src/effect/bootstrap-runtime.ts b/packages/opencode/src/effect/bootstrap-runtime.ts index 7d34b4bd48..9be456b095 100644 --- a/packages/opencode/src/effect/bootstrap-runtime.ts +++ b/packages/opencode/src/effect/bootstrap-runtime.ts @@ -5,7 +5,7 @@ import { Plugin } from "@/plugin" import { LSP } from "@/lsp" import { FileWatcher } from "@/file/watcher" import { Format } from "@/format" -import { ShareNext } from "@/share/share-next" +import { ShareNext } from "@/share" import { File } from "@/file" import { Vcs } from "@/project" import { Snapshot } from "@/snapshot" diff --git a/packages/opencode/src/project/bootstrap.ts b/packages/opencode/src/project/bootstrap.ts index c88eb8e039..27ed35b7f0 100644 --- a/packages/opencode/src/project/bootstrap.ts +++ b/packages/opencode/src/project/bootstrap.ts @@ -10,7 +10,7 @@ import { Command } from "../command" import { Instance } from "./instance" import { Log } from "@/util" import { FileWatcher } from "@/file/watcher" -import { ShareNext } from "@/share/share-next" +import { ShareNext } from "@/share" import * as Effect from "effect/Effect" export const InstanceBootstrap = Effect.gen(function* () { diff --git a/packages/opencode/src/server/instance/session.ts b/packages/opencode/src/server/instance/session.ts index 06495b628c..1511e99e8d 100644 --- a/packages/opencode/src/server/instance/session.ts +++ b/packages/opencode/src/server/instance/session.ts @@ -9,7 +9,7 @@ import { SessionPrompt } from "../../session/prompt" import { SessionRunState } from "@/session/run-state" import { SessionCompaction } from "../../session/compaction" import { SessionRevert } from "../../session/revert" -import { SessionShare } from "@/share/session" +import { SessionShare } from "@/share" import { SessionStatus } from "@/session/status" import { SessionSummary } from "@/session/summary" import { Todo } from "../../session/todo" diff --git a/packages/opencode/src/share/index.ts b/packages/opencode/src/share/index.ts new file mode 100644 index 0000000000..534375a0ac --- /dev/null +++ b/packages/opencode/src/share/index.ts @@ -0,0 +1,2 @@ +export * as ShareNext from "./share-next" +export * as SessionShare from "./session" diff --git a/packages/opencode/src/share/session.ts b/packages/opencode/src/share/session.ts index 0a673f81c6..71fa17c889 100644 --- a/packages/opencode/src/share/session.ts +++ b/packages/opencode/src/share/session.ts @@ -4,56 +4,54 @@ import { SyncEvent } from "@/sync" import { Effect, Layer, Scope, Context } from "effect" import { Config } from "../config" import { Flag } from "../flag/flag" -import { ShareNext } from "./share-next" +import { ShareNext } from "." -export namespace SessionShare { - export interface Interface { - readonly create: (input?: Session.CreateInput) => Effect.Effect - readonly share: (sessionID: SessionID) => Effect.Effect<{ url: string }, unknown> - readonly unshare: (sessionID: SessionID) => Effect.Effect - } - - export class Service extends Context.Service()("@opencode/SessionShare") {} - - export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const cfg = yield* Config.Service - const session = yield* Session.Service - const shareNext = yield* ShareNext.Service - const scope = yield* Scope.Scope - - const share = Effect.fn("SessionShare.share")(function* (sessionID: SessionID) { - const conf = yield* cfg.get() - if (conf.share === "disabled") throw new Error("Sharing is disabled in configuration") - const result = yield* shareNext.create(sessionID) - yield* Effect.sync(() => - SyncEvent.run(Session.Event.Updated, { sessionID, info: { share: { url: result.url } } }), - ) - return result - }) - - const unshare = Effect.fn("SessionShare.unshare")(function* (sessionID: SessionID) { - yield* shareNext.remove(sessionID) - yield* Effect.sync(() => SyncEvent.run(Session.Event.Updated, { sessionID, info: { share: { url: null } } })) - }) - - const create = Effect.fn("SessionShare.create")(function* (input?: Session.CreateInput) { - const result = yield* session.create(input) - if (result.parentID) return result - const conf = yield* cfg.get() - if (!(Flag.OPENCODE_AUTO_SHARE || conf.share === "auto")) return result - yield* share(result.id).pipe(Effect.ignore, Effect.forkIn(scope)) - return result - }) - - return Service.of({ create, share, unshare }) - }), - ) - - export const defaultLayer = layer.pipe( - Layer.provide(ShareNext.defaultLayer), - Layer.provide(Session.defaultLayer), - Layer.provide(Config.defaultLayer), - ) +export interface Interface { + readonly create: (input?: Session.CreateInput) => Effect.Effect + readonly share: (sessionID: SessionID) => Effect.Effect<{ url: string }, unknown> + readonly unshare: (sessionID: SessionID) => Effect.Effect } + +export class Service extends Context.Service()("@opencode/SessionShare") {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const cfg = yield* Config.Service + const session = yield* Session.Service + const shareNext = yield* ShareNext.Service + const scope = yield* Scope.Scope + + const share = Effect.fn("SessionShare.share")(function* (sessionID: SessionID) { + const conf = yield* cfg.get() + if (conf.share === "disabled") throw new Error("Sharing is disabled in configuration") + const result = yield* shareNext.create(sessionID) + yield* Effect.sync(() => + SyncEvent.run(Session.Event.Updated, { sessionID, info: { share: { url: result.url } } }), + ) + return result + }) + + const unshare = Effect.fn("SessionShare.unshare")(function* (sessionID: SessionID) { + yield* shareNext.remove(sessionID) + yield* Effect.sync(() => SyncEvent.run(Session.Event.Updated, { sessionID, info: { share: { url: null } } })) + }) + + const create = Effect.fn("SessionShare.create")(function* (input?: Session.CreateInput) { + const result = yield* session.create(input) + if (result.parentID) return result + const conf = yield* cfg.get() + if (!(Flag.OPENCODE_AUTO_SHARE || conf.share === "auto")) return result + yield* share(result.id).pipe(Effect.ignore, Effect.forkIn(scope)) + return result + }) + + return Service.of({ create, share, unshare }) + }), +) + +export const defaultLayer = layer.pipe( + Layer.provide(ShareNext.defaultLayer), + Layer.provide(Session.defaultLayer), + Layer.provide(Config.defaultLayer), +) diff --git a/packages/opencode/src/share/share-next.ts b/packages/opencode/src/share/share-next.ts index bcb1fcc962..a7656e840c 100644 --- a/packages/opencode/src/share/share-next.ts +++ b/packages/opencode/src/share/share-next.ts @@ -14,337 +14,335 @@ import { Config } from "@/config" import { Log } from "@/util" import { SessionShareTable } from "./share.sql" -export namespace ShareNext { - const log = Log.create({ service: "share-next" }) - const disabled = process.env["OPENCODE_DISABLE_SHARE"] === "true" || process.env["OPENCODE_DISABLE_SHARE"] === "1" +const log = Log.create({ service: "share-next" }) +const disabled = process.env["OPENCODE_DISABLE_SHARE"] === "true" || process.env["OPENCODE_DISABLE_SHARE"] === "1" - export type Api = { - create: string - sync: (shareID: string) => string - remove: (shareID: string) => string - data: (shareID: string) => string - } +export type Api = { + create: string + sync: (shareID: string) => string + remove: (shareID: string) => string + data: (shareID: string) => string +} - export type Req = { - headers: Record - api: Api - baseUrl: string - } +export type Req = { + headers: Record + api: Api + baseUrl: string +} - const ShareSchema = Schema.Struct({ - id: Schema.String, - url: Schema.String, - secret: Schema.String, - }) - export type Share = typeof ShareSchema.Type +const ShareSchema = Schema.Struct({ + id: Schema.String, + url: Schema.String, + secret: Schema.String, +}) +export type Share = typeof ShareSchema.Type - type State = { - queue: Map }> - scope: Scope.Closeable - } +type State = { + queue: Map }> + scope: Scope.Closeable +} - type Data = - | { - type: "session" - data: SDK.Session - } - | { - type: "message" - data: SDK.Message - } - | { - type: "part" - data: SDK.Part - } - | { - type: "session_diff" - data: SDK.SnapshotFileDiff[] - } - | { - type: "model" - data: SDK.Model[] - } - - export interface Interface { - readonly init: () => Effect.Effect - readonly url: () => Effect.Effect - readonly request: () => Effect.Effect - readonly create: (sessionID: SessionID) => Effect.Effect - readonly remove: (sessionID: SessionID) => Effect.Effect - } - - export class Service extends Context.Service()("@opencode/ShareNext") {} - - const db = (fn: (d: Parameters[0] extends (trx: infer D) => any ? D : never) => T) => - Effect.sync(() => Database.use(fn)) - - function api(resource: string): Api { - return { - create: `/api/${resource}`, - sync: (shareID) => `/api/${resource}/${shareID}/sync`, - remove: (shareID) => `/api/${resource}/${shareID}`, - data: (shareID) => `/api/${resource}/${shareID}/data`, +type Data = + | { + type: "session" + data: SDK.Session } - } - - const legacyApi = api("share") - const consoleApi = api("shares") - - function key(item: Data) { - switch (item.type) { - case "session": - return "session" - case "message": - return `message/${item.data.id}` - case "part": - return `part/${item.data.messageID}/${item.data.id}` - case "session_diff": - return "session_diff" - case "model": - return "model" + | { + type: "message" + data: SDK.Message } + | { + type: "part" + data: SDK.Part + } + | { + type: "session_diff" + data: SDK.SnapshotFileDiff[] + } + | { + type: "model" + data: SDK.Model[] + } + +export interface Interface { + readonly init: () => Effect.Effect + readonly url: () => Effect.Effect + readonly request: () => Effect.Effect + readonly create: (sessionID: SessionID) => Effect.Effect + readonly remove: (sessionID: SessionID) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/ShareNext") {} + +const db = (fn: (d: Parameters[0] extends (trx: infer D) => any ? D : never) => T) => + Effect.sync(() => Database.use(fn)) + +function api(resource: string): Api { + return { + create: `/api/${resource}`, + sync: (shareID) => `/api/${resource}/${shareID}/sync`, + remove: (shareID) => `/api/${resource}/${shareID}`, + data: (shareID) => `/api/${resource}/${shareID}/data`, } +} - export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const account = yield* Account.Service - const bus = yield* Bus.Service - const cfg = yield* Config.Service - const http = yield* HttpClient.HttpClient - const httpOk = HttpClient.filterStatusOk(http) - const provider = yield* Provider.Service - const session = yield* Session.Service +const legacyApi = api("share") +const consoleApi = api("shares") - function sync(sessionID: SessionID, data: Data[]): Effect.Effect { - return Effect.gen(function* () { - if (disabled) return - const s = yield* InstanceState.get(state) - const existing = s.queue.get(sessionID) - if (existing) { - for (const item of data) { - existing.data.set(key(item), item) - } - return +function key(item: Data) { + switch (item.type) { + case "session": + return "session" + case "message": + return `message/${item.data.id}` + case "part": + return `part/${item.data.messageID}/${item.data.id}` + case "session_diff": + return "session_diff" + case "model": + return "model" + } +} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const account = yield* Account.Service + const bus = yield* Bus.Service + const cfg = yield* Config.Service + const http = yield* HttpClient.HttpClient + const httpOk = HttpClient.filterStatusOk(http) + const provider = yield* Provider.Service + const session = yield* Session.Service + + function sync(sessionID: SessionID, data: Data[]): Effect.Effect { + return Effect.gen(function* () { + if (disabled) return + const s = yield* InstanceState.get(state) + const existing = s.queue.get(sessionID) + if (existing) { + for (const item of data) { + existing.data.set(key(item), item) } - - const next = new Map(data.map((item) => [key(item), item])) - s.queue.set(sessionID, { data: next }) - yield* flush(sessionID).pipe( - Effect.delay(1000), - Effect.catchCause((cause) => - Effect.sync(() => { - log.error("share flush failed", { sessionID, cause }) - }), - ), - Effect.forkIn(s.scope), - ) - }) - } - - const state: InstanceState.InstanceState = yield* InstanceState.make( - Effect.fn("ShareNext.state")(function* (_ctx) { - const cache: State = { queue: new Map(), scope: yield* Scope.make() } - - yield* Effect.addFinalizer(() => - Scope.close(cache.scope, Exit.void).pipe( - Effect.andThen( - Effect.sync(() => { - cache.queue.clear() - }), - ), - ), - ) - - if (disabled) return cache - - const watch = ( - def: D, - fn: (evt: { properties: any }) => Effect.Effect, - ) => - bus.subscribe(def as never).pipe( - Stream.runForEach((evt) => - fn(evt).pipe( - Effect.catchCause((cause) => - Effect.sync(() => { - log.error("share subscriber failed", { type: def.type, cause }) - }), - ), - ), - ), - Effect.forkScoped, - ) - - yield* watch(Session.Event.Updated, (evt) => - Effect.gen(function* () { - const info = yield* session.get(evt.properties.sessionID) - yield* sync(info.id, [{ type: "session", data: info }]) - }), - ) - yield* watch(MessageV2.Event.Updated, (evt) => - Effect.gen(function* () { - const info = evt.properties.info - yield* sync(info.sessionID, [{ type: "message", data: info }]) - if (info.role !== "user") return - const model = yield* provider.getModel(info.model.providerID, info.model.modelID) - yield* sync(info.sessionID, [{ type: "model", data: [model] }]) - }), - ) - yield* watch(MessageV2.Event.PartUpdated, (evt) => - sync(evt.properties.part.sessionID, [{ type: "part", data: evt.properties.part }]), - ) - yield* watch(Session.Event.Diff, (evt) => - sync(evt.properties.sessionID, [{ type: "session_diff", data: evt.properties.diff }]), - ) - yield* watch(Session.Event.Deleted, (evt) => remove(evt.properties.sessionID)) - - return cache - }), - ) - - const request = Effect.fn("ShareNext.request")(function* () { - const headers: Record = {} - const active = yield* account.active() - if (Option.isNone(active) || !active.value.active_org_id) { - const baseUrl = (yield* cfg.get()).enterprise?.url ?? "https://opncd.ai" - return { headers, api: legacyApi, baseUrl } satisfies Req + return } - const token = yield* account.token(active.value.id) - if (Option.isNone(token)) { - throw new Error("No active account token available for sharing") - } - - headers.authorization = `Bearer ${token.value}` - headers["x-org-id"] = active.value.active_org_id - return { headers, api: consoleApi, baseUrl: active.value.url } satisfies Req - }) - - const get = Effect.fnUntraced(function* (sessionID: SessionID) { - const row = yield* db((db) => - db.select().from(SessionShareTable).where(eq(SessionShareTable.session_id, sessionID)).get(), - ) - if (!row) return - return { id: row.id, secret: row.secret, url: row.url } satisfies Share - }) - - const flush = Effect.fn("ShareNext.flush")(function* (sessionID: SessionID) { - if (disabled) return - const s = yield* InstanceState.get(state) - const queued = s.queue.get(sessionID) - if (!queued) return - - s.queue.delete(sessionID) - - const share = yield* get(sessionID) - if (!share) return - - const req = yield* request() - const res = yield* HttpClientRequest.post(`${req.baseUrl}${req.api.sync(share.id)}`).pipe( - HttpClientRequest.setHeaders(req.headers), - HttpClientRequest.bodyJson({ secret: share.secret, data: Array.from(queued.data.values()) }), - Effect.flatMap((r) => http.execute(r)), - ) - - if (res.status >= 400) { - log.warn("failed to sync share", { sessionID, shareID: share.id, status: res.status }) - } - }) - - const full = Effect.fn("ShareNext.full")(function* (sessionID: SessionID) { - log.info("full sync", { sessionID }) - const info = yield* session.get(sessionID) - const diffs = yield* session.diff(sessionID) - const messages = yield* Effect.sync(() => Array.from(MessageV2.stream(sessionID))) - const models = yield* Effect.forEach( - Array.from( - new Map( - messages - .filter((msg) => msg.info.role === "user") - .map((msg) => (msg.info as SDK.UserMessage).model) - .map((item) => [`${item.providerID}/${item.modelID}`, item] as const), - ).values(), - ), - (item) => provider.getModel(ProviderID.make(item.providerID), ModelID.make(item.modelID)), - { concurrency: 8 }, - ) - - yield* sync(sessionID, [ - { type: "session", data: info }, - ...messages.map((item) => ({ type: "message" as const, data: item.info })), - ...messages.flatMap((item) => item.parts.map((part) => ({ type: "part" as const, data: part }))), - { type: "session_diff", data: diffs }, - { type: "model", data: models }, - ]) - }) - - const init = Effect.fn("ShareNext.init")(function* () { - if (disabled) return - yield* InstanceState.get(state) - }) - - const url = Effect.fn("ShareNext.url")(function* () { - return (yield* request()).baseUrl - }) - - const create = Effect.fn("ShareNext.create")(function* (sessionID: SessionID) { - if (disabled) return { id: "", url: "", secret: "" } - log.info("creating share", { sessionID }) - const req = yield* request() - const result = yield* HttpClientRequest.post(`${req.baseUrl}${req.api.create}`).pipe( - HttpClientRequest.setHeaders(req.headers), - HttpClientRequest.bodyJson({ sessionID }), - Effect.flatMap((r) => httpOk.execute(r)), - Effect.flatMap(HttpClientResponse.schemaBodyJson(ShareSchema)), - ) - yield* db((db) => - db - .insert(SessionShareTable) - .values({ session_id: sessionID, id: result.id, secret: result.secret, url: result.url }) - .onConflictDoUpdate({ - target: SessionShareTable.session_id, - set: { id: result.id, secret: result.secret, url: result.url }, - }) - .run(), - ) - const s = yield* InstanceState.get(state) - yield* full(sessionID).pipe( + const next = new Map(data.map((item) => [key(item), item])) + s.queue.set(sessionID, { data: next }) + yield* flush(sessionID).pipe( + Effect.delay(1000), Effect.catchCause((cause) => Effect.sync(() => { - log.error("share full sync failed", { sessionID, cause }) + log.error("share flush failed", { sessionID, cause }) }), ), Effect.forkIn(s.scope), ) - return result }) + } - const remove = Effect.fn("ShareNext.remove")(function* (sessionID: SessionID) { - if (disabled) return - log.info("removing share", { sessionID }) - const share = yield* get(sessionID) - if (!share) return + const state: InstanceState.InstanceState = yield* InstanceState.make( + Effect.fn("ShareNext.state")(function* (_ctx) { + const cache: State = { queue: new Map(), scope: yield* Scope.make() } - const req = yield* request() - yield* HttpClientRequest.delete(`${req.baseUrl}${req.api.remove(share.id)}`).pipe( - HttpClientRequest.setHeaders(req.headers), - HttpClientRequest.bodyJson({ secret: share.secret }), - Effect.flatMap((r) => httpOk.execute(r)), + yield* Effect.addFinalizer(() => + Scope.close(cache.scope, Exit.void).pipe( + Effect.andThen( + Effect.sync(() => { + cache.queue.clear() + }), + ), + ), ) - yield* db((db) => db.delete(SessionShareTable).where(eq(SessionShareTable.session_id, sessionID)).run()) - }) + if (disabled) return cache - return Service.of({ init, url, request, create, remove }) - }), - ) + const watch = ( + def: D, + fn: (evt: { properties: any }) => Effect.Effect, + ) => + bus.subscribe(def as never).pipe( + Stream.runForEach((evt) => + fn(evt).pipe( + Effect.catchCause((cause) => + Effect.sync(() => { + log.error("share subscriber failed", { type: def.type, cause }) + }), + ), + ), + ), + Effect.forkScoped, + ) - export const defaultLayer = layer.pipe( - Layer.provide(Bus.layer), - Layer.provide(Account.defaultLayer), - Layer.provide(Config.defaultLayer), - Layer.provide(FetchHttpClient.layer), - Layer.provide(Provider.defaultLayer), - Layer.provide(Session.defaultLayer), - ) -} + yield* watch(Session.Event.Updated, (evt) => + Effect.gen(function* () { + const info = yield* session.get(evt.properties.sessionID) + yield* sync(info.id, [{ type: "session", data: info }]) + }), + ) + yield* watch(MessageV2.Event.Updated, (evt) => + Effect.gen(function* () { + const info = evt.properties.info + yield* sync(info.sessionID, [{ type: "message", data: info }]) + if (info.role !== "user") return + const model = yield* provider.getModel(info.model.providerID, info.model.modelID) + yield* sync(info.sessionID, [{ type: "model", data: [model] }]) + }), + ) + yield* watch(MessageV2.Event.PartUpdated, (evt) => + sync(evt.properties.part.sessionID, [{ type: "part", data: evt.properties.part }]), + ) + yield* watch(Session.Event.Diff, (evt) => + sync(evt.properties.sessionID, [{ type: "session_diff", data: evt.properties.diff }]), + ) + yield* watch(Session.Event.Deleted, (evt) => remove(evt.properties.sessionID)) + + return cache + }), + ) + + const request = Effect.fn("ShareNext.request")(function* () { + const headers: Record = {} + const active = yield* account.active() + if (Option.isNone(active) || !active.value.active_org_id) { + const baseUrl = (yield* cfg.get()).enterprise?.url ?? "https://opncd.ai" + return { headers, api: legacyApi, baseUrl } satisfies Req + } + + const token = yield* account.token(active.value.id) + if (Option.isNone(token)) { + throw new Error("No active account token available for sharing") + } + + headers.authorization = `Bearer ${token.value}` + headers["x-org-id"] = active.value.active_org_id + return { headers, api: consoleApi, baseUrl: active.value.url } satisfies Req + }) + + const get = Effect.fnUntraced(function* (sessionID: SessionID) { + const row = yield* db((db) => + db.select().from(SessionShareTable).where(eq(SessionShareTable.session_id, sessionID)).get(), + ) + if (!row) return + return { id: row.id, secret: row.secret, url: row.url } satisfies Share + }) + + const flush = Effect.fn("ShareNext.flush")(function* (sessionID: SessionID) { + if (disabled) return + const s = yield* InstanceState.get(state) + const queued = s.queue.get(sessionID) + if (!queued) return + + s.queue.delete(sessionID) + + const share = yield* get(sessionID) + if (!share) return + + const req = yield* request() + const res = yield* HttpClientRequest.post(`${req.baseUrl}${req.api.sync(share.id)}`).pipe( + HttpClientRequest.setHeaders(req.headers), + HttpClientRequest.bodyJson({ secret: share.secret, data: Array.from(queued.data.values()) }), + Effect.flatMap((r) => http.execute(r)), + ) + + if (res.status >= 400) { + log.warn("failed to sync share", { sessionID, shareID: share.id, status: res.status }) + } + }) + + const full = Effect.fn("ShareNext.full")(function* (sessionID: SessionID) { + log.info("full sync", { sessionID }) + const info = yield* session.get(sessionID) + const diffs = yield* session.diff(sessionID) + const messages = yield* Effect.sync(() => Array.from(MessageV2.stream(sessionID))) + const models = yield* Effect.forEach( + Array.from( + new Map( + messages + .filter((msg) => msg.info.role === "user") + .map((msg) => (msg.info as SDK.UserMessage).model) + .map((item) => [`${item.providerID}/${item.modelID}`, item] as const), + ).values(), + ), + (item) => provider.getModel(ProviderID.make(item.providerID), ModelID.make(item.modelID)), + { concurrency: 8 }, + ) + + yield* sync(sessionID, [ + { type: "session", data: info }, + ...messages.map((item) => ({ type: "message" as const, data: item.info })), + ...messages.flatMap((item) => item.parts.map((part) => ({ type: "part" as const, data: part }))), + { type: "session_diff", data: diffs }, + { type: "model", data: models }, + ]) + }) + + const init = Effect.fn("ShareNext.init")(function* () { + if (disabled) return + yield* InstanceState.get(state) + }) + + const url = Effect.fn("ShareNext.url")(function* () { + return (yield* request()).baseUrl + }) + + const create = Effect.fn("ShareNext.create")(function* (sessionID: SessionID) { + if (disabled) return { id: "", url: "", secret: "" } + log.info("creating share", { sessionID }) + const req = yield* request() + const result = yield* HttpClientRequest.post(`${req.baseUrl}${req.api.create}`).pipe( + HttpClientRequest.setHeaders(req.headers), + HttpClientRequest.bodyJson({ sessionID }), + Effect.flatMap((r) => httpOk.execute(r)), + Effect.flatMap(HttpClientResponse.schemaBodyJson(ShareSchema)), + ) + yield* db((db) => + db + .insert(SessionShareTable) + .values({ session_id: sessionID, id: result.id, secret: result.secret, url: result.url }) + .onConflictDoUpdate({ + target: SessionShareTable.session_id, + set: { id: result.id, secret: result.secret, url: result.url }, + }) + .run(), + ) + const s = yield* InstanceState.get(state) + yield* full(sessionID).pipe( + Effect.catchCause((cause) => + Effect.sync(() => { + log.error("share full sync failed", { sessionID, cause }) + }), + ), + Effect.forkIn(s.scope), + ) + return result + }) + + const remove = Effect.fn("ShareNext.remove")(function* (sessionID: SessionID) { + if (disabled) return + log.info("removing share", { sessionID }) + const share = yield* get(sessionID) + if (!share) return + + const req = yield* request() + yield* HttpClientRequest.delete(`${req.baseUrl}${req.api.remove(share.id)}`).pipe( + HttpClientRequest.setHeaders(req.headers), + HttpClientRequest.bodyJson({ secret: share.secret }), + Effect.flatMap((r) => httpOk.execute(r)), + ) + + yield* db((db) => db.delete(SessionShareTable).where(eq(SessionShareTable.session_id, sessionID)).run()) + }) + + return Service.of({ init, url, request, create, remove }) + }), +) + +export const defaultLayer = layer.pipe( + Layer.provide(Bus.layer), + Layer.provide(Account.defaultLayer), + Layer.provide(Config.defaultLayer), + Layer.provide(FetchHttpClient.layer), + Layer.provide(Provider.defaultLayer), + Layer.provide(Session.defaultLayer), +) diff --git a/packages/opencode/test/share/share-next.test.ts b/packages/opencode/test/share/share-next.test.ts index 8150e03623..ac3f7b79e0 100644 --- a/packages/opencode/test/share/share-next.test.ts +++ b/packages/opencode/test/share/share-next.test.ts @@ -12,7 +12,7 @@ import { Config } from "../../src/config" import { Provider } from "../../src/provider" import { Session } from "../../src/session" import type { SessionID } from "../../src/session/schema" -import { ShareNext } from "../../src/share/share-next" +import { ShareNext } from "../../src/share" import { SessionShareTable } from "../../src/share/share.sql" import { Database, eq } from "../../src/storage/db" import { provideTmpdirInstance } from "../fixture/fixture" From d4cfbd020da730ad8e9d72ffe61d6496d48ccf30 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 23:29:12 -0400 Subject: [PATCH 215/300] feat: unwrap effect namespaces to flat exports + barrel (#22745) --- packages/opencode/src/effect/app-runtime.ts | 2 +- .../opencode/src/effect/bootstrap-runtime.ts | 2 +- packages/opencode/src/effect/index.ts | 3 + .../opencode/src/effect/instance-state.ts | 2 +- packages/opencode/src/effect/logger.ts | 122 +++--- packages/opencode/src/effect/observability.ts | 134 ++++--- packages/opencode/src/effect/run-service.ts | 2 +- packages/opencode/src/effect/runner.ts | 362 +++++++++--------- .../src/server/instance/httpapi/server.ts | 2 +- packages/opencode/src/session/message-v2.ts | 2 +- packages/opencode/src/session/prompt.ts | 2 +- packages/opencode/src/session/run-state.ts | 4 +- .../opencode/src/tool/external-directory.ts | 2 +- packages/opencode/src/tool/skill.ts | 2 +- .../test/effect/app-runtime-logger.test.ts | 2 +- packages/opencode/test/effect/runner.test.ts | 2 +- 16 files changed, 322 insertions(+), 325 deletions(-) diff --git a/packages/opencode/src/effect/app-runtime.ts b/packages/opencode/src/effect/app-runtime.ts index 495cf9eea8..bd27df3435 100644 --- a/packages/opencode/src/effect/app-runtime.ts +++ b/packages/opencode/src/effect/app-runtime.ts @@ -1,6 +1,6 @@ import { Layer, ManagedRuntime } from "effect" import { attach, memoMap } from "./run-service" -import { Observability } from "./observability" +import { Observability } from "." import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { Bus } from "@/bus" diff --git a/packages/opencode/src/effect/bootstrap-runtime.ts b/packages/opencode/src/effect/bootstrap-runtime.ts index 9be456b095..208a83bf85 100644 --- a/packages/opencode/src/effect/bootstrap-runtime.ts +++ b/packages/opencode/src/effect/bootstrap-runtime.ts @@ -10,7 +10,7 @@ import { File } from "@/file" import { Vcs } from "@/project" import { Snapshot } from "@/snapshot" import { Bus } from "@/bus" -import { Observability } from "./observability" +import { Observability } from "." export const BootstrapLayer = Layer.mergeAll( Plugin.defaultLayer, diff --git a/packages/opencode/src/effect/index.ts b/packages/opencode/src/effect/index.ts index d10afdff2b..410ce00c22 100644 --- a/packages/opencode/src/effect/index.ts +++ b/packages/opencode/src/effect/index.ts @@ -1,2 +1,5 @@ export * as InstanceState from "./instance-state" export * as EffectBridge from "./bridge" +export * as Runner from "./runner" +export * as Observability from "./observability" +export * as EffectLogger from "./logger" diff --git a/packages/opencode/src/effect/instance-state.ts b/packages/opencode/src/effect/instance-state.ts index b6249db049..d71f82df97 100644 --- a/packages/opencode/src/effect/instance-state.ts +++ b/packages/opencode/src/effect/instance-state.ts @@ -1,5 +1,5 @@ import { Effect, Fiber, ScopedCache, Scope, Context } from "effect" -import { EffectLogger } from "@/effect/logger" +import { EffectLogger } from "@/effect" import { Instance, type InstanceContext } from "@/project/instance" import { LocalContext } from "@/util" import { InstanceRef, WorkspaceRef } from "./instance-ref" diff --git a/packages/opencode/src/effect/logger.ts b/packages/opencode/src/effect/logger.ts index 7f21084ddc..97b614fc0a 100644 --- a/packages/opencode/src/effect/logger.ts +++ b/packages/opencode/src/effect/logger.ts @@ -1,67 +1,65 @@ import { Cause, Effect, Logger, References } from "effect" import { Log } from "@/util" -export namespace EffectLogger { - type Fields = Record +type Fields = Record - export interface Handle { - readonly debug: (msg?: unknown, extra?: Fields) => Effect.Effect - readonly info: (msg?: unknown, extra?: Fields) => Effect.Effect - readonly warn: (msg?: unknown, extra?: Fields) => Effect.Effect - readonly error: (msg?: unknown, extra?: Fields) => Effect.Effect - readonly with: (extra: Fields) => Handle - } - - const clean = (input?: Fields): Fields => - Object.fromEntries(Object.entries(input ?? {}).filter((entry) => entry[1] !== undefined && entry[1] !== null)) - - const text = (input: unknown): string => { - if (Array.isArray(input)) return input.map((item) => String(item)).join(" ") - return input === undefined ? "" : String(input) - } - - const call = (run: (msg?: unknown) => Effect.Effect, base: Fields, msg?: unknown, extra?: Fields) => { - const ann = clean({ ...base, ...extra }) - const fx = run(msg) - return Object.keys(ann).length ? Effect.annotateLogs(fx, ann) : fx - } - - export const logger = Logger.make((opts) => { - const extra = clean(opts.fiber.getRef(References.CurrentLogAnnotations)) - const now = opts.date.getTime() - for (const [key, start] of opts.fiber.getRef(References.CurrentLogSpans)) { - extra[`logSpan.${key}`] = `${now - start}ms` - } - if (opts.cause.reasons.length > 0) { - extra.cause = Cause.pretty(opts.cause) - } - - const svc = typeof extra.service === "string" ? extra.service : undefined - if (svc) delete extra.service - const log = svc ? Log.create({ service: svc }) : Log.Default - const msg = text(opts.message) - - switch (opts.logLevel) { - case "Trace": - case "Debug": - return log.debug(msg, extra) - case "Warn": - return log.warn(msg, extra) - case "Error": - case "Fatal": - return log.error(msg, extra) - default: - return log.info(msg, extra) - } - }) - - export const layer = Logger.layer([logger], { mergeWithExisting: false }) - - export const create = (base: Fields = {}): Handle => ({ - debug: (msg, extra) => call((item) => Effect.logDebug(item), base, msg, extra), - info: (msg, extra) => call((item) => Effect.logInfo(item), base, msg, extra), - warn: (msg, extra) => call((item) => Effect.logWarning(item), base, msg, extra), - error: (msg, extra) => call((item) => Effect.logError(item), base, msg, extra), - with: (extra) => create({ ...base, ...extra }), - }) +export interface Handle { + readonly debug: (msg?: unknown, extra?: Fields) => Effect.Effect + readonly info: (msg?: unknown, extra?: Fields) => Effect.Effect + readonly warn: (msg?: unknown, extra?: Fields) => Effect.Effect + readonly error: (msg?: unknown, extra?: Fields) => Effect.Effect + readonly with: (extra: Fields) => Handle } + +const clean = (input?: Fields): Fields => + Object.fromEntries(Object.entries(input ?? {}).filter((entry) => entry[1] !== undefined && entry[1] !== null)) + +const text = (input: unknown): string => { + if (Array.isArray(input)) return input.map((item) => String(item)).join(" ") + return input === undefined ? "" : String(input) +} + +const call = (run: (msg?: unknown) => Effect.Effect, base: Fields, msg?: unknown, extra?: Fields) => { + const ann = clean({ ...base, ...extra }) + const fx = run(msg) + return Object.keys(ann).length ? Effect.annotateLogs(fx, ann) : fx +} + +export const logger = Logger.make((opts) => { + const extra = clean(opts.fiber.getRef(References.CurrentLogAnnotations)) + const now = opts.date.getTime() + for (const [key, start] of opts.fiber.getRef(References.CurrentLogSpans)) { + extra[`logSpan.${key}`] = `${now - start}ms` + } + if (opts.cause.reasons.length > 0) { + extra.cause = Cause.pretty(opts.cause) + } + + const svc = typeof extra.service === "string" ? extra.service : undefined + if (svc) delete extra.service + const log = svc ? Log.create({ service: svc }) : Log.Default + const msg = text(opts.message) + + switch (opts.logLevel) { + case "Trace": + case "Debug": + return log.debug(msg, extra) + case "Warn": + return log.warn(msg, extra) + case "Error": + case "Fatal": + return log.error(msg, extra) + default: + return log.info(msg, extra) + } +}) + +export const layer = Logger.layer([logger], { mergeWithExisting: false }) + +export const create = (base: Fields = {}): Handle => ({ + debug: (msg, extra) => call((item) => Effect.logDebug(item), base, msg, extra), + info: (msg, extra) => call((item) => Effect.logInfo(item), base, msg, extra), + warn: (msg, extra) => call((item) => Effect.logWarning(item), base, msg, extra), + error: (msg, extra) => call((item) => Effect.logError(item), base, msg, extra), + with: (extra) => create({ ...base, ...extra }), +}) diff --git a/packages/opencode/src/effect/observability.ts b/packages/opencode/src/effect/observability.ts index f79306bf1e..4e8ae22217 100644 --- a/packages/opencode/src/effect/observability.ts +++ b/packages/opencode/src/effect/observability.ts @@ -1,80 +1,78 @@ import { Effect, Layer, Logger } from "effect" import { FetchHttpClient } from "effect/unstable/http" import { OtlpLogger, OtlpSerialization } from "effect/unstable/observability" -import { EffectLogger } from "@/effect/logger" +import { EffectLogger } from "@/effect" import { Flag } from "@/flag/flag" import { CHANNEL, VERSION } from "@/installation/meta" -export namespace Observability { - const base = Flag.OTEL_EXPORTER_OTLP_ENDPOINT - export const enabled = !!base +const base = Flag.OTEL_EXPORTER_OTLP_ENDPOINT +export const enabled = !!base - const headers = Flag.OTEL_EXPORTER_OTLP_HEADERS - ? Flag.OTEL_EXPORTER_OTLP_HEADERS.split(",").reduce( - (acc, x) => { - const [key, ...value] = x.split("=") - acc[key] = value.join("=") - return acc - }, - {} as Record, - ) - : undefined +const headers = Flag.OTEL_EXPORTER_OTLP_HEADERS + ? Flag.OTEL_EXPORTER_OTLP_HEADERS.split(",").reduce( + (acc, x) => { + const [key, ...value] = x.split("=") + acc[key] = value.join("=") + return acc + }, + {} as Record, + ) + : undefined - const resource = { - serviceName: "opencode", - serviceVersion: VERSION, - attributes: { - "deployment.environment.name": CHANNEL === "local" ? "local" : CHANNEL, - "opencode.client": Flag.OPENCODE_CLIENT, - }, - } +const resource = { + serviceName: "opencode", + serviceVersion: VERSION, + attributes: { + "deployment.environment.name": CHANNEL === "local" ? "local" : CHANNEL, + "opencode.client": Flag.OPENCODE_CLIENT, + }, +} - const logs = Logger.layer( - [ - EffectLogger.logger, - OtlpLogger.make({ - url: `${base}/v1/logs`, - resource, +const logs = Logger.layer( + [ + EffectLogger.logger, + OtlpLogger.make({ + url: `${base}/v1/logs`, + resource, + headers, + }), + ], + { mergeWithExisting: false }, +).pipe(Layer.provide(OtlpSerialization.layerJson), Layer.provide(FetchHttpClient.layer)) + +const traces = async () => { + const NodeSdk = await import("@effect/opentelemetry/NodeSdk") + const OTLP = await import("@opentelemetry/exporter-trace-otlp-http") + const SdkBase = await import("@opentelemetry/sdk-trace-base") + + // @effect/opentelemetry creates a NodeTracerProvider but never calls + // register(), so the global @opentelemetry/api context manager stays + // as the no-op default. Non-Effect code (like the AI SDK) that calls + // tracer.startActiveSpan() relies on context.active() to find the + // parent span — without a real context manager every span starts a + // new trace. Registering AsyncLocalStorageContextManager fixes this. + const { AsyncLocalStorageContextManager } = await import("@opentelemetry/context-async-hooks") + const { context } = await import("@opentelemetry/api") + const mgr = new AsyncLocalStorageContextManager() + mgr.enable() + context.setGlobalContextManager(mgr) + + return NodeSdk.layer(() => ({ + resource, + spanProcessor: new SdkBase.BatchSpanProcessor( + new OTLP.OTLPTraceExporter({ + url: `${base}/v1/traces`, headers, }), - ], - { mergeWithExisting: false }, - ).pipe(Layer.provide(OtlpSerialization.layerJson), Layer.provide(FetchHttpClient.layer)) - - const traces = async () => { - const NodeSdk = await import("@effect/opentelemetry/NodeSdk") - const OTLP = await import("@opentelemetry/exporter-trace-otlp-http") - const SdkBase = await import("@opentelemetry/sdk-trace-base") - - // @effect/opentelemetry creates a NodeTracerProvider but never calls - // register(), so the global @opentelemetry/api context manager stays - // as the no-op default. Non-Effect code (like the AI SDK) that calls - // tracer.startActiveSpan() relies on context.active() to find the - // parent span — without a real context manager every span starts a - // new trace. Registering AsyncLocalStorageContextManager fixes this. - const { AsyncLocalStorageContextManager } = await import("@opentelemetry/context-async-hooks") - const { context } = await import("@opentelemetry/api") - const mgr = new AsyncLocalStorageContextManager() - mgr.enable() - context.setGlobalContextManager(mgr) - - return NodeSdk.layer(() => ({ - resource, - spanProcessor: new SdkBase.BatchSpanProcessor( - new OTLP.OTLPTraceExporter({ - url: `${base}/v1/traces`, - headers, - }), - ), - })) - } - - export const layer = !base - ? EffectLogger.layer - : Layer.unwrap( - Effect.gen(function* () { - const trace = yield* Effect.promise(traces) - return Layer.mergeAll(trace, logs) - }), - ) + ), + })) } + +export const layer = !base + ? EffectLogger.layer + : Layer.unwrap( + Effect.gen(function* () { + const trace = yield* Effect.promise(traces) + return Layer.mergeAll(trace, logs) + }), + ) diff --git a/packages/opencode/src/effect/run-service.ts b/packages/opencode/src/effect/run-service.ts index 9553e7a3aa..a9d653b108 100644 --- a/packages/opencode/src/effect/run-service.ts +++ b/packages/opencode/src/effect/run-service.ts @@ -3,7 +3,7 @@ import * as Context from "effect/Context" import { Instance } from "@/project/instance" import { LocalContext } from "@/util" import { InstanceRef, WorkspaceRef } from "./instance-ref" -import { Observability } from "./observability" +import { Observability } from "." import { WorkspaceContext } from "@/control-plane/workspace-context" import type { InstanceContext } from "@/project/instance" diff --git a/packages/opencode/src/effect/runner.ts b/packages/opencode/src/effect/runner.ts index 38c45a6342..925c268f8e 100644 --- a/packages/opencode/src/effect/runner.ts +++ b/packages/opencode/src/effect/runner.ts @@ -1,208 +1,206 @@ import { Cause, Deferred, Effect, Exit, Fiber, Schema, Scope, SynchronizedRef } from "effect" export interface Runner { - readonly state: Runner.State + readonly state: State readonly busy: boolean readonly ensureRunning: (work: Effect.Effect) => Effect.Effect readonly startShell: (work: Effect.Effect) => Effect.Effect readonly cancel: Effect.Effect } -export namespace Runner { - export class Cancelled extends Schema.TaggedErrorClass()("RunnerCancelled", {}) {} +export class Cancelled extends Schema.TaggedErrorClass()("RunnerCancelled", {}) {} - interface RunHandle { - id: number - done: Deferred.Deferred - fiber: Fiber.Fiber +interface RunHandle { + id: number + done: Deferred.Deferred + fiber: Fiber.Fiber +} + +interface ShellHandle { + id: number + fiber: Fiber.Fiber +} + +interface PendingHandle { + id: number + done: Deferred.Deferred + work: Effect.Effect +} + +export type State = + | { readonly _tag: "Idle" } + | { readonly _tag: "Running"; readonly run: RunHandle } + | { readonly _tag: "Shell"; readonly shell: ShellHandle } + | { readonly _tag: "ShellThenRun"; readonly shell: ShellHandle; readonly run: PendingHandle } + +export const make = ( + scope: Scope.Scope, + opts?: { + onIdle?: Effect.Effect + onBusy?: Effect.Effect + onInterrupt?: Effect.Effect + busy?: () => never + }, +): Runner => { + const ref = SynchronizedRef.makeUnsafe>({ _tag: "Idle" }) + const idle = opts?.onIdle ?? Effect.void + const busy = opts?.onBusy ?? Effect.void + const onInterrupt = opts?.onInterrupt + let ids = 0 + + const state = () => SynchronizedRef.getUnsafe(ref) + const next = () => { + ids += 1 + return ids } - interface ShellHandle { - id: number - fiber: Fiber.Fiber - } + const complete = (done: Deferred.Deferred, exit: Exit.Exit) => + Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause) + ? Deferred.fail(done, new Cancelled()).pipe(Effect.asVoid) + : Deferred.done(done, exit).pipe(Effect.asVoid) - interface PendingHandle { - id: number - done: Deferred.Deferred - work: Effect.Effect - } + const idleIfCurrent = () => + SynchronizedRef.modify(ref, (st) => [st._tag === "Idle" ? idle : Effect.void, st] as const).pipe(Effect.flatten) - export type State = - | { readonly _tag: "Idle" } - | { readonly _tag: "Running"; readonly run: RunHandle } - | { readonly _tag: "Shell"; readonly shell: ShellHandle } - | { readonly _tag: "ShellThenRun"; readonly shell: ShellHandle; readonly run: PendingHandle } + const finishRun = (id: number, done: Deferred.Deferred, exit: Exit.Exit) => + SynchronizedRef.modify( + ref, + (st) => + [ + Effect.gen(function* () { + if (st._tag === "Running" && st.run.id === id) yield* idle + yield* complete(done, exit) + }), + st._tag === "Running" && st.run.id === id ? ({ _tag: "Idle" } as const) : st, + ] as const, + ).pipe(Effect.flatten) - export const make = ( - scope: Scope.Scope, - opts?: { - onIdle?: Effect.Effect - onBusy?: Effect.Effect - onInterrupt?: Effect.Effect - busy?: () => never - }, - ): Runner => { - const ref = SynchronizedRef.makeUnsafe>({ _tag: "Idle" }) - const idle = opts?.onIdle ?? Effect.void - const busy = opts?.onBusy ?? Effect.void - const onInterrupt = opts?.onInterrupt - let ids = 0 - - const state = () => SynchronizedRef.getUnsafe(ref) - const next = () => { - ids += 1 - return ids - } - - const complete = (done: Deferred.Deferred, exit: Exit.Exit) => - Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause) - ? Deferred.fail(done, new Cancelled()).pipe(Effect.asVoid) - : Deferred.done(done, exit).pipe(Effect.asVoid) - - const idleIfCurrent = () => - SynchronizedRef.modify(ref, (st) => [st._tag === "Idle" ? idle : Effect.void, st] as const).pipe(Effect.flatten) - - const finishRun = (id: number, done: Deferred.Deferred, exit: Exit.Exit) => - SynchronizedRef.modify( - ref, - (st) => - [ - Effect.gen(function* () { - if (st._tag === "Running" && st.run.id === id) yield* idle - yield* complete(done, exit) - }), - st._tag === "Running" && st.run.id === id ? ({ _tag: "Idle" } as const) : st, - ] as const, - ).pipe(Effect.flatten) - - const startRun = (work: Effect.Effect, done: Deferred.Deferred) => - Effect.gen(function* () { - const id = next() - const fiber = yield* work.pipe( - Effect.onExit((exit) => finishRun(id, done, exit)), - Effect.forkIn(scope), - ) - return { id, done, fiber } satisfies RunHandle - }) - - const finishShell = (id: number) => - SynchronizedRef.modifyEffect( - ref, - Effect.fnUntraced(function* (st) { - if (st._tag === "Shell" && st.shell.id === id) return [idle, { _tag: "Idle" }] as const - if (st._tag === "ShellThenRun" && st.shell.id === id) { - const run = yield* startRun(st.run.work, st.run.done) - return [Effect.void, { _tag: "Running", run }] as const - } - return [Effect.void, st] as const - }), - ).pipe(Effect.flatten) - - const stopShell = (shell: ShellHandle) => Fiber.interrupt(shell.fiber) - - const ensureRunning = (work: Effect.Effect) => - SynchronizedRef.modifyEffect( - ref, - Effect.fnUntraced(function* (st) { - switch (st._tag) { - case "Running": - case "ShellThenRun": - return [Deferred.await(st.run.done), st] as const - case "Shell": { - const run = { - id: next(), - done: yield* Deferred.make(), - work, - } satisfies PendingHandle - return [Deferred.await(run.done), { _tag: "ShellThenRun", shell: st.shell, run }] as const - } - case "Idle": { - const done = yield* Deferred.make() - const run = yield* startRun(work, done) - return [Deferred.await(done), { _tag: "Running", run }] as const - } - } - }), - ).pipe( - Effect.flatten, - Effect.catch( - (e): Effect.Effect => (e instanceof Cancelled ? (onInterrupt ?? Effect.die(e)) : Effect.fail(e as E)), - ), + const startRun = (work: Effect.Effect, done: Deferred.Deferred) => + Effect.gen(function* () { + const id = next() + const fiber = yield* work.pipe( + Effect.onExit((exit) => finishRun(id, done, exit)), + Effect.forkIn(scope), ) + return { id, done, fiber } satisfies RunHandle + }) - const startShell = (work: Effect.Effect) => - SynchronizedRef.modifyEffect( - ref, - Effect.fnUntraced(function* (st) { - if (st._tag !== "Idle") { - return [ - Effect.sync(() => { - if (opts?.busy) opts.busy() - throw new Error("Runner is busy") - }), - st, - ] as const + const finishShell = (id: number) => + SynchronizedRef.modifyEffect( + ref, + Effect.fnUntraced(function* (st) { + if (st._tag === "Shell" && st.shell.id === id) return [idle, { _tag: "Idle" }] as const + if (st._tag === "ShellThenRun" && st.shell.id === id) { + const run = yield* startRun(st.run.work, st.run.done) + return [Effect.void, { _tag: "Running", run }] as const + } + return [Effect.void, st] as const + }), + ).pipe(Effect.flatten) + + const stopShell = (shell: ShellHandle) => Fiber.interrupt(shell.fiber) + + const ensureRunning = (work: Effect.Effect) => + SynchronizedRef.modifyEffect( + ref, + Effect.fnUntraced(function* (st) { + switch (st._tag) { + case "Running": + case "ShellThenRun": + return [Deferred.await(st.run.done), st] as const + case "Shell": { + const run = { + id: next(), + done: yield* Deferred.make(), + work, + } satisfies PendingHandle + return [Deferred.await(run.done), { _tag: "ShellThenRun", shell: st.shell, run }] as const } - yield* busy - const id = next() - const fiber = yield* work.pipe(Effect.ensuring(finishShell(id)), Effect.forkChild) - const shell = { id, fiber } satisfies ShellHandle - return [ - Effect.gen(function* () { - const exit = yield* Fiber.await(fiber) - if (Exit.isSuccess(exit)) return exit.value - if (Cause.hasInterruptsOnly(exit.cause) && onInterrupt) return yield* onInterrupt - return yield* Effect.failCause(exit.cause) - }), - { _tag: "Shell", shell }, - ] as const - }), - ).pipe(Effect.flatten) + case "Idle": { + const done = yield* Deferred.make() + const run = yield* startRun(work, done) + return [Deferred.await(done), { _tag: "Running", run }] as const + } + } + }), + ).pipe( + Effect.flatten, + Effect.catch( + (e): Effect.Effect => (e instanceof Cancelled ? (onInterrupt ?? Effect.die(e)) : Effect.fail(e as E)), + ), + ) - const cancel = SynchronizedRef.modify(ref, (st) => { - switch (st._tag) { - case "Idle": - return [Effect.void, st] as const - case "Running": + const startShell = (work: Effect.Effect) => + SynchronizedRef.modifyEffect( + ref, + Effect.fnUntraced(function* (st) { + if (st._tag !== "Idle") { return [ - Effect.gen(function* () { - yield* Fiber.interrupt(st.run.fiber) - yield* Deferred.await(st.run.done).pipe(Effect.exit, Effect.asVoid) - yield* idleIfCurrent() + Effect.sync(() => { + if (opts?.busy) opts.busy() + throw new Error("Runner is busy") }), - { _tag: "Idle" } as const, + st, ] as const - case "Shell": - return [ - Effect.gen(function* () { - yield* stopShell(st.shell) - yield* idleIfCurrent() - }), - { _tag: "Idle" } as const, - ] as const - case "ShellThenRun": - return [ - Effect.gen(function* () { - yield* Deferred.fail(st.run.done, new Cancelled()).pipe(Effect.asVoid) - yield* stopShell(st.shell) - yield* idleIfCurrent() - }), - { _tag: "Idle" } as const, - ] as const - } - }).pipe(Effect.flatten) + } + yield* busy + const id = next() + const fiber = yield* work.pipe(Effect.ensuring(finishShell(id)), Effect.forkChild) + const shell = { id, fiber } satisfies ShellHandle + return [ + Effect.gen(function* () { + const exit = yield* Fiber.await(fiber) + if (Exit.isSuccess(exit)) return exit.value + if (Cause.hasInterruptsOnly(exit.cause) && onInterrupt) return yield* onInterrupt + return yield* Effect.failCause(exit.cause) + }), + { _tag: "Shell", shell }, + ] as const + }), + ).pipe(Effect.flatten) - return { - get state() { - return state() - }, - get busy() { - return state()._tag !== "Idle" - }, - ensureRunning, - startShell, - cancel, + const cancel = SynchronizedRef.modify(ref, (st) => { + switch (st._tag) { + case "Idle": + return [Effect.void, st] as const + case "Running": + return [ + Effect.gen(function* () { + yield* Fiber.interrupt(st.run.fiber) + yield* Deferred.await(st.run.done).pipe(Effect.exit, Effect.asVoid) + yield* idleIfCurrent() + }), + { _tag: "Idle" } as const, + ] as const + case "Shell": + return [ + Effect.gen(function* () { + yield* stopShell(st.shell) + yield* idleIfCurrent() + }), + { _tag: "Idle" } as const, + ] as const + case "ShellThenRun": + return [ + Effect.gen(function* () { + yield* Deferred.fail(st.run.done, new Cancelled()).pipe(Effect.asVoid) + yield* stopShell(st.shell) + yield* idleIfCurrent() + }), + { _tag: "Idle" } as const, + ] as const } + }).pipe(Effect.flatten) + + return { + get state() { + return state() + }, + get busy() { + return state()._tag !== "Idle" + }, + ensureRunning, + startShell, + cancel, } } diff --git a/packages/opencode/src/server/instance/httpapi/server.ts b/packages/opencode/src/server/instance/httpapi/server.ts index 62ffb5940d..299a177f50 100644 --- a/packages/opencode/src/server/instance/httpapi/server.ts +++ b/packages/opencode/src/server/instance/httpapi/server.ts @@ -3,7 +3,7 @@ import { HttpApiBuilder, HttpApiMiddleware, HttpApiSecurity } from "effect/unsta import { HttpRouter, HttpServer, HttpServerRequest } from "effect/unstable/http" import { AppRuntime } from "@/effect/app-runtime" import { InstanceRef, WorkspaceRef } from "@/effect/instance-ref" -import { Observability } from "@/effect/observability" +import { Observability } from "@/effect" import { memoMap } from "@/effect/run-service" import { Flag } from "@/flag/flag" import { InstanceBootstrap } from "@/project/bootstrap" diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 2a501167a5..f4a7235e15 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -15,7 +15,7 @@ import type { SystemError } from "bun" import type { Provider } from "@/provider" import { ModelID, ProviderID } from "@/provider/schema" import { Effect } from "effect" -import { EffectLogger } from "@/effect/logger" +import { EffectLogger } from "@/effect" /** Error shape thrown by Bun's fetch() when gzip/br decompression fails mid-stream */ interface FetchDecompressionError extends Error { diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index a072633aa7..157533af0a 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -44,7 +44,7 @@ import { Truncate } from "@/tool/truncate" import { decodeDataUrl } from "@/util/data-url" import { Process } from "@/util" import { Cause, Effect, Exit, Layer, Option, Scope, Context } from "effect" -import { EffectLogger } from "@/effect/logger" +import { EffectLogger } from "@/effect" import { InstanceState } from "@/effect" import { TaskTool, type TaskPromptOps } from "@/tool/task" import { SessionRunState } from "./run-state" diff --git a/packages/opencode/src/session/run-state.ts b/packages/opencode/src/session/run-state.ts index 922daf1178..179f287fa8 100644 --- a/packages/opencode/src/session/run-state.ts +++ b/packages/opencode/src/session/run-state.ts @@ -1,5 +1,5 @@ import { InstanceState } from "@/effect" -import { Runner } from "@/effect/runner" +import { Runner } from "@/effect" import { Effect, Layer, Scope, Context } from "effect" import { Session } from "." import { MessageV2 } from "./message-v2" @@ -32,7 +32,7 @@ export namespace SessionRunState { const state = yield* InstanceState.make( Effect.fn("SessionRunState.state")(function* () { const scope = yield* Scope.Scope - const runners = new Map>() + const runners = new Map>() yield* Effect.addFinalizer( Effect.fnUntraced(function* () { yield* Effect.forEach(runners.values(), (runner) => runner.cancel, { diff --git a/packages/opencode/src/tool/external-directory.ts b/packages/opencode/src/tool/external-directory.ts index c91b698038..810206f817 100644 --- a/packages/opencode/src/tool/external-directory.ts +++ b/packages/opencode/src/tool/external-directory.ts @@ -1,6 +1,6 @@ import path from "path" import { Effect } from "effect" -import { EffectLogger } from "@/effect/logger" +import { EffectLogger } from "@/effect" import { InstanceState } from "@/effect" import type { Tool } from "./tool" import { Instance } from "../project/instance" diff --git a/packages/opencode/src/tool/skill.ts b/packages/opencode/src/tool/skill.ts index d5f3787ed6..eaec667e58 100644 --- a/packages/opencode/src/tool/skill.ts +++ b/packages/opencode/src/tool/skill.ts @@ -3,7 +3,7 @@ import { pathToFileURL } from "url" import z from "zod" import { Effect } from "effect" import * as Stream from "effect/Stream" -import { EffectLogger } from "@/effect/logger" +import { EffectLogger } from "@/effect" import { Ripgrep } from "../file/ripgrep" import { Skill } from "../skill" import { Tool } from "./tool" diff --git a/packages/opencode/test/effect/app-runtime-logger.test.ts b/packages/opencode/test/effect/app-runtime-logger.test.ts index 91f367ff3e..8d5649a20c 100644 --- a/packages/opencode/test/effect/app-runtime-logger.test.ts +++ b/packages/opencode/test/effect/app-runtime-logger.test.ts @@ -3,7 +3,7 @@ import { Context, Effect, Layer, Logger } from "effect" import { AppRuntime } from "../../src/effect/app-runtime" import { EffectBridge } from "../../src/effect" import { InstanceRef } from "../../src/effect/instance-ref" -import { EffectLogger } from "../../src/effect/logger" +import { EffectLogger } from "../../src/effect" import { makeRuntime } from "../../src/effect/run-service" import { Instance } from "../../src/project/instance" import { tmpdir } from "../fixture/fixture" diff --git a/packages/opencode/test/effect/runner.test.ts b/packages/opencode/test/effect/runner.test.ts index a91df76ebf..241e7c2a88 100644 --- a/packages/opencode/test/effect/runner.test.ts +++ b/packages/opencode/test/effect/runner.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" import { Deferred, Effect, Exit, Fiber, Ref, Scope } from "effect" -import { Runner } from "../../src/effect/runner" +import { Runner } from "../../src/effect" import { it } from "../lib/effect" describe("Runner", () => { From 1ca257e356e404a659d6ee39d5e26a41e729ca54 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 23:29:14 -0400 Subject: [PATCH 216/300] feat: unwrap config namespaces to flat exports + barrel (#22746) --- packages/opencode/script/schema.ts | 2 +- packages/opencode/src/cli/cmd/plug.ts | 2 +- packages/opencode/src/cli/cmd/tui/app.tsx | 2 +- packages/opencode/src/cli/cmd/tui/attach.ts | 2 +- .../src/cli/cmd/tui/context/keybind.tsx | 2 +- .../src/cli/cmd/tui/context/tui-config.tsx | 2 +- .../opencode/src/cli/cmd/tui/plugin/api.tsx | 2 +- .../src/cli/cmd/tui/plugin/runtime.ts | 2 +- packages/opencode/src/cli/cmd/tui/thread.ts | 2 +- .../opencode/src/cli/cmd/tui/util/scroll.ts | 2 +- packages/opencode/src/cli/error.ts | 2 +- packages/opencode/src/config/config.ts | 4 +- packages/opencode/src/config/index.ts | 3 + packages/opencode/src/config/markdown.ts | 182 +++++---- packages/opencode/src/config/paths.ts | 312 ++++++++------- packages/opencode/src/config/tui-migrate.ts | 2 +- packages/opencode/src/config/tui.ts | 370 +++++++++--------- packages/opencode/src/plugin/install.ts | 2 +- packages/opencode/src/session/prompt.ts | 2 +- packages/opencode/src/skill/skill.ts | 2 +- .../opencode/test/cli/tui/plugin-add.test.ts | 2 +- .../test/cli/tui/plugin-install.test.ts | 2 +- .../cli/tui/plugin-loader-entrypoint.test.ts | 2 +- .../test/cli/tui/plugin-loader-pure.test.ts | 2 +- .../test/cli/tui/plugin-loader.test.ts | 2 +- .../test/cli/tui/plugin-toggle.test.ts | 2 +- packages/opencode/test/cli/tui/thread.test.ts | 2 +- .../opencode/test/config/markdown.test.ts | 2 +- packages/opencode/test/config/tui.test.ts | 2 +- packages/opencode/test/fixture/tui-runtime.ts | 2 +- 30 files changed, 459 insertions(+), 462 deletions(-) diff --git a/packages/opencode/script/schema.ts b/packages/opencode/script/schema.ts index 4ea68d9bbb..4aa27423ba 100755 --- a/packages/opencode/script/schema.ts +++ b/packages/opencode/script/schema.ts @@ -2,7 +2,7 @@ import { z } from "zod" import { Config } from "../src/config" -import { TuiConfig } from "../src/config/tui" +import { TuiConfig } from "../src/config" function generate(schema: z.ZodType) { const result = z.toJSONSchema(schema, { diff --git a/packages/opencode/src/cli/cmd/plug.ts b/packages/opencode/src/cli/cmd/plug.ts index 42d06ff47f..9dfda16d64 100644 --- a/packages/opencode/src/cli/cmd/plug.ts +++ b/packages/opencode/src/cli/cmd/plug.ts @@ -1,7 +1,7 @@ import { intro, log, outro, spinner } from "@clack/prompts" import type { Argv } from "yargs" -import { ConfigPaths } from "../../config/paths" +import { ConfigPaths } from "../../config" import { Global } from "../../global" import { installPlugin, patchPluginConfig, readPluginManifest } from "../../plugin/install" import { resolvePluginTarget } from "../../plugin/shared" diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index e7e9fd9cd2..9e96d5dcbc 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -57,7 +57,7 @@ import { ArgsProvider, useArgs, type Args } from "./context/args" import open from "open" import { PromptRefProvider, usePromptRef } from "./context/prompt" import { TuiConfigProvider, useTuiConfig } from "./context/tui-config" -import { TuiConfig } from "@/config/tui" +import { TuiConfig } from "@/config" import { createTuiApi, TuiPluginRuntime, type RouteMap } from "./plugin" import { FormatError, FormatUnknownError } from "@/cli/error" diff --git a/packages/opencode/src/cli/cmd/tui/attach.ts b/packages/opencode/src/cli/cmd/tui/attach.ts index e892f9922d..9fcbf4c1f3 100644 --- a/packages/opencode/src/cli/cmd/tui/attach.ts +++ b/packages/opencode/src/cli/cmd/tui/attach.ts @@ -2,7 +2,7 @@ import { cmd } from "../cmd" import { UI } from "@/cli/ui" import { tui } from "./app" import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32" -import { TuiConfig } from "@/config/tui" +import { TuiConfig } from "@/config" import { Instance } from "@/project/instance" import { existsSync } from "fs" diff --git a/packages/opencode/src/cli/cmd/tui/context/keybind.tsx b/packages/opencode/src/cli/cmd/tui/context/keybind.tsx index 9c883aa205..b1dcdd7808 100644 --- a/packages/opencode/src/cli/cmd/tui/context/keybind.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/keybind.tsx @@ -1,7 +1,7 @@ import { createMemo } from "solid-js" import { Keybind } from "@/util" import { pipe, mapValues } from "remeda" -import type { TuiConfig } from "@/config/tui" +import type { TuiConfig } from "@/config" import type { ParsedKey, Renderable } from "@opentui/core" import { createStore } from "solid-js/store" import { useKeyboard, useRenderer } from "@opentui/solid" diff --git a/packages/opencode/src/cli/cmd/tui/context/tui-config.tsx b/packages/opencode/src/cli/cmd/tui/context/tui-config.tsx index 62dbf1ebd1..cfe59ba803 100644 --- a/packages/opencode/src/cli/cmd/tui/context/tui-config.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/tui-config.tsx @@ -1,4 +1,4 @@ -import { TuiConfig } from "@/config/tui" +import { TuiConfig } from "@/config" import { createSimpleContext } from "./helper" export const { use: useTuiConfig, provider: TuiConfigProvider } = createSimpleContext({ diff --git a/packages/opencode/src/cli/cmd/tui/plugin/api.tsx b/packages/opencode/src/cli/cmd/tui/plugin/api.tsx index 3af70d8c25..42988fcb1f 100644 --- a/packages/opencode/src/cli/cmd/tui/plugin/api.tsx +++ b/packages/opencode/src/cli/cmd/tui/plugin/api.tsx @@ -8,7 +8,7 @@ import type { useSDK } from "@tui/context/sdk" import type { useSync } from "@tui/context/sync" import type { useTheme } from "@tui/context/theme" import { Dialog as DialogUI, type useDialog } from "@tui/ui/dialog" -import type { TuiConfig } from "@/config/tui" +import type { TuiConfig } from "@/config" import { createPluginKeybind } from "../context/plugin-keybinds" import type { useKV } from "../context/kv" import { DialogAlert } from "../ui/dialog-alert" diff --git a/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts b/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts index dd873b753a..da003607c4 100644 --- a/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts +++ b/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts @@ -14,7 +14,7 @@ import path from "path" import { fileURLToPath } from "url" import { Config } from "@/config" -import { TuiConfig } from "@/config/tui" +import { TuiConfig } from "@/config" import { Log } from "@/util" import { errorData, errorMessage } from "@/util/error" import { isRecord } from "@/util/record" diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index 3aaa5a54f8..89b32d166e 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -13,7 +13,7 @@ import { Filesystem } from "@/util" import type { GlobalEvent } from "@opencode-ai/sdk/v2" import type { EventSource } from "./context/sdk" import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32" -import { TuiConfig } from "@/config/tui" +import { TuiConfig } from "@/config" import { Instance } from "@/project/instance" import { writeHeapSnapshot } from "v8" diff --git a/packages/opencode/src/cli/cmd/tui/util/scroll.ts b/packages/opencode/src/cli/cmd/tui/util/scroll.ts index 9b9398f302..d27bdb90ce 100644 --- a/packages/opencode/src/cli/cmd/tui/util/scroll.ts +++ b/packages/opencode/src/cli/cmd/tui/util/scroll.ts @@ -1,5 +1,5 @@ import { MacOSScrollAccel, type ScrollAcceleration } from "@opentui/core" -import type { TuiConfig } from "@/config/tui" +import type { TuiConfig } from "@/config" export class CustomSpeedScroll implements ScrollAcceleration { constructor(private speed: number) {} diff --git a/packages/opencode/src/cli/error.ts b/packages/opencode/src/cli/error.ts index 6ba110d34f..735f1a721e 100644 --- a/packages/opencode/src/cli/error.ts +++ b/packages/opencode/src/cli/error.ts @@ -1,5 +1,5 @@ import { AccountServiceError, AccountTransportError } from "@/account" -import { ConfigMarkdown } from "@/config/markdown" +import { ConfigMarkdown } from "@/config" import { errorFormat } from "@/util/error" import { Config } from "../config" import { MCP } from "../mcp" diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 3da2dd6bdb..04801098b4 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -21,7 +21,7 @@ import { import { Instance, type InstanceContext } from "../project/instance" import { LSPServer } from "../lsp/server" import { Installation } from "@/installation" -import { ConfigMarkdown } from "./markdown" +import { ConfigMarkdown } from "." import { existsSync } from "fs" import { Bus } from "@/bus" import { GlobalBus } from "@/bus/global" @@ -29,7 +29,7 @@ import { Event } from "../server/event" import { Glob } from "@opencode-ai/shared/util/glob" import { Account } from "@/account" import { isRecord } from "@/util/record" -import { ConfigPaths } from "./paths" +import { ConfigPaths } from "." import type { ConsoleState } from "./console-state" import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { InstanceState } from "@/effect" diff --git a/packages/opencode/src/config/index.ts b/packages/opencode/src/config/index.ts index 60e39c3163..d878fc99a2 100644 --- a/packages/opencode/src/config/index.ts +++ b/packages/opencode/src/config/index.ts @@ -1 +1,4 @@ export * as Config from "./config" +export * as ConfigMarkdown from "./markdown" +export * as ConfigPaths from "./paths" +export * as TuiConfig from "./tui" diff --git a/packages/opencode/src/config/markdown.ts b/packages/opencode/src/config/markdown.ts index 8b5392be5e..7cad692665 100644 --- a/packages/opencode/src/config/markdown.ts +++ b/packages/opencode/src/config/markdown.ts @@ -3,97 +3,95 @@ import matter from "gray-matter" import { z } from "zod" import { Filesystem } from "../util" -export namespace ConfigMarkdown { - export const FILE_REGEX = /(?" || value === "|" || value.startsWith('"') || value.startsWith("'")) { - result.push(line) - continue - } - - // if value contains a colon, convert to block scalar - if (value.includes(":")) { - result.push(`${key}: |-`) - result.push(` ${value}`) - continue - } - - result.push(line) - } - - const processed = result.join("\n") - return content.replace(frontmatter, () => processed) - } - - export async function parse(filePath: string) { - const template = await Filesystem.readText(filePath) - - try { - const md = matter(template) - return md - } catch { - try { - return matter(fallbackSanitization(template)) - } catch (err) { - throw new FrontmatterError( - { - path: filePath, - message: `${filePath}: Failed to parse YAML frontmatter: ${err instanceof Error ? err.message : String(err)}`, - }, - { cause: err }, - ) - } - } - } - - export const FrontmatterError = NamedError.create( - "ConfigFrontmatterError", - z.object({ - path: z.string(), - message: z.string(), - }), - ) +export function files(template: string) { + return Array.from(template.matchAll(FILE_REGEX)) } + +export function shell(template: string) { + return Array.from(template.matchAll(SHELL_REGEX)) +} + +// other coding agents like claude code allow invalid yaml in their +// frontmatter, we need to fallback to a more permissive parser for those cases +export function fallbackSanitization(content: string): string { + const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/) + if (!match) return content + + const frontmatter = match[1] + const lines = frontmatter.split(/\r?\n/) + const result: string[] = [] + + for (const line of lines) { + // skip comments and empty lines + if (line.trim().startsWith("#") || line.trim() === "") { + result.push(line) + continue + } + + // skip lines that are continuations (indented) + if (line.match(/^\s+/)) { + result.push(line) + continue + } + + // match key: value pattern + const kvMatch = line.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(.*)$/) + if (!kvMatch) { + result.push(line) + continue + } + + const key = kvMatch[1] + const value = kvMatch[2].trim() + + // skip if value is empty, already quoted, or uses block scalar + if (value === "" || value === ">" || value === "|" || value.startsWith('"') || value.startsWith("'")) { + result.push(line) + continue + } + + // if value contains a colon, convert to block scalar + if (value.includes(":")) { + result.push(`${key}: |-`) + result.push(` ${value}`) + continue + } + + result.push(line) + } + + const processed = result.join("\n") + return content.replace(frontmatter, () => processed) +} + +export async function parse(filePath: string) { + const template = await Filesystem.readText(filePath) + + try { + const md = matter(template) + return md + } catch { + try { + return matter(fallbackSanitization(template)) + } catch (err) { + throw new FrontmatterError( + { + path: filePath, + message: `${filePath}: Failed to parse YAML frontmatter: ${err instanceof Error ? err.message : String(err)}`, + }, + { cause: err }, + ) + } + } +} + +export const FrontmatterError = NamedError.create( + "ConfigFrontmatterError", + z.object({ + path: z.string(), + message: z.string(), + }), +) diff --git a/packages/opencode/src/config/paths.ts b/packages/opencode/src/config/paths.ts index c5eb105c9d..82dde2df9f 100644 --- a/packages/opencode/src/config/paths.ts +++ b/packages/opencode/src/config/paths.ts @@ -7,161 +7,159 @@ import { Filesystem } from "@/util" import { Flag } from "@/flag/flag" import { Global } from "@/global" -export namespace ConfigPaths { - export async function projectFiles(name: string, directory: string, worktree: string) { - return Filesystem.findUp([`${name}.json`, `${name}.jsonc`], directory, worktree, { rootFirst: true }) - } - - export async function directories(directory: string, worktree: string) { - return [ - Global.Path.config, - ...(!Flag.OPENCODE_DISABLE_PROJECT_CONFIG - ? await Array.fromAsync( - Filesystem.up({ - targets: [".opencode"], - start: directory, - stop: worktree, - }), - ) - : []), - ...(await Array.fromAsync( - Filesystem.up({ - targets: [".opencode"], - start: Global.Path.home, - stop: Global.Path.home, - }), - )), - ...(Flag.OPENCODE_CONFIG_DIR ? [Flag.OPENCODE_CONFIG_DIR] : []), - ] - } - - export function fileInDirectory(dir: string, name: string) { - return [path.join(dir, `${name}.json`), path.join(dir, `${name}.jsonc`)] - } - - export const JsonError = NamedError.create( - "ConfigJsonError", - z.object({ - path: z.string(), - message: z.string().optional(), - }), - ) - - export const InvalidError = NamedError.create( - "ConfigInvalidError", - z.object({ - path: z.string(), - issues: z.custom().optional(), - message: z.string().optional(), - }), - ) - - /** Read a config file, returning undefined for missing files and throwing JsonError for other failures. */ - export async function readFile(filepath: string) { - return Filesystem.readText(filepath).catch((err: NodeJS.ErrnoException) => { - if (err.code === "ENOENT") return - throw new JsonError({ path: filepath }, { cause: err }) - }) - } - - type ParseSource = string | { source: string; dir: string } - - function source(input: ParseSource) { - return typeof input === "string" ? input : input.source - } - - function dir(input: ParseSource) { - return typeof input === "string" ? path.dirname(input) : input.dir - } - - /** Apply {env:VAR} and {file:path} substitutions to config text. */ - async function substitute(text: string, input: ParseSource, missing: "error" | "empty" = "error") { - text = text.replace(/\{env:([^}]+)\}/g, (_, varName) => { - return process.env[varName] || "" - }) - - const fileMatches = Array.from(text.matchAll(/\{file:[^}]+\}/g)) - if (!fileMatches.length) return text - - const configDir = dir(input) - const configSource = source(input) - let out = "" - let cursor = 0 - - for (const match of fileMatches) { - const token = match[0] - const index = match.index! - out += text.slice(cursor, index) - - const lineStart = text.lastIndexOf("\n", index - 1) + 1 - const prefix = text.slice(lineStart, index).trimStart() - if (prefix.startsWith("//")) { - out += token - cursor = index + token.length - continue - } - - let filePath = token.replace(/^\{file:/, "").replace(/\}$/, "") - if (filePath.startsWith("~/")) { - filePath = path.join(os.homedir(), filePath.slice(2)) - } - - const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(configDir, filePath) - const fileContent = ( - await Filesystem.readText(resolvedPath).catch((error: NodeJS.ErrnoException) => { - if (missing === "empty") return "" - - const errMsg = `bad file reference: "${token}"` - if (error.code === "ENOENT") { - throw new InvalidError( - { - path: configSource, - message: errMsg + ` ${resolvedPath} does not exist`, - }, - { cause: error }, - ) - } - throw new InvalidError({ path: configSource, message: errMsg }, { cause: error }) - }) - ).trim() - - out += JSON.stringify(fileContent).slice(1, -1) - cursor = index + token.length - } - - out += text.slice(cursor) - return out - } - - /** Substitute and parse JSONC text, throwing JsonError on syntax errors. */ - export async function parseText(text: string, input: ParseSource, missing: "error" | "empty" = "error") { - const configSource = source(input) - text = await substitute(text, input, missing) - - const errors: JsoncParseError[] = [] - const data = parseJsonc(text, errors, { allowTrailingComma: true }) - if (errors.length) { - const lines = text.split("\n") - const errorDetails = errors - .map((e) => { - const beforeOffset = text.substring(0, e.offset).split("\n") - const line = beforeOffset.length - const column = beforeOffset[beforeOffset.length - 1].length + 1 - const problemLine = lines[line - 1] - - const error = `${printParseErrorCode(e.error)} at line ${line}, column ${column}` - if (!problemLine) return error - - return `${error}\n Line ${line}: ${problemLine}\n${"".padStart(column + 9)}^` - }) - .join("\n") - - throw new JsonError({ - path: configSource, - message: `\n--- JSONC Input ---\n${text}\n--- Errors ---\n${errorDetails}\n--- End ---`, - }) - } - - return data - } +export async function projectFiles(name: string, directory: string, worktree: string) { + return Filesystem.findUp([`${name}.json`, `${name}.jsonc`], directory, worktree, { rootFirst: true }) +} + +export async function directories(directory: string, worktree: string) { + return [ + Global.Path.config, + ...(!Flag.OPENCODE_DISABLE_PROJECT_CONFIG + ? await Array.fromAsync( + Filesystem.up({ + targets: [".opencode"], + start: directory, + stop: worktree, + }), + ) + : []), + ...(await Array.fromAsync( + Filesystem.up({ + targets: [".opencode"], + start: Global.Path.home, + stop: Global.Path.home, + }), + )), + ...(Flag.OPENCODE_CONFIG_DIR ? [Flag.OPENCODE_CONFIG_DIR] : []), + ] +} + +export function fileInDirectory(dir: string, name: string) { + return [path.join(dir, `${name}.json`), path.join(dir, `${name}.jsonc`)] +} + +export const JsonError = NamedError.create( + "ConfigJsonError", + z.object({ + path: z.string(), + message: z.string().optional(), + }), +) + +export const InvalidError = NamedError.create( + "ConfigInvalidError", + z.object({ + path: z.string(), + issues: z.custom().optional(), + message: z.string().optional(), + }), +) + +/** Read a config file, returning undefined for missing files and throwing JsonError for other failures. */ +export async function readFile(filepath: string) { + return Filesystem.readText(filepath).catch((err: NodeJS.ErrnoException) => { + if (err.code === "ENOENT") return + throw new JsonError({ path: filepath }, { cause: err }) + }) +} + +type ParseSource = string | { source: string; dir: string } + +function source(input: ParseSource) { + return typeof input === "string" ? input : input.source +} + +function dir(input: ParseSource) { + return typeof input === "string" ? path.dirname(input) : input.dir +} + +/** Apply {env:VAR} and {file:path} substitutions to config text. */ +async function substitute(text: string, input: ParseSource, missing: "error" | "empty" = "error") { + text = text.replace(/\{env:([^}]+)\}/g, (_, varName) => { + return process.env[varName] || "" + }) + + const fileMatches = Array.from(text.matchAll(/\{file:[^}]+\}/g)) + if (!fileMatches.length) return text + + const configDir = dir(input) + const configSource = source(input) + let out = "" + let cursor = 0 + + for (const match of fileMatches) { + const token = match[0] + const index = match.index! + out += text.slice(cursor, index) + + const lineStart = text.lastIndexOf("\n", index - 1) + 1 + const prefix = text.slice(lineStart, index).trimStart() + if (prefix.startsWith("//")) { + out += token + cursor = index + token.length + continue + } + + let filePath = token.replace(/^\{file:/, "").replace(/\}$/, "") + if (filePath.startsWith("~/")) { + filePath = path.join(os.homedir(), filePath.slice(2)) + } + + const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(configDir, filePath) + const fileContent = ( + await Filesystem.readText(resolvedPath).catch((error: NodeJS.ErrnoException) => { + if (missing === "empty") return "" + + const errMsg = `bad file reference: "${token}"` + if (error.code === "ENOENT") { + throw new InvalidError( + { + path: configSource, + message: errMsg + ` ${resolvedPath} does not exist`, + }, + { cause: error }, + ) + } + throw new InvalidError({ path: configSource, message: errMsg }, { cause: error }) + }) + ).trim() + + out += JSON.stringify(fileContent).slice(1, -1) + cursor = index + token.length + } + + out += text.slice(cursor) + return out +} + +/** Substitute and parse JSONC text, throwing JsonError on syntax errors. */ +export async function parseText(text: string, input: ParseSource, missing: "error" | "empty" = "error") { + const configSource = source(input) + text = await substitute(text, input, missing) + + const errors: JsoncParseError[] = [] + const data = parseJsonc(text, errors, { allowTrailingComma: true }) + if (errors.length) { + const lines = text.split("\n") + const errorDetails = errors + .map((e) => { + const beforeOffset = text.substring(0, e.offset).split("\n") + const line = beforeOffset.length + const column = beforeOffset[beforeOffset.length - 1].length + 1 + const problemLine = lines[line - 1] + + const error = `${printParseErrorCode(e.error)} at line ${line}, column ${column}` + if (!problemLine) return error + + return `${error}\n Line ${line}: ${problemLine}\n${"".padStart(column + 9)}^` + }) + .join("\n") + + throw new JsonError({ + path: configSource, + message: `\n--- JSONC Input ---\n${text}\n--- Errors ---\n${errorDetails}\n--- End ---`, + }) + } + + return data } diff --git a/packages/opencode/src/config/tui-migrate.ts b/packages/opencode/src/config/tui-migrate.ts index f9d37e479e..18cee554d5 100644 --- a/packages/opencode/src/config/tui-migrate.ts +++ b/packages/opencode/src/config/tui-migrate.ts @@ -2,7 +2,7 @@ import path from "path" import { type ParseError as JsoncParseError, applyEdits, modify, parse as parseJsonc } from "jsonc-parser" import { unique } from "remeda" import z from "zod" -import { ConfigPaths } from "./paths" +import { ConfigPaths } from "." import { TuiInfo, TuiOptions } from "./tui-schema" import { Instance } from "@/project/instance" import { Flag } from "@/flag/flag" diff --git a/packages/opencode/src/config/tui.ts b/packages/opencode/src/config/tui.ts index c1e2b6e6b4..43f1bce460 100644 --- a/packages/opencode/src/config/tui.ts +++ b/packages/opencode/src/config/tui.ts @@ -3,7 +3,7 @@ import z from "zod" import { mergeDeep, unique } from "remeda" import { Context, Effect, Fiber, Layer } from "effect" import { Config } from "." -import { ConfigPaths } from "./paths" +import { ConfigPaths } from "." import { migrateTuiConfig } from "./tui-migrate" import { TuiInfo } from "./tui-schema" import { Flag } from "@/flag/flag" @@ -14,201 +14,199 @@ import { InstanceState } from "@/effect" import { makeRuntime } from "@/effect/run-service" import { AppFileSystem } from "@opencode-ai/shared/filesystem" -export namespace TuiConfig { - const log = Log.create({ service: "tui.config" }) +const log = Log.create({ service: "tui.config" }) - export const Info = TuiInfo +export const Info = TuiInfo - type Acc = { - result: Info - } +type Acc = { + result: Info +} - type State = { - config: Info - deps: Array> - } +type State = { + config: Info + deps: Array> +} - export type Info = z.output & { - // Internal resolved plugin list used by runtime loading. - plugin_origins?: Config.PluginOrigin[] - } +export type Info = z.output & { + // Internal resolved plugin list used by runtime loading. + plugin_origins?: Config.PluginOrigin[] +} - export interface Interface { - readonly get: () => Effect.Effect - readonly waitForDependencies: () => Effect.Effect - } +export interface Interface { + readonly get: () => Effect.Effect + readonly waitForDependencies: () => Effect.Effect +} - export class Service extends Context.Service()("@opencode/TuiConfig") {} +export class Service extends Context.Service()("@opencode/TuiConfig") {} - function pluginScope(file: string, ctx: { directory: string; worktree: string }): Config.PluginScope { - if (AppFileSystem.contains(ctx.directory, file)) return "local" - if (ctx.worktree !== "/" && AppFileSystem.contains(ctx.worktree, file)) return "local" - return "global" - } +function pluginScope(file: string, ctx: { directory: string; worktree: string }): Config.PluginScope { + if (AppFileSystem.contains(ctx.directory, file)) return "local" + if (ctx.worktree !== "/" && AppFileSystem.contains(ctx.worktree, file)) return "local" + return "global" +} - function customPath() { - return Flag.OPENCODE_TUI_CONFIG - } +function customPath() { + return Flag.OPENCODE_TUI_CONFIG +} - function normalize(raw: Record) { - const data = { ...raw } - if (!("tui" in data)) return data - if (!isRecord(data.tui)) { - delete data.tui - return data - } - - const tui = data.tui +function normalize(raw: Record) { + const data = { ...raw } + if (!("tui" in data)) return data + if (!isRecord(data.tui)) { delete data.tui - return { - ...tui, - ...data, - } - } - - async function mergeFile(acc: Acc, file: string, ctx: { directory: string; worktree: string }) { - const data = await loadFile(file) - acc.result = mergeDeep(acc.result, data) - if (!data.plugin?.length) return - - const scope = pluginScope(file, ctx) - const plugins = Config.deduplicatePluginOrigins([ - ...(acc.result.plugin_origins ?? []), - ...data.plugin.map((spec) => ({ spec, scope, source: file })), - ]) - acc.result.plugin = plugins.map((item) => item.spec) - acc.result.plugin_origins = plugins - } - - async function loadState(ctx: { directory: string; worktree: string }) { - let projectFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG - ? [] - : await ConfigPaths.projectFiles("tui", ctx.directory, ctx.worktree) - const directories = await ConfigPaths.directories(ctx.directory, ctx.worktree) - const custom = customPath() - const managed = Config.managedConfigDir() - await migrateTuiConfig({ directories, custom, managed }) - // Re-compute after migration since migrateTuiConfig may have created new tui.json files - projectFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG - ? [] - : await ConfigPaths.projectFiles("tui", ctx.directory, ctx.worktree) - - const acc: Acc = { - result: {}, - } - - for (const file of ConfigPaths.fileInDirectory(Global.Path.config, "tui")) { - await mergeFile(acc, file, ctx) - } - - if (custom) { - await mergeFile(acc, custom, ctx) - log.debug("loaded custom tui config", { path: custom }) - } - - for (const file of projectFiles) { - await mergeFile(acc, file, ctx) - } - - const dirs = unique(directories).filter((dir) => dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) - - for (const dir of dirs) { - if (!dir.endsWith(".opencode") && dir !== Flag.OPENCODE_CONFIG_DIR) continue - for (const file of ConfigPaths.fileInDirectory(dir, "tui")) { - await mergeFile(acc, file, ctx) - } - } - - if (existsSync(managed)) { - for (const file of ConfigPaths.fileInDirectory(managed, "tui")) { - await mergeFile(acc, file, ctx) - } - } - - const keybinds = { ...acc.result.keybinds } - if (process.platform === "win32") { - // Native Windows terminals do not support POSIX suspend, so prefer prompt undo. - keybinds.terminal_suspend = "none" - keybinds.input_undo ??= unique(["ctrl+z", ...Config.Keybinds.shape.input_undo.parse(undefined).split(",")]).join( - ",", - ) - } - acc.result.keybinds = Config.Keybinds.parse(keybinds) - - return { - config: acc.result, - dirs: acc.result.plugin?.length ? dirs : [], - } - } - - export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const cfg = yield* Config.Service - const state = yield* InstanceState.make( - Effect.fn("TuiConfig.state")(function* (ctx) { - const data = yield* Effect.promise(() => loadState(ctx)) - const deps = yield* Effect.forEach(data.dirs, (dir) => cfg.installDependencies(dir).pipe(Effect.forkScoped), { - concurrency: "unbounded", - }) - return { config: data.config, deps } - }), - ) - - const get = Effect.fn("TuiConfig.get")(() => InstanceState.use(state, (s) => s.config)) - - const waitForDependencies = Effect.fn("TuiConfig.waitForDependencies")(() => - InstanceState.useEffect(state, (s) => - Effect.forEach(s.deps, Fiber.join, { concurrency: "unbounded" }).pipe(Effect.asVoid), - ), - ) - - return Service.of({ get, waitForDependencies }) - }), - ) - - export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer)) - - const { runPromise } = makeRuntime(Service, defaultLayer) - - export async function get() { - return runPromise((svc) => svc.get()) - } - - export async function waitForDependencies() { - await runPromise((svc) => svc.waitForDependencies()) - } - - async function loadFile(filepath: string): Promise { - const text = await ConfigPaths.readFile(filepath) - if (!text) return {} - return load(text, filepath).catch((error) => { - log.warn("failed to load tui config", { path: filepath, error }) - return {} - }) - } - - async function load(text: string, configFilepath: string): Promise { - const raw = await ConfigPaths.parseText(text, configFilepath, "empty") - if (!isRecord(raw)) return {} - - // Flatten a nested "tui" key so users who wrote `{ "tui": { ... } }` inside tui.json - // (mirroring the old opencode.json shape) still get their settings applied. - const normalized = normalize(raw) - - const parsed = Info.safeParse(normalized) - if (!parsed.success) { - log.warn("invalid tui config", { path: configFilepath, issues: parsed.error.issues }) - return {} - } - - const data = parsed.data - if (data.plugin) { - for (let i = 0; i < data.plugin.length; i++) { - data.plugin[i] = await Config.resolvePluginSpec(data.plugin[i], configFilepath) - } - } - return data } + + const tui = data.tui + delete data.tui + return { + ...tui, + ...data, + } +} + +async function mergeFile(acc: Acc, file: string, ctx: { directory: string; worktree: string }) { + const data = await loadFile(file) + acc.result = mergeDeep(acc.result, data) + if (!data.plugin?.length) return + + const scope = pluginScope(file, ctx) + const plugins = Config.deduplicatePluginOrigins([ + ...(acc.result.plugin_origins ?? []), + ...data.plugin.map((spec) => ({ spec, scope, source: file })), + ]) + acc.result.plugin = plugins.map((item) => item.spec) + acc.result.plugin_origins = plugins +} + +async function loadState(ctx: { directory: string; worktree: string }) { + let projectFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG + ? [] + : await ConfigPaths.projectFiles("tui", ctx.directory, ctx.worktree) + const directories = await ConfigPaths.directories(ctx.directory, ctx.worktree) + const custom = customPath() + const managed = Config.managedConfigDir() + await migrateTuiConfig({ directories, custom, managed }) + // Re-compute after migration since migrateTuiConfig may have created new tui.json files + projectFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG + ? [] + : await ConfigPaths.projectFiles("tui", ctx.directory, ctx.worktree) + + const acc: Acc = { + result: {}, + } + + for (const file of ConfigPaths.fileInDirectory(Global.Path.config, "tui")) { + await mergeFile(acc, file, ctx) + } + + if (custom) { + await mergeFile(acc, custom, ctx) + log.debug("loaded custom tui config", { path: custom }) + } + + for (const file of projectFiles) { + await mergeFile(acc, file, ctx) + } + + const dirs = unique(directories).filter((dir) => dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) + + for (const dir of dirs) { + if (!dir.endsWith(".opencode") && dir !== Flag.OPENCODE_CONFIG_DIR) continue + for (const file of ConfigPaths.fileInDirectory(dir, "tui")) { + await mergeFile(acc, file, ctx) + } + } + + if (existsSync(managed)) { + for (const file of ConfigPaths.fileInDirectory(managed, "tui")) { + await mergeFile(acc, file, ctx) + } + } + + const keybinds = { ...acc.result.keybinds } + if (process.platform === "win32") { + // Native Windows terminals do not support POSIX suspend, so prefer prompt undo. + keybinds.terminal_suspend = "none" + keybinds.input_undo ??= unique(["ctrl+z", ...Config.Keybinds.shape.input_undo.parse(undefined).split(",")]).join( + ",", + ) + } + acc.result.keybinds = Config.Keybinds.parse(keybinds) + + return { + config: acc.result, + dirs: acc.result.plugin?.length ? dirs : [], + } +} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const cfg = yield* Config.Service + const state = yield* InstanceState.make( + Effect.fn("TuiConfig.state")(function* (ctx) { + const data = yield* Effect.promise(() => loadState(ctx)) + const deps = yield* Effect.forEach(data.dirs, (dir) => cfg.installDependencies(dir).pipe(Effect.forkScoped), { + concurrency: "unbounded", + }) + return { config: data.config, deps } + }), + ) + + const get = Effect.fn("TuiConfig.get")(() => InstanceState.use(state, (s) => s.config)) + + const waitForDependencies = Effect.fn("TuiConfig.waitForDependencies")(() => + InstanceState.useEffect(state, (s) => + Effect.forEach(s.deps, Fiber.join, { concurrency: "unbounded" }).pipe(Effect.asVoid), + ), + ) + + return Service.of({ get, waitForDependencies }) + }), +) + +export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer)) + +const { runPromise } = makeRuntime(Service, defaultLayer) + +export async function get() { + return runPromise((svc) => svc.get()) +} + +export async function waitForDependencies() { + await runPromise((svc) => svc.waitForDependencies()) +} + +async function loadFile(filepath: string): Promise { + const text = await ConfigPaths.readFile(filepath) + if (!text) return {} + return load(text, filepath).catch((error) => { + log.warn("failed to load tui config", { path: filepath, error }) + return {} + }) +} + +async function load(text: string, configFilepath: string): Promise { + const raw = await ConfigPaths.parseText(text, configFilepath, "empty") + if (!isRecord(raw)) return {} + + // Flatten a nested "tui" key so users who wrote `{ "tui": { ... } }` inside tui.json + // (mirroring the old opencode.json shape) still get their settings applied. + const normalized = normalize(raw) + + const parsed = Info.safeParse(normalized) + if (!parsed.success) { + log.warn("invalid tui config", { path: configFilepath, issues: parsed.error.issues }) + return {} + } + + const data = parsed.data + if (data.plugin) { + for (let i = 0; i < data.plugin.length; i++) { + data.plugin[i] = await Config.resolvePluginSpec(data.plugin[i], configFilepath) + } + } + + return data } diff --git a/packages/opencode/src/plugin/install.ts b/packages/opencode/src/plugin/install.ts index 0a6256d6f2..8b7e30c40e 100644 --- a/packages/opencode/src/plugin/install.ts +++ b/packages/opencode/src/plugin/install.ts @@ -7,7 +7,7 @@ import { printParseErrorCode, } from "jsonc-parser" -import { ConfigPaths } from "@/config/paths" +import { ConfigPaths } from "@/config" import { Global } from "@/global" import { Filesystem } from "@/util" import { Flock } from "@opencode-ai/shared/util/flock" diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 157533af0a..1d4bb66bc5 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -30,7 +30,7 @@ import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" import * as Stream from "effect/Stream" import { Command } from "../command" import { pathToFileURL, fileURLToPath } from "url" -import { ConfigMarkdown } from "../config/markdown" +import { ConfigMarkdown } from "../config" import { SessionSummary } from "./summary" import { NamedError } from "@opencode-ai/shared/util/error" import { SessionProcessor } from "./processor" diff --git a/packages/opencode/src/skill/skill.ts b/packages/opencode/src/skill/skill.ts index ef9f661cb5..f8ff7b8f5f 100644 --- a/packages/opencode/src/skill/skill.ts +++ b/packages/opencode/src/skill/skill.ts @@ -12,7 +12,7 @@ import { Global } from "@/global" import { Permission } from "@/permission" import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { Config } from "../config" -import { ConfigMarkdown } from "../config/markdown" +import { ConfigMarkdown } from "../config" import { Glob } from "@opencode-ai/shared/util/glob" import { Log } from "../util" import { Discovery } from "./discovery" diff --git a/packages/opencode/test/cli/tui/plugin-add.test.ts b/packages/opencode/test/cli/tui/plugin-add.test.ts index 748f291728..11865beddd 100644 --- a/packages/opencode/test/cli/tui/plugin-add.test.ts +++ b/packages/opencode/test/cli/tui/plugin-add.test.ts @@ -4,7 +4,7 @@ import path from "path" import { pathToFileURL } from "url" import { tmpdir } from "../../fixture/fixture" import { createTuiPluginApi } from "../../fixture/tui-plugin" -import { TuiConfig } from "../../../src/config/tui" +import { TuiConfig } from "../../../src/config" const { TuiPluginRuntime } = await import("../../../src/cli/cmd/tui/plugin/runtime") diff --git a/packages/opencode/test/cli/tui/plugin-install.test.ts b/packages/opencode/test/cli/tui/plugin-install.test.ts index 290a7eea13..bd490ac4f9 100644 --- a/packages/opencode/test/cli/tui/plugin-install.test.ts +++ b/packages/opencode/test/cli/tui/plugin-install.test.ts @@ -4,7 +4,7 @@ import path from "path" import { pathToFileURL } from "url" import { tmpdir } from "../../fixture/fixture" import { createTuiPluginApi } from "../../fixture/tui-plugin" -import { TuiConfig } from "../../../src/config/tui" +import { TuiConfig } from "../../../src/config" const { TuiPluginRuntime } = await import("../../../src/cli/cmd/tui/plugin/runtime") diff --git a/packages/opencode/test/cli/tui/plugin-loader-entrypoint.test.ts b/packages/opencode/test/cli/tui/plugin-loader-entrypoint.test.ts index 68c3df4475..7020ac7426 100644 --- a/packages/opencode/test/cli/tui/plugin-loader-entrypoint.test.ts +++ b/packages/opencode/test/cli/tui/plugin-loader-entrypoint.test.ts @@ -4,7 +4,7 @@ import path from "path" import { pathToFileURL } from "url" import { tmpdir } from "../../fixture/fixture" import { createTuiPluginApi } from "../../fixture/tui-plugin" -import { TuiConfig } from "../../../src/config/tui" +import { TuiConfig } from "../../../src/config" import { Npm } from "../../../src/npm" const { TuiPluginRuntime } = await import("../../../src/cli/cmd/tui/plugin/runtime") diff --git a/packages/opencode/test/cli/tui/plugin-loader-pure.test.ts b/packages/opencode/test/cli/tui/plugin-loader-pure.test.ts index f92d742924..25233adaa5 100644 --- a/packages/opencode/test/cli/tui/plugin-loader-pure.test.ts +++ b/packages/opencode/test/cli/tui/plugin-loader-pure.test.ts @@ -4,7 +4,7 @@ import path from "path" import { pathToFileURL } from "url" import { tmpdir } from "../../fixture/fixture" import { createTuiPluginApi } from "../../fixture/tui-plugin" -import { TuiConfig } from "../../../src/config/tui" +import { TuiConfig } from "../../../src/config" const { TuiPluginRuntime } = await import("../../../src/cli/cmd/tui/plugin/runtime") diff --git a/packages/opencode/test/cli/tui/plugin-loader.test.ts b/packages/opencode/test/cli/tui/plugin-loader.test.ts index 8446570cc3..4dc2aeccd4 100644 --- a/packages/opencode/test/cli/tui/plugin-loader.test.ts +++ b/packages/opencode/test/cli/tui/plugin-loader.test.ts @@ -5,7 +5,7 @@ import { pathToFileURL } from "url" import { tmpdir } from "../../fixture/fixture" import { createTuiPluginApi } from "../../fixture/tui-plugin" import { Global } from "../../../src/global" -import { TuiConfig } from "../../../src/config/tui" +import { TuiConfig } from "../../../src/config" import { Filesystem } from "../../../src/util" const { allThemes, addTheme } = await import("../../../src/cli/cmd/tui/context/theme") diff --git a/packages/opencode/test/cli/tui/plugin-toggle.test.ts b/packages/opencode/test/cli/tui/plugin-toggle.test.ts index 10ddfe8e1c..3f04e3c6fa 100644 --- a/packages/opencode/test/cli/tui/plugin-toggle.test.ts +++ b/packages/opencode/test/cli/tui/plugin-toggle.test.ts @@ -4,7 +4,7 @@ import path from "path" import { pathToFileURL } from "url" import { tmpdir } from "../../fixture/fixture" import { createTuiPluginApi } from "../../fixture/tui-plugin" -import { TuiConfig } from "../../../src/config/tui" +import { TuiConfig } from "../../../src/config" const { TuiPluginRuntime } = await import("../../../src/cli/cmd/tui/plugin/runtime") diff --git a/packages/opencode/test/cli/tui/thread.test.ts b/packages/opencode/test/cli/tui/thread.test.ts index 1c5c7e65e4..7b781c49e8 100644 --- a/packages/opencode/test/cli/tui/thread.test.ts +++ b/packages/opencode/test/cli/tui/thread.test.ts @@ -8,7 +8,7 @@ import { UI } from "../../../src/cli/ui" import * as Timeout from "../../../src/util/timeout" import * as Network from "../../../src/cli/network" import * as Win32 from "../../../src/cli/cmd/tui/win32" -import { TuiConfig } from "../../../src/config/tui" +import { TuiConfig } from "../../../src/config" import { Instance } from "../../../src/project/instance" const stop = new Error("stop") diff --git a/packages/opencode/test/config/markdown.test.ts b/packages/opencode/test/config/markdown.test.ts index 865af21077..b807850c39 100644 --- a/packages/opencode/test/config/markdown.test.ts +++ b/packages/opencode/test/config/markdown.test.ts @@ -1,5 +1,5 @@ import { expect, test, describe } from "bun:test" -import { ConfigMarkdown } from "../../src/config/markdown" +import { ConfigMarkdown } from "../../src/config" describe("ConfigMarkdown: normal template", () => { const template = `This is a @valid/path/to/a/file and it should also match at diff --git a/packages/opencode/test/config/tui.test.ts b/packages/opencode/test/config/tui.test.ts index c80905cd1d..62587d2704 100644 --- a/packages/opencode/test/config/tui.test.ts +++ b/packages/opencode/test/config/tui.test.ts @@ -4,7 +4,7 @@ import fs from "fs/promises" import { tmpdir } from "../fixture/fixture" import { Instance } from "../../src/project/instance" import { Config } from "../../src/config" -import { TuiConfig } from "../../src/config/tui" +import { TuiConfig } from "../../src/config" import { Global } from "../../src/global" import { Filesystem } from "../../src/util" import { AppRuntime } from "../../src/effect/app-runtime" diff --git a/packages/opencode/test/fixture/tui-runtime.ts b/packages/opencode/test/fixture/tui-runtime.ts index fdd3b6cfff..493b23f7e8 100644 --- a/packages/opencode/test/fixture/tui-runtime.ts +++ b/packages/opencode/test/fixture/tui-runtime.ts @@ -1,6 +1,6 @@ import { spyOn } from "bun:test" import path from "path" -import { TuiConfig } from "../../src/config/tui" +import { TuiConfig } from "../../src/config" type PluginSpec = string | [string, Record] From f24207844f84d43536b1ac5655e6f3cb80237f9f Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 23:30:49 -0400 Subject: [PATCH 217/300] feat: unwrap storage namespaces to flat exports + barrel (#22747) --- packages/opencode/src/account/repo.ts | 2 +- packages/opencode/src/cli/cmd/db.ts | 4 +- packages/opencode/src/cli/cmd/import.ts | 2 +- packages/opencode/src/cli/cmd/stats.ts | 2 +- .../opencode/src/control-plane/workspace.ts | 8 +- packages/opencode/src/effect/app-runtime.ts | 2 +- packages/opencode/src/index.ts | 4 +- packages/opencode/src/node.ts | 4 +- .../opencode/src/permission/permission.ts | 2 +- packages/opencode/src/project/project.ts | 2 +- packages/opencode/src/server/error.ts | 2 +- packages/opencode/src/server/fence.ts | 2 +- packages/opencode/src/server/instance/pty.ts | 2 +- packages/opencode/src/server/instance/sync.ts | 2 +- packages/opencode/src/server/middleware.ts | 2 +- packages/opencode/src/server/projectors.ts | 2 +- packages/opencode/src/session/compaction.ts | 2 +- packages/opencode/src/session/message-v2.ts | 2 +- packages/opencode/src/session/projectors.ts | 2 +- packages/opencode/src/session/revert.ts | 2 +- packages/opencode/src/session/session.ts | 6 +- packages/opencode/src/session/summary.ts | 2 +- packages/opencode/src/session/todo.ts | 2 +- packages/opencode/src/share/share-next.ts | 2 +- packages/opencode/src/storage/db.ts | 244 +++--- packages/opencode/src/storage/index.ts | 26 + .../opencode/src/storage/json-migration.ts | 802 +++++++++--------- packages/opencode/src/storage/storage.ts | 566 ++++++------ packages/opencode/src/sync/sync-event.ts | 2 +- packages/opencode/src/worktree/worktree.ts | 2 +- packages/opencode/test/account/repo.test.ts | 2 +- .../opencode/test/account/service.test.ts | 2 +- packages/opencode/test/fixture/db.ts | 2 +- packages/opencode/test/preload.ts | 2 +- .../test/project/migrate-global.test.ts | 4 +- .../opencode/test/share/share-next.test.ts | 2 +- packages/opencode/test/storage/db.test.ts | 2 +- .../test/storage/json-migration.test.ts | 2 +- .../opencode/test/storage/storage.test.ts | 2 +- packages/opencode/test/sync/index.test.ts | 2 +- 40 files changed, 874 insertions(+), 854 deletions(-) create mode 100644 packages/opencode/src/storage/index.ts diff --git a/packages/opencode/src/account/repo.ts b/packages/opencode/src/account/repo.ts index b2b084c08d..5d8a8e33f6 100644 --- a/packages/opencode/src/account/repo.ts +++ b/packages/opencode/src/account/repo.ts @@ -1,7 +1,7 @@ import { eq } from "drizzle-orm" import { Effect, Layer, Option, Schema, Context } from "effect" -import { Database } from "@/storage/db" +import { Database } from "@/storage" import { AccountStateTable, AccountTable } from "./account.sql" import { AccessToken, AccountID, AccountRepoError, Info, OrgID, RefreshToken } from "./schema" import { normalizeServerUrl } from "./url" diff --git a/packages/opencode/src/cli/cmd/db.ts b/packages/opencode/src/cli/cmd/db.ts index d2a2ca5706..235b59793f 100644 --- a/packages/opencode/src/cli/cmd/db.ts +++ b/packages/opencode/src/cli/cmd/db.ts @@ -1,11 +1,11 @@ import type { Argv } from "yargs" import { spawn } from "child_process" -import { Database } from "../../storage/db" +import { Database } from "../../storage" import { drizzle } from "drizzle-orm/bun-sqlite" import { Database as BunDatabase } from "bun:sqlite" import { UI } from "../ui" import { cmd } from "./cmd" -import { JsonMigration } from "../../storage/json-migration" +import { JsonMigration } from "../../storage" import { EOL } from "os" import { errorMessage } from "../../util/error" diff --git a/packages/opencode/src/cli/cmd/import.ts b/packages/opencode/src/cli/cmd/import.ts index 38d2376bc5..8da254f159 100644 --- a/packages/opencode/src/cli/cmd/import.ts +++ b/packages/opencode/src/cli/cmd/import.ts @@ -4,7 +4,7 @@ import { Session } from "../../session" import { MessageV2 } from "../../session/message-v2" import { cmd } from "./cmd" import { bootstrap } from "../bootstrap" -import { Database } from "../../storage/db" +import { Database } from "../../storage" import { SessionTable, MessageTable, PartTable } from "../../session/session.sql" import { Instance } from "../../project/instance" import { ShareNext } from "../../share" diff --git a/packages/opencode/src/cli/cmd/stats.ts b/packages/opencode/src/cli/cmd/stats.ts index d66ac252fa..34af56ad7a 100644 --- a/packages/opencode/src/cli/cmd/stats.ts +++ b/packages/opencode/src/cli/cmd/stats.ts @@ -2,7 +2,7 @@ import type { Argv } from "yargs" import { cmd } from "./cmd" import { Session } from "../../session" import { bootstrap } from "../bootstrap" -import { Database } from "../../storage/db" +import { Database } from "../../storage" import { SessionTable } from "../../session/session.sql" import { Project } from "../../project" import { Instance } from "../../project/instance" diff --git a/packages/opencode/src/control-plane/workspace.ts b/packages/opencode/src/control-plane/workspace.ts index f38b27e6f8..b43fe848ba 100644 --- a/packages/opencode/src/control-plane/workspace.ts +++ b/packages/opencode/src/control-plane/workspace.ts @@ -1,7 +1,7 @@ import z from "zod" import { setTimeout as sleep } from "node:timers/promises" import { fn } from "@/util/fn" -import { Database, asc, eq, inArray } from "@/storage/db" +import { Database, asc, eq, inArray } from "@/storage" import { Project } from "@/project" import { BusEvent } from "@/bus/bus-event" import { GlobalBus } from "@/bus/global" @@ -114,7 +114,7 @@ export namespace Workspace { await adaptor.create(config) - void startSync(info) + startSync(info) await waitEvent({ timeout: TIMEOUT, @@ -294,7 +294,7 @@ export namespace Workspace { ) const spaces = rows.map(fromRow).sort((a, b) => a.id.localeCompare(b.id)) - for (const space of spaces) void startSync(space) + for (const space of spaces) startSync(space) return spaces } @@ -307,7 +307,7 @@ export namespace Workspace { export const get = fn(WorkspaceID.zod, async (id) => { const space = lookup(id) if (!space) return - void startSync(space) + startSync(space) return space }) diff --git a/packages/opencode/src/effect/app-runtime.ts b/packages/opencode/src/effect/app-runtime.ts index bd27df3435..3e28183448 100644 --- a/packages/opencode/src/effect/app-runtime.ts +++ b/packages/opencode/src/effect/app-runtime.ts @@ -12,7 +12,7 @@ import { Ripgrep } from "@/file/ripgrep" import { FileTime } from "@/file/time" import { File } from "@/file" import { FileWatcher } from "@/file/watcher" -import { Storage } from "@/storage/storage" +import { Storage } from "@/storage" import { Snapshot } from "@/snapshot" import { Plugin } from "@/plugin" import { Provider } from "@/provider" diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index ab3ccb712a..d9f4651fbf 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -31,8 +31,8 @@ import { SessionCommand } from "./cli/cmd/session" import { DbCommand } from "./cli/cmd/db" import path from "path" import { Global } from "./global" -import { JsonMigration } from "./storage/json-migration" -import { Database } from "./storage/db" +import { JsonMigration } from "./storage" +import { Database } from "./storage" import { errorMessage } from "./util/error" import { PluginCommand } from "./cli/cmd/plug" import { Heap } from "./cli/heap" diff --git a/packages/opencode/src/node.ts b/packages/opencode/src/node.ts index a30783fb21..1cb30d8082 100644 --- a/packages/opencode/src/node.ts +++ b/packages/opencode/src/node.ts @@ -2,5 +2,5 @@ export { Config } from "./config" export { Server } from "./server/server" export { bootstrap } from "./cli/bootstrap" export { Log } from "./util" -export { Database } from "./storage/db" -export { JsonMigration } from "./storage/json-migration" +export { Database } from "./storage" +export { JsonMigration } from "./storage" diff --git a/packages/opencode/src/permission/permission.ts b/packages/opencode/src/permission/permission.ts index a8463510c4..fe7fb85455 100644 --- a/packages/opencode/src/permission/permission.ts +++ b/packages/opencode/src/permission/permission.ts @@ -5,7 +5,7 @@ import { InstanceState } from "@/effect" import { ProjectID } from "@/project/schema" import { MessageID, SessionID } from "@/session/schema" import { PermissionTable } from "@/session/session.sql" -import { Database, eq } from "@/storage/db" +import { Database, eq } from "@/storage" import { zod } from "@/util/effect-zod" import { Log } from "@/util" import { withStatics } from "@/util/schema" diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index 99fe88ff16..050951a606 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -1,5 +1,5 @@ import z from "zod" -import { and, Database, eq } from "../storage/db" +import { and, Database, eq } from "../storage" import { ProjectTable } from "./project.sql" import { SessionTable } from "../session/session.sql" import { Log } from "../util" diff --git a/packages/opencode/src/server/error.ts b/packages/opencode/src/server/error.ts index cc5fa96187..73d28e7350 100644 --- a/packages/opencode/src/server/error.ts +++ b/packages/opencode/src/server/error.ts @@ -1,6 +1,6 @@ import { resolver } from "hono-openapi" import z from "zod" -import { NotFoundError } from "../storage/db" +import { NotFoundError } from "../storage" export const ERRORS = { 400: { diff --git a/packages/opencode/src/server/fence.ts b/packages/opencode/src/server/fence.ts index 87771745c8..b461a9dac2 100644 --- a/packages/opencode/src/server/fence.ts +++ b/packages/opencode/src/server/fence.ts @@ -1,5 +1,5 @@ import type { MiddlewareHandler } from "hono" -import { Database, inArray } from "@/storage/db" +import { Database, inArray } from "@/storage" import { EventSequenceTable } from "@/sync/event.sql" import { Workspace } from "@/control-plane/workspace" import type { WorkspaceID } from "@/control-plane/schema" diff --git a/packages/opencode/src/server/instance/pty.ts b/packages/opencode/src/server/instance/pty.ts index 3cb8dbfe2e..7943725120 100644 --- a/packages/opencode/src/server/instance/pty.ts +++ b/packages/opencode/src/server/instance/pty.ts @@ -6,7 +6,7 @@ import z from "zod" import { AppRuntime } from "@/effect/app-runtime" import { Pty } from "@/pty" import { PtyID } from "@/pty/schema" -import { NotFoundError } from "../../storage/db" +import { NotFoundError } from "../../storage" import { errors } from "../error" export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) { diff --git a/packages/opencode/src/server/instance/sync.ts b/packages/opencode/src/server/instance/sync.ts index 2513e519ee..633e77f10e 100644 --- a/packages/opencode/src/server/instance/sync.ts +++ b/packages/opencode/src/server/instance/sync.ts @@ -2,7 +2,7 @@ import z from "zod" import { Hono } from "hono" import { describeRoute, validator, resolver } from "hono-openapi" import { SyncEvent } from "@/sync" -import { Database, asc, and, not, or, lte, eq } from "@/storage/db" +import { Database, asc, and, not, or, lte, eq } from "@/storage" import { EventTable } from "@/sync/event.sql" import { lazy } from "@/util/lazy" import { Log } from "@/util" diff --git a/packages/opencode/src/server/middleware.ts b/packages/opencode/src/server/middleware.ts index e0958196a5..b67d15f550 100644 --- a/packages/opencode/src/server/middleware.ts +++ b/packages/opencode/src/server/middleware.ts @@ -1,6 +1,6 @@ import { Provider } from "../provider" import { NamedError } from "@opencode-ai/shared/util/error" -import { NotFoundError } from "../storage/db" +import { NotFoundError } from "../storage" import { Session } from "../session" import type { ContentfulStatusCode } from "hono/utils/http-status" import type { ErrorHandler, MiddlewareHandler } from "hono" diff --git a/packages/opencode/src/server/projectors.ts b/packages/opencode/src/server/projectors.ts index eb85a8017f..cfecce5265 100644 --- a/packages/opencode/src/server/projectors.ts +++ b/packages/opencode/src/server/projectors.ts @@ -3,7 +3,7 @@ import sessionProjectors from "../session/projectors" import { SyncEvent } from "@/sync" import { Session } from "@/session" import { SessionTable } from "@/session/session.sql" -import { Database, eq } from "@/storage/db" +import { Database, eq } from "@/storage" export function initProjectors() { SyncEvent.init({ diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index 72b9963215..5ad80b6b02 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -11,7 +11,7 @@ import { SessionProcessor } from "./processor" import { Agent } from "@/agent/agent" import { Plugin } from "@/plugin" import { Config } from "@/config" -import { NotFoundError } from "@/storage/db" +import { NotFoundError } from "@/storage" import { ModelID, ProviderID } from "@/provider/schema" import { Effect, Layer, Context } from "effect" import { InstanceState } from "@/effect" diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index f4a7235e15..5dcf0dcd1c 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -6,7 +6,7 @@ import { APICallError, convertToModelMessages, LoadAPIKeyError, type ModelMessag import { LSP } from "../lsp" import { Snapshot } from "@/snapshot" import { SyncEvent } from "../sync" -import { Database, NotFoundError, and, desc, eq, inArray, lt, or } from "@/storage/db" +import { Database, NotFoundError, and, desc, eq, inArray, lt, or } from "@/storage" import { MessageTable, PartTable, SessionTable } from "./session.sql" import { ProviderError } from "@/provider/error" import { iife } from "@/util/iife" diff --git a/packages/opencode/src/session/projectors.ts b/packages/opencode/src/session/projectors.ts index 1e092b07e0..9a36ef5b3b 100644 --- a/packages/opencode/src/session/projectors.ts +++ b/packages/opencode/src/session/projectors.ts @@ -1,4 +1,4 @@ -import { NotFoundError, eq, and } from "../storage/db" +import { NotFoundError, eq, and } from "../storage" import { SyncEvent } from "@/sync" import { Session } from "." import { MessageV2 } from "./message-v2" diff --git a/packages/opencode/src/session/revert.ts b/packages/opencode/src/session/revert.ts index 383fe08e87..93d0e6219c 100644 --- a/packages/opencode/src/session/revert.ts +++ b/packages/opencode/src/session/revert.ts @@ -2,7 +2,7 @@ import z from "zod" import { Effect, Layer, Context } from "effect" import { Bus } from "../bus" import { Snapshot } from "../snapshot" -import { Storage } from "@/storage/storage" +import { Storage } from "@/storage" import { SyncEvent } from "../sync" import { Log } from "../util" import { Session } from "." diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts index a4bf446a1a..9ebddf8dee 100644 --- a/packages/opencode/src/session/session.ts +++ b/packages/opencode/src/session/session.ts @@ -8,12 +8,12 @@ import { type ProviderMetadata, type LanguageModelUsage } from "ai" import { Flag } from "../flag/flag" import { Installation } from "../installation" -import { Database, NotFoundError, eq, and, gte, isNull, desc, like, inArray, lt } from "../storage/db" +import { Database, NotFoundError, eq, and, gte, isNull, desc, like, inArray, lt } from "../storage" import { SyncEvent } from "../sync" -import type { SQL } from "../storage/db" +import type { SQL } from "../storage" import { PartTable, SessionTable } from "./session.sql" import { ProjectTable } from "../project/project.sql" -import { Storage } from "@/storage/storage" +import { Storage } from "@/storage" import { Log } from "../util" import { updateSchema } from "../util/update-schema" import { MessageV2 } from "./message-v2" diff --git a/packages/opencode/src/session/summary.ts b/packages/opencode/src/session/summary.ts index 2c973c5df7..21203c326b 100644 --- a/packages/opencode/src/session/summary.ts +++ b/packages/opencode/src/session/summary.ts @@ -2,7 +2,7 @@ import z from "zod" import { Effect, Layer, Context } from "effect" import { Bus } from "@/bus" import { Snapshot } from "@/snapshot" -import { Storage } from "@/storage/storage" +import { Storage } from "@/storage" import { Session } from "." import { MessageV2 } from "./message-v2" import { SessionID, MessageID } from "./schema" diff --git a/packages/opencode/src/session/todo.ts b/packages/opencode/src/session/todo.ts index 1fd9cbaa5a..eec2bb3a30 100644 --- a/packages/opencode/src/session/todo.ts +++ b/packages/opencode/src/session/todo.ts @@ -3,7 +3,7 @@ import { Bus } from "@/bus" import { SessionID } from "./schema" import { Effect, Layer, Context } from "effect" import z from "zod" -import { Database, eq, asc } from "../storage/db" +import { Database, eq, asc } from "../storage" import { TodoTable } from "./session.sql" export namespace Todo { diff --git a/packages/opencode/src/share/share-next.ts b/packages/opencode/src/share/share-next.ts index a7656e840c..1991e75ff6 100644 --- a/packages/opencode/src/share/share-next.ts +++ b/packages/opencode/src/share/share-next.ts @@ -9,7 +9,7 @@ import { ModelID, ProviderID } from "@/provider/schema" import { Session } from "@/session" import { MessageV2 } from "@/session/message-v2" import type { SessionID } from "@/session/schema" -import { Database, eq } from "@/storage/db" +import { Database, eq } from "@/storage" import { Config } from "@/config" import { Log } from "@/util" import { SessionShareTable } from "./share.sql" diff --git a/packages/opencode/src/storage/db.ts b/packages/opencode/src/storage/db.ts index 7acd458dcd..1b6b2d9b37 100644 --- a/packages/opencode/src/storage/db.ts +++ b/packages/opencode/src/storage/db.ts @@ -27,148 +27,146 @@ export const NotFoundError = NamedError.create( const log = Log.create({ service: "db" }) -export namespace Database { - export function getChannelPath() { - if (["latest", "beta", "prod"].includes(CHANNEL) || Flag.OPENCODE_DISABLE_CHANNEL_DB) - return path.join(Global.Path.data, "opencode.db") - const safe = CHANNEL.replace(/[^a-zA-Z0-9._-]/g, "-") - return path.join(Global.Path.data, `opencode-${safe}.db`) +export function getChannelPath() { + if (["latest", "beta", "prod"].includes(CHANNEL) || Flag.OPENCODE_DISABLE_CHANNEL_DB) + return path.join(Global.Path.data, "opencode.db") + const safe = CHANNEL.replace(/[^a-zA-Z0-9._-]/g, "-") + return path.join(Global.Path.data, `opencode-${safe}.db`) +} + +export const Path = iife(() => { + if (Flag.OPENCODE_DB) { + if (Flag.OPENCODE_DB === ":memory:" || path.isAbsolute(Flag.OPENCODE_DB)) return Flag.OPENCODE_DB + return path.join(Global.Path.data, Flag.OPENCODE_DB) } + return getChannelPath() +}) - export const Path = iife(() => { - if (Flag.OPENCODE_DB) { - if (Flag.OPENCODE_DB === ":memory:" || path.isAbsolute(Flag.OPENCODE_DB)) return Flag.OPENCODE_DB - return path.join(Global.Path.data, Flag.OPENCODE_DB) - } - return getChannelPath() - }) +export type Transaction = SQLiteTransaction<"sync", void> - export type Transaction = SQLiteTransaction<"sync", void> +type Client = SQLiteBunDatabase - type Client = SQLiteBunDatabase +type Journal = { sql: string; timestamp: number; name: string }[] - type Journal = { sql: string; timestamp: number; name: string }[] +function time(tag: string) { + const match = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/.exec(tag) + if (!match) return 0 + return Date.UTC( + Number(match[1]), + Number(match[2]) - 1, + Number(match[3]), + Number(match[4]), + Number(match[5]), + Number(match[6]), + ) +} - function time(tag: string) { - const match = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/.exec(tag) - if (!match) return 0 - return Date.UTC( - Number(match[1]), - Number(match[2]) - 1, - Number(match[3]), - Number(match[4]), - Number(match[5]), - Number(match[6]), - ) - } +function migrations(dir: string): Journal { + const dirs = readdirSync(dir, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) - function migrations(dir: string): Journal { - const dirs = readdirSync(dir, { withFileTypes: true }) - .filter((entry) => entry.isDirectory()) - .map((entry) => entry.name) - - const sql = dirs - .map((name) => { - const file = path.join(dir, name, "migration.sql") - if (!existsSync(file)) return - return { - sql: readFileSync(file, "utf-8"), - timestamp: time(name), - name, - } - }) - .filter(Boolean) as Journal - - return sql.sort((a, b) => a.timestamp - b.timestamp) - } - - export const Client = lazy(() => { - log.info("opening database", { path: Path }) - - const db = init(Path) - - db.run("PRAGMA journal_mode = WAL") - db.run("PRAGMA synchronous = NORMAL") - db.run("PRAGMA busy_timeout = 5000") - db.run("PRAGMA cache_size = -64000") - db.run("PRAGMA foreign_keys = ON") - db.run("PRAGMA wal_checkpoint(PASSIVE)") - - // Apply schema migrations - const entries = - typeof OPENCODE_MIGRATIONS !== "undefined" - ? OPENCODE_MIGRATIONS - : migrations(path.join(import.meta.dirname, "../../migration")) - if (entries.length > 0) { - log.info("applying migrations", { - count: entries.length, - mode: typeof OPENCODE_MIGRATIONS !== "undefined" ? "bundled" : "dev", - }) - if (Flag.OPENCODE_SKIP_MIGRATIONS) { - for (const item of entries) { - item.sql = "select 1;" - } + const sql = dirs + .map((name) => { + const file = path.join(dir, name, "migration.sql") + if (!existsSync(file)) return + return { + sql: readFileSync(file, "utf-8"), + timestamp: time(name), + name, } - migrate(db, entries) - } + }) + .filter(Boolean) as Journal - return db - }) + return sql.sort((a, b) => a.timestamp - b.timestamp) +} - export function close() { - Client().$client.close() - Client.reset() - } +export const Client = lazy(() => { + log.info("opening database", { path: Path }) - export type TxOrDb = Transaction | Client + const db = init(Path) - const ctx = LocalContext.create<{ - tx: TxOrDb - effects: (() => void | Promise)[] - }>("database") + db.run("PRAGMA journal_mode = WAL") + db.run("PRAGMA synchronous = NORMAL") + db.run("PRAGMA busy_timeout = 5000") + db.run("PRAGMA cache_size = -64000") + db.run("PRAGMA foreign_keys = ON") + db.run("PRAGMA wal_checkpoint(PASSIVE)") - export function use(callback: (trx: TxOrDb) => T): T { - try { - return callback(ctx.use().tx) - } catch (err) { - if (err instanceof LocalContext.NotFound) { - const effects: (() => void | Promise)[] = [] - const result = ctx.provide({ effects, tx: Client() }, () => callback(Client())) - for (const effect of effects) void effect() - return result + // Apply schema migrations + const entries = + typeof OPENCODE_MIGRATIONS !== "undefined" + ? OPENCODE_MIGRATIONS + : migrations(path.join(import.meta.dirname, "../../migration")) + if (entries.length > 0) { + log.info("applying migrations", { + count: entries.length, + mode: typeof OPENCODE_MIGRATIONS !== "undefined" ? "bundled" : "dev", + }) + if (Flag.OPENCODE_SKIP_MIGRATIONS) { + for (const item of entries) { + item.sql = "select 1;" } - throw err } + migrate(db, entries) } - export function effect(fn: () => any | Promise) { - const bound = InstanceState.bind(fn) - try { - ctx.use().effects.push(bound) - } catch { - void bound() - } - } + return db +}) - type NotPromise = T extends Promise ? never : T +export function close() { + Client().$client.close() + Client.reset() +} - export function transaction( - callback: (tx: TxOrDb) => NotPromise, - options?: { - behavior?: "deferred" | "immediate" | "exclusive" - }, - ): NotPromise { - try { - return callback(ctx.use().tx) - } catch (err) { - if (err instanceof LocalContext.NotFound) { - const effects: (() => void | Promise)[] = [] - const txCallback = InstanceState.bind((tx: TxOrDb) => ctx.provide({ tx, effects }, () => callback(tx))) - const result = Client().transaction(txCallback, { behavior: options?.behavior }) - for (const effect of effects) void effect() - return result as NotPromise - } - throw err +export type TxOrDb = Transaction | Client + +const ctx = LocalContext.create<{ + tx: TxOrDb + effects: (() => void | Promise)[] +}>("database") + +export function use(callback: (trx: TxOrDb) => T): T { + try { + return callback(ctx.use().tx) + } catch (err) { + if (err instanceof LocalContext.NotFound) { + const effects: (() => void | Promise)[] = [] + const result = ctx.provide({ effects, tx: Client() }, () => callback(Client())) + for (const effect of effects) effect() + return result } + throw err + } +} + +export function effect(fn: () => any | Promise) { + const bound = InstanceState.bind(fn) + try { + ctx.use().effects.push(bound) + } catch { + bound() + } +} + +type NotPromise = T extends Promise ? never : T + +export function transaction( + callback: (tx: TxOrDb) => NotPromise, + options?: { + behavior?: "deferred" | "immediate" | "exclusive" + }, +): NotPromise { + try { + return callback(ctx.use().tx) + } catch (err) { + if (err instanceof LocalContext.NotFound) { + const effects: (() => void | Promise)[] = [] + const txCallback = InstanceState.bind((tx: TxOrDb) => ctx.provide({ tx, effects }, () => callback(tx))) + const result = Client().transaction(txCallback, { behavior: options?.behavior }) + for (const effect of effects) effect() + return result as NotPromise + } + throw err } } diff --git a/packages/opencode/src/storage/index.ts b/packages/opencode/src/storage/index.ts new file mode 100644 index 0000000000..212c9eecfd --- /dev/null +++ b/packages/opencode/src/storage/index.ts @@ -0,0 +1,26 @@ +export * as JsonMigration from "./json-migration" +export * as Database from "./db" +export * as Storage from "./storage" +export { + asc, + eq, + and, + or, + inArray, + desc, + not, + sql, + isNull, + isNotNull, + count, + like, + exists, + between, + gt, + gte, + lt, + lte, + ne, +} from "drizzle-orm" +export type { SQL } from "drizzle-orm" +export { NotFoundError } from "./storage" diff --git a/packages/opencode/src/storage/json-migration.ts b/packages/opencode/src/storage/json-migration.ts index 4bf75f5a1c..4803d452fe 100644 --- a/packages/opencode/src/storage/json-migration.ts +++ b/packages/opencode/src/storage/json-migration.ts @@ -10,47 +10,24 @@ import { existsSync } from "fs" import { Filesystem } from "../util" import { Glob } from "@opencode-ai/shared/util/glob" -export namespace JsonMigration { - const log = Log.create({ service: "json-migration" }) +const log = Log.create({ service: "json-migration" }) - export type Progress = { - current: number - total: number - label: string - } +export type Progress = { + current: number + total: number + label: string +} - type Options = { - progress?: (event: Progress) => void - } +type Options = { + progress?: (event: Progress) => void +} - export async function run(db: SQLiteBunDatabase | NodeSQLiteDatabase, options?: Options) { - const storageDir = path.join(Global.Path.data, "storage") +export async function run(db: SQLiteBunDatabase | NodeSQLiteDatabase, options?: Options) { + const storageDir = path.join(Global.Path.data, "storage") - if (!existsSync(storageDir)) { - log.info("storage directory does not exist, skipping migration") - return { - projects: 0, - sessions: 0, - messages: 0, - parts: 0, - todos: 0, - permissions: 0, - shares: 0, - errors: [] as string[], - } - } - - log.info("starting json to sqlite migration", { storageDir }) - const start = performance.now() - - // const db = drizzle({ client: sqlite }) - - // Optimize SQLite for bulk inserts - db.run("PRAGMA journal_mode = WAL") - db.run("PRAGMA synchronous = OFF") - db.run("PRAGMA cache_size = 10000") - db.run("PRAGMA temp_store = MEMORY") - const stats = { + if (!existsSync(storageDir)) { + log.info("storage directory does not exist, skipping migration") + return { projects: 0, sessions: 0, messages: 0, @@ -60,370 +37,391 @@ export namespace JsonMigration { shares: 0, errors: [] as string[], } - const orphans = { - sessions: 0, - todos: 0, - permissions: 0, - shares: 0, - } - const errs = stats.errors - - const batchSize = 1000 - const now = Date.now() - - async function list(pattern: string) { - return Glob.scan(pattern, { cwd: storageDir, absolute: true }) - } - - async function read(files: string[], start: number, end: number) { - const count = end - start - // oxlint-disable-next-line unicorn/no-new-array -- pre-allocated for index-based batch fill - const tasks = new Array(count) - for (let i = 0; i < count; i++) { - tasks[i] = Filesystem.readJson(files[start + i]) - } - const results = await Promise.allSettled(tasks) - // oxlint-disable-next-line unicorn/no-new-array -- pre-allocated for index-based batch fill - const items = new Array(count) - for (let i = 0; i < results.length; i++) { - const result = results[i] - if (result.status === "fulfilled") { - items[i] = result.value - continue - } - errs.push(`failed to read ${files[start + i]}: ${result.reason}`) - } - return items - } - - function insert(values: any[], table: any, label: string) { - if (values.length === 0) return 0 - try { - db.insert(table).values(values).onConflictDoNothing().run() - return values.length - } catch (e) { - errs.push(`failed to migrate ${label} batch: ${e}`) - return 0 - } - } - - // Pre-scan all files upfront to avoid repeated glob operations - log.info("scanning files...") - const [projectFiles, sessionFiles, messageFiles, partFiles, todoFiles, permFiles, shareFiles] = await Promise.all([ - list("project/*.json"), - list("session/*/*.json"), - list("message/*/*.json"), - list("part/*/*.json"), - list("todo/*.json"), - list("permission/*.json"), - list("session_share/*.json"), - ]) - - log.info("file scan complete", { - projects: projectFiles.length, - sessions: sessionFiles.length, - messages: messageFiles.length, - parts: partFiles.length, - todos: todoFiles.length, - permissions: permFiles.length, - shares: shareFiles.length, - }) - - const total = Math.max( - 1, - projectFiles.length + - sessionFiles.length + - messageFiles.length + - partFiles.length + - todoFiles.length + - permFiles.length + - shareFiles.length, - ) - const progress = options?.progress - let current = 0 - const step = (label: string, count: number) => { - current = Math.min(total, current + count) - progress?.({ current, total, label }) - } - - progress?.({ current, total, label: "starting" }) - - db.run("BEGIN TRANSACTION") - - // Migrate projects first (no FK deps) - // Derive all IDs from file paths, not JSON content - const projectIds = new Set() - const projectValues = [] as any[] - for (let i = 0; i < projectFiles.length; i += batchSize) { - const end = Math.min(i + batchSize, projectFiles.length) - const batch = await read(projectFiles, i, end) - projectValues.length = 0 - for (let j = 0; j < batch.length; j++) { - const data = batch[j] - if (!data) continue - const id = path.basename(projectFiles[i + j], ".json") - projectIds.add(id) - projectValues.push({ - id, - worktree: data.worktree ?? "/", - vcs: data.vcs, - name: data.name ?? undefined, - icon_url: data.icon?.url, - icon_color: data.icon?.color, - time_created: data.time?.created ?? now, - time_updated: data.time?.updated ?? now, - time_initialized: data.time?.initialized, - sandboxes: data.sandboxes ?? [], - commands: data.commands, - }) - } - stats.projects += insert(projectValues, ProjectTable, "project") - step("projects", end - i) - } - log.info("migrated projects", { count: stats.projects, duration: Math.round(performance.now() - start) }) - - // Migrate sessions (depends on projects) - // Derive all IDs from directory/file paths, not JSON content, since earlier - // migrations may have moved sessions to new directories without updating the JSON - const sessionProjects = sessionFiles.map((file) => path.basename(path.dirname(file))) - const sessionIds = new Set() - const sessionValues = [] as any[] - for (let i = 0; i < sessionFiles.length; i += batchSize) { - const end = Math.min(i + batchSize, sessionFiles.length) - const batch = await read(sessionFiles, i, end) - sessionValues.length = 0 - for (let j = 0; j < batch.length; j++) { - const data = batch[j] - if (!data) continue - const id = path.basename(sessionFiles[i + j], ".json") - const projectID = sessionProjects[i + j] - if (!projectIds.has(projectID)) { - orphans.sessions++ - continue - } - sessionIds.add(id) - sessionValues.push({ - id, - project_id: projectID, - parent_id: data.parentID ?? null, - slug: data.slug ?? "", - directory: data.directory ?? "", - title: data.title ?? "", - version: data.version ?? "", - share_url: data.share?.url ?? null, - summary_additions: data.summary?.additions ?? null, - summary_deletions: data.summary?.deletions ?? null, - summary_files: data.summary?.files ?? null, - summary_diffs: data.summary?.diffs ?? null, - revert: data.revert ?? null, - permission: data.permission ?? null, - time_created: data.time?.created ?? now, - time_updated: data.time?.updated ?? now, - time_compacting: data.time?.compacting ?? null, - time_archived: data.time?.archived ?? null, - }) - } - stats.sessions += insert(sessionValues, SessionTable, "session") - step("sessions", end - i) - } - log.info("migrated sessions", { count: stats.sessions }) - if (orphans.sessions > 0) { - log.warn("skipped orphaned sessions", { count: orphans.sessions }) - } - - // Migrate messages using pre-scanned file map - const allMessageFiles = [] as string[] - const allMessageSessions = [] as string[] - const messageSessions = new Map() - for (const file of messageFiles) { - const sessionID = path.basename(path.dirname(file)) - if (!sessionIds.has(sessionID)) continue - allMessageFiles.push(file) - allMessageSessions.push(sessionID) - } - - for (let i = 0; i < allMessageFiles.length; i += batchSize) { - const end = Math.min(i + batchSize, allMessageFiles.length) - const batch = await read(allMessageFiles, i, end) - // oxlint-disable-next-line unicorn/no-new-array -- pre-allocated for index-based batch fill - const values = new Array(batch.length) - let count = 0 - for (let j = 0; j < batch.length; j++) { - const data = batch[j] - if (!data) continue - const file = allMessageFiles[i + j] - const id = path.basename(file, ".json") - const sessionID = allMessageSessions[i + j] - messageSessions.set(id, sessionID) - const rest = data - delete rest.id - delete rest.sessionID - values[count++] = { - id, - session_id: sessionID, - time_created: data.time?.created ?? now, - time_updated: data.time?.updated ?? now, - data: rest, - } - } - values.length = count - stats.messages += insert(values, MessageTable, "message") - step("messages", end - i) - } - log.info("migrated messages", { count: stats.messages }) - - // Migrate parts using pre-scanned file map - for (let i = 0; i < partFiles.length; i += batchSize) { - const end = Math.min(i + batchSize, partFiles.length) - const batch = await read(partFiles, i, end) - // oxlint-disable-next-line unicorn/no-new-array -- pre-allocated for index-based batch fill - const values = new Array(batch.length) - let count = 0 - for (let j = 0; j < batch.length; j++) { - const data = batch[j] - if (!data) continue - const file = partFiles[i + j] - const id = path.basename(file, ".json") - const messageID = path.basename(path.dirname(file)) - const sessionID = messageSessions.get(messageID) - if (!sessionID) { - errs.push(`part missing message session: ${file}`) - continue - } - if (!sessionIds.has(sessionID)) continue - const rest = data - delete rest.id - delete rest.messageID - delete rest.sessionID - values[count++] = { - id, - message_id: messageID, - session_id: sessionID, - time_created: data.time?.created ?? now, - time_updated: data.time?.updated ?? now, - data: rest, - } - } - values.length = count - stats.parts += insert(values, PartTable, "part") - step("parts", end - i) - } - log.info("migrated parts", { count: stats.parts }) - - // Migrate todos - const todoSessions = todoFiles.map((file) => path.basename(file, ".json")) - for (let i = 0; i < todoFiles.length; i += batchSize) { - const end = Math.min(i + batchSize, todoFiles.length) - const batch = await read(todoFiles, i, end) - const values = [] as any[] - for (let j = 0; j < batch.length; j++) { - const data = batch[j] - if (!data) continue - const sessionID = todoSessions[i + j] - if (!sessionIds.has(sessionID)) { - orphans.todos++ - continue - } - if (!Array.isArray(data)) { - errs.push(`todo not an array: ${todoFiles[i + j]}`) - continue - } - for (let position = 0; position < data.length; position++) { - const todo = data[position] - if (!todo?.content || !todo?.status || !todo?.priority) continue - values.push({ - session_id: sessionID, - content: todo.content, - status: todo.status, - priority: todo.priority, - position, - time_created: now, - time_updated: now, - }) - } - } - stats.todos += insert(values, TodoTable, "todo") - step("todos", end - i) - } - log.info("migrated todos", { count: stats.todos }) - if (orphans.todos > 0) { - log.warn("skipped orphaned todos", { count: orphans.todos }) - } - - // Migrate permissions - const permProjects = permFiles.map((file) => path.basename(file, ".json")) - const permValues = [] as any[] - for (let i = 0; i < permFiles.length; i += batchSize) { - const end = Math.min(i + batchSize, permFiles.length) - const batch = await read(permFiles, i, end) - permValues.length = 0 - for (let j = 0; j < batch.length; j++) { - const data = batch[j] - if (!data) continue - const projectID = permProjects[i + j] - if (!projectIds.has(projectID)) { - orphans.permissions++ - continue - } - permValues.push({ project_id: projectID, data }) - } - stats.permissions += insert(permValues, PermissionTable, "permission") - step("permissions", end - i) - } - log.info("migrated permissions", { count: stats.permissions }) - if (orphans.permissions > 0) { - log.warn("skipped orphaned permissions", { count: orphans.permissions }) - } - - // Migrate session shares - const shareSessions = shareFiles.map((file) => path.basename(file, ".json")) - const shareValues = [] as any[] - for (let i = 0; i < shareFiles.length; i += batchSize) { - const end = Math.min(i + batchSize, shareFiles.length) - const batch = await read(shareFiles, i, end) - shareValues.length = 0 - for (let j = 0; j < batch.length; j++) { - const data = batch[j] - if (!data) continue - const sessionID = shareSessions[i + j] - if (!sessionIds.has(sessionID)) { - orphans.shares++ - continue - } - if (!data?.id || !data?.secret || !data?.url) { - errs.push(`session_share missing id/secret/url: ${shareFiles[i + j]}`) - continue - } - shareValues.push({ session_id: sessionID, id: data.id, secret: data.secret, url: data.url }) - } - stats.shares += insert(shareValues, SessionShareTable, "session_share") - step("shares", end - i) - } - log.info("migrated session shares", { count: stats.shares }) - if (orphans.shares > 0) { - log.warn("skipped orphaned session shares", { count: orphans.shares }) - } - - db.run("COMMIT") - - log.info("json migration complete", { - projects: stats.projects, - sessions: stats.sessions, - messages: stats.messages, - parts: stats.parts, - todos: stats.todos, - permissions: stats.permissions, - shares: stats.shares, - errorCount: stats.errors.length, - duration: Math.round(performance.now() - start), - }) - - if (stats.errors.length > 0) { - log.warn("migration errors", { errors: stats.errors.slice(0, 20) }) - } - - progress?.({ current: total, total, label: "complete" }) - - return stats } + + log.info("starting json to sqlite migration", { storageDir }) + const start = performance.now() + + // const db = drizzle({ client: sqlite }) + + // Optimize SQLite for bulk inserts + db.run("PRAGMA journal_mode = WAL") + db.run("PRAGMA synchronous = OFF") + db.run("PRAGMA cache_size = 10000") + db.run("PRAGMA temp_store = MEMORY") + const stats = { + projects: 0, + sessions: 0, + messages: 0, + parts: 0, + todos: 0, + permissions: 0, + shares: 0, + errors: [] as string[], + } + const orphans = { + sessions: 0, + todos: 0, + permissions: 0, + shares: 0, + } + const errs = stats.errors + + const batchSize = 1000 + const now = Date.now() + + async function list(pattern: string) { + return Glob.scan(pattern, { cwd: storageDir, absolute: true }) + } + + async function read(files: string[], start: number, end: number) { + const count = end - start + // oxlint-disable-next-line unicorn/no-new-array -- pre-allocated for index-based batch fill + const tasks = new Array(count) + for (let i = 0; i < count; i++) { + tasks[i] = Filesystem.readJson(files[start + i]) + } + const results = await Promise.allSettled(tasks) + // oxlint-disable-next-line unicorn/no-new-array -- pre-allocated for index-based batch fill + const items = new Array(count) + for (let i = 0; i < results.length; i++) { + const result = results[i] + if (result.status === "fulfilled") { + items[i] = result.value + continue + } + errs.push(`failed to read ${files[start + i]}: ${result.reason}`) + } + return items + } + + function insert(values: any[], table: any, label: string) { + if (values.length === 0) return 0 + try { + db.insert(table).values(values).onConflictDoNothing().run() + return values.length + } catch (e) { + errs.push(`failed to migrate ${label} batch: ${e}`) + return 0 + } + } + + // Pre-scan all files upfront to avoid repeated glob operations + log.info("scanning files...") + const [projectFiles, sessionFiles, messageFiles, partFiles, todoFiles, permFiles, shareFiles] = await Promise.all([ + list("project/*.json"), + list("session/*/*.json"), + list("message/*/*.json"), + list("part/*/*.json"), + list("todo/*.json"), + list("permission/*.json"), + list("session_share/*.json"), + ]) + + log.info("file scan complete", { + projects: projectFiles.length, + sessions: sessionFiles.length, + messages: messageFiles.length, + parts: partFiles.length, + todos: todoFiles.length, + permissions: permFiles.length, + shares: shareFiles.length, + }) + + const total = Math.max( + 1, + projectFiles.length + + sessionFiles.length + + messageFiles.length + + partFiles.length + + todoFiles.length + + permFiles.length + + shareFiles.length, + ) + const progress = options?.progress + let current = 0 + const step = (label: string, count: number) => { + current = Math.min(total, current + count) + progress?.({ current, total, label }) + } + + progress?.({ current, total, label: "starting" }) + + db.run("BEGIN TRANSACTION") + + // Migrate projects first (no FK deps) + // Derive all IDs from file paths, not JSON content + const projectIds = new Set() + const projectValues = [] as any[] + for (let i = 0; i < projectFiles.length; i += batchSize) { + const end = Math.min(i + batchSize, projectFiles.length) + const batch = await read(projectFiles, i, end) + projectValues.length = 0 + for (let j = 0; j < batch.length; j++) { + const data = batch[j] + if (!data) continue + const id = path.basename(projectFiles[i + j], ".json") + projectIds.add(id) + projectValues.push({ + id, + worktree: data.worktree ?? "/", + vcs: data.vcs, + name: data.name ?? undefined, + icon_url: data.icon?.url, + icon_color: data.icon?.color, + time_created: data.time?.created ?? now, + time_updated: data.time?.updated ?? now, + time_initialized: data.time?.initialized, + sandboxes: data.sandboxes ?? [], + commands: data.commands, + }) + } + stats.projects += insert(projectValues, ProjectTable, "project") + step("projects", end - i) + } + log.info("migrated projects", { count: stats.projects, duration: Math.round(performance.now() - start) }) + + // Migrate sessions (depends on projects) + // Derive all IDs from directory/file paths, not JSON content, since earlier + // migrations may have moved sessions to new directories without updating the JSON + const sessionProjects = sessionFiles.map((file) => path.basename(path.dirname(file))) + const sessionIds = new Set() + const sessionValues = [] as any[] + for (let i = 0; i < sessionFiles.length; i += batchSize) { + const end = Math.min(i + batchSize, sessionFiles.length) + const batch = await read(sessionFiles, i, end) + sessionValues.length = 0 + for (let j = 0; j < batch.length; j++) { + const data = batch[j] + if (!data) continue + const id = path.basename(sessionFiles[i + j], ".json") + const projectID = sessionProjects[i + j] + if (!projectIds.has(projectID)) { + orphans.sessions++ + continue + } + sessionIds.add(id) + sessionValues.push({ + id, + project_id: projectID, + parent_id: data.parentID ?? null, + slug: data.slug ?? "", + directory: data.directory ?? "", + title: data.title ?? "", + version: data.version ?? "", + share_url: data.share?.url ?? null, + summary_additions: data.summary?.additions ?? null, + summary_deletions: data.summary?.deletions ?? null, + summary_files: data.summary?.files ?? null, + summary_diffs: data.summary?.diffs ?? null, + revert: data.revert ?? null, + permission: data.permission ?? null, + time_created: data.time?.created ?? now, + time_updated: data.time?.updated ?? now, + time_compacting: data.time?.compacting ?? null, + time_archived: data.time?.archived ?? null, + }) + } + stats.sessions += insert(sessionValues, SessionTable, "session") + step("sessions", end - i) + } + log.info("migrated sessions", { count: stats.sessions }) + if (orphans.sessions > 0) { + log.warn("skipped orphaned sessions", { count: orphans.sessions }) + } + + // Migrate messages using pre-scanned file map + const allMessageFiles = [] as string[] + const allMessageSessions = [] as string[] + const messageSessions = new Map() + for (const file of messageFiles) { + const sessionID = path.basename(path.dirname(file)) + if (!sessionIds.has(sessionID)) continue + allMessageFiles.push(file) + allMessageSessions.push(sessionID) + } + + for (let i = 0; i < allMessageFiles.length; i += batchSize) { + const end = Math.min(i + batchSize, allMessageFiles.length) + const batch = await read(allMessageFiles, i, end) + // oxlint-disable-next-line unicorn/no-new-array -- pre-allocated for index-based batch fill + const values = new Array(batch.length) + let count = 0 + for (let j = 0; j < batch.length; j++) { + const data = batch[j] + if (!data) continue + const file = allMessageFiles[i + j] + const id = path.basename(file, ".json") + const sessionID = allMessageSessions[i + j] + messageSessions.set(id, sessionID) + const rest = data + delete rest.id + delete rest.sessionID + values[count++] = { + id, + session_id: sessionID, + time_created: data.time?.created ?? now, + time_updated: data.time?.updated ?? now, + data: rest, + } + } + values.length = count + stats.messages += insert(values, MessageTable, "message") + step("messages", end - i) + } + log.info("migrated messages", { count: stats.messages }) + + // Migrate parts using pre-scanned file map + for (let i = 0; i < partFiles.length; i += batchSize) { + const end = Math.min(i + batchSize, partFiles.length) + const batch = await read(partFiles, i, end) + // oxlint-disable-next-line unicorn/no-new-array -- pre-allocated for index-based batch fill + const values = new Array(batch.length) + let count = 0 + for (let j = 0; j < batch.length; j++) { + const data = batch[j] + if (!data) continue + const file = partFiles[i + j] + const id = path.basename(file, ".json") + const messageID = path.basename(path.dirname(file)) + const sessionID = messageSessions.get(messageID) + if (!sessionID) { + errs.push(`part missing message session: ${file}`) + continue + } + if (!sessionIds.has(sessionID)) continue + const rest = data + delete rest.id + delete rest.messageID + delete rest.sessionID + values[count++] = { + id, + message_id: messageID, + session_id: sessionID, + time_created: data.time?.created ?? now, + time_updated: data.time?.updated ?? now, + data: rest, + } + } + values.length = count + stats.parts += insert(values, PartTable, "part") + step("parts", end - i) + } + log.info("migrated parts", { count: stats.parts }) + + // Migrate todos + const todoSessions = todoFiles.map((file) => path.basename(file, ".json")) + for (let i = 0; i < todoFiles.length; i += batchSize) { + const end = Math.min(i + batchSize, todoFiles.length) + const batch = await read(todoFiles, i, end) + const values = [] as any[] + for (let j = 0; j < batch.length; j++) { + const data = batch[j] + if (!data) continue + const sessionID = todoSessions[i + j] + if (!sessionIds.has(sessionID)) { + orphans.todos++ + continue + } + if (!Array.isArray(data)) { + errs.push(`todo not an array: ${todoFiles[i + j]}`) + continue + } + for (let position = 0; position < data.length; position++) { + const todo = data[position] + if (!todo?.content || !todo?.status || !todo?.priority) continue + values.push({ + session_id: sessionID, + content: todo.content, + status: todo.status, + priority: todo.priority, + position, + time_created: now, + time_updated: now, + }) + } + } + stats.todos += insert(values, TodoTable, "todo") + step("todos", end - i) + } + log.info("migrated todos", { count: stats.todos }) + if (orphans.todos > 0) { + log.warn("skipped orphaned todos", { count: orphans.todos }) + } + + // Migrate permissions + const permProjects = permFiles.map((file) => path.basename(file, ".json")) + const permValues = [] as any[] + for (let i = 0; i < permFiles.length; i += batchSize) { + const end = Math.min(i + batchSize, permFiles.length) + const batch = await read(permFiles, i, end) + permValues.length = 0 + for (let j = 0; j < batch.length; j++) { + const data = batch[j] + if (!data) continue + const projectID = permProjects[i + j] + if (!projectIds.has(projectID)) { + orphans.permissions++ + continue + } + permValues.push({ project_id: projectID, data }) + } + stats.permissions += insert(permValues, PermissionTable, "permission") + step("permissions", end - i) + } + log.info("migrated permissions", { count: stats.permissions }) + if (orphans.permissions > 0) { + log.warn("skipped orphaned permissions", { count: orphans.permissions }) + } + + // Migrate session shares + const shareSessions = shareFiles.map((file) => path.basename(file, ".json")) + const shareValues = [] as any[] + for (let i = 0; i < shareFiles.length; i += batchSize) { + const end = Math.min(i + batchSize, shareFiles.length) + const batch = await read(shareFiles, i, end) + shareValues.length = 0 + for (let j = 0; j < batch.length; j++) { + const data = batch[j] + if (!data) continue + const sessionID = shareSessions[i + j] + if (!sessionIds.has(sessionID)) { + orphans.shares++ + continue + } + if (!data?.id || !data?.secret || !data?.url) { + errs.push(`session_share missing id/secret/url: ${shareFiles[i + j]}`) + continue + } + shareValues.push({ session_id: sessionID, id: data.id, secret: data.secret, url: data.url }) + } + stats.shares += insert(shareValues, SessionShareTable, "session_share") + step("shares", end - i) + } + log.info("migrated session shares", { count: stats.shares }) + if (orphans.shares > 0) { + log.warn("skipped orphaned session shares", { count: orphans.shares }) + } + + db.run("COMMIT") + + log.info("json migration complete", { + projects: stats.projects, + sessions: stats.sessions, + messages: stats.messages, + parts: stats.parts, + todos: stats.todos, + permissions: stats.permissions, + shares: stats.shares, + errorCount: stats.errors.length, + duration: Math.round(performance.now() - start), + }) + + if (stats.errors.length > 0) { + log.warn("migration errors", { errors: stats.errors.slice(0, 20) }) + } + + progress?.({ current: total, total, label: "complete" }) + + return stats } diff --git a/packages/opencode/src/storage/storage.ts b/packages/opencode/src/storage/storage.ts index f4793c6204..b1685e689b 100644 --- a/packages/opencode/src/storage/storage.ts +++ b/packages/opencode/src/storage/storage.ts @@ -7,327 +7,325 @@ import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { Effect, Exit, Layer, Option, RcMap, Schema, Context, TxReentrantLock } from "effect" import { Git } from "@/git" -export namespace Storage { - const log = Log.create({ service: "storage" }) +const log = Log.create({ service: "storage" }) - type Migration = ( - dir: string, - fs: AppFileSystem.Interface, - git: Git.Interface, - ) => Effect.Effect +type Migration = ( + dir: string, + fs: AppFileSystem.Interface, + git: Git.Interface, +) => Effect.Effect - export const NotFoundError = NamedError.create( - "NotFoundError", - z.object({ - message: z.string(), +export const NotFoundError = NamedError.create( + "NotFoundError", + z.object({ + message: z.string(), + }), +) + +export type Error = AppFileSystem.Error | InstanceType + +const RootFile = Schema.Struct({ + path: Schema.optional( + Schema.Struct({ + root: Schema.optional(Schema.String), }), - ) + ), +}) - export type Error = AppFileSystem.Error | InstanceType +const SessionFile = Schema.Struct({ + id: Schema.String, +}) - const RootFile = Schema.Struct({ - path: Schema.optional( - Schema.Struct({ - root: Schema.optional(Schema.String), - }), - ), - }) +const MessageFile = Schema.Struct({ + id: Schema.String, +}) - const SessionFile = Schema.Struct({ - id: Schema.String, - }) +const DiffFile = Schema.Struct({ + additions: Schema.Number, + deletions: Schema.Number, +}) - const MessageFile = Schema.Struct({ - id: Schema.String, - }) +const SummaryFile = Schema.Struct({ + id: Schema.String, + projectID: Schema.String, + summary: Schema.Struct({ diffs: Schema.Array(DiffFile) }), +}) - const DiffFile = Schema.Struct({ - additions: Schema.Number, - deletions: Schema.Number, - }) +const decodeRoot = Schema.decodeUnknownOption(RootFile) +const decodeSession = Schema.decodeUnknownOption(SessionFile) +const decodeMessage = Schema.decodeUnknownOption(MessageFile) +const decodeSummary = Schema.decodeUnknownOption(SummaryFile) - const SummaryFile = Schema.Struct({ - id: Schema.String, - projectID: Schema.String, - summary: Schema.Struct({ diffs: Schema.Array(DiffFile) }), - }) +export interface Interface { + readonly remove: (key: string[]) => Effect.Effect + readonly read: (key: string[]) => Effect.Effect + readonly update: (key: string[], fn: (draft: T) => void) => Effect.Effect + readonly write: (key: string[], content: T) => Effect.Effect + readonly list: (prefix: string[]) => Effect.Effect +} - const decodeRoot = Schema.decodeUnknownOption(RootFile) - const decodeSession = Schema.decodeUnknownOption(SessionFile) - const decodeMessage = Schema.decodeUnknownOption(MessageFile) - const decodeSummary = Schema.decodeUnknownOption(SummaryFile) +export class Service extends Context.Service()("@opencode/Storage") {} - export interface Interface { - readonly remove: (key: string[]) => Effect.Effect - readonly read: (key: string[]) => Effect.Effect - readonly update: (key: string[], fn: (draft: T) => void) => Effect.Effect - readonly write: (key: string[], content: T) => Effect.Effect - readonly list: (prefix: string[]) => Effect.Effect +function file(dir: string, key: string[]) { + return path.join(dir, ...key) + ".json" +} + +function missing(err: unknown) { + if (!err || typeof err !== "object") return false + if ("code" in err && err.code === "ENOENT") return true + if ("reason" in err && err.reason && typeof err.reason === "object" && "_tag" in err.reason) { + return err.reason._tag === "NotFound" } + return false +} - export class Service extends Context.Service()("@opencode/Storage") {} +function parseMigration(text: string) { + const value = Number.parseInt(text, 10) + return Number.isNaN(value) ? 0 : value +} - function file(dir: string, key: string[]) { - return path.join(dir, ...key) + ".json" - } +const MIGRATIONS: Migration[] = [ + Effect.fn("Storage.migration.1")(function* (dir: string, fs: AppFileSystem.Interface, git: Git.Interface) { + const project = path.resolve(dir, "../project") + if (!(yield* fs.isDir(project))) return + const projectDirs = yield* fs.glob("*", { + cwd: project, + include: "all", + }) + for (const projectDir of projectDirs) { + const full = path.join(project, projectDir) + if (!(yield* fs.isDir(full))) continue + log.info(`migrating project ${projectDir}`) + let projectID = projectDir + let worktree = "/" - function missing(err: unknown) { - if (!err || typeof err !== "object") return false - if ("code" in err && err.code === "ENOENT") return true - if ("reason" in err && err.reason && typeof err.reason === "object" && "_tag" in err.reason) { - return err.reason._tag === "NotFound" - } - return false - } - - function parseMigration(text: string) { - const value = Number.parseInt(text, 10) - return Number.isNaN(value) ? 0 : value - } - - const MIGRATIONS: Migration[] = [ - Effect.fn("Storage.migration.1")(function* (dir: string, fs: AppFileSystem.Interface, git: Git.Interface) { - const project = path.resolve(dir, "../project") - if (!(yield* fs.isDir(project))) return - const projectDirs = yield* fs.glob("*", { - cwd: project, - include: "all", - }) - for (const projectDir of projectDirs) { - const full = path.join(project, projectDir) - if (!(yield* fs.isDir(full))) continue - log.info(`migrating project ${projectDir}`) - let projectID = projectDir - let worktree = "/" - - if (projectID !== "global") { - for (const msgFile of yield* fs.glob("storage/session/message/*/*.json", { - cwd: full, - absolute: true, - })) { - const json = decodeRoot(yield* fs.readJson(msgFile), { onExcessProperty: "preserve" }) - const root = Option.isSome(json) ? json.value.path?.root : undefined - if (!root) continue - worktree = root - break - } - if (!worktree) continue - if (!(yield* fs.isDir(worktree))) continue - const result = yield* git.run(["rev-list", "--max-parents=0", "--all"], { - cwd: worktree, - }) - const [id] = result - .text() - .split("\n") - .filter(Boolean) - .map((x) => x.trim()) - .toSorted() - if (!id) continue - projectID = id - - yield* fs.writeWithDirs( - path.join(dir, "project", projectID + ".json"), - JSON.stringify( - { - id, - vcs: "git", - worktree, - time: { - created: Date.now(), - initialized: Date.now(), - }, - }, - null, - 2, - ), - ) - - log.info(`migrating sessions for project ${projectID}`) - for (const sessionFile of yield* fs.glob("storage/session/info/*.json", { - cwd: full, - absolute: true, - })) { - const dest = path.join(dir, "session", projectID, path.basename(sessionFile)) - log.info("copying", { sessionFile, dest }) - const session = yield* fs.readJson(sessionFile) - const info = decodeSession(session, { onExcessProperty: "preserve" }) - yield* fs.writeWithDirs(dest, JSON.stringify(session, null, 2)) - if (Option.isNone(info)) continue - log.info(`migrating messages for session ${info.value.id}`) - for (const msgFile of yield* fs.glob(`storage/session/message/${info.value.id}/*.json`, { - cwd: full, - absolute: true, - })) { - const next = path.join(dir, "message", info.value.id, path.basename(msgFile)) - log.info("copying", { - msgFile, - dest: next, - }) - const message = yield* fs.readJson(msgFile) - const item = decodeMessage(message, { onExcessProperty: "preserve" }) - yield* fs.writeWithDirs(next, JSON.stringify(message, null, 2)) - if (Option.isNone(item)) continue - - log.info(`migrating parts for message ${item.value.id}`) - for (const partFile of yield* fs.glob(`storage/session/part/${info.value.id}/${item.value.id}/*.json`, { - cwd: full, - absolute: true, - })) { - const out = path.join(dir, "part", item.value.id, path.basename(partFile)) - const part = yield* fs.readJson(partFile) - log.info("copying", { - partFile, - dest: out, - }) - yield* fs.writeWithDirs(out, JSON.stringify(part, null, 2)) - } - } - } + if (projectID !== "global") { + for (const msgFile of yield* fs.glob("storage/session/message/*/*.json", { + cwd: full, + absolute: true, + })) { + const json = decodeRoot(yield* fs.readJson(msgFile), { onExcessProperty: "preserve" }) + const root = Option.isSome(json) ? json.value.path?.root : undefined + if (!root) continue + worktree = root + break } - } - }), - Effect.fn("Storage.migration.2")(function* (dir: string, fs: AppFileSystem.Interface) { - for (const item of yield* fs.glob("session/*/*.json", { - cwd: dir, - absolute: true, - })) { - const raw = yield* fs.readJson(item) - const session = decodeSummary(raw, { onExcessProperty: "preserve" }) - if (Option.isNone(session)) continue - const diffs = session.value.summary.diffs + if (!worktree) continue + if (!(yield* fs.isDir(worktree))) continue + const result = yield* git.run(["rev-list", "--max-parents=0", "--all"], { + cwd: worktree, + }) + const [id] = result + .text() + .split("\n") + .filter(Boolean) + .map((x) => x.trim()) + .toSorted() + if (!id) continue + projectID = id + yield* fs.writeWithDirs( - path.join(dir, "session_diff", session.value.id + ".json"), - JSON.stringify(diffs, null, 2), - ) - yield* fs.writeWithDirs( - path.join(dir, "session", session.value.projectID, session.value.id + ".json"), + path.join(dir, "project", projectID + ".json"), JSON.stringify( { - ...(raw as Record), - summary: { - additions: diffs.reduce((sum, x) => sum + x.additions, 0), - deletions: diffs.reduce((sum, x) => sum + x.deletions, 0), + id, + vcs: "git", + worktree, + time: { + created: Date.now(), + initialized: Date.now(), }, }, null, 2, ), ) - } - }), - ] - export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const fs = yield* AppFileSystem.Service - const git = yield* Git.Service - const locks = yield* RcMap.make({ - lookup: () => TxReentrantLock.make(), - idleTimeToLive: 0, - }) - const state = yield* Effect.cached( - Effect.gen(function* () { - const dir = path.join(Global.Path.data, "storage") - const marker = path.join(dir, "migration") - const migration = yield* fs.readFileString(marker).pipe( - Effect.map(parseMigration), - Effect.catchIf(missing, () => Effect.succeed(0)), - Effect.orElseSucceed(() => 0), - ) - for (let i = migration; i < MIGRATIONS.length; i++) { - log.info("running migration", { index: i }) - const step = MIGRATIONS[i]! - const exit = yield* Effect.exit(step(dir, fs, git)) - if (Exit.isFailure(exit)) { - log.error("failed to run migration", { index: i, cause: exit.cause }) - break + log.info(`migrating sessions for project ${projectID}`) + for (const sessionFile of yield* fs.glob("storage/session/info/*.json", { + cwd: full, + absolute: true, + })) { + const dest = path.join(dir, "session", projectID, path.basename(sessionFile)) + log.info("copying", { sessionFile, dest }) + const session = yield* fs.readJson(sessionFile) + const info = decodeSession(session, { onExcessProperty: "preserve" }) + yield* fs.writeWithDirs(dest, JSON.stringify(session, null, 2)) + if (Option.isNone(info)) continue + log.info(`migrating messages for session ${info.value.id}`) + for (const msgFile of yield* fs.glob(`storage/session/message/${info.value.id}/*.json`, { + cwd: full, + absolute: true, + })) { + const next = path.join(dir, "message", info.value.id, path.basename(msgFile)) + log.info("copying", { + msgFile, + dest: next, + }) + const message = yield* fs.readJson(msgFile) + const item = decodeMessage(message, { onExcessProperty: "preserve" }) + yield* fs.writeWithDirs(next, JSON.stringify(message, null, 2)) + if (Option.isNone(item)) continue + + log.info(`migrating parts for message ${item.value.id}`) + for (const partFile of yield* fs.glob(`storage/session/part/${info.value.id}/${item.value.id}/*.json`, { + cwd: full, + absolute: true, + })) { + const out = path.join(dir, "part", item.value.id, path.basename(partFile)) + const part = yield* fs.readJson(partFile) + log.info("copying", { + partFile, + dest: out, + }) + yield* fs.writeWithDirs(out, JSON.stringify(part, null, 2)) } - yield* fs.writeWithDirs(marker, String(i + 1)) } - return { dir } + } + } + } + }), + Effect.fn("Storage.migration.2")(function* (dir: string, fs: AppFileSystem.Interface) { + for (const item of yield* fs.glob("session/*/*.json", { + cwd: dir, + absolute: true, + })) { + const raw = yield* fs.readJson(item) + const session = decodeSummary(raw, { onExcessProperty: "preserve" }) + if (Option.isNone(session)) continue + const diffs = session.value.summary.diffs + yield* fs.writeWithDirs( + path.join(dir, "session_diff", session.value.id + ".json"), + JSON.stringify(diffs, null, 2), + ) + yield* fs.writeWithDirs( + path.join(dir, "session", session.value.projectID, session.value.id + ".json"), + JSON.stringify( + { + ...(raw as Record), + summary: { + additions: diffs.reduce((sum, x) => sum + x.additions, 0), + deletions: diffs.reduce((sum, x) => sum + x.deletions, 0), + }, + }, + null, + 2, + ), + ) + } + }), +] + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + const git = yield* Git.Service + const locks = yield* RcMap.make({ + lookup: () => TxReentrantLock.make(), + idleTimeToLive: 0, + }) + const state = yield* Effect.cached( + Effect.gen(function* () { + const dir = path.join(Global.Path.data, "storage") + const marker = path.join(dir, "migration") + const migration = yield* fs.readFileString(marker).pipe( + Effect.map(parseMigration), + Effect.catchIf(missing, () => Effect.succeed(0)), + Effect.orElseSucceed(() => 0), + ) + for (let i = migration; i < MIGRATIONS.length; i++) { + log.info("running migration", { index: i }) + const step = MIGRATIONS[i]! + const exit = yield* Effect.exit(step(dir, fs, git)) + if (Exit.isFailure(exit)) { + log.error("failed to run migration", { index: i, cause: exit.cause }) + break + } + yield* fs.writeWithDirs(marker, String(i + 1)) + } + return { dir } + }), + ) + + const fail = (target: string): Effect.Effect> => + Effect.fail(new NotFoundError({ message: `Resource not found: ${target}` })) + + const wrap = (target: string, body: Effect.Effect) => + body.pipe(Effect.catchIf(missing, () => fail(target))) + + const writeJson = Effect.fnUntraced(function* (target: string, content: unknown) { + yield* fs.writeWithDirs(target, JSON.stringify(content, null, 2)) + }) + + const withResolved = ( + key: string[], + fn: (target: string, rw: TxReentrantLock.TxReentrantLock) => Effect.Effect, + ): Effect.Effect => + Effect.scoped( + Effect.gen(function* () { + const target = file((yield* state).dir, key) + return yield* fn(target, yield* RcMap.get(locks, target)) }), ) - const fail = (target: string): Effect.Effect> => - Effect.fail(new NotFoundError({ message: `Resource not found: ${target}` })) + const remove: Interface["remove"] = Effect.fn("Storage.remove")(function* (key: string[]) { + yield* withResolved(key, (target, rw) => + TxReentrantLock.withWriteLock(rw, fs.remove(target).pipe(Effect.catchIf(missing, () => Effect.void))), + ) + }) - const wrap = (target: string, body: Effect.Effect) => - body.pipe(Effect.catchIf(missing, () => fail(target))) - - const writeJson = Effect.fnUntraced(function* (target: string, content: unknown) { - yield* fs.writeWithDirs(target, JSON.stringify(content, null, 2)) - }) - - const withResolved = ( - key: string[], - fn: (target: string, rw: TxReentrantLock.TxReentrantLock) => Effect.Effect, - ): Effect.Effect => - Effect.scoped( - Effect.gen(function* () { - const target = file((yield* state).dir, key) - return yield* fn(target, yield* RcMap.get(locks, target)) - }), + const read: Interface["read"] = (key: string[]) => + Effect.gen(function* () { + const value = yield* withResolved(key, (target, rw) => + TxReentrantLock.withReadLock(rw, wrap(target, fs.readJson(target))), ) + return value as T + }) - const remove: Interface["remove"] = Effect.fn("Storage.remove")(function* (key: string[]) { - yield* withResolved(key, (target, rw) => - TxReentrantLock.withWriteLock(rw, fs.remove(target).pipe(Effect.catchIf(missing, () => Effect.void))), + const update: Interface["update"] = (key: string[], fn: (draft: T) => void) => + Effect.gen(function* () { + const value = yield* withResolved(key, (target, rw) => + TxReentrantLock.withWriteLock( + rw, + Effect.gen(function* () { + const content = yield* wrap(target, fs.readJson(target)) + fn(content as T) + yield* writeJson(target, content) + return content + }), + ), ) + return value as T }) - const read: Interface["read"] = (key: string[]) => - Effect.gen(function* () { - const value = yield* withResolved(key, (target, rw) => - TxReentrantLock.withReadLock(rw, wrap(target, fs.readJson(target))), - ) - return value as T - }) - - const update: Interface["update"] = (key: string[], fn: (draft: T) => void) => - Effect.gen(function* () { - const value = yield* withResolved(key, (target, rw) => - TxReentrantLock.withWriteLock( - rw, - Effect.gen(function* () { - const content = yield* wrap(target, fs.readJson(target)) - fn(content as T) - yield* writeJson(target, content) - return content - }), - ), - ) - return value as T - }) - - const write: Interface["write"] = (key: string[], content: unknown) => - Effect.gen(function* () { - yield* withResolved(key, (target, rw) => TxReentrantLock.withWriteLock(rw, writeJson(target, content))) - }) - - const list: Interface["list"] = Effect.fn("Storage.list")(function* (prefix: string[]) { - const dir = (yield* state).dir - const cwd = path.join(dir, ...prefix) - const result = yield* fs - .glob("**/*", { - cwd, - include: "file", - }) - .pipe(Effect.catch(() => Effect.succeed([]))) - return result - .map((x) => [...prefix, ...x.slice(0, -5).split(path.sep)]) - .toSorted((a, b) => a.join("/").localeCompare(b.join("/"))) + const write: Interface["write"] = (key: string[], content: unknown) => + Effect.gen(function* () { + yield* withResolved(key, (target, rw) => TxReentrantLock.withWriteLock(rw, writeJson(target, content))) }) - return Service.of({ - remove, - read, - update, - write, - list, - }) - }), - ) + const list: Interface["list"] = Effect.fn("Storage.list")(function* (prefix: string[]) { + const dir = (yield* state).dir + const cwd = path.join(dir, ...prefix) + const result = yield* fs + .glob("**/*", { + cwd, + include: "file", + }) + .pipe(Effect.catch(() => Effect.succeed([]))) + return result + .map((x) => [...prefix, ...x.slice(0, -5).split(path.sep)]) + .toSorted((a, b) => a.join("/").localeCompare(b.join("/"))) + }) - export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer), Layer.provide(Git.defaultLayer)) -} + return Service.of({ + remove, + read, + update, + write, + list, + }) + }), +) + +export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer), Layer.provide(Git.defaultLayer)) diff --git a/packages/opencode/src/sync/sync-event.ts b/packages/opencode/src/sync/sync-event.ts index d4ad860409..db487ddd24 100644 --- a/packages/opencode/src/sync/sync-event.ts +++ b/packages/opencode/src/sync/sync-event.ts @@ -1,6 +1,6 @@ import z from "zod" import type { ZodObject } from "zod" -import { Database, eq } from "@/storage/db" +import { Database, eq } from "@/storage" import { GlobalBus } from "@/bus/global" import { Bus as ProjectBus } from "@/bus" import { BusEvent } from "@/bus/bus-event" diff --git a/packages/opencode/src/worktree/worktree.ts b/packages/opencode/src/worktree/worktree.ts index 8eea6445aa..d4fab2030b 100644 --- a/packages/opencode/src/worktree/worktree.ts +++ b/packages/opencode/src/worktree/worktree.ts @@ -4,7 +4,7 @@ import { Global } from "../global" import { Instance } from "../project/instance" import { InstanceBootstrap } from "../project/bootstrap" import { Project } from "../project" -import { Database, eq } from "../storage/db" +import { Database, eq } from "../storage" import { ProjectTable } from "../project/project.sql" import type { ProjectID } from "../project/schema" import { Log } from "../util" diff --git a/packages/opencode/test/account/repo.test.ts b/packages/opencode/test/account/repo.test.ts index 2f17d1b22f..93d0481521 100644 --- a/packages/opencode/test/account/repo.test.ts +++ b/packages/opencode/test/account/repo.test.ts @@ -3,7 +3,7 @@ import { Effect, Layer, Option } from "effect" import { AccountRepo } from "../../src/account/repo" import { AccessToken, AccountID, OrgID, RefreshToken } from "../../src/account/schema" -import { Database } from "../../src/storage/db" +import { Database } from "../../src/storage" import { testEffect } from "../lib/effect" const truncate = Layer.effectDiscard( diff --git a/packages/opencode/test/account/service.test.ts b/packages/opencode/test/account/service.test.ts index 28592a0988..053fd2a0ed 100644 --- a/packages/opencode/test/account/service.test.ts +++ b/packages/opencode/test/account/service.test.ts @@ -15,7 +15,7 @@ import { RefreshToken, UserCode, } from "../../src/account/schema" -import { Database } from "../../src/storage/db" +import { Database } from "../../src/storage" import { testEffect } from "../lib/effect" const truncate = Layer.effectDiscard( diff --git a/packages/opencode/test/fixture/db.ts b/packages/opencode/test/fixture/db.ts index f11f0b9036..581739a6f9 100644 --- a/packages/opencode/test/fixture/db.ts +++ b/packages/opencode/test/fixture/db.ts @@ -1,6 +1,6 @@ import { rm } from "fs/promises" import { Instance } from "../../src/project/instance" -import { Database } from "../../src/storage/db" +import { Database } from "../../src/storage" export async function resetDatabase() { await Instance.disposeAll().catch(() => undefined) diff --git a/packages/opencode/test/preload.ts b/packages/opencode/test/preload.ts index 7c6f04c796..a2592286ad 100644 --- a/packages/opencode/test/preload.ts +++ b/packages/opencode/test/preload.ts @@ -10,7 +10,7 @@ import { afterAll } from "bun:test" const dir = path.join(os.tmpdir(), "opencode-test-data-" + process.pid) await fs.mkdir(dir, { recursive: true }) afterAll(async () => { - const { Database } = await import("../src/storage/db") + const { Database } = await import("../src/storage") Database.close() const busy = (error: unknown) => typeof error === "object" && error !== null && "code" in error && error.code === "EBUSY" diff --git a/packages/opencode/test/project/migrate-global.test.ts b/packages/opencode/test/project/migrate-global.test.ts index a63ac1cd98..8c9982afb8 100644 --- a/packages/opencode/test/project/migrate-global.test.ts +++ b/packages/opencode/test/project/migrate-global.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" import { Project } from "../../src/project" -import { Database, eq } from "../../src/storage/db" +import { Database, eq } from "../../src/storage" import { SessionTable } from "../../src/session/session.sql" import { ProjectTable } from "../../src/project/project.sql" import { ProjectID } from "../../src/project/schema" @@ -10,7 +10,7 @@ import { $ } from "bun" import { tmpdir } from "../fixture/fixture" import { Effect } from "effect" -void Log.init({ print: false }) +Log.init({ print: false }) function run(fn: (svc: Project.Interface) => Effect.Effect) { return Effect.runPromise( diff --git a/packages/opencode/test/share/share-next.test.ts b/packages/opencode/test/share/share-next.test.ts index ac3f7b79e0..2359f06a31 100644 --- a/packages/opencode/test/share/share-next.test.ts +++ b/packages/opencode/test/share/share-next.test.ts @@ -14,7 +14,7 @@ import { Session } from "../../src/session" import type { SessionID } from "../../src/session/schema" import { ShareNext } from "../../src/share" import { SessionShareTable } from "../../src/share/share.sql" -import { Database, eq } from "../../src/storage/db" +import { Database, eq } from "../../src/storage" import { provideTmpdirInstance } from "../fixture/fixture" import { resetDatabase } from "../fixture/db" import { testEffect } from "../lib/effect" diff --git a/packages/opencode/test/storage/db.test.ts b/packages/opencode/test/storage/db.test.ts index f6b6055595..7edc862c4c 100644 --- a/packages/opencode/test/storage/db.test.ts +++ b/packages/opencode/test/storage/db.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test" import path from "path" import { Global } from "../../src/global" import { Installation } from "../../src/installation" -import { Database } from "../../src/storage/db" +import { Database } from "../../src/storage" describe("Database.Path", () => { test("returns database path for the current channel", () => { diff --git a/packages/opencode/test/storage/json-migration.test.ts b/packages/opencode/test/storage/json-migration.test.ts index e76401ae75..019faf061c 100644 --- a/packages/opencode/test/storage/json-migration.test.ts +++ b/packages/opencode/test/storage/json-migration.test.ts @@ -5,7 +5,7 @@ import { migrate } from "drizzle-orm/bun-sqlite/migrator" import path from "path" import fs from "fs/promises" import { readFileSync, readdirSync } from "fs" -import { JsonMigration } from "../../src/storage/json-migration" +import { JsonMigration } from "../../src/storage" import { Global } from "../../src/global" import { ProjectTable } from "../../src/project/project.sql" import { ProjectID } from "../../src/project/schema" diff --git a/packages/opencode/test/storage/storage.test.ts b/packages/opencode/test/storage/storage.test.ts index 60b458bb30..c35244bb7a 100644 --- a/packages/opencode/test/storage/storage.test.ts +++ b/packages/opencode/test/storage/storage.test.ts @@ -5,7 +5,7 @@ import { AppFileSystem } from "@opencode-ai/shared/filesystem" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" import { Git } from "../../src/git" import { Global } from "../../src/global" -import { Storage } from "../../src/storage/storage" +import { Storage } from "../../src/storage" import { tmpdirScoped } from "../fixture/fixture" import { testEffect } from "../lib/effect" diff --git a/packages/opencode/test/sync/index.test.ts b/packages/opencode/test/sync/index.test.ts index 5304f4ea8b..2ba716cac0 100644 --- a/packages/opencode/test/sync/index.test.ts +++ b/packages/opencode/test/sync/index.test.ts @@ -4,7 +4,7 @@ import z from "zod" import { Bus } from "../../src/bus" import { Instance } from "../../src/project/instance" import { SyncEvent } from "../../src/sync" -import { Database } from "../../src/storage/db" +import { Database } from "../../src/storage" import { EventTable } from "../../src/sync/event.sql" import { Identifier } from "../../src/id/id" import { Flag } from "../../src/flag/flag" From 509bc11f81430575c58887960a02e63fa0107c03 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 23:30:52 -0400 Subject: [PATCH 218/300] feat: unwrap lsp namespaces to flat exports + barrel (#22748) --- packages/opencode/src/config/config.ts | 2 +- packages/opencode/src/lsp/client.ts | 408 +- packages/opencode/src/lsp/index.ts | 540 +-- packages/opencode/src/lsp/lsp.ts | 535 +++ packages/opencode/src/lsp/server.ts | 3618 +++++++++--------- packages/opencode/test/lsp/client.test.ts | 4 +- packages/opencode/test/lsp/index.test.ts | 2 +- packages/opencode/test/lsp/lifecycle.test.ts | 2 +- 8 files changed, 2554 insertions(+), 2557 deletions(-) create mode 100644 packages/opencode/src/lsp/lsp.ts diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 04801098b4..d8cfd5e48f 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -19,7 +19,7 @@ import { printParseErrorCode, } from "jsonc-parser" import { Instance, type InstanceContext } from "../project/instance" -import { LSPServer } from "../lsp/server" +import { LSPServer } from "../lsp" import { Installation } from "@/installation" import { ConfigMarkdown } from "." import { existsSync } from "fs" diff --git a/packages/opencode/src/lsp/client.ts b/packages/opencode/src/lsp/client.ts index 27301e79a7..fed2bf5c99 100644 --- a/packages/opencode/src/lsp/client.ts +++ b/packages/opencode/src/lsp/client.ts @@ -8,7 +8,7 @@ import { Log } from "../util" import { Process } from "../util" import { LANGUAGE_EXTENSIONS } from "./language" import z from "zod" -import type { LSPServer } from "./server" +import type { LSPServer } from "." import { NamedError } from "@opencode-ai/shared/util/error" import { withTimeout } from "../util/timeout" import { Instance } from "../project/instance" @@ -16,237 +16,235 @@ import { Filesystem } from "../util" const DIAGNOSTICS_DEBOUNCE_MS = 150 -export namespace LSPClient { - const log = Log.create({ service: "lsp.client" }) +const log = Log.create({ service: "lsp.client" }) - export type Info = NonNullable>> +export type Info = NonNullable>> - export type Diagnostic = VSCodeDiagnostic +export type Diagnostic = VSCodeDiagnostic - export const InitializeError = NamedError.create( - "LSPInitializeError", +export const InitializeError = NamedError.create( + "LSPInitializeError", + z.object({ + serverID: z.string(), + }), +) + +export const Event = { + Diagnostics: BusEvent.define( + "lsp.client.diagnostics", z.object({ serverID: z.string(), + path: z.string(), }), + ), +} + +export async function create(input: { serverID: string; server: LSPServer.Handle; root: string }) { + const l = log.clone().tag("serverID", input.serverID) + l.info("starting client") + + const connection = createMessageConnection( + new StreamMessageReader(input.server.process.stdout as any), + new StreamMessageWriter(input.server.process.stdin as any), ) - export const Event = { - Diagnostics: BusEvent.define( - "lsp.client.diagnostics", - z.object({ - serverID: z.string(), - path: z.string(), - }), - ), + const diagnostics = new Map() + connection.onNotification("textDocument/publishDiagnostics", (params) => { + const filePath = Filesystem.normalizePath(fileURLToPath(params.uri)) + l.info("textDocument/publishDiagnostics", { + path: filePath, + count: params.diagnostics.length, + }) + const exists = diagnostics.has(filePath) + diagnostics.set(filePath, params.diagnostics) + if (!exists && input.serverID === "typescript") return + Bus.publish(Event.Diagnostics, { path: filePath, serverID: input.serverID }) + }) + connection.onRequest("window/workDoneProgress/create", (params) => { + l.info("window/workDoneProgress/create", params) + return null + }) + connection.onRequest("workspace/configuration", async () => { + // Return server initialization options + return [input.server.initialization ?? {}] + }) + connection.onRequest("client/registerCapability", async () => {}) + connection.onRequest("client/unregisterCapability", async () => {}) + connection.onRequest("workspace/workspaceFolders", async () => [ + { + name: "workspace", + uri: pathToFileURL(input.root).href, + }, + ]) + connection.listen() + + l.info("sending initialize") + await withTimeout( + connection.sendRequest("initialize", { + rootUri: pathToFileURL(input.root).href, + processId: input.server.process.pid, + workspaceFolders: [ + { + name: "workspace", + uri: pathToFileURL(input.root).href, + }, + ], + initializationOptions: { + ...input.server.initialization, + }, + capabilities: { + window: { + workDoneProgress: true, + }, + workspace: { + configuration: true, + didChangeWatchedFiles: { + dynamicRegistration: true, + }, + }, + textDocument: { + synchronization: { + didOpen: true, + didChange: true, + }, + publishDiagnostics: { + versionSupport: true, + }, + }, + }, + }), + 45_000, + ).catch((err) => { + l.error("initialize error", { error: err }) + throw new InitializeError( + { serverID: input.serverID }, + { + cause: err, + }, + ) + }) + + await connection.sendNotification("initialized", {}) + + if (input.server.initialization) { + await connection.sendNotification("workspace/didChangeConfiguration", { + settings: input.server.initialization, + }) } - export async function create(input: { serverID: string; server: LSPServer.Handle; root: string }) { - const l = log.clone().tag("serverID", input.serverID) - l.info("starting client") + const files: { + [path: string]: number + } = {} - const connection = createMessageConnection( - new StreamMessageReader(input.server.process.stdout as any), - new StreamMessageWriter(input.server.process.stdin as any), - ) - - const diagnostics = new Map() - connection.onNotification("textDocument/publishDiagnostics", (params) => { - const filePath = Filesystem.normalizePath(fileURLToPath(params.uri)) - l.info("textDocument/publishDiagnostics", { - path: filePath, - count: params.diagnostics.length, - }) - const exists = diagnostics.has(filePath) - diagnostics.set(filePath, params.diagnostics) - if (!exists && input.serverID === "typescript") return - void Bus.publish(Event.Diagnostics, { path: filePath, serverID: input.serverID }) - }) - connection.onRequest("window/workDoneProgress/create", (params) => { - l.info("window/workDoneProgress/create", params) - return null - }) - connection.onRequest("workspace/configuration", async () => { - // Return server initialization options - return [input.server.initialization ?? {}] - }) - connection.onRequest("client/registerCapability", async () => {}) - connection.onRequest("client/unregisterCapability", async () => {}) - connection.onRequest("workspace/workspaceFolders", async () => [ - { - name: "workspace", - uri: pathToFileURL(input.root).href, - }, - ]) - connection.listen() - - l.info("sending initialize") - await withTimeout( - connection.sendRequest("initialize", { - rootUri: pathToFileURL(input.root).href, - processId: input.server.process.pid, - workspaceFolders: [ - { - name: "workspace", - uri: pathToFileURL(input.root).href, - }, - ], - initializationOptions: { - ...input.server.initialization, - }, - capabilities: { - window: { - workDoneProgress: true, - }, - workspace: { - configuration: true, - didChangeWatchedFiles: { - dynamicRegistration: true, - }, - }, - textDocument: { - synchronization: { - didOpen: true, - didChange: true, - }, - publishDiagnostics: { - versionSupport: true, - }, - }, - }, - }), - 45_000, - ).catch((err) => { - l.error("initialize error", { error: err }) - throw new InitializeError( - { serverID: input.serverID }, - { - cause: err, - }, - ) - }) - - await connection.sendNotification("initialized", {}) - - if (input.server.initialization) { - await connection.sendNotification("workspace/didChangeConfiguration", { - settings: input.server.initialization, - }) - } - - const files: { - [path: string]: number - } = {} - - const result = { - root: input.root, - get serverID() { - return input.serverID - }, - get connection() { - return connection - }, - notify: { - async open(input: { path: string }) { - input.path = path.isAbsolute(input.path) ? input.path : path.resolve(Instance.directory, input.path) - const text = await Filesystem.readText(input.path) - const extension = path.extname(input.path) - const languageId = LANGUAGE_EXTENSIONS[extension] ?? "plaintext" - - const version = files[input.path] - if (version !== undefined) { - log.info("workspace/didChangeWatchedFiles", input) - await connection.sendNotification("workspace/didChangeWatchedFiles", { - changes: [ - { - uri: pathToFileURL(input.path).href, - type: 2, // Changed - }, - ], - }) - - const next = version + 1 - files[input.path] = next - log.info("textDocument/didChange", { - path: input.path, - version: next, - }) - await connection.sendNotification("textDocument/didChange", { - textDocument: { - uri: pathToFileURL(input.path).href, - version: next, - }, - contentChanges: [{ text }], - }) - return - } + const result = { + root: input.root, + get serverID() { + return input.serverID + }, + get connection() { + return connection + }, + notify: { + async open(input: { path: string }) { + input.path = path.isAbsolute(input.path) ? input.path : path.resolve(Instance.directory, input.path) + const text = await Filesystem.readText(input.path) + const extension = path.extname(input.path) + const languageId = LANGUAGE_EXTENSIONS[extension] ?? "plaintext" + const version = files[input.path] + if (version !== undefined) { log.info("workspace/didChangeWatchedFiles", input) await connection.sendNotification("workspace/didChangeWatchedFiles", { changes: [ { uri: pathToFileURL(input.path).href, - type: 1, // Created + type: 2, // Changed }, ], }) - log.info("textDocument/didOpen", input) - diagnostics.delete(input.path) - await connection.sendNotification("textDocument/didOpen", { + const next = version + 1 + files[input.path] = next + log.info("textDocument/didChange", { + path: input.path, + version: next, + }) + await connection.sendNotification("textDocument/didChange", { textDocument: { uri: pathToFileURL(input.path).href, - languageId, - version: 0, - text, + version: next, }, + contentChanges: [{ text }], }) - files[input.path] = 0 return - }, + } + + log.info("workspace/didChangeWatchedFiles", input) + await connection.sendNotification("workspace/didChangeWatchedFiles", { + changes: [ + { + uri: pathToFileURL(input.path).href, + type: 1, // Created + }, + ], + }) + + log.info("textDocument/didOpen", input) + diagnostics.delete(input.path) + await connection.sendNotification("textDocument/didOpen", { + textDocument: { + uri: pathToFileURL(input.path).href, + languageId, + version: 0, + text, + }, + }) + files[input.path] = 0 + return }, - get diagnostics() { - return diagnostics - }, - async waitForDiagnostics(input: { path: string }) { - const normalizedPath = Filesystem.normalizePath( - path.isAbsolute(input.path) ? input.path : path.resolve(Instance.directory, input.path), - ) - log.info("waiting for diagnostics", { path: normalizedPath }) - let unsub: () => void - let debounceTimer: ReturnType | undefined - return await withTimeout( - new Promise((resolve) => { - unsub = Bus.subscribe(Event.Diagnostics, (event) => { - if (event.properties.path === normalizedPath && event.properties.serverID === result.serverID) { - // Debounce to allow LSP to send follow-up diagnostics (e.g., semantic after syntax) - if (debounceTimer) clearTimeout(debounceTimer) - debounceTimer = setTimeout(() => { - log.info("got diagnostics", { path: normalizedPath }) - unsub?.() - resolve() - }, DIAGNOSTICS_DEBOUNCE_MS) - } - }) - }), - 3000, - ) - .catch(() => {}) - .finally(() => { - if (debounceTimer) clearTimeout(debounceTimer) - unsub?.() + }, + get diagnostics() { + return diagnostics + }, + async waitForDiagnostics(input: { path: string }) { + const normalizedPath = Filesystem.normalizePath( + path.isAbsolute(input.path) ? input.path : path.resolve(Instance.directory, input.path), + ) + log.info("waiting for diagnostics", { path: normalizedPath }) + let unsub: () => void + let debounceTimer: ReturnType | undefined + return await withTimeout( + new Promise((resolve) => { + unsub = Bus.subscribe(Event.Diagnostics, (event) => { + if (event.properties.path === normalizedPath && event.properties.serverID === result.serverID) { + // Debounce to allow LSP to send follow-up diagnostics (e.g., semantic after syntax) + if (debounceTimer) clearTimeout(debounceTimer) + debounceTimer = setTimeout(() => { + log.info("got diagnostics", { path: normalizedPath }) + unsub?.() + resolve() + }, DIAGNOSTICS_DEBOUNCE_MS) + } }) - }, - async shutdown() { - l.info("shutting down") - connection.end() - connection.dispose() - await Process.stop(input.server.process) - l.info("shutdown") - }, - } - - l.info("initialized") - - return result + }), + 3000, + ) + .catch(() => {}) + .finally(() => { + if (debounceTimer) clearTimeout(debounceTimer) + unsub?.() + }) + }, + async shutdown() { + l.info("shutting down") + connection.end() + connection.dispose() + await Process.stop(input.server.process) + l.info("shutdown") + }, } + + l.info("initialized") + + return result } diff --git a/packages/opencode/src/lsp/index.ts b/packages/opencode/src/lsp/index.ts index 5146c40abe..9fc06fa21b 100644 --- a/packages/opencode/src/lsp/index.ts +++ b/packages/opencode/src/lsp/index.ts @@ -1,537 +1,3 @@ -import { BusEvent } from "@/bus/bus-event" -import { Bus } from "@/bus" -import { Log } from "../util" -import { LSPClient } from "./client" -import path from "path" -import { pathToFileURL, fileURLToPath } from "url" -import { LSPServer } from "./server" -import z from "zod" -import { Config } from "../config" -import { Instance } from "../project/instance" -import { Flag } from "@/flag/flag" -import { Process } from "../util" -import { spawn as lspspawn } from "./launch" -import { Effect, Layer, Context } from "effect" -import { InstanceState } from "@/effect" - -export namespace LSP { - const log = Log.create({ service: "lsp" }) - - export const Event = { - Updated: BusEvent.define("lsp.updated", z.object({})), - } - - export const Range = z - .object({ - start: z.object({ - line: z.number(), - character: z.number(), - }), - end: z.object({ - line: z.number(), - character: z.number(), - }), - }) - .meta({ - ref: "Range", - }) - export type Range = z.infer - - export const Symbol = z - .object({ - name: z.string(), - kind: z.number(), - location: z.object({ - uri: z.string(), - range: Range, - }), - }) - .meta({ - ref: "Symbol", - }) - export type Symbol = z.infer - - export const DocumentSymbol = z - .object({ - name: z.string(), - detail: z.string().optional(), - kind: z.number(), - range: Range, - selectionRange: Range, - }) - .meta({ - ref: "DocumentSymbol", - }) - export type DocumentSymbol = z.infer - - export const Status = z - .object({ - id: z.string(), - name: z.string(), - root: z.string(), - status: z.union([z.literal("connected"), z.literal("error")]), - }) - .meta({ - ref: "LSPStatus", - }) - export type Status = z.infer - - enum SymbolKind { - File = 1, - Module = 2, - Namespace = 3, - Package = 4, - Class = 5, - Method = 6, - Property = 7, - Field = 8, - Constructor = 9, - Enum = 10, - Interface = 11, - Function = 12, - Variable = 13, - Constant = 14, - String = 15, - Number = 16, - Boolean = 17, - Array = 18, - Object = 19, - Key = 20, - Null = 21, - EnumMember = 22, - Struct = 23, - Event = 24, - Operator = 25, - TypeParameter = 26, - } - - const kinds = [ - SymbolKind.Class, - SymbolKind.Function, - SymbolKind.Method, - SymbolKind.Interface, - SymbolKind.Variable, - SymbolKind.Constant, - SymbolKind.Struct, - SymbolKind.Enum, - ] - - const filterExperimentalServers = (servers: Record) => { - if (Flag.OPENCODE_EXPERIMENTAL_LSP_TY) { - if (servers["pyright"]) { - log.info("LSP server pyright is disabled because OPENCODE_EXPERIMENTAL_LSP_TY is enabled") - delete servers["pyright"] - } - } else { - if (servers["ty"]) { - delete servers["ty"] - } - } - } - - type LocInput = { file: string; line: number; character: number } - - interface State { - clients: LSPClient.Info[] - servers: Record - broken: Set - spawning: Map> - } - - export interface Interface { - readonly init: () => Effect.Effect - readonly status: () => Effect.Effect - readonly hasClients: (file: string) => Effect.Effect - readonly touchFile: (input: string, waitForDiagnostics?: boolean) => Effect.Effect - readonly diagnostics: () => Effect.Effect> - readonly hover: (input: LocInput) => Effect.Effect - readonly definition: (input: LocInput) => Effect.Effect - readonly references: (input: LocInput) => Effect.Effect - readonly implementation: (input: LocInput) => Effect.Effect - readonly documentSymbol: (uri: string) => Effect.Effect<(LSP.DocumentSymbol | LSP.Symbol)[]> - readonly workspaceSymbol: (query: string) => Effect.Effect - readonly prepareCallHierarchy: (input: LocInput) => Effect.Effect - readonly incomingCalls: (input: LocInput) => Effect.Effect - readonly outgoingCalls: (input: LocInput) => Effect.Effect - } - - export class Service extends Context.Service()("@opencode/LSP") {} - - export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const config = yield* Config.Service - - const state = yield* InstanceState.make( - Effect.fn("LSP.state")(function* () { - const cfg = yield* config.get() - - const servers: Record = {} - - if (cfg.lsp === false) { - log.info("all LSPs are disabled") - } else { - for (const server of Object.values(LSPServer)) { - servers[server.id] = server - } - - filterExperimentalServers(servers) - - for (const [name, item] of Object.entries(cfg.lsp ?? {})) { - const existing = servers[name] - if (item.disabled) { - log.info(`LSP server ${name} is disabled`) - delete servers[name] - continue - } - servers[name] = { - ...existing, - id: name, - root: existing?.root ?? (async () => Instance.directory), - extensions: item.extensions ?? existing?.extensions ?? [], - spawn: async (root) => ({ - process: lspspawn(item.command[0], item.command.slice(1), { - cwd: root, - env: { ...process.env, ...item.env }, - }), - initialization: item.initialization, - }), - } - } - - log.info("enabled LSP servers", { - serverIds: Object.values(servers) - .map((server) => server.id) - .join(", "), - }) - } - - const s: State = { - clients: [], - servers, - broken: new Set(), - spawning: new Map(), - } - - yield* Effect.addFinalizer(() => - Effect.promise(async () => { - await Promise.all(s.clients.map((client) => client.shutdown())) - }), - ) - - return s - }), - ) - - const getClients = Effect.fnUntraced(function* (file: string) { - if (!Instance.containsPath(file)) return [] as LSPClient.Info[] - const s = yield* InstanceState.get(state) - return yield* Effect.promise(async () => { - const extension = path.parse(file).ext || file - const result: LSPClient.Info[] = [] - - async function schedule(server: LSPServer.Info, root: string, key: string) { - const handle = await server - .spawn(root) - .then((value) => { - if (!value) s.broken.add(key) - return value - }) - .catch((err) => { - s.broken.add(key) - log.error(`Failed to spawn LSP server ${server.id}`, { error: err }) - return undefined - }) - - if (!handle) return undefined - log.info("spawned lsp server", { serverID: server.id, root }) - - const client = await LSPClient.create({ - serverID: server.id, - server: handle, - root, - }).catch(async (err) => { - s.broken.add(key) - await Process.stop(handle.process) - log.error(`Failed to initialize LSP client ${server.id}`, { error: err }) - return undefined - }) - - if (!client) return undefined - - const existing = s.clients.find((x) => x.root === root && x.serverID === server.id) - if (existing) { - await Process.stop(handle.process) - return existing - } - - s.clients.push(client) - return client - } - - for (const server of Object.values(s.servers)) { - if (server.extensions.length && !server.extensions.includes(extension)) continue - - const root = await server.root(file) - if (!root) continue - if (s.broken.has(root + server.id)) continue - - const match = s.clients.find((x) => x.root === root && x.serverID === server.id) - if (match) { - result.push(match) - continue - } - - const inflight = s.spawning.get(root + server.id) - if (inflight) { - const client = await inflight - if (!client) continue - result.push(client) - continue - } - - const task = schedule(server, root, root + server.id) - s.spawning.set(root + server.id, task) - - void task.finally(() => { - if (s.spawning.get(root + server.id) === task) { - s.spawning.delete(root + server.id) - } - }) - - const client = await task - if (!client) continue - - result.push(client) - void Bus.publish(Event.Updated, {}) - } - - return result - }) - }) - - const run = Effect.fnUntraced(function* (file: string, fn: (client: LSPClient.Info) => Promise) { - const clients = yield* getClients(file) - return yield* Effect.promise(() => Promise.all(clients.map((x) => fn(x)))) - }) - - const runAll = Effect.fnUntraced(function* (fn: (client: LSPClient.Info) => Promise) { - const s = yield* InstanceState.get(state) - return yield* Effect.promise(() => Promise.all(s.clients.map((x) => fn(x)))) - }) - - const init = Effect.fn("LSP.init")(function* () { - yield* InstanceState.get(state) - }) - - const status = Effect.fn("LSP.status")(function* () { - const s = yield* InstanceState.get(state) - const result: Status[] = [] - for (const client of s.clients) { - result.push({ - id: client.serverID, - name: s.servers[client.serverID].id, - root: path.relative(Instance.directory, client.root), - status: "connected", - }) - } - return result - }) - - const hasClients = Effect.fn("LSP.hasClients")(function* (file: string) { - const s = yield* InstanceState.get(state) - return yield* Effect.promise(async () => { - const extension = path.parse(file).ext || file - for (const server of Object.values(s.servers)) { - if (server.extensions.length && !server.extensions.includes(extension)) continue - const root = await server.root(file) - if (!root) continue - if (s.broken.has(root + server.id)) continue - return true - } - return false - }) - }) - - const touchFile = Effect.fn("LSP.touchFile")(function* (input: string, waitForDiagnostics?: boolean) { - log.info("touching file", { file: input }) - const clients = yield* getClients(input) - yield* Effect.promise(() => - Promise.all( - clients.map(async (client) => { - const wait = waitForDiagnostics ? client.waitForDiagnostics({ path: input }) : Promise.resolve() - await client.notify.open({ path: input }) - return wait - }), - ).catch((err) => { - log.error("failed to touch file", { err, file: input }) - }), - ) - }) - - const diagnostics = Effect.fn("LSP.diagnostics")(function* () { - const results: Record = {} - const all = yield* runAll(async (client) => client.diagnostics) - for (const result of all) { - for (const [p, diags] of result.entries()) { - const arr = results[p] || [] - arr.push(...diags) - results[p] = arr - } - } - return results - }) - - const hover = Effect.fn("LSP.hover")(function* (input: LocInput) { - return yield* run(input.file, (client) => - client.connection - .sendRequest("textDocument/hover", { - textDocument: { uri: pathToFileURL(input.file).href }, - position: { line: input.line, character: input.character }, - }) - .catch(() => null), - ) - }) - - const definition = Effect.fn("LSP.definition")(function* (input: LocInput) { - const results = yield* run(input.file, (client) => - client.connection - .sendRequest("textDocument/definition", { - textDocument: { uri: pathToFileURL(input.file).href }, - position: { line: input.line, character: input.character }, - }) - .catch(() => null), - ) - return results.flat().filter(Boolean) - }) - - const references = Effect.fn("LSP.references")(function* (input: LocInput) { - const results = yield* run(input.file, (client) => - client.connection - .sendRequest("textDocument/references", { - textDocument: { uri: pathToFileURL(input.file).href }, - position: { line: input.line, character: input.character }, - context: { includeDeclaration: true }, - }) - .catch(() => []), - ) - return results.flat().filter(Boolean) - }) - - const implementation = Effect.fn("LSP.implementation")(function* (input: LocInput) { - const results = yield* run(input.file, (client) => - client.connection - .sendRequest("textDocument/implementation", { - textDocument: { uri: pathToFileURL(input.file).href }, - position: { line: input.line, character: input.character }, - }) - .catch(() => null), - ) - return results.flat().filter(Boolean) - }) - - const documentSymbol = Effect.fn("LSP.documentSymbol")(function* (uri: string) { - const file = fileURLToPath(uri) - const results = yield* run(file, (client) => - client.connection.sendRequest("textDocument/documentSymbol", { textDocument: { uri } }).catch(() => []), - ) - return (results.flat() as (LSP.DocumentSymbol | LSP.Symbol)[]).filter(Boolean) - }) - - const workspaceSymbol = Effect.fn("LSP.workspaceSymbol")(function* (query: string) { - const results = yield* runAll((client) => - client.connection - .sendRequest("workspace/symbol", { query }) - .then((result: any) => result.filter((x: LSP.Symbol) => kinds.includes(x.kind))) - .then((result: any) => result.slice(0, 10)) - .catch(() => []), - ) - return results.flat() as LSP.Symbol[] - }) - - const prepareCallHierarchy = Effect.fn("LSP.prepareCallHierarchy")(function* (input: LocInput) { - const results = yield* run(input.file, (client) => - client.connection - .sendRequest("textDocument/prepareCallHierarchy", { - textDocument: { uri: pathToFileURL(input.file).href }, - position: { line: input.line, character: input.character }, - }) - .catch(() => []), - ) - return results.flat().filter(Boolean) - }) - - const callHierarchyRequest = Effect.fnUntraced(function* ( - input: LocInput, - direction: "callHierarchy/incomingCalls" | "callHierarchy/outgoingCalls", - ) { - const results = yield* run(input.file, async (client) => { - const items = (await client.connection - .sendRequest("textDocument/prepareCallHierarchy", { - textDocument: { uri: pathToFileURL(input.file).href }, - position: { line: input.line, character: input.character }, - }) - .catch(() => [])) as any[] - if (!items?.length) return [] - return client.connection.sendRequest(direction, { item: items[0] }).catch(() => []) - }) - return results.flat().filter(Boolean) - }) - - const incomingCalls = Effect.fn("LSP.incomingCalls")(function* (input: LocInput) { - return yield* callHierarchyRequest(input, "callHierarchy/incomingCalls") - }) - - const outgoingCalls = Effect.fn("LSP.outgoingCalls")(function* (input: LocInput) { - return yield* callHierarchyRequest(input, "callHierarchy/outgoingCalls") - }) - - return Service.of({ - init, - status, - hasClients, - touchFile, - diagnostics, - hover, - definition, - references, - implementation, - documentSymbol, - workspaceSymbol, - prepareCallHierarchy, - incomingCalls, - outgoingCalls, - }) - }), - ) - - export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer)) - - export namespace Diagnostic { - const MAX_PER_FILE = 20 - - export function pretty(diagnostic: LSPClient.Diagnostic) { - const severityMap = { - 1: "ERROR", - 2: "WARN", - 3: "INFO", - 4: "HINT", - } - - const severity = severityMap[diagnostic.severity || 1] - const line = diagnostic.range.start.line + 1 - const col = diagnostic.range.start.character + 1 - - return `${severity} [${line}:${col}] ${diagnostic.message}` - } - - export function report(file: string, issues: LSPClient.Diagnostic[]) { - const errors = issues.filter((item) => item.severity === 1) - if (errors.length === 0) return "" - const limited = errors.slice(0, MAX_PER_FILE) - const more = errors.length - MAX_PER_FILE - const suffix = more > 0 ? `\n... and ${more} more` : "" - return `\n${limited.map(pretty).join("\n")}${suffix}\n` - } - } -} +export * as LSP from "./lsp" +export * as LSPClient from "./client" +export * as LSPServer from "./server" diff --git a/packages/opencode/src/lsp/lsp.ts b/packages/opencode/src/lsp/lsp.ts new file mode 100644 index 0000000000..7f5b36313d --- /dev/null +++ b/packages/opencode/src/lsp/lsp.ts @@ -0,0 +1,535 @@ +import { BusEvent } from "@/bus/bus-event" +import { Bus } from "@/bus" +import { Log } from "../util" +import { LSPClient } from "." +import path from "path" +import { pathToFileURL, fileURLToPath } from "url" +import { LSPServer } from "." +import z from "zod" +import { Config } from "../config" +import { Instance } from "../project/instance" +import { Flag } from "@/flag/flag" +import { Process } from "../util" +import { spawn as lspspawn } from "./launch" +import { Effect, Layer, Context } from "effect" +import { InstanceState } from "@/effect" + +const log = Log.create({ service: "lsp" }) + +export const Event = { + Updated: BusEvent.define("lsp.updated", z.object({})), +} + +export const Range = z + .object({ + start: z.object({ + line: z.number(), + character: z.number(), + }), + end: z.object({ + line: z.number(), + character: z.number(), + }), + }) + .meta({ + ref: "Range", + }) +export type Range = z.infer + +export const Symbol = z + .object({ + name: z.string(), + kind: z.number(), + location: z.object({ + uri: z.string(), + range: Range, + }), + }) + .meta({ + ref: "Symbol", + }) +export type Symbol = z.infer + +export const DocumentSymbol = z + .object({ + name: z.string(), + detail: z.string().optional(), + kind: z.number(), + range: Range, + selectionRange: Range, + }) + .meta({ + ref: "DocumentSymbol", + }) +export type DocumentSymbol = z.infer + +export const Status = z + .object({ + id: z.string(), + name: z.string(), + root: z.string(), + status: z.union([z.literal("connected"), z.literal("error")]), + }) + .meta({ + ref: "LSPStatus", + }) +export type Status = z.infer + +enum SymbolKind { + File = 1, + Module = 2, + Namespace = 3, + Package = 4, + Class = 5, + Method = 6, + Property = 7, + Field = 8, + Constructor = 9, + Enum = 10, + Interface = 11, + Function = 12, + Variable = 13, + Constant = 14, + String = 15, + Number = 16, + Boolean = 17, + Array = 18, + Object = 19, + Key = 20, + Null = 21, + EnumMember = 22, + Struct = 23, + Event = 24, + Operator = 25, + TypeParameter = 26, +} + +const kinds = [ + SymbolKind.Class, + SymbolKind.Function, + SymbolKind.Method, + SymbolKind.Interface, + SymbolKind.Variable, + SymbolKind.Constant, + SymbolKind.Struct, + SymbolKind.Enum, +] + +const filterExperimentalServers = (servers: Record) => { + if (Flag.OPENCODE_EXPERIMENTAL_LSP_TY) { + if (servers["pyright"]) { + log.info("LSP server pyright is disabled because OPENCODE_EXPERIMENTAL_LSP_TY is enabled") + delete servers["pyright"] + } + } else { + if (servers["ty"]) { + delete servers["ty"] + } + } +} + +type LocInput = { file: string; line: number; character: number } + +interface State { + clients: LSPClient.Info[] + servers: Record + broken: Set + spawning: Map> +} + +export interface Interface { + readonly init: () => Effect.Effect + readonly status: () => Effect.Effect + readonly hasClients: (file: string) => Effect.Effect + readonly touchFile: (input: string, waitForDiagnostics?: boolean) => Effect.Effect + readonly diagnostics: () => Effect.Effect> + readonly hover: (input: LocInput) => Effect.Effect + readonly definition: (input: LocInput) => Effect.Effect + readonly references: (input: LocInput) => Effect.Effect + readonly implementation: (input: LocInput) => Effect.Effect + readonly documentSymbol: (uri: string) => Effect.Effect<(DocumentSymbol | Symbol)[]> + readonly workspaceSymbol: (query: string) => Effect.Effect + readonly prepareCallHierarchy: (input: LocInput) => Effect.Effect + readonly incomingCalls: (input: LocInput) => Effect.Effect + readonly outgoingCalls: (input: LocInput) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/LSP") {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const config = yield* Config.Service + + const state = yield* InstanceState.make( + Effect.fn("LSP.state")(function* () { + const cfg = yield* config.get() + + const servers: Record = {} + + if (cfg.lsp === false) { + log.info("all LSPs are disabled") + } else { + for (const server of Object.values(LSPServer)) { + servers[server.id] = server + } + + filterExperimentalServers(servers) + + for (const [name, item] of Object.entries(cfg.lsp ?? {})) { + const existing = servers[name] + if (item.disabled) { + log.info(`LSP server ${name} is disabled`) + delete servers[name] + continue + } + servers[name] = { + ...existing, + id: name, + root: existing?.root ?? (async () => Instance.directory), + extensions: item.extensions ?? existing?.extensions ?? [], + spawn: async (root) => ({ + process: lspspawn(item.command[0], item.command.slice(1), { + cwd: root, + env: { ...process.env, ...item.env }, + }), + initialization: item.initialization, + }), + } + } + + log.info("enabled LSP servers", { + serverIds: Object.values(servers) + .map((server) => server.id) + .join(", "), + }) + } + + const s: State = { + clients: [], + servers, + broken: new Set(), + spawning: new Map(), + } + + yield* Effect.addFinalizer(() => + Effect.promise(async () => { + await Promise.all(s.clients.map((client) => client.shutdown())) + }), + ) + + return s + }), + ) + + const getClients = Effect.fnUntraced(function* (file: string) { + if (!Instance.containsPath(file)) return [] as LSPClient.Info[] + const s = yield* InstanceState.get(state) + return yield* Effect.promise(async () => { + const extension = path.parse(file).ext || file + const result: LSPClient.Info[] = [] + + async function schedule(server: LSPServer.Info, root: string, key: string) { + const handle = await server + .spawn(root) + .then((value) => { + if (!value) s.broken.add(key) + return value + }) + .catch((err) => { + s.broken.add(key) + log.error(`Failed to spawn LSP server ${server.id}`, { error: err }) + return undefined + }) + + if (!handle) return undefined + log.info("spawned lsp server", { serverID: server.id, root }) + + const client = await LSPClient.create({ + serverID: server.id, + server: handle, + root, + }).catch(async (err) => { + s.broken.add(key) + await Process.stop(handle.process) + log.error(`Failed to initialize LSP client ${server.id}`, { error: err }) + return undefined + }) + + if (!client) return undefined + + const existing = s.clients.find((x) => x.root === root && x.serverID === server.id) + if (existing) { + await Process.stop(handle.process) + return existing + } + + s.clients.push(client) + return client + } + + for (const server of Object.values(s.servers)) { + if (server.extensions.length && !server.extensions.includes(extension)) continue + + const root = await server.root(file) + if (!root) continue + if (s.broken.has(root + server.id)) continue + + const match = s.clients.find((x) => x.root === root && x.serverID === server.id) + if (match) { + result.push(match) + continue + } + + const inflight = s.spawning.get(root + server.id) + if (inflight) { + const client = await inflight + if (!client) continue + result.push(client) + continue + } + + const task = schedule(server, root, root + server.id) + s.spawning.set(root + server.id, task) + + task.finally(() => { + if (s.spawning.get(root + server.id) === task) { + s.spawning.delete(root + server.id) + } + }) + + const client = await task + if (!client) continue + + result.push(client) + Bus.publish(Event.Updated, {}) + } + + return result + }) + }) + + const run = Effect.fnUntraced(function* (file: string, fn: (client: LSPClient.Info) => Promise) { + const clients = yield* getClients(file) + return yield* Effect.promise(() => Promise.all(clients.map((x) => fn(x)))) + }) + + const runAll = Effect.fnUntraced(function* (fn: (client: LSPClient.Info) => Promise) { + const s = yield* InstanceState.get(state) + return yield* Effect.promise(() => Promise.all(s.clients.map((x) => fn(x)))) + }) + + const init = Effect.fn("LSP.init")(function* () { + yield* InstanceState.get(state) + }) + + const status = Effect.fn("LSP.status")(function* () { + const s = yield* InstanceState.get(state) + const result: Status[] = [] + for (const client of s.clients) { + result.push({ + id: client.serverID, + name: s.servers[client.serverID].id, + root: path.relative(Instance.directory, client.root), + status: "connected", + }) + } + return result + }) + + const hasClients = Effect.fn("LSP.hasClients")(function* (file: string) { + const s = yield* InstanceState.get(state) + return yield* Effect.promise(async () => { + const extension = path.parse(file).ext || file + for (const server of Object.values(s.servers)) { + if (server.extensions.length && !server.extensions.includes(extension)) continue + const root = await server.root(file) + if (!root) continue + if (s.broken.has(root + server.id)) continue + return true + } + return false + }) + }) + + const touchFile = Effect.fn("LSP.touchFile")(function* (input: string, waitForDiagnostics?: boolean) { + log.info("touching file", { file: input }) + const clients = yield* getClients(input) + yield* Effect.promise(() => + Promise.all( + clients.map(async (client) => { + const wait = waitForDiagnostics ? client.waitForDiagnostics({ path: input }) : Promise.resolve() + await client.notify.open({ path: input }) + return wait + }), + ).catch((err) => { + log.error("failed to touch file", { err, file: input }) + }), + ) + }) + + const diagnostics = Effect.fn("LSP.diagnostics")(function* () { + const results: Record = {} + const all = yield* runAll(async (client) => client.diagnostics) + for (const result of all) { + for (const [p, diags] of result.entries()) { + const arr = results[p] || [] + arr.push(...diags) + results[p] = arr + } + } + return results + }) + + const hover = Effect.fn("LSP.hover")(function* (input: LocInput) { + return yield* run(input.file, (client) => + client.connection + .sendRequest("textDocument/hover", { + textDocument: { uri: pathToFileURL(input.file).href }, + position: { line: input.line, character: input.character }, + }) + .catch(() => null), + ) + }) + + const definition = Effect.fn("LSP.definition")(function* (input: LocInput) { + const results = yield* run(input.file, (client) => + client.connection + .sendRequest("textDocument/definition", { + textDocument: { uri: pathToFileURL(input.file).href }, + position: { line: input.line, character: input.character }, + }) + .catch(() => null), + ) + return results.flat().filter(Boolean) + }) + + const references = Effect.fn("LSP.references")(function* (input: LocInput) { + const results = yield* run(input.file, (client) => + client.connection + .sendRequest("textDocument/references", { + textDocument: { uri: pathToFileURL(input.file).href }, + position: { line: input.line, character: input.character }, + context: { includeDeclaration: true }, + }) + .catch(() => []), + ) + return results.flat().filter(Boolean) + }) + + const implementation = Effect.fn("LSP.implementation")(function* (input: LocInput) { + const results = yield* run(input.file, (client) => + client.connection + .sendRequest("textDocument/implementation", { + textDocument: { uri: pathToFileURL(input.file).href }, + position: { line: input.line, character: input.character }, + }) + .catch(() => null), + ) + return results.flat().filter(Boolean) + }) + + const documentSymbol = Effect.fn("LSP.documentSymbol")(function* (uri: string) { + const file = fileURLToPath(uri) + const results = yield* run(file, (client) => + client.connection.sendRequest("textDocument/documentSymbol", { textDocument: { uri } }).catch(() => []), + ) + return (results.flat() as (DocumentSymbol | Symbol)[]).filter(Boolean) + }) + + const workspaceSymbol = Effect.fn("LSP.workspaceSymbol")(function* (query: string) { + const results = yield* runAll((client) => + client.connection + .sendRequest("workspace/symbol", { query }) + .then((result: any) => result.filter((x: Symbol) => kinds.includes(x.kind))) + .then((result: any) => result.slice(0, 10)) + .catch(() => []), + ) + return results.flat() as Symbol[] + }) + + const prepareCallHierarchy = Effect.fn("LSP.prepareCallHierarchy")(function* (input: LocInput) { + const results = yield* run(input.file, (client) => + client.connection + .sendRequest("textDocument/prepareCallHierarchy", { + textDocument: { uri: pathToFileURL(input.file).href }, + position: { line: input.line, character: input.character }, + }) + .catch(() => []), + ) + return results.flat().filter(Boolean) + }) + + const callHierarchyRequest = Effect.fnUntraced(function* ( + input: LocInput, + direction: "callHierarchy/incomingCalls" | "callHierarchy/outgoingCalls", + ) { + const results = yield* run(input.file, async (client) => { + const items = (await client.connection + .sendRequest("textDocument/prepareCallHierarchy", { + textDocument: { uri: pathToFileURL(input.file).href }, + position: { line: input.line, character: input.character }, + }) + .catch(() => [])) as any[] + if (!items?.length) return [] + return client.connection.sendRequest(direction, { item: items[0] }).catch(() => []) + }) + return results.flat().filter(Boolean) + }) + + const incomingCalls = Effect.fn("LSP.incomingCalls")(function* (input: LocInput) { + return yield* callHierarchyRequest(input, "callHierarchy/incomingCalls") + }) + + const outgoingCalls = Effect.fn("LSP.outgoingCalls")(function* (input: LocInput) { + return yield* callHierarchyRequest(input, "callHierarchy/outgoingCalls") + }) + + return Service.of({ + init, + status, + hasClients, + touchFile, + diagnostics, + hover, + definition, + references, + implementation, + documentSymbol, + workspaceSymbol, + prepareCallHierarchy, + incomingCalls, + outgoingCalls, + }) + }), +) + +export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer)) + +export namespace Diagnostic { + const MAX_PER_FILE = 20 + + export function pretty(diagnostic: LSPClient.Diagnostic) { + const severityMap = { + 1: "ERROR", + 2: "WARN", + 3: "INFO", + 4: "HINT", + } + + const severity = severityMap[diagnostic.severity || 1] + const line = diagnostic.range.start.line + 1 + const col = diagnostic.range.start.character + 1 + + return `${severity} [${line}:${col}] ${diagnostic.message}` + } + + export function report(file: string, issues: LSPClient.Diagnostic[]) { + const errors = issues.filter((item) => item.severity === 1) + if (errors.length === 0) return "" + const limited = errors.slice(0, MAX_PER_FILE) + const more = errors.length - MAX_PER_FILE + const suffix = more > 0 ? `\n... and ${more} more` : "" + return `\n${limited.map(pretty).join("\n")}${suffix}\n` + } +} diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index 8110e86082..25aaaa36a4 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -15,972 +15,673 @@ import { Module } from "@opencode-ai/shared/util/module" import { spawn } from "./launch" import { Npm } from "../npm" -export namespace LSPServer { - const log = Log.create({ service: "lsp.server" }) - const pathExists = async (p: string) => - fs - .stat(p) - .then(() => true) - .catch(() => false) - const run = (cmd: string[], opts: Process.RunOptions = {}) => Process.run(cmd, { ...opts, nothrow: true }) - const output = (cmd: string[], opts: Process.RunOptions = {}) => Process.text(cmd, { ...opts, nothrow: true }) +const log = Log.create({ service: "lsp.server" }) +const pathExists = async (p: string) => + fs + .stat(p) + .then(() => true) + .catch(() => false) +const run = (cmd: string[], opts: Process.RunOptions = {}) => Process.run(cmd, { ...opts, nothrow: true }) +const output = (cmd: string[], opts: Process.RunOptions = {}) => Process.text(cmd, { ...opts, nothrow: true }) - export interface Handle { - process: ChildProcessWithoutNullStreams - initialization?: Record - } +export interface Handle { + process: ChildProcessWithoutNullStreams + initialization?: Record +} - type RootFunction = (file: string) => Promise +type RootFunction = (file: string) => Promise - const NearestRoot = (includePatterns: string[], excludePatterns?: string[]): RootFunction => { - return async (file) => { - if (excludePatterns) { - const excludedFiles = Filesystem.up({ - targets: excludePatterns, - start: path.dirname(file), - stop: Instance.directory, - }) - const excluded = await excludedFiles.next() - await excludedFiles.return() - if (excluded.value) return undefined - } - const files = Filesystem.up({ - targets: includePatterns, +const NearestRoot = (includePatterns: string[], excludePatterns?: string[]): RootFunction => { + return async (file) => { + if (excludePatterns) { + const excludedFiles = Filesystem.up({ + targets: excludePatterns, start: path.dirname(file), stop: Instance.directory, }) - const first = await files.next() - await files.return() - if (!first.value) return Instance.directory - return path.dirname(first.value) + const excluded = await excludedFiles.next() + await excludedFiles.return() + if (excluded.value) return undefined } + const files = Filesystem.up({ + targets: includePatterns, + start: path.dirname(file), + stop: Instance.directory, + }) + const first = await files.next() + await files.return() + if (!first.value) return Instance.directory + return path.dirname(first.value) } +} - export interface Info { - id: string - extensions: string[] - global?: boolean - root: RootFunction - spawn(root: string): Promise - } +export interface Info { + id: string + extensions: string[] + global?: boolean + root: RootFunction + spawn(root: string): Promise +} - export const Deno: Info = { - id: "deno", - root: async (file) => { - const files = Filesystem.up({ - targets: ["deno.json", "deno.jsonc"], - start: path.dirname(file), - stop: Instance.directory, - }) - const first = await files.next() - await files.return() - if (!first.value) return undefined - return path.dirname(first.value) - }, - extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs"], - async spawn(root) { - const deno = which("deno") - if (!deno) { - log.info("deno not found, please install deno first") - return - } - return { - process: spawn(deno, ["lsp"], { - cwd: root, - }), - } - }, - } - - export const Typescript: Info = { - id: "typescript", - root: NearestRoot( - ["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"], - ["deno.json", "deno.jsonc"], - ), - extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"], - async spawn(root) { - const tsserver = Module.resolve("typescript/lib/tsserver.js", Instance.directory) - log.info("typescript server", { tsserver }) - if (!tsserver) return - const bin = await Npm.which("typescript-language-server") - if (!bin) return - const proc = spawn(bin, ["--stdio"], { - cwd: root, - env: { - ...process.env, - }, - }) - return { - process: proc, - initialization: { - tsserver: { - path: tsserver, - }, - }, - } - }, - } - - export const Vue: Info = { - id: "vue", - extensions: [".vue"], - root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]), - async spawn(root) { - let binary = which("vue-language-server") - const args: string[] = [] - if (!binary) { - if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return - const resolved = await Npm.which("@vue/language-server") - if (!resolved) return - binary = resolved - } - args.push("--stdio") - const proc = spawn(binary, args, { - cwd: root, - env: { - ...process.env, - }, - }) - return { - process: proc, - initialization: { - // Leave empty; the server will auto-detect workspace TypeScript. - }, - } - }, - } - - export const ESLint: Info = { - id: "eslint", - root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]), - extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".vue"], - async spawn(root) { - const eslint = Module.resolve("eslint", Instance.directory) - if (!eslint) return - log.info("spawning eslint server") - const serverPath = path.join(Global.Path.bin, "vscode-eslint", "server", "out", "eslintServer.js") - if (!(await Filesystem.exists(serverPath))) { - if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return - log.info("downloading and building VS Code ESLint server") - const response = await fetch("https://github.com/microsoft/vscode-eslint/archive/refs/heads/main.zip") - if (!response.ok) return - - const zipPath = path.join(Global.Path.bin, "vscode-eslint.zip") - if (response.body) await Filesystem.writeStream(zipPath, response.body) - - const ok = await Archive.extractZip(zipPath, Global.Path.bin) - .then(() => true) - .catch((error) => { - log.error("Failed to extract vscode-eslint archive", { error }) - return false - }) - if (!ok) return - await fs.rm(zipPath, { force: true }) - - const extractedPath = path.join(Global.Path.bin, "vscode-eslint-main") - const finalPath = path.join(Global.Path.bin, "vscode-eslint") - - const stats = await fs.stat(finalPath).catch(() => undefined) - if (stats) { - log.info("removing old eslint installation", { path: finalPath }) - await fs.rm(finalPath, { force: true, recursive: true }) - } - await fs.rename(extractedPath, finalPath) - - const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm" - await Process.run([npmCmd, "install"], { cwd: finalPath }) - await Process.run([npmCmd, "run", "compile"], { cwd: finalPath }) - - log.info("installed VS Code ESLint server", { serverPath }) - } - - const proc = spawn("node", [serverPath, "--stdio"], { - cwd: root, - env: { - ...process.env, - }, - }) - - return { - process: proc, - } - }, - } - - export const Oxlint: Info = { - id: "oxlint", - root: NearestRoot([ - ".oxlintrc.json", - "package-lock.json", - "bun.lockb", - "bun.lock", - "pnpm-lock.yaml", - "yarn.lock", - "package.json", - ]), - extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".vue", ".astro", ".svelte"], - async spawn(root) { - const ext = process.platform === "win32" ? ".cmd" : "" - - const serverTarget = path.join("node_modules", ".bin", "oxc_language_server" + ext) - const lintTarget = path.join("node_modules", ".bin", "oxlint" + ext) - - const resolveBin = async (target: string) => { - const localBin = path.join(root, target) - if (await Filesystem.exists(localBin)) return localBin - - const candidates = Filesystem.up({ - targets: [target], - start: root, - stop: Instance.worktree, - }) - const first = await candidates.next() - await candidates.return() - if (first.value) return first.value - - return undefined - } - - let lintBin = await resolveBin(lintTarget) - if (!lintBin) { - const found = which("oxlint") - if (found) lintBin = found - } - - if (lintBin) { - const proc = spawn(lintBin, ["--help"]) - await proc.exited - if (proc.stdout) { - const help = await text(proc.stdout) - if (help.includes("--lsp")) { - return { - process: spawn(lintBin, ["--lsp"], { - cwd: root, - }), - } - } - } - } - - let serverBin = await resolveBin(serverTarget) - if (!serverBin) { - const found = which("oxc_language_server") - if (found) serverBin = found - } - if (serverBin) { - return { - process: spawn(serverBin, [], { - cwd: root, - }), - } - } - - log.info("oxlint not found, please install oxlint") +export const Deno: Info = { + id: "deno", + root: async (file) => { + const files = Filesystem.up({ + targets: ["deno.json", "deno.jsonc"], + start: path.dirname(file), + stop: Instance.directory, + }) + const first = await files.next() + await files.return() + if (!first.value) return undefined + return path.dirname(first.value) + }, + extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs"], + async spawn(root) { + const deno = which("deno") + if (!deno) { + log.info("deno not found, please install deno first") return - }, - } - - export const Biome: Info = { - id: "biome", - root: NearestRoot([ - "biome.json", - "biome.jsonc", - "package-lock.json", - "bun.lockb", - "bun.lock", - "pnpm-lock.yaml", - "yarn.lock", - ]), - extensions: [ - ".ts", - ".tsx", - ".js", - ".jsx", - ".mjs", - ".cjs", - ".mts", - ".cts", - ".json", - ".jsonc", - ".vue", - ".astro", - ".svelte", - ".css", - ".graphql", - ".gql", - ".html", - ], - async spawn(root) { - const localBin = path.join(root, "node_modules", ".bin", "biome") - let bin: string | undefined - if (await Filesystem.exists(localBin)) bin = localBin - if (!bin) { - const found = which("biome") - if (found) bin = found - } - - let args = ["lsp-proxy", "--stdio"] - - if (!bin) { - const resolved = Module.resolve("biome", root) - if (!resolved) return - bin = await Npm.which("biome") - if (!bin) return - args = ["lsp-proxy", "--stdio"] - } - - const proc = spawn(bin, args, { + } + return { + process: spawn(deno, ["lsp"], { cwd: root, - env: { - ...process.env, + }), + } + }, +} + +export const Typescript: Info = { + id: "typescript", + root: NearestRoot( + ["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"], + ["deno.json", "deno.jsonc"], + ), + extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"], + async spawn(root) { + const tsserver = Module.resolve("typescript/lib/tsserver.js", Instance.directory) + log.info("typescript server", { tsserver }) + if (!tsserver) return + const bin = await Npm.which("typescript-language-server") + if (!bin) return + const proc = spawn(bin, ["--stdio"], { + cwd: root, + env: { + ...process.env, + }, + }) + return { + process: proc, + initialization: { + tsserver: { + path: tsserver, }, + }, + } + }, +} + +export const Vue: Info = { + id: "vue", + extensions: [".vue"], + root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]), + async spawn(root) { + let binary = which("vue-language-server") + const args: string[] = [] + if (!binary) { + if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return + const resolved = await Npm.which("@vue/language-server") + if (!resolved) return + binary = resolved + } + args.push("--stdio") + const proc = spawn(binary, args, { + cwd: root, + env: { + ...process.env, + }, + }) + return { + process: proc, + initialization: { + // Leave empty; the server will auto-detect workspace TypeScript. + }, + } + }, +} + +export const ESLint: Info = { + id: "eslint", + root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]), + extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".vue"], + async spawn(root) { + const eslint = Module.resolve("eslint", Instance.directory) + if (!eslint) return + log.info("spawning eslint server") + const serverPath = path.join(Global.Path.bin, "vscode-eslint", "server", "out", "eslintServer.js") + if (!(await Filesystem.exists(serverPath))) { + if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return + log.info("downloading and building VS Code ESLint server") + const response = await fetch("https://github.com/microsoft/vscode-eslint/archive/refs/heads/main.zip") + if (!response.ok) return + + const zipPath = path.join(Global.Path.bin, "vscode-eslint.zip") + if (response.body) await Filesystem.writeStream(zipPath, response.body) + + const ok = await Archive.extractZip(zipPath, Global.Path.bin) + .then(() => true) + .catch((error) => { + log.error("Failed to extract vscode-eslint archive", { error }) + return false + }) + if (!ok) return + await fs.rm(zipPath, { force: true }) + + const extractedPath = path.join(Global.Path.bin, "vscode-eslint-main") + const finalPath = path.join(Global.Path.bin, "vscode-eslint") + + const stats = await fs.stat(finalPath).catch(() => undefined) + if (stats) { + log.info("removing old eslint installation", { path: finalPath }) + await fs.rm(finalPath, { force: true, recursive: true }) + } + await fs.rename(extractedPath, finalPath) + + const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm" + await Process.run([npmCmd, "install"], { cwd: finalPath }) + await Process.run([npmCmd, "run", "compile"], { cwd: finalPath }) + + log.info("installed VS Code ESLint server", { serverPath }) + } + + const proc = spawn("node", [serverPath, "--stdio"], { + cwd: root, + env: { + ...process.env, + }, + }) + + return { + process: proc, + } + }, +} + +export const Oxlint: Info = { + id: "oxlint", + root: NearestRoot([ + ".oxlintrc.json", + "package-lock.json", + "bun.lockb", + "bun.lock", + "pnpm-lock.yaml", + "yarn.lock", + "package.json", + ]), + extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".vue", ".astro", ".svelte"], + async spawn(root) { + const ext = process.platform === "win32" ? ".cmd" : "" + + const serverTarget = path.join("node_modules", ".bin", "oxc_language_server" + ext) + const lintTarget = path.join("node_modules", ".bin", "oxlint" + ext) + + const resolveBin = async (target: string) => { + const localBin = path.join(root, target) + if (await Filesystem.exists(localBin)) return localBin + + const candidates = Filesystem.up({ + targets: [target], + start: root, + stop: Instance.worktree, }) - - return { - process: proc, - } - }, - } - - export const Gopls: Info = { - id: "gopls", - root: async (file) => { - const work = await NearestRoot(["go.work"])(file) - if (work) return work - return NearestRoot(["go.mod", "go.sum"])(file) - }, - extensions: [".go"], - async spawn(root) { - let bin = which("gopls") - if (!bin) { - if (!which("go")) return - if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return - - log.info("installing gopls") - const proc = Process.spawn(["go", "install", "golang.org/x/tools/gopls@latest"], { - env: { ...process.env, GOBIN: Global.Path.bin }, - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - }) - const exit = await proc.exited - if (exit !== 0) { - log.error("Failed to install gopls") - return - } - bin = path.join(Global.Path.bin, "gopls" + (process.platform === "win32" ? ".exe" : "")) - log.info(`installed gopls`, { - bin, - }) - } - return { - process: spawn(bin!, { - cwd: root, - }), - } - }, - } - - export const Rubocop: Info = { - id: "ruby-lsp", - root: NearestRoot(["Gemfile"]), - extensions: [".rb", ".rake", ".gemspec", ".ru"], - async spawn(root) { - let bin = which("rubocop") - if (!bin) { - const ruby = which("ruby") - const gem = which("gem") - if (!ruby || !gem) { - log.info("Ruby not found, please install Ruby first") - return - } - if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return - log.info("installing rubocop") - const proc = Process.spawn(["gem", "install", "rubocop", "--bindir", Global.Path.bin], { - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - }) - const exit = await proc.exited - if (exit !== 0) { - log.error("Failed to install rubocop") - return - } - bin = path.join(Global.Path.bin, "rubocop" + (process.platform === "win32" ? ".exe" : "")) - log.info(`installed rubocop`, { - bin, - }) - } - return { - process: spawn(bin!, ["--lsp"], { - cwd: root, - }), - } - }, - } - - export const Ty: Info = { - id: "ty", - extensions: [".py", ".pyi"], - root: NearestRoot([ - "pyproject.toml", - "ty.toml", - "setup.py", - "setup.cfg", - "requirements.txt", - "Pipfile", - "pyrightconfig.json", - ]), - async spawn(root) { - if (!Flag.OPENCODE_EXPERIMENTAL_LSP_TY) { - return undefined - } - - let binary = which("ty") - - const initialization: Record = {} - - const potentialVenvPaths = [process.env["VIRTUAL_ENV"], path.join(root, ".venv"), path.join(root, "venv")].filter( - (p): p is string => p !== undefined, - ) - for (const venvPath of potentialVenvPaths) { - const isWindows = process.platform === "win32" - const potentialPythonPath = isWindows - ? path.join(venvPath, "Scripts", "python.exe") - : path.join(venvPath, "bin", "python") - if (await Filesystem.exists(potentialPythonPath)) { - initialization["pythonPath"] = potentialPythonPath - break - } - } - - if (!binary) { - for (const venvPath of potentialVenvPaths) { - const isWindows = process.platform === "win32" - const potentialTyPath = isWindows - ? path.join(venvPath, "Scripts", "ty.exe") - : path.join(venvPath, "bin", "ty") - if (await Filesystem.exists(potentialTyPath)) { - binary = potentialTyPath - break - } - } - } - - if (!binary) { - log.error("ty not found, please install ty first") - return - } - - const proc = spawn(binary, ["server"], { - cwd: root, - }) - - return { - process: proc, - initialization, - } - }, - } - - export const Pyright: Info = { - id: "pyright", - extensions: [".py", ".pyi"], - root: NearestRoot(["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile", "pyrightconfig.json"]), - async spawn(root) { - let binary = which("pyright-langserver") - const args = [] - if (!binary) { - if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return - const resolved = await Npm.which("pyright") - if (!resolved) return - binary = resolved - } - args.push("--stdio") - - const initialization: Record = {} - - const potentialVenvPaths = [process.env["VIRTUAL_ENV"], path.join(root, ".venv"), path.join(root, "venv")].filter( - (p): p is string => p !== undefined, - ) - for (const venvPath of potentialVenvPaths) { - const isWindows = process.platform === "win32" - const potentialPythonPath = isWindows - ? path.join(venvPath, "Scripts", "python.exe") - : path.join(venvPath, "bin", "python") - if (await Filesystem.exists(potentialPythonPath)) { - initialization["pythonPath"] = potentialPythonPath - break - } - } - - const proc = spawn(binary, args, { - cwd: root, - env: { - ...process.env, - }, - }) - return { - process: proc, - initialization, - } - }, - } - - export const ElixirLS: Info = { - id: "elixir-ls", - extensions: [".ex", ".exs"], - root: NearestRoot(["mix.exs", "mix.lock"]), - async spawn(root) { - let binary = which("elixir-ls") - if (!binary) { - const elixirLsPath = path.join(Global.Path.bin, "elixir-ls") - binary = path.join( - Global.Path.bin, - "elixir-ls-master", - "release", - process.platform === "win32" ? "language_server.bat" : "language_server.sh", - ) - - if (!(await Filesystem.exists(binary))) { - const elixir = which("elixir") - if (!elixir) { - log.error("elixir is required to run elixir-ls") - return - } - - if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return - log.info("downloading elixir-ls from GitHub releases") - - const response = await fetch("https://github.com/elixir-lsp/elixir-ls/archive/refs/heads/master.zip") - if (!response.ok) return - const zipPath = path.join(Global.Path.bin, "elixir-ls.zip") - if (response.body) await Filesystem.writeStream(zipPath, response.body) - - const ok = await Archive.extractZip(zipPath, Global.Path.bin) - .then(() => true) - .catch((error) => { - log.error("Failed to extract elixir-ls archive", { error }) - return false - }) - if (!ok) return - - await fs.rm(zipPath, { - force: true, - recursive: true, - }) - - const cwd = path.join(Global.Path.bin, "elixir-ls-master") - const env = { MIX_ENV: "prod", ...process.env } - await Process.run(["mix", "deps.get"], { cwd, env }) - await Process.run(["mix", "compile"], { cwd, env }) - await Process.run(["mix", "elixir_ls.release2", "-o", "release"], { cwd, env }) - - log.info(`installed elixir-ls`, { - path: elixirLsPath, - }) - } - } - - return { - process: spawn(binary, { - cwd: root, - }), - } - }, - } - - export const Zls: Info = { - id: "zls", - extensions: [".zig", ".zon"], - root: NearestRoot(["build.zig"]), - async spawn(root) { - let bin = which("zls") - - if (!bin) { - const zig = which("zig") - if (!zig) { - log.error("Zig is required to use zls. Please install Zig first.") - return - } - - if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return - log.info("downloading zls from GitHub releases") - - const releaseResponse = await fetch("https://api.github.com/repos/zigtools/zls/releases/latest") - if (!releaseResponse.ok) { - log.error("Failed to fetch zls release info") - return - } - - const release = (await releaseResponse.json()) as any - - const platform = process.platform - const arch = process.arch - let assetName = "" - - let zlsArch: string = arch - if (arch === "arm64") zlsArch = "aarch64" - else if (arch === "x64") zlsArch = "x86_64" - else if (arch === "ia32") zlsArch = "x86" - - let zlsPlatform: string = platform - if (platform === "darwin") zlsPlatform = "macos" - else if (platform === "win32") zlsPlatform = "windows" - - const ext = platform === "win32" ? "zip" : "tar.xz" - - assetName = `zls-${zlsArch}-${zlsPlatform}.${ext}` - - const supportedCombos = [ - "zls-x86_64-linux.tar.xz", - "zls-x86_64-macos.tar.xz", - "zls-x86_64-windows.zip", - "zls-aarch64-linux.tar.xz", - "zls-aarch64-macos.tar.xz", - "zls-aarch64-windows.zip", - "zls-x86-linux.tar.xz", - "zls-x86-windows.zip", - ] - - if (!supportedCombos.includes(assetName)) { - log.error(`Platform ${platform} and architecture ${arch} is not supported by zls`) - return - } - - const asset = release.assets.find((a: any) => a.name === assetName) - if (!asset) { - log.error(`Could not find asset ${assetName} in latest zls release`) - return - } - - const downloadUrl = asset.browser_download_url - const downloadResponse = await fetch(downloadUrl) - if (!downloadResponse.ok) { - log.error("Failed to download zls") - return - } - - const tempPath = path.join(Global.Path.bin, assetName) - if (downloadResponse.body) await Filesystem.writeStream(tempPath, downloadResponse.body) - - if (ext === "zip") { - const ok = await Archive.extractZip(tempPath, Global.Path.bin) - .then(() => true) - .catch((error) => { - log.error("Failed to extract zls archive", { error }) - return false - }) - if (!ok) return - } else { - await run(["tar", "-xf", tempPath], { cwd: Global.Path.bin }) - } - - await fs.rm(tempPath, { force: true }) - - bin = path.join(Global.Path.bin, "zls" + (platform === "win32" ? ".exe" : "")) - - if (!(await Filesystem.exists(bin))) { - log.error("Failed to extract zls binary") - return - } - - if (platform !== "win32") { - await fs.chmod(bin, 0o755).catch(() => {}) - } - - log.info(`installed zls`, { bin }) - } - - return { - process: spawn(bin, { - cwd: root, - }), - } - }, - } - - export const CSharp: Info = { - id: "csharp", - root: NearestRoot([".slnx", ".sln", ".csproj", "global.json"]), - extensions: [".cs"], - async spawn(root) { - let bin = which("csharp-ls") - if (!bin) { - if (!which("dotnet")) { - log.error(".NET SDK is required to install csharp-ls") - return - } - - if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return - log.info("installing csharp-ls via dotnet tool") - const proc = Process.spawn(["dotnet", "tool", "install", "csharp-ls", "--tool-path", Global.Path.bin], { - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - }) - const exit = await proc.exited - if (exit !== 0) { - log.error("Failed to install csharp-ls") - return - } - - bin = path.join(Global.Path.bin, "csharp-ls" + (process.platform === "win32" ? ".exe" : "")) - log.info(`installed csharp-ls`, { bin }) - } - - return { - process: spawn(bin, { - cwd: root, - }), - } - }, - } - - export const FSharp: Info = { - id: "fsharp", - root: NearestRoot([".slnx", ".sln", ".fsproj", "global.json"]), - extensions: [".fs", ".fsi", ".fsx", ".fsscript"], - async spawn(root) { - let bin = which("fsautocomplete") - if (!bin) { - if (!which("dotnet")) { - log.error(".NET SDK is required to install fsautocomplete") - return - } - - if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return - log.info("installing fsautocomplete via dotnet tool") - const proc = Process.spawn(["dotnet", "tool", "install", "fsautocomplete", "--tool-path", Global.Path.bin], { - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - }) - const exit = await proc.exited - if (exit !== 0) { - log.error("Failed to install fsautocomplete") - return - } - - bin = path.join(Global.Path.bin, "fsautocomplete" + (process.platform === "win32" ? ".exe" : "")) - log.info(`installed fsautocomplete`, { bin }) - } - - return { - process: spawn(bin, { - cwd: root, - }), - } - }, - } - - export const SourceKit: Info = { - id: "sourcekit-lsp", - extensions: [".swift", ".objc", "objcpp"], - root: NearestRoot(["Package.swift", "*.xcodeproj", "*.xcworkspace"]), - async spawn(root) { - // Check if sourcekit-lsp is available in the PATH - // This is installed with the Swift toolchain - const sourcekit = which("sourcekit-lsp") - if (sourcekit) { - return { - process: spawn(sourcekit, { - cwd: root, - }), - } - } - - // If sourcekit-lsp not found, check if xcrun is available - // This is specific to macOS where sourcekit-lsp is typically installed with Xcode - if (!which("xcrun")) return - - const lspLoc = await output(["xcrun", "--find", "sourcekit-lsp"]) - - if (lspLoc.code !== 0) return - - const bin = lspLoc.text.trim() - - return { - process: spawn(bin, { - cwd: root, - }), - } - }, - } - - export const RustAnalyzer: Info = { - id: "rust", - root: async (root) => { - const crateRoot = await NearestRoot(["Cargo.toml", "Cargo.lock"])(root) - if (crateRoot === undefined) { - return undefined - } - let currentDir = crateRoot - - while (currentDir !== path.dirname(currentDir)) { - // Stop at filesystem root - const cargoTomlPath = path.join(currentDir, "Cargo.toml") - try { - const cargoTomlContent = await Filesystem.readText(cargoTomlPath) - if (cargoTomlContent.includes("[workspace]")) { - return currentDir - } - } catch { - // File doesn't exist or can't be read, continue searching up - } - - const parentDir = path.dirname(currentDir) - if (parentDir === currentDir) break // Reached filesystem root - currentDir = parentDir - - // Stop if we've gone above the app root - if (!currentDir.startsWith(Instance.worktree)) break - } - - return crateRoot - }, - extensions: [".rs"], - async spawn(root) { - const bin = which("rust-analyzer") - if (!bin) { - log.info("rust-analyzer not found in path, please install it") - return - } - return { - process: spawn(bin, { - cwd: root, - }), - } - }, - } - - export const Clangd: Info = { - id: "clangd", - root: NearestRoot(["compile_commands.json", "compile_flags.txt", ".clangd"]), - extensions: [".c", ".cpp", ".cc", ".cxx", ".c++", ".h", ".hpp", ".hh", ".hxx", ".h++"], - async spawn(root) { - const args = ["--background-index", "--clang-tidy"] - const fromPath = which("clangd") - if (fromPath) { - return { - process: spawn(fromPath, args, { - cwd: root, - }), - } - } - - const ext = process.platform === "win32" ? ".exe" : "" - const direct = path.join(Global.Path.bin, "clangd" + ext) - if (await Filesystem.exists(direct)) { - return { - process: spawn(direct, args, { - cwd: root, - }), - } - } - - const entries = await fs.readdir(Global.Path.bin, { withFileTypes: true }).catch(() => []) - for (const entry of entries) { - if (!entry.isDirectory()) continue - if (!entry.name.startsWith("clangd_")) continue - const candidate = path.join(Global.Path.bin, entry.name, "bin", "clangd" + ext) - if (await Filesystem.exists(candidate)) { + const first = await candidates.next() + await candidates.return() + if (first.value) return first.value + + return undefined + } + + let lintBin = await resolveBin(lintTarget) + if (!lintBin) { + const found = which("oxlint") + if (found) lintBin = found + } + + if (lintBin) { + const proc = spawn(lintBin, ["--help"]) + await proc.exited + if (proc.stdout) { + const help = await text(proc.stdout) + if (help.includes("--lsp")) { return { - process: spawn(candidate, args, { + process: spawn(lintBin, ["--lsp"], { cwd: root, }), } } } + } + let serverBin = await resolveBin(serverTarget) + if (!serverBin) { + const found = which("oxc_language_server") + if (found) serverBin = found + } + if (serverBin) { + return { + process: spawn(serverBin, [], { + cwd: root, + }), + } + } + + log.info("oxlint not found, please install oxlint") + return + }, +} + +export const Biome: Info = { + id: "biome", + root: NearestRoot([ + "biome.json", + "biome.jsonc", + "package-lock.json", + "bun.lockb", + "bun.lock", + "pnpm-lock.yaml", + "yarn.lock", + ]), + extensions: [ + ".ts", + ".tsx", + ".js", + ".jsx", + ".mjs", + ".cjs", + ".mts", + ".cts", + ".json", + ".jsonc", + ".vue", + ".astro", + ".svelte", + ".css", + ".graphql", + ".gql", + ".html", + ], + async spawn(root) { + const localBin = path.join(root, "node_modules", ".bin", "biome") + let bin: string | undefined + if (await Filesystem.exists(localBin)) bin = localBin + if (!bin) { + const found = which("biome") + if (found) bin = found + } + + let args = ["lsp-proxy", "--stdio"] + + if (!bin) { + const resolved = Module.resolve("biome", root) + if (!resolved) return + bin = await Npm.which("biome") + if (!bin) return + args = ["lsp-proxy", "--stdio"] + } + + const proc = spawn(bin, args, { + cwd: root, + env: { + ...process.env, + }, + }) + + return { + process: proc, + } + }, +} + +export const Gopls: Info = { + id: "gopls", + root: async (file) => { + const work = await NearestRoot(["go.work"])(file) + if (work) return work + return NearestRoot(["go.mod", "go.sum"])(file) + }, + extensions: [".go"], + async spawn(root) { + let bin = which("gopls") + if (!bin) { + if (!which("go")) return if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return - log.info("downloading clangd from GitHub releases") - const releaseResponse = await fetch("https://api.github.com/repos/clangd/clangd/releases/latest") - if (!releaseResponse.ok) { - log.error("Failed to fetch clangd release info") + log.info("installing gopls") + const proc = Process.spawn(["go", "install", "golang.org/x/tools/gopls@latest"], { + env: { ...process.env, GOBIN: Global.Path.bin }, + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + }) + const exit = await proc.exited + if (exit !== 0) { + log.error("Failed to install gopls") return } + bin = path.join(Global.Path.bin, "gopls" + (process.platform === "win32" ? ".exe" : "")) + log.info(`installed gopls`, { + bin, + }) + } + return { + process: spawn(bin!, { + cwd: root, + }), + } + }, +} - const release: { - tag_name?: string - assets?: { name?: string; browser_download_url?: string }[] - } = await releaseResponse.json() - - const tag = release.tag_name - if (!tag) { - log.error("clangd release did not include a tag name") +export const Rubocop: Info = { + id: "ruby-lsp", + root: NearestRoot(["Gemfile"]), + extensions: [".rb", ".rake", ".gemspec", ".ru"], + async spawn(root) { + let bin = which("rubocop") + if (!bin) { + const ruby = which("ruby") + const gem = which("gem") + if (!ruby || !gem) { + log.info("Ruby not found, please install Ruby first") return } - const platform = process.platform - const tokens: Record = { - darwin: "mac", - linux: "linux", - win32: "windows", - } - const token = tokens[platform] - if (!token) { - log.error(`Platform ${platform} is not supported by clangd auto-download`) + if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return + log.info("installing rubocop") + const proc = Process.spawn(["gem", "install", "rubocop", "--bindir", Global.Path.bin], { + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + }) + const exit = await proc.exited + if (exit !== 0) { + log.error("Failed to install rubocop") return } + bin = path.join(Global.Path.bin, "rubocop" + (process.platform === "win32" ? ".exe" : "")) + log.info(`installed rubocop`, { + bin, + }) + } + return { + process: spawn(bin!, ["--lsp"], { + cwd: root, + }), + } + }, +} - const assets = release.assets ?? [] - const valid = (item: { name?: string; browser_download_url?: string }) => { - if (!item.name) return false - if (!item.browser_download_url) return false - if (!item.name.includes(token)) return false - return item.name.includes(tag) +export const Ty: Info = { + id: "ty", + extensions: [".py", ".pyi"], + root: NearestRoot([ + "pyproject.toml", + "ty.toml", + "setup.py", + "setup.cfg", + "requirements.txt", + "Pipfile", + "pyrightconfig.json", + ]), + async spawn(root) { + if (!Flag.OPENCODE_EXPERIMENTAL_LSP_TY) { + return undefined + } + + let binary = which("ty") + + const initialization: Record = {} + + const potentialVenvPaths = [process.env["VIRTUAL_ENV"], path.join(root, ".venv"), path.join(root, "venv")].filter( + (p): p is string => p !== undefined, + ) + for (const venvPath of potentialVenvPaths) { + const isWindows = process.platform === "win32" + const potentialPythonPath = isWindows + ? path.join(venvPath, "Scripts", "python.exe") + : path.join(venvPath, "bin", "python") + if (await Filesystem.exists(potentialPythonPath)) { + initialization["pythonPath"] = potentialPythonPath + break } + } - const asset = - assets.find((item) => valid(item) && item.name?.endsWith(".zip")) ?? - assets.find((item) => valid(item) && item.name?.endsWith(".tar.xz")) ?? - assets.find((item) => valid(item)) - if (!asset?.name || !asset.browser_download_url) { - log.error("clangd could not match release asset", { tag, platform }) - return + if (!binary) { + for (const venvPath of potentialVenvPaths) { + const isWindows = process.platform === "win32" + const potentialTyPath = isWindows + ? path.join(venvPath, "Scripts", "ty.exe") + : path.join(venvPath, "bin", "ty") + if (await Filesystem.exists(potentialTyPath)) { + binary = potentialTyPath + break + } } + } - const name = asset.name - const downloadResponse = await fetch(asset.browser_download_url) - if (!downloadResponse.ok) { - log.error("Failed to download clangd") - return + if (!binary) { + log.error("ty not found, please install ty first") + return + } + + const proc = spawn(binary, ["server"], { + cwd: root, + }) + + return { + process: proc, + initialization, + } + }, +} + +export const Pyright: Info = { + id: "pyright", + extensions: [".py", ".pyi"], + root: NearestRoot(["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile", "pyrightconfig.json"]), + async spawn(root) { + let binary = which("pyright-langserver") + const args = [] + if (!binary) { + if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return + const resolved = await Npm.which("pyright") + if (!resolved) return + binary = resolved + } + args.push("--stdio") + + const initialization: Record = {} + + const potentialVenvPaths = [process.env["VIRTUAL_ENV"], path.join(root, ".venv"), path.join(root, "venv")].filter( + (p): p is string => p !== undefined, + ) + for (const venvPath of potentialVenvPaths) { + const isWindows = process.platform === "win32" + const potentialPythonPath = isWindows + ? path.join(venvPath, "Scripts", "python.exe") + : path.join(venvPath, "bin", "python") + if (await Filesystem.exists(potentialPythonPath)) { + initialization["pythonPath"] = potentialPythonPath + break } + } - const archive = path.join(Global.Path.bin, name) - const buf = await downloadResponse.arrayBuffer() - if (buf.byteLength === 0) { - log.error("Failed to write clangd archive") - return - } - await Filesystem.write(archive, Buffer.from(buf)) + const proc = spawn(binary, args, { + cwd: root, + env: { + ...process.env, + }, + }) + return { + process: proc, + initialization, + } + }, +} - const zip = name.endsWith(".zip") - const tar = name.endsWith(".tar.xz") - if (!zip && !tar) { - log.error("clangd encountered unsupported asset", { asset: name }) - return - } +export const ElixirLS: Info = { + id: "elixir-ls", + extensions: [".ex", ".exs"], + root: NearestRoot(["mix.exs", "mix.lock"]), + async spawn(root) { + let binary = which("elixir-ls") + if (!binary) { + const elixirLsPath = path.join(Global.Path.bin, "elixir-ls") + binary = path.join( + Global.Path.bin, + "elixir-ls-master", + "release", + process.platform === "win32" ? "language_server.bat" : "language_server.sh", + ) - if (zip) { - const ok = await Archive.extractZip(archive, Global.Path.bin) + if (!(await Filesystem.exists(binary))) { + const elixir = which("elixir") + if (!elixir) { + log.error("elixir is required to run elixir-ls") + return + } + + if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return + log.info("downloading elixir-ls from GitHub releases") + + const response = await fetch("https://github.com/elixir-lsp/elixir-ls/archive/refs/heads/master.zip") + if (!response.ok) return + const zipPath = path.join(Global.Path.bin, "elixir-ls.zip") + if (response.body) await Filesystem.writeStream(zipPath, response.body) + + const ok = await Archive.extractZip(zipPath, Global.Path.bin) .then(() => true) .catch((error) => { - log.error("Failed to extract clangd archive", { error }) + log.error("Failed to extract elixir-ls archive", { error }) return false }) if (!ok) return - } - if (tar) { - await run(["tar", "-xf", archive], { cwd: Global.Path.bin }) - } - await fs.rm(archive, { force: true }) - const bin = path.join(Global.Path.bin, "clangd_" + tag, "bin", "clangd" + ext) + await fs.rm(zipPath, { + force: true, + recursive: true, + }) + + const cwd = path.join(Global.Path.bin, "elixir-ls-master") + const env = { MIX_ENV: "prod", ...process.env } + await Process.run(["mix", "deps.get"], { cwd, env }) + await Process.run(["mix", "compile"], { cwd, env }) + await Process.run(["mix", "elixir_ls.release2", "-o", "release"], { cwd, env }) + + log.info(`installed elixir-ls`, { + path: elixirLsPath, + }) + } + } + + return { + process: spawn(binary, { + cwd: root, + }), + } + }, +} + +export const Zls: Info = { + id: "zls", + extensions: [".zig", ".zon"], + root: NearestRoot(["build.zig"]), + async spawn(root) { + let bin = which("zls") + + if (!bin) { + const zig = which("zig") + if (!zig) { + log.error("Zig is required to use zls. Please install Zig first.") + return + } + + if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return + log.info("downloading zls from GitHub releases") + + const releaseResponse = await fetch("https://api.github.com/repos/zigtools/zls/releases/latest") + if (!releaseResponse.ok) { + log.error("Failed to fetch zls release info") + return + } + + const release = (await releaseResponse.json()) as any + + const platform = process.platform + const arch = process.arch + let assetName = "" + + let zlsArch: string = arch + if (arch === "arm64") zlsArch = "aarch64" + else if (arch === "x64") zlsArch = "x86_64" + else if (arch === "ia32") zlsArch = "x86" + + let zlsPlatform: string = platform + if (platform === "darwin") zlsPlatform = "macos" + else if (platform === "win32") zlsPlatform = "windows" + + const ext = platform === "win32" ? "zip" : "tar.xz" + + assetName = `zls-${zlsArch}-${zlsPlatform}.${ext}` + + const supportedCombos = [ + "zls-x86_64-linux.tar.xz", + "zls-x86_64-macos.tar.xz", + "zls-x86_64-windows.zip", + "zls-aarch64-linux.tar.xz", + "zls-aarch64-macos.tar.xz", + "zls-aarch64-windows.zip", + "zls-x86-linux.tar.xz", + "zls-x86-windows.zip", + ] + + if (!supportedCombos.includes(assetName)) { + log.error(`Platform ${platform} and architecture ${arch} is not supported by zls`) + return + } + + const asset = release.assets.find((a: any) => a.name === assetName) + if (!asset) { + log.error(`Could not find asset ${assetName} in latest zls release`) + return + } + + const downloadUrl = asset.browser_download_url + const downloadResponse = await fetch(downloadUrl) + if (!downloadResponse.ok) { + log.error("Failed to download zls") + return + } + + const tempPath = path.join(Global.Path.bin, assetName) + if (downloadResponse.body) await Filesystem.writeStream(tempPath, downloadResponse.body) + + if (ext === "zip") { + const ok = await Archive.extractZip(tempPath, Global.Path.bin) + .then(() => true) + .catch((error) => { + log.error("Failed to extract zls archive", { error }) + return false + }) + if (!ok) return + } else { + await run(["tar", "-xf", tempPath], { cwd: Global.Path.bin }) + } + + await fs.rm(tempPath, { force: true }) + + bin = path.join(Global.Path.bin, "zls" + (platform === "win32" ? ".exe" : "")) + if (!(await Filesystem.exists(bin))) { - log.error("Failed to extract clangd binary") + log.error("Failed to extract zls binary") return } @@ -988,971 +689,1268 @@ export namespace LSPServer { await fs.chmod(bin, 0o755).catch(() => {}) } - await fs.unlink(path.join(Global.Path.bin, "clangd")).catch(() => {}) - await fs.symlink(bin, path.join(Global.Path.bin, "clangd")).catch(() => {}) + log.info(`installed zls`, { bin }) + } - log.info(`installed clangd`, { bin }) + return { + process: spawn(bin, { + cwd: root, + }), + } + }, +} +export const CSharp: Info = { + id: "csharp", + root: NearestRoot([".slnx", ".sln", ".csproj", "global.json"]), + extensions: [".cs"], + async spawn(root) { + let bin = which("csharp-ls") + if (!bin) { + if (!which("dotnet")) { + log.error(".NET SDK is required to install csharp-ls") + return + } + + if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return + log.info("installing csharp-ls via dotnet tool") + const proc = Process.spawn(["dotnet", "tool", "install", "csharp-ls", "--tool-path", Global.Path.bin], { + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + }) + const exit = await proc.exited + if (exit !== 0) { + log.error("Failed to install csharp-ls") + return + } + + bin = path.join(Global.Path.bin, "csharp-ls" + (process.platform === "win32" ? ".exe" : "")) + log.info(`installed csharp-ls`, { bin }) + } + + return { + process: spawn(bin, { + cwd: root, + }), + } + }, +} + +export const FSharp: Info = { + id: "fsharp", + root: NearestRoot([".slnx", ".sln", ".fsproj", "global.json"]), + extensions: [".fs", ".fsi", ".fsx", ".fsscript"], + async spawn(root) { + let bin = which("fsautocomplete") + if (!bin) { + if (!which("dotnet")) { + log.error(".NET SDK is required to install fsautocomplete") + return + } + + if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return + log.info("installing fsautocomplete via dotnet tool") + const proc = Process.spawn(["dotnet", "tool", "install", "fsautocomplete", "--tool-path", Global.Path.bin], { + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + }) + const exit = await proc.exited + if (exit !== 0) { + log.error("Failed to install fsautocomplete") + return + } + + bin = path.join(Global.Path.bin, "fsautocomplete" + (process.platform === "win32" ? ".exe" : "")) + log.info(`installed fsautocomplete`, { bin }) + } + + return { + process: spawn(bin, { + cwd: root, + }), + } + }, +} + +export const SourceKit: Info = { + id: "sourcekit-lsp", + extensions: [".swift", ".objc", "objcpp"], + root: NearestRoot(["Package.swift", "*.xcodeproj", "*.xcworkspace"]), + async spawn(root) { + // Check if sourcekit-lsp is available in the PATH + // This is installed with the Swift toolchain + const sourcekit = which("sourcekit-lsp") + if (sourcekit) { return { - process: spawn(bin, args, { + process: spawn(sourcekit, { cwd: root, }), } - }, - } + } - export const Svelte: Info = { - id: "svelte", - extensions: [".svelte"], - root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]), - async spawn(root) { - let binary = which("svelteserver") - const args: string[] = [] - if (!binary) { - if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return - const resolved = await Npm.which("svelte-language-server") - if (!resolved) return - binary = resolved - } - args.push("--stdio") - const proc = spawn(binary, args, { + // If sourcekit-lsp not found, check if xcrun is available + // This is specific to macOS where sourcekit-lsp is typically installed with Xcode + if (!which("xcrun")) return + + const lspLoc = await output(["xcrun", "--find", "sourcekit-lsp"]) + + if (lspLoc.code !== 0) return + + const bin = lspLoc.text.trim() + + return { + process: spawn(bin, { cwd: root, - env: { - ...process.env, - }, - }) - return { - process: proc, - initialization: {}, - } - }, - } + }), + } + }, +} - export const Astro: Info = { - id: "astro", - extensions: [".astro"], - root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]), - async spawn(root) { - const tsserver = Module.resolve("typescript/lib/tsserver.js", Instance.directory) - if (!tsserver) { - log.info("typescript not found, required for Astro language server") - return - } - const tsdk = path.dirname(tsserver) +export const RustAnalyzer: Info = { + id: "rust", + root: async (root) => { + const crateRoot = await NearestRoot(["Cargo.toml", "Cargo.lock"])(root) + if (crateRoot === undefined) { + return undefined + } + let currentDir = crateRoot - let binary = which("astro-ls") - const args: string[] = [] - if (!binary) { - if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return - const resolved = await Npm.which("@astrojs/language-server") - if (!resolved) return - binary = resolved + while (currentDir !== path.dirname(currentDir)) { + // Stop at filesystem root + const cargoTomlPath = path.join(currentDir, "Cargo.toml") + try { + const cargoTomlContent = await Filesystem.readText(cargoTomlPath) + if (cargoTomlContent.includes("[workspace]")) { + return currentDir + } + } catch { + // File doesn't exist or can't be read, continue searching up } - args.push("--stdio") - const proc = spawn(binary, args, { + + const parentDir = path.dirname(currentDir) + if (parentDir === currentDir) break // Reached filesystem root + currentDir = parentDir + + // Stop if we've gone above the app root + if (!currentDir.startsWith(Instance.worktree)) break + } + + return crateRoot + }, + extensions: [".rs"], + async spawn(root) { + const bin = which("rust-analyzer") + if (!bin) { + log.info("rust-analyzer not found in path, please install it") + return + } + return { + process: spawn(bin, { cwd: root, - env: { - ...process.env, - }, - }) + }), + } + }, +} + +export const Clangd: Info = { + id: "clangd", + root: NearestRoot(["compile_commands.json", "compile_flags.txt", ".clangd"]), + extensions: [".c", ".cpp", ".cc", ".cxx", ".c++", ".h", ".hpp", ".hh", ".hxx", ".h++"], + async spawn(root) { + const args = ["--background-index", "--clang-tidy"] + const fromPath = which("clangd") + if (fromPath) { return { - process: proc, - initialization: { - typescript: { - tsdk, - }, - }, + process: spawn(fromPath, args, { + cwd: root, + }), } - }, - } + } - export const JDTLS: Info = { - id: "jdtls", - root: async (file) => { - // Without exclusions, NearestRoot defaults to instance directory so we can't - // distinguish between a) no project found and b) project found at instance dir. - // So we can't choose the root from (potential) monorepo markers first. - // Look for potential subproject markers first while excluding potential monorepo markers. - const settingsMarkers = ["settings.gradle", "settings.gradle.kts"] - const gradleMarkers = ["gradlew", "gradlew.bat"] - const exclusionsForMonorepos = gradleMarkers.concat(settingsMarkers) - - const [projectRoot, wrapperRoot, settingsRoot] = await Promise.all([ - NearestRoot( - ["pom.xml", "build.gradle", "build.gradle.kts", ".project", ".classpath"], - exclusionsForMonorepos, - )(file), - NearestRoot(gradleMarkers, settingsMarkers)(file), - NearestRoot(settingsMarkers)(file), - ]) - - // If projectRoot is undefined we know we are in a monorepo or no project at all. - // So can safely fall through to the other roots - if (projectRoot) return projectRoot - if (wrapperRoot) return wrapperRoot - if (settingsRoot) return settingsRoot - }, - extensions: [".java"], - async spawn(root) { - const java = which("java") - if (!java) { - log.error("Java 21 or newer is required to run the JDTLS. Please install it first.") - return - } - const javaMajorVersion = await run(["java", "-version"]).then((result) => { - const m = /"(\d+)\.\d+\.\d+"/.exec(result.stderr.toString()) - return !m ? undefined : parseInt(m[1]) - }) - if (javaMajorVersion == null || javaMajorVersion < 21) { - log.error("JDTLS requires at least Java 21.") - return - } - const distPath = path.join(Global.Path.bin, "jdtls") - const launcherDir = path.join(distPath, "plugins") - const installed = await pathExists(launcherDir) - if (!installed) { - if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return - log.info("Downloading JDTLS LSP server.") - await fs.mkdir(distPath, { recursive: true }) - const releaseURL = - "https://www.eclipse.org/downloads/download.php?file=/jdtls/snapshots/jdt-language-server-latest.tar.gz" - const archiveName = "release.tar.gz" - - log.info("Downloading JDTLS archive", { url: releaseURL, dest: distPath }) - const download = await fetch(releaseURL) - if (!download.ok || !download.body) { - log.error("Failed to download JDTLS", { status: download.status, statusText: download.statusText }) - return - } - await Filesystem.writeStream(path.join(distPath, archiveName), download.body) - - log.info("Extracting JDTLS archive") - const tarResult = await run(["tar", "-xzf", archiveName], { cwd: distPath }) - if (tarResult.code !== 0) { - log.error("Failed to extract JDTLS", { exitCode: tarResult.code, stderr: tarResult.stderr.toString() }) - return - } - - await fs.rm(path.join(distPath, archiveName), { force: true }) - log.info("JDTLS download and extraction completed") - } - const jarFileName = - (await fs.readdir(launcherDir).catch(() => [])) - .find((item) => /^org\.eclipse\.equinox\.launcher_.*\.jar$/.test(item)) - ?.trim() ?? "" - const launcherJar = path.join(launcherDir, jarFileName) - if (!(await pathExists(launcherJar))) { - log.error(`Failed to locate the JDTLS launcher module in the installed directory: ${distPath}.`) - return - } - const configFile = path.join( - distPath, - (() => { - switch (process.platform) { - case "darwin": - return "config_mac" - case "linux": - return "config_linux" - case "win32": - return "config_win" - default: - return "config_linux" - } - })(), - ) - const dataDir = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-jdtls-data")) + const ext = process.platform === "win32" ? ".exe" : "" + const direct = path.join(Global.Path.bin, "clangd" + ext) + if (await Filesystem.exists(direct)) { return { - process: spawn( - java, - [ - "-jar", - launcherJar, - "-configuration", - configFile, - "-data", - dataDir, - "-Declipse.application=org.eclipse.jdt.ls.core.id1", - "-Dosgi.bundles.defaultStartLevel=4", - "-Declipse.product=org.eclipse.jdt.ls.core.product", - "-Dlog.level=ALL", - "--add-modules=ALL-SYSTEM", - "--add-opens java.base/java.util=ALL-UNNAMED", - "--add-opens java.base/java.lang=ALL-UNNAMED", - ], - { + process: spawn(direct, args, { + cwd: root, + }), + } + } + + const entries = await fs.readdir(Global.Path.bin, { withFileTypes: true }).catch(() => []) + for (const entry of entries) { + if (!entry.isDirectory()) continue + if (!entry.name.startsWith("clangd_")) continue + const candidate = path.join(Global.Path.bin, entry.name, "bin", "clangd" + ext) + if (await Filesystem.exists(candidate)) { + return { + process: spawn(candidate, args, { cwd: root, - }, - ), + }), + } } - }, - } + } - export const KotlinLS: Info = { - id: "kotlin-ls", - extensions: [".kt", ".kts"], - root: async (file) => { - // 1) Nearest Gradle root (multi-project or included build) - const settingsRoot = await NearestRoot(["settings.gradle.kts", "settings.gradle"])(file) - if (settingsRoot) return settingsRoot - // 2) Gradle wrapper (strong root signal) - const wrapperRoot = await NearestRoot(["gradlew", "gradlew.bat"])(file) - if (wrapperRoot) return wrapperRoot - // 3) Single-project or module-level build - const buildRoot = await NearestRoot(["build.gradle.kts", "build.gradle"])(file) - if (buildRoot) return buildRoot - // 4) Maven fallback - return NearestRoot(["pom.xml"])(file) - }, - async spawn(root) { - const distPath = path.join(Global.Path.bin, "kotlin-ls") - const launcherScript = - process.platform === "win32" ? path.join(distPath, "kotlin-lsp.cmd") : path.join(distPath, "kotlin-lsp.sh") - const installed = await Filesystem.exists(launcherScript) - if (!installed) { - if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return - log.info("Downloading Kotlin Language Server from GitHub.") + if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return + log.info("downloading clangd from GitHub releases") - const releaseResponse = await fetch("https://api.github.com/repos/Kotlin/kotlin-lsp/releases/latest") - if (!releaseResponse.ok) { - log.error("Failed to fetch kotlin-lsp release info") - return + const releaseResponse = await fetch("https://api.github.com/repos/clangd/clangd/releases/latest") + if (!releaseResponse.ok) { + log.error("Failed to fetch clangd release info") + return + } + + const release: { + tag_name?: string + assets?: { name?: string; browser_download_url?: string }[] + } = await releaseResponse.json() + + const tag = release.tag_name + if (!tag) { + log.error("clangd release did not include a tag name") + return + } + const platform = process.platform + const tokens: Record = { + darwin: "mac", + linux: "linux", + win32: "windows", + } + const token = tokens[platform] + if (!token) { + log.error(`Platform ${platform} is not supported by clangd auto-download`) + return + } + + const assets = release.assets ?? [] + const valid = (item: { name?: string; browser_download_url?: string }) => { + if (!item.name) return false + if (!item.browser_download_url) return false + if (!item.name.includes(token)) return false + return item.name.includes(tag) + } + + const asset = + assets.find((item) => valid(item) && item.name?.endsWith(".zip")) ?? + assets.find((item) => valid(item) && item.name?.endsWith(".tar.xz")) ?? + assets.find((item) => valid(item)) + if (!asset?.name || !asset.browser_download_url) { + log.error("clangd could not match release asset", { tag, platform }) + return + } + + const name = asset.name + const downloadResponse = await fetch(asset.browser_download_url) + if (!downloadResponse.ok) { + log.error("Failed to download clangd") + return + } + + const archive = path.join(Global.Path.bin, name) + const buf = await downloadResponse.arrayBuffer() + if (buf.byteLength === 0) { + log.error("Failed to write clangd archive") + return + } + await Filesystem.write(archive, Buffer.from(buf)) + + const zip = name.endsWith(".zip") + const tar = name.endsWith(".tar.xz") + if (!zip && !tar) { + log.error("clangd encountered unsupported asset", { asset: name }) + return + } + + if (zip) { + const ok = await Archive.extractZip(archive, Global.Path.bin) + .then(() => true) + .catch((error) => { + log.error("Failed to extract clangd archive", { error }) + return false + }) + if (!ok) return + } + if (tar) { + await run(["tar", "-xf", archive], { cwd: Global.Path.bin }) + } + await fs.rm(archive, { force: true }) + + const bin = path.join(Global.Path.bin, "clangd_" + tag, "bin", "clangd" + ext) + if (!(await Filesystem.exists(bin))) { + log.error("Failed to extract clangd binary") + return + } + + if (platform !== "win32") { + await fs.chmod(bin, 0o755).catch(() => {}) + } + + await fs.unlink(path.join(Global.Path.bin, "clangd")).catch(() => {}) + await fs.symlink(bin, path.join(Global.Path.bin, "clangd")).catch(() => {}) + + log.info(`installed clangd`, { bin }) + + return { + process: spawn(bin, args, { + cwd: root, + }), + } + }, +} + +export const Svelte: Info = { + id: "svelte", + extensions: [".svelte"], + root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]), + async spawn(root) { + let binary = which("svelteserver") + const args: string[] = [] + if (!binary) { + if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return + const resolved = await Npm.which("svelte-language-server") + if (!resolved) return + binary = resolved + } + args.push("--stdio") + const proc = spawn(binary, args, { + cwd: root, + env: { + ...process.env, + }, + }) + return { + process: proc, + initialization: {}, + } + }, +} + +export const Astro: Info = { + id: "astro", + extensions: [".astro"], + root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]), + async spawn(root) { + const tsserver = Module.resolve("typescript/lib/tsserver.js", Instance.directory) + if (!tsserver) { + log.info("typescript not found, required for Astro language server") + return + } + const tsdk = path.dirname(tsserver) + + let binary = which("astro-ls") + const args: string[] = [] + if (!binary) { + if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return + const resolved = await Npm.which("@astrojs/language-server") + if (!resolved) return + binary = resolved + } + args.push("--stdio") + const proc = spawn(binary, args, { + cwd: root, + env: { + ...process.env, + }, + }) + return { + process: proc, + initialization: { + typescript: { + tsdk, + }, + }, + } + }, +} + +export const JDTLS: Info = { + id: "jdtls", + root: async (file) => { + // Without exclusions, NearestRoot defaults to instance directory so we can't + // distinguish between a) no project found and b) project found at instance dir. + // So we can't choose the root from (potential) monorepo markers first. + // Look for potential subproject markers first while excluding potential monorepo markers. + const settingsMarkers = ["settings.gradle", "settings.gradle.kts"] + const gradleMarkers = ["gradlew", "gradlew.bat"] + const exclusionsForMonorepos = gradleMarkers.concat(settingsMarkers) + + const [projectRoot, wrapperRoot, settingsRoot] = await Promise.all([ + NearestRoot( + ["pom.xml", "build.gradle", "build.gradle.kts", ".project", ".classpath"], + exclusionsForMonorepos, + )(file), + NearestRoot(gradleMarkers, settingsMarkers)(file), + NearestRoot(settingsMarkers)(file), + ]) + + // If projectRoot is undefined we know we are in a monorepo or no project at all. + // So can safely fall through to the other roots + if (projectRoot) return projectRoot + if (wrapperRoot) return wrapperRoot + if (settingsRoot) return settingsRoot + }, + extensions: [".java"], + async spawn(root) { + const java = which("java") + if (!java) { + log.error("Java 21 or newer is required to run the JDTLS. Please install it first.") + return + } + const javaMajorVersion = await run(["java", "-version"]).then((result) => { + const m = /"(\d+)\.\d+\.\d+"/.exec(result.stderr.toString()) + return !m ? undefined : parseInt(m[1]) + }) + if (javaMajorVersion == null || javaMajorVersion < 21) { + log.error("JDTLS requires at least Java 21.") + return + } + const distPath = path.join(Global.Path.bin, "jdtls") + const launcherDir = path.join(distPath, "plugins") + const installed = await pathExists(launcherDir) + if (!installed) { + if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return + log.info("Downloading JDTLS LSP server.") + await fs.mkdir(distPath, { recursive: true }) + const releaseURL = + "https://www.eclipse.org/downloads/download.php?file=/jdtls/snapshots/jdt-language-server-latest.tar.gz" + const archiveName = "release.tar.gz" + + log.info("Downloading JDTLS archive", { url: releaseURL, dest: distPath }) + const download = await fetch(releaseURL) + if (!download.ok || !download.body) { + log.error("Failed to download JDTLS", { status: download.status, statusText: download.statusText }) + return + } + await Filesystem.writeStream(path.join(distPath, archiveName), download.body) + + log.info("Extracting JDTLS archive") + const tarResult = await run(["tar", "-xzf", archiveName], { cwd: distPath }) + if (tarResult.code !== 0) { + log.error("Failed to extract JDTLS", { exitCode: tarResult.code, stderr: tarResult.stderr.toString() }) + return + } + + await fs.rm(path.join(distPath, archiveName), { force: true }) + log.info("JDTLS download and extraction completed") + } + const jarFileName = + (await fs.readdir(launcherDir).catch(() => [])) + .find((item) => /^org\.eclipse\.equinox\.launcher_.*\.jar$/.test(item)) + ?.trim() ?? "" + const launcherJar = path.join(launcherDir, jarFileName) + if (!(await pathExists(launcherJar))) { + log.error(`Failed to locate the JDTLS launcher module in the installed directory: ${distPath}.`) + return + } + const configFile = path.join( + distPath, + (() => { + switch (process.platform) { + case "darwin": + return "config_mac" + case "linux": + return "config_linux" + case "win32": + return "config_win" + default: + return "config_linux" } + })(), + ) + const dataDir = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-jdtls-data")) + return { + process: spawn( + java, + [ + "-jar", + launcherJar, + "-configuration", + configFile, + "-data", + dataDir, + "-Declipse.application=org.eclipse.jdt.ls.core.id1", + "-Dosgi.bundles.defaultStartLevel=4", + "-Declipse.product=org.eclipse.jdt.ls.core.product", + "-Dlog.level=ALL", + "--add-modules=ALL-SYSTEM", + "--add-opens java.base/java.util=ALL-UNNAMED", + "--add-opens java.base/java.lang=ALL-UNNAMED", + ], + { + cwd: root, + }, + ), + } + }, +} - const release = await releaseResponse.json() - const version = release.name?.replace(/^v/, "") +export const KotlinLS: Info = { + id: "kotlin-ls", + extensions: [".kt", ".kts"], + root: async (file) => { + // 1) Nearest Gradle root (multi-project or included build) + const settingsRoot = await NearestRoot(["settings.gradle.kts", "settings.gradle"])(file) + if (settingsRoot) return settingsRoot + // 2) Gradle wrapper (strong root signal) + const wrapperRoot = await NearestRoot(["gradlew", "gradlew.bat"])(file) + if (wrapperRoot) return wrapperRoot + // 3) Single-project or module-level build + const buildRoot = await NearestRoot(["build.gradle.kts", "build.gradle"])(file) + if (buildRoot) return buildRoot + // 4) Maven fallback + return NearestRoot(["pom.xml"])(file) + }, + async spawn(root) { + const distPath = path.join(Global.Path.bin, "kotlin-ls") + const launcherScript = + process.platform === "win32" ? path.join(distPath, "kotlin-lsp.cmd") : path.join(distPath, "kotlin-lsp.sh") + const installed = await Filesystem.exists(launcherScript) + if (!installed) { + if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return + log.info("Downloading Kotlin Language Server from GitHub.") - if (!version) { - log.error("Could not determine Kotlin LSP version from release") - return - } + const releaseResponse = await fetch("https://api.github.com/repos/Kotlin/kotlin-lsp/releases/latest") + if (!releaseResponse.ok) { + log.error("Failed to fetch kotlin-lsp release info") + return + } - const platform = process.platform - const arch = process.arch + const release = await releaseResponse.json() + const version = release.name?.replace(/^v/, "") - let kotlinArch: string = arch - if (arch === "arm64") kotlinArch = "aarch64" - else if (arch === "x64") kotlinArch = "x64" + if (!version) { + log.error("Could not determine Kotlin LSP version from release") + return + } - let kotlinPlatform: string = platform - if (platform === "darwin") kotlinPlatform = "mac" - else if (platform === "linux") kotlinPlatform = "linux" - else if (platform === "win32") kotlinPlatform = "win" + const platform = process.platform + const arch = process.arch - const supportedCombos = ["mac-x64", "mac-aarch64", "linux-x64", "linux-aarch64", "win-x64", "win-aarch64"] + let kotlinArch: string = arch + if (arch === "arm64") kotlinArch = "aarch64" + else if (arch === "x64") kotlinArch = "x64" - const combo = `${kotlinPlatform}-${kotlinArch}` + let kotlinPlatform: string = platform + if (platform === "darwin") kotlinPlatform = "mac" + else if (platform === "linux") kotlinPlatform = "linux" + else if (platform === "win32") kotlinPlatform = "win" - if (!supportedCombos.includes(combo)) { - log.error(`Platform ${platform}/${arch} is not supported by Kotlin LSP`) - return - } + const supportedCombos = ["mac-x64", "mac-aarch64", "linux-x64", "linux-aarch64", "win-x64", "win-aarch64"] - const assetName = `kotlin-lsp-${version}-${kotlinPlatform}-${kotlinArch}.zip` - const releaseURL = `https://download-cdn.jetbrains.com/kotlin-lsp/${version}/${assetName}` + const combo = `${kotlinPlatform}-${kotlinArch}` - await fs.mkdir(distPath, { recursive: true }) - const archivePath = path.join(distPath, "kotlin-ls.zip") - const download = await fetch(releaseURL) - if (!download.ok || !download.body) { - log.error("Failed to download Kotlin Language Server", { - status: download.status, - statusText: download.statusText, - }) - return - } - await Filesystem.writeStream(archivePath, download.body) - const ok = await Archive.extractZip(archivePath, distPath) + if (!supportedCombos.includes(combo)) { + log.error(`Platform ${platform}/${arch} is not supported by Kotlin LSP`) + return + } + + const assetName = `kotlin-lsp-${version}-${kotlinPlatform}-${kotlinArch}.zip` + const releaseURL = `https://download-cdn.jetbrains.com/kotlin-lsp/${version}/${assetName}` + + await fs.mkdir(distPath, { recursive: true }) + const archivePath = path.join(distPath, "kotlin-ls.zip") + const download = await fetch(releaseURL) + if (!download.ok || !download.body) { + log.error("Failed to download Kotlin Language Server", { + status: download.status, + statusText: download.statusText, + }) + return + } + await Filesystem.writeStream(archivePath, download.body) + const ok = await Archive.extractZip(archivePath, distPath) + .then(() => true) + .catch((error) => { + log.error("Failed to extract Kotlin LS archive", { error }) + return false + }) + if (!ok) return + await fs.rm(archivePath, { force: true }) + if (process.platform !== "win32") { + await fs.chmod(launcherScript, 0o755).catch(() => {}) + } + log.info("Installed Kotlin Language Server", { path: launcherScript }) + } + if (!(await Filesystem.exists(launcherScript))) { + log.error(`Failed to locate the Kotlin LS launcher script in the installed directory: ${distPath}.`) + return + } + return { + process: spawn(launcherScript, ["--stdio"], { + cwd: root, + }), + } + }, +} + +export const YamlLS: Info = { + id: "yaml-ls", + extensions: [".yaml", ".yml"], + root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]), + async spawn(root) { + let binary = which("yaml-language-server") + const args: string[] = [] + if (!binary) { + if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return + const resolved = await Npm.which("yaml-language-server") + if (!resolved) return + binary = resolved + } + args.push("--stdio") + const proc = spawn(binary, args, { + cwd: root, + env: { + ...process.env, + }, + }) + return { + process: proc, + } + }, +} + +export const LuaLS: Info = { + id: "lua-ls", + root: NearestRoot([ + ".luarc.json", + ".luarc.jsonc", + ".luacheckrc", + ".stylua.toml", + "stylua.toml", + "selene.toml", + "selene.yml", + ]), + extensions: [".lua"], + async spawn(root) { + let bin = which("lua-language-server") + + if (!bin) { + if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return + log.info("downloading lua-language-server from GitHub releases") + + const releaseResponse = await fetch("https://api.github.com/repos/LuaLS/lua-language-server/releases/latest") + if (!releaseResponse.ok) { + log.error("Failed to fetch lua-language-server release info") + return + } + + const release = await releaseResponse.json() + + const platform = process.platform + const arch = process.arch + let assetName = "" + + let lualsArch: string = arch + if (arch === "arm64") lualsArch = "arm64" + else if (arch === "x64") lualsArch = "x64" + else if (arch === "ia32") lualsArch = "ia32" + + let lualsPlatform: string = platform + if (platform === "darwin") lualsPlatform = "darwin" + else if (platform === "linux") lualsPlatform = "linux" + else if (platform === "win32") lualsPlatform = "win32" + + const ext = platform === "win32" ? "zip" : "tar.gz" + + assetName = `lua-language-server-${release.tag_name}-${lualsPlatform}-${lualsArch}.${ext}` + + const supportedCombos = [ + "darwin-arm64.tar.gz", + "darwin-x64.tar.gz", + "linux-x64.tar.gz", + "linux-arm64.tar.gz", + "win32-x64.zip", + "win32-ia32.zip", + ] + + const assetSuffix = `${lualsPlatform}-${lualsArch}.${ext}` + if (!supportedCombos.includes(assetSuffix)) { + log.error(`Platform ${platform} and architecture ${arch} is not supported by lua-language-server`) + return + } + + const asset = release.assets.find((a: any) => a.name === assetName) + if (!asset) { + log.error(`Could not find asset ${assetName} in latest lua-language-server release`) + return + } + + const downloadUrl = asset.browser_download_url + const downloadResponse = await fetch(downloadUrl) + if (!downloadResponse.ok) { + log.error("Failed to download lua-language-server") + return + } + + const tempPath = path.join(Global.Path.bin, assetName) + if (downloadResponse.body) await Filesystem.writeStream(tempPath, downloadResponse.body) + + // Unlike zls which is a single self-contained binary, + // lua-language-server needs supporting files (meta/, locale/, etc.) + // Extract entire archive to dedicated directory to preserve all files + const installDir = path.join(Global.Path.bin, `lua-language-server-${lualsArch}-${lualsPlatform}`) + + // Remove old installation if exists + const stats = await fs.stat(installDir).catch(() => undefined) + if (stats) { + await fs.rm(installDir, { force: true, recursive: true }) + } + + await fs.mkdir(installDir, { recursive: true }) + + if (ext === "zip") { + const ok = await Archive.extractZip(tempPath, installDir) .then(() => true) .catch((error) => { - log.error("Failed to extract Kotlin LS archive", { error }) + log.error("Failed to extract lua-language-server archive", { error }) + return false + }) + if (!ok) return + } else { + const ok = await run(["tar", "-xzf", tempPath, "-C", installDir]) + .then((result) => result.code === 0) + .catch((error: unknown) => { + log.error("Failed to extract lua-language-server archive", { error }) return false }) if (!ok) return - await fs.rm(archivePath, { force: true }) - if (process.platform !== "win32") { - await fs.chmod(launcherScript, 0o755).catch(() => {}) - } - log.info("Installed Kotlin Language Server", { path: launcherScript }) } - if (!(await Filesystem.exists(launcherScript))) { - log.error(`Failed to locate the Kotlin LS launcher script in the installed directory: ${distPath}.`) + + await fs.rm(tempPath, { force: true }) + + // Binary is located in bin/ subdirectory within the extracted archive + bin = path.join(installDir, "bin", "lua-language-server" + (platform === "win32" ? ".exe" : "")) + + if (!(await Filesystem.exists(bin))) { + log.error("Failed to extract lua-language-server binary") return } - return { - process: spawn(launcherScript, ["--stdio"], { - cwd: root, - }), - } - }, - } - export const YamlLS: Info = { - id: "yaml-ls", - extensions: [".yaml", ".yml"], - root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]), - async spawn(root) { - let binary = which("yaml-language-server") - const args: string[] = [] - if (!binary) { - if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return - const resolved = await Npm.which("yaml-language-server") - if (!resolved) return - binary = resolved - } - args.push("--stdio") - const proc = spawn(binary, args, { - cwd: root, - env: { - ...process.env, - }, - }) - return { - process: proc, - } - }, - } - - export const LuaLS: Info = { - id: "lua-ls", - root: NearestRoot([ - ".luarc.json", - ".luarc.jsonc", - ".luacheckrc", - ".stylua.toml", - "stylua.toml", - "selene.toml", - "selene.yml", - ]), - extensions: [".lua"], - async spawn(root) { - let bin = which("lua-language-server") - - if (!bin) { - if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return - log.info("downloading lua-language-server from GitHub releases") - - const releaseResponse = await fetch("https://api.github.com/repos/LuaLS/lua-language-server/releases/latest") - if (!releaseResponse.ok) { - log.error("Failed to fetch lua-language-server release info") - return - } - - const release = await releaseResponse.json() - - const platform = process.platform - const arch = process.arch - let assetName = "" - - let lualsArch: string = arch - if (arch === "arm64") lualsArch = "arm64" - else if (arch === "x64") lualsArch = "x64" - else if (arch === "ia32") lualsArch = "ia32" - - let lualsPlatform: string = platform - if (platform === "darwin") lualsPlatform = "darwin" - else if (platform === "linux") lualsPlatform = "linux" - else if (platform === "win32") lualsPlatform = "win32" - - const ext = platform === "win32" ? "zip" : "tar.gz" - - assetName = `lua-language-server-${release.tag_name}-${lualsPlatform}-${lualsArch}.${ext}` - - const supportedCombos = [ - "darwin-arm64.tar.gz", - "darwin-x64.tar.gz", - "linux-x64.tar.gz", - "linux-arm64.tar.gz", - "win32-x64.zip", - "win32-ia32.zip", - ] - - const assetSuffix = `${lualsPlatform}-${lualsArch}.${ext}` - if (!supportedCombos.includes(assetSuffix)) { - log.error(`Platform ${platform} and architecture ${arch} is not supported by lua-language-server`) - return - } - - const asset = release.assets.find((a: any) => a.name === assetName) - if (!asset) { - log.error(`Could not find asset ${assetName} in latest lua-language-server release`) - return - } - - const downloadUrl = asset.browser_download_url - const downloadResponse = await fetch(downloadUrl) - if (!downloadResponse.ok) { - log.error("Failed to download lua-language-server") - return - } - - const tempPath = path.join(Global.Path.bin, assetName) - if (downloadResponse.body) await Filesystem.writeStream(tempPath, downloadResponse.body) - - // Unlike zls which is a single self-contained binary, - // lua-language-server needs supporting files (meta/, locale/, etc.) - // Extract entire archive to dedicated directory to preserve all files - const installDir = path.join(Global.Path.bin, `lua-language-server-${lualsArch}-${lualsPlatform}`) - - // Remove old installation if exists - const stats = await fs.stat(installDir).catch(() => undefined) - if (stats) { - await fs.rm(installDir, { force: true, recursive: true }) - } - - await fs.mkdir(installDir, { recursive: true }) - - if (ext === "zip") { - const ok = await Archive.extractZip(tempPath, installDir) - .then(() => true) - .catch((error) => { - log.error("Failed to extract lua-language-server archive", { error }) - return false + if (platform !== "win32") { + const ok = await fs + .chmod(bin, 0o755) + .then(() => true) + .catch((error: unknown) => { + log.error("Failed to set executable permission for lua-language-server binary", { + error, }) - if (!ok) return - } else { - const ok = await run(["tar", "-xzf", tempPath, "-C", installDir]) - .then((result) => result.code === 0) - .catch((error: unknown) => { - log.error("Failed to extract lua-language-server archive", { error }) - return false - }) - if (!ok) return - } - - await fs.rm(tempPath, { force: true }) - - // Binary is located in bin/ subdirectory within the extracted archive - bin = path.join(installDir, "bin", "lua-language-server" + (platform === "win32" ? ".exe" : "")) - - if (!(await Filesystem.exists(bin))) { - log.error("Failed to extract lua-language-server binary") - return - } - - if (platform !== "win32") { - const ok = await fs - .chmod(bin, 0o755) - .then(() => true) - .catch((error: unknown) => { - log.error("Failed to set executable permission for lua-language-server binary", { - error, - }) - return false - }) - if (!ok) return - } - - log.info(`installed lua-language-server`, { bin }) + return false + }) + if (!ok) return } - return { - process: spawn(bin, { - cwd: root, - }), - } - }, - } + log.info(`installed lua-language-server`, { bin }) + } - export const PHPIntelephense: Info = { - id: "php intelephense", - extensions: [".php"], - root: NearestRoot(["composer.json", "composer.lock", ".php-version"]), - async spawn(root) { - let binary = which("intelephense") - const args: string[] = [] - if (!binary) { - if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return - const resolved = await Npm.which("intelephense") - if (!resolved) return - binary = resolved - } - args.push("--stdio") - const proc = spawn(binary, args, { + return { + process: spawn(bin, { cwd: root, - env: { - ...process.env, + }), + } + }, +} + +export const PHPIntelephense: Info = { + id: "php intelephense", + extensions: [".php"], + root: NearestRoot(["composer.json", "composer.lock", ".php-version"]), + async spawn(root) { + let binary = which("intelephense") + const args: string[] = [] + if (!binary) { + if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return + const resolved = await Npm.which("intelephense") + if (!resolved) return + binary = resolved + } + args.push("--stdio") + const proc = spawn(binary, args, { + cwd: root, + env: { + ...process.env, + }, + }) + return { + process: proc, + initialization: { + telemetry: { + enabled: false, }, - }) - return { - process: proc, - initialization: { - telemetry: { - enabled: false, - }, - }, - } - }, - } + }, + } + }, +} - export const Prisma: Info = { - id: "prisma", - extensions: [".prisma"], - root: NearestRoot(["schema.prisma", "prisma/schema.prisma", "prisma"], ["package.json"]), - async spawn(root) { - const prisma = which("prisma") - if (!prisma) { - log.info("prisma not found, please install prisma") - return - } - return { - process: spawn(prisma, ["language-server"], { - cwd: root, - }), - } - }, - } - - export const Dart: Info = { - id: "dart", - extensions: [".dart"], - root: NearestRoot(["pubspec.yaml", "analysis_options.yaml"]), - async spawn(root) { - const dart = which("dart") - if (!dart) { - log.info("dart not found, please install dart first") - return - } - return { - process: spawn(dart, ["language-server", "--lsp"], { - cwd: root, - }), - } - }, - } - - export const Ocaml: Info = { - id: "ocaml-lsp", - extensions: [".ml", ".mli"], - root: NearestRoot(["dune-project", "dune-workspace", ".merlin", "opam"]), - async spawn(root) { - const bin = which("ocamllsp") - if (!bin) { - log.info("ocamllsp not found, please install ocaml-lsp-server") - return - } - return { - process: spawn(bin, { - cwd: root, - }), - } - }, - } - export const BashLS: Info = { - id: "bash", - extensions: [".sh", ".bash", ".zsh", ".ksh"], - root: async () => Instance.directory, - async spawn(root) { - let binary = which("bash-language-server") - const args: string[] = [] - if (!binary) { - if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return - const resolved = await Npm.which("bash-language-server") - if (!resolved) return - binary = resolved - } - args.push("start") - const proc = spawn(binary, args, { +export const Prisma: Info = { + id: "prisma", + extensions: [".prisma"], + root: NearestRoot(["schema.prisma", "prisma/schema.prisma", "prisma"], ["package.json"]), + async spawn(root) { + const prisma = which("prisma") + if (!prisma) { + log.info("prisma not found, please install prisma") + return + } + return { + process: spawn(prisma, ["language-server"], { cwd: root, - env: { - ...process.env, - }, - }) - return { - process: proc, + }), + } + }, +} + +export const Dart: Info = { + id: "dart", + extensions: [".dart"], + root: NearestRoot(["pubspec.yaml", "analysis_options.yaml"]), + async spawn(root) { + const dart = which("dart") + if (!dart) { + log.info("dart not found, please install dart first") + return + } + return { + process: spawn(dart, ["language-server", "--lsp"], { + cwd: root, + }), + } + }, +} + +export const Ocaml: Info = { + id: "ocaml-lsp", + extensions: [".ml", ".mli"], + root: NearestRoot(["dune-project", "dune-workspace", ".merlin", "opam"]), + async spawn(root) { + const bin = which("ocamllsp") + if (!bin) { + log.info("ocamllsp not found, please install ocaml-lsp-server") + return + } + return { + process: spawn(bin, { + cwd: root, + }), + } + }, +} +export const BashLS: Info = { + id: "bash", + extensions: [".sh", ".bash", ".zsh", ".ksh"], + root: async () => Instance.directory, + async spawn(root) { + let binary = which("bash-language-server") + const args: string[] = [] + if (!binary) { + if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return + const resolved = await Npm.which("bash-language-server") + if (!resolved) return + binary = resolved + } + args.push("start") + const proc = spawn(binary, args, { + cwd: root, + env: { + ...process.env, + }, + }) + return { + process: proc, + } + }, +} + +export const TerraformLS: Info = { + id: "terraform", + extensions: [".tf", ".tfvars"], + root: NearestRoot([".terraform.lock.hcl", "terraform.tfstate", "*.tf"]), + async spawn(root) { + let bin = which("terraform-ls") + + if (!bin) { + if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return + log.info("downloading terraform-ls from HashiCorp releases") + + const releaseResponse = await fetch("https://api.releases.hashicorp.com/v1/releases/terraform-ls/latest") + if (!releaseResponse.ok) { + log.error("Failed to fetch terraform-ls release info") + return } - }, - } - export const TerraformLS: Info = { - id: "terraform", - extensions: [".tf", ".tfvars"], - root: NearestRoot([".terraform.lock.hcl", "terraform.tfstate", "*.tf"]), - async spawn(root) { - let bin = which("terraform-ls") + const release = (await releaseResponse.json()) as { + version?: string + builds?: { arch?: string; os?: string; url?: string }[] + } - if (!bin) { - if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return - log.info("downloading terraform-ls from HashiCorp releases") + const platform = process.platform + const arch = process.arch - const releaseResponse = await fetch("https://api.releases.hashicorp.com/v1/releases/terraform-ls/latest") - if (!releaseResponse.ok) { - log.error("Failed to fetch terraform-ls release info") - return - } + const tfArch = arch === "arm64" ? "arm64" : "amd64" + const tfPlatform = platform === "win32" ? "windows" : platform - const release = (await releaseResponse.json()) as { - version?: string - builds?: { arch?: string; os?: string; url?: string }[] - } + const builds = release.builds ?? [] + const build = builds.find((b) => b.arch === tfArch && b.os === tfPlatform) + if (!build?.url) { + log.error(`Could not find build for ${tfPlatform}/${tfArch} terraform-ls release version ${release.version}`) + return + } - const platform = process.platform - const arch = process.arch + const downloadResponse = await fetch(build.url) + if (!downloadResponse.ok) { + log.error("Failed to download terraform-ls") + return + } - const tfArch = arch === "arm64" ? "arm64" : "amd64" - const tfPlatform = platform === "win32" ? "windows" : platform + const tempPath = path.join(Global.Path.bin, "terraform-ls.zip") + if (downloadResponse.body) await Filesystem.writeStream(tempPath, downloadResponse.body) - const builds = release.builds ?? [] - const build = builds.find((b) => b.arch === tfArch && b.os === tfPlatform) - if (!build?.url) { - log.error(`Could not find build for ${tfPlatform}/${tfArch} terraform-ls release version ${release.version}`) - return - } + const ok = await Archive.extractZip(tempPath, Global.Path.bin) + .then(() => true) + .catch((error) => { + log.error("Failed to extract terraform-ls archive", { error }) + return false + }) + if (!ok) return + await fs.rm(tempPath, { force: true }) - const downloadResponse = await fetch(build.url) - if (!downloadResponse.ok) { - log.error("Failed to download terraform-ls") - return - } + bin = path.join(Global.Path.bin, "terraform-ls" + (platform === "win32" ? ".exe" : "")) - const tempPath = path.join(Global.Path.bin, "terraform-ls.zip") - if (downloadResponse.body) await Filesystem.writeStream(tempPath, downloadResponse.body) + if (!(await Filesystem.exists(bin))) { + log.error("Failed to extract terraform-ls binary") + return + } + if (platform !== "win32") { + await fs.chmod(bin, 0o755).catch(() => {}) + } + + log.info(`installed terraform-ls`, { bin }) + } + + return { + process: spawn(bin, ["serve"], { + cwd: root, + }), + initialization: { + experimentalFeatures: { + prefillRequiredFields: true, + validateOnSave: true, + }, + }, + } + }, +} + +export const TexLab: Info = { + id: "texlab", + extensions: [".tex", ".bib"], + root: NearestRoot([".latexmkrc", "latexmkrc", ".texlabroot", "texlabroot"]), + async spawn(root) { + let bin = which("texlab") + + if (!bin) { + if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return + log.info("downloading texlab from GitHub releases") + + const response = await fetch("https://api.github.com/repos/latex-lsp/texlab/releases/latest") + if (!response.ok) { + log.error("Failed to fetch texlab release info") + return + } + + const release = (await response.json()) as { + tag_name?: string + assets?: { name?: string; browser_download_url?: string }[] + } + const version = release.tag_name?.replace("v", "") + if (!version) { + log.error("texlab release did not include a version tag") + return + } + + const platform = process.platform + const arch = process.arch + + const texArch = arch === "arm64" ? "aarch64" : "x86_64" + const texPlatform = platform === "darwin" ? "macos" : platform === "win32" ? "windows" : "linux" + const ext = platform === "win32" ? "zip" : "tar.gz" + const assetName = `texlab-${texArch}-${texPlatform}.${ext}` + + const assets = release.assets ?? [] + const asset = assets.find((a) => a.name === assetName) + if (!asset?.browser_download_url) { + log.error(`Could not find asset ${assetName} in texlab release`) + return + } + + const downloadResponse = await fetch(asset.browser_download_url) + if (!downloadResponse.ok) { + log.error("Failed to download texlab") + return + } + + const tempPath = path.join(Global.Path.bin, assetName) + if (downloadResponse.body) await Filesystem.writeStream(tempPath, downloadResponse.body) + + if (ext === "zip") { const ok = await Archive.extractZip(tempPath, Global.Path.bin) .then(() => true) .catch((error) => { - log.error("Failed to extract terraform-ls archive", { error }) + log.error("Failed to extract texlab archive", { error }) return false }) if (!ok) return - await fs.rm(tempPath, { force: true }) - - bin = path.join(Global.Path.bin, "terraform-ls" + (platform === "win32" ? ".exe" : "")) - - if (!(await Filesystem.exists(bin))) { - log.error("Failed to extract terraform-ls binary") - return - } - - if (platform !== "win32") { - await fs.chmod(bin, 0o755).catch(() => {}) - } - - log.info(`installed terraform-ls`, { bin }) + } + if (ext === "tar.gz") { + await run(["tar", "-xzf", tempPath], { cwd: Global.Path.bin }) } - return { - process: spawn(bin, ["serve"], { - cwd: root, - }), - initialization: { - experimentalFeatures: { - prefillRequiredFields: true, - validateOnSave: true, - }, - }, - } - }, - } + await fs.rm(tempPath, { force: true }) - export const TexLab: Info = { - id: "texlab", - extensions: [".tex", ".bib"], - root: NearestRoot([".latexmkrc", "latexmkrc", ".texlabroot", "texlabroot"]), - async spawn(root) { - let bin = which("texlab") + bin = path.join(Global.Path.bin, "texlab" + (platform === "win32" ? ".exe" : "")) - if (!bin) { - if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return - log.info("downloading texlab from GitHub releases") - - const response = await fetch("https://api.github.com/repos/latex-lsp/texlab/releases/latest") - if (!response.ok) { - log.error("Failed to fetch texlab release info") - return - } - - const release = (await response.json()) as { - tag_name?: string - assets?: { name?: string; browser_download_url?: string }[] - } - const version = release.tag_name?.replace("v", "") - if (!version) { - log.error("texlab release did not include a version tag") - return - } - - const platform = process.platform - const arch = process.arch - - const texArch = arch === "arm64" ? "aarch64" : "x86_64" - const texPlatform = platform === "darwin" ? "macos" : platform === "win32" ? "windows" : "linux" - const ext = platform === "win32" ? "zip" : "tar.gz" - const assetName = `texlab-${texArch}-${texPlatform}.${ext}` - - const assets = release.assets ?? [] - const asset = assets.find((a) => a.name === assetName) - if (!asset?.browser_download_url) { - log.error(`Could not find asset ${assetName} in texlab release`) - return - } - - const downloadResponse = await fetch(asset.browser_download_url) - if (!downloadResponse.ok) { - log.error("Failed to download texlab") - return - } - - const tempPath = path.join(Global.Path.bin, assetName) - if (downloadResponse.body) await Filesystem.writeStream(tempPath, downloadResponse.body) - - if (ext === "zip") { - const ok = await Archive.extractZip(tempPath, Global.Path.bin) - .then(() => true) - .catch((error) => { - log.error("Failed to extract texlab archive", { error }) - return false - }) - if (!ok) return - } - if (ext === "tar.gz") { - await run(["tar", "-xzf", tempPath], { cwd: Global.Path.bin }) - } - - await fs.rm(tempPath, { force: true }) - - bin = path.join(Global.Path.bin, "texlab" + (platform === "win32" ? ".exe" : "")) - - if (!(await Filesystem.exists(bin))) { - log.error("Failed to extract texlab binary") - return - } - - if (platform !== "win32") { - await fs.chmod(bin, 0o755).catch(() => {}) - } - - log.info("installed texlab", { bin }) + if (!(await Filesystem.exists(bin))) { + log.error("Failed to extract texlab binary") + return } - return { - process: spawn(bin, { - cwd: root, - }), + if (platform !== "win32") { + await fs.chmod(bin, 0o755).catch(() => {}) } - }, - } - export const DockerfileLS: Info = { - id: "dockerfile", - extensions: [".dockerfile", "Dockerfile"], - root: async () => Instance.directory, - async spawn(root) { - let binary = which("docker-langserver") - const args: string[] = [] - if (!binary) { - if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return - const resolved = await Npm.which("dockerfile-language-server-nodejs") - if (!resolved) return - binary = resolved - } - args.push("--stdio") - const proc = spawn(binary, args, { + log.info("installed texlab", { bin }) + } + + return { + process: spawn(bin, { + cwd: root, + }), + } + }, +} + +export const DockerfileLS: Info = { + id: "dockerfile", + extensions: [".dockerfile", "Dockerfile"], + root: async () => Instance.directory, + async spawn(root) { + let binary = which("docker-langserver") + const args: string[] = [] + if (!binary) { + if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return + const resolved = await Npm.which("dockerfile-language-server-nodejs") + if (!resolved) return + binary = resolved + } + args.push("--stdio") + const proc = spawn(binary, args, { + cwd: root, + env: { + ...process.env, + }, + }) + return { + process: proc, + } + }, +} + +export const Gleam: Info = { + id: "gleam", + extensions: [".gleam"], + root: NearestRoot(["gleam.toml"]), + async spawn(root) { + const gleam = which("gleam") + if (!gleam) { + log.info("gleam not found, please install gleam first") + return + } + return { + process: spawn(gleam, ["lsp"], { + cwd: root, + }), + } + }, +} + +export const Clojure: Info = { + id: "clojure-lsp", + extensions: [".clj", ".cljs", ".cljc", ".edn"], + root: NearestRoot(["deps.edn", "project.clj", "shadow-cljs.edn", "bb.edn", "build.boot"]), + async spawn(root) { + let bin = which("clojure-lsp") + if (!bin && process.platform === "win32") { + bin = which("clojure-lsp.exe") + } + if (!bin) { + log.info("clojure-lsp not found, please install clojure-lsp first") + return + } + return { + process: spawn(bin, ["listen"], { + cwd: root, + }), + } + }, +} + +export const Nixd: Info = { + id: "nixd", + extensions: [".nix"], + root: async (file) => { + // First, look for flake.nix - the most reliable Nix project root indicator + const flakeRoot = await NearestRoot(["flake.nix"])(file) + if (flakeRoot && flakeRoot !== Instance.directory) return flakeRoot + + // If no flake.nix, fall back to git repository root + if (Instance.worktree && Instance.worktree !== Instance.directory) return Instance.worktree + + // Finally, use the instance directory as fallback + return Instance.directory + }, + async spawn(root) { + const nixd = which("nixd") + if (!nixd) { + log.info("nixd not found, please install nixd first") + return + } + return { + process: spawn(nixd, [], { cwd: root, env: { ...process.env, }, - }) - return { - process: proc, - } - }, - } - - export const Gleam: Info = { - id: "gleam", - extensions: [".gleam"], - root: NearestRoot(["gleam.toml"]), - async spawn(root) { - const gleam = which("gleam") - if (!gleam) { - log.info("gleam not found, please install gleam first") - return - } - return { - process: spawn(gleam, ["lsp"], { - cwd: root, - }), - } - }, - } - - export const Clojure: Info = { - id: "clojure-lsp", - extensions: [".clj", ".cljs", ".cljc", ".edn"], - root: NearestRoot(["deps.edn", "project.clj", "shadow-cljs.edn", "bb.edn", "build.boot"]), - async spawn(root) { - let bin = which("clojure-lsp") - if (!bin && process.platform === "win32") { - bin = which("clojure-lsp.exe") - } - if (!bin) { - log.info("clojure-lsp not found, please install clojure-lsp first") - return - } - return { - process: spawn(bin, ["listen"], { - cwd: root, - }), - } - }, - } - - export const Nixd: Info = { - id: "nixd", - extensions: [".nix"], - root: async (file) => { - // First, look for flake.nix - the most reliable Nix project root indicator - const flakeRoot = await NearestRoot(["flake.nix"])(file) - if (flakeRoot && flakeRoot !== Instance.directory) return flakeRoot - - // If no flake.nix, fall back to git repository root - if (Instance.worktree && Instance.worktree !== Instance.directory) return Instance.worktree - - // Finally, use the instance directory as fallback - return Instance.directory - }, - async spawn(root) { - const nixd = which("nixd") - if (!nixd) { - log.info("nixd not found, please install nixd first") - return - } - return { - process: spawn(nixd, [], { - cwd: root, - env: { - ...process.env, - }, - }), - } - }, - } - - export const Tinymist: Info = { - id: "tinymist", - extensions: [".typ", ".typc"], - root: NearestRoot(["typst.toml"]), - async spawn(root) { - let bin = which("tinymist") - - if (!bin) { - if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return - log.info("downloading tinymist from GitHub releases") - - const response = await fetch("https://api.github.com/repos/Myriad-Dreamin/tinymist/releases/latest") - if (!response.ok) { - log.error("Failed to fetch tinymist release info") - return - } - - const release = (await response.json()) as { - tag_name?: string - assets?: { name?: string; browser_download_url?: string }[] - } - - const platform = process.platform - const arch = process.arch - - const tinymistArch = arch === "arm64" ? "aarch64" : "x86_64" - let tinymistPlatform: string - let ext: string - - if (platform === "darwin") { - tinymistPlatform = "apple-darwin" - ext = "tar.gz" - } else if (platform === "win32") { - tinymistPlatform = "pc-windows-msvc" - ext = "zip" - } else { - tinymistPlatform = "unknown-linux-gnu" - ext = "tar.gz" - } - - const assetName = `tinymist-${tinymistArch}-${tinymistPlatform}.${ext}` - - const assets = release.assets ?? [] - const asset = assets.find((a) => a.name === assetName) - if (!asset?.browser_download_url) { - log.error(`Could not find asset ${assetName} in tinymist release`) - return - } - - const downloadResponse = await fetch(asset.browser_download_url) - if (!downloadResponse.ok) { - log.error("Failed to download tinymist") - return - } - - const tempPath = path.join(Global.Path.bin, assetName) - if (downloadResponse.body) await Filesystem.writeStream(tempPath, downloadResponse.body) - - if (ext === "zip") { - const ok = await Archive.extractZip(tempPath, Global.Path.bin) - .then(() => true) - .catch((error) => { - log.error("Failed to extract tinymist archive", { error }) - return false - }) - if (!ok) return - } else { - await run(["tar", "-xzf", tempPath, "--strip-components=1"], { cwd: Global.Path.bin }) - } - - await fs.rm(tempPath, { force: true }) - - bin = path.join(Global.Path.bin, "tinymist" + (platform === "win32" ? ".exe" : "")) - - if (!(await Filesystem.exists(bin))) { - log.error("Failed to extract tinymist binary") - return - } - - if (platform !== "win32") { - await fs.chmod(bin, 0o755).catch(() => {}) - } - - log.info("installed tinymist", { bin }) - } - - return { - process: spawn(bin, { cwd: root }), - } - }, - } - - export const HLS: Info = { - id: "haskell-language-server", - extensions: [".hs", ".lhs"], - root: NearestRoot(["stack.yaml", "cabal.project", "hie.yaml", "*.cabal"]), - async spawn(root) { - const bin = which("haskell-language-server-wrapper") - if (!bin) { - log.info("haskell-language-server-wrapper not found, please install haskell-language-server") - return - } - return { - process: spawn(bin, ["--lsp"], { - cwd: root, - }), - } - }, - } - - export const JuliaLS: Info = { - id: "julials", - extensions: [".jl"], - root: NearestRoot(["Project.toml", "Manifest.toml", "*.jl"]), - async spawn(root) { - const julia = which("julia") - if (!julia) { - log.info("julia not found, please install julia first (https://julialang.org/downloads/)") - return - } - return { - process: spawn(julia, ["--startup-file=no", "--history-file=no", "-e", "using LanguageServer; runserver()"], { - cwd: root, - }), - } - }, - } + }), + } + }, +} + +export const Tinymist: Info = { + id: "tinymist", + extensions: [".typ", ".typc"], + root: NearestRoot(["typst.toml"]), + async spawn(root) { + let bin = which("tinymist") + + if (!bin) { + if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return + log.info("downloading tinymist from GitHub releases") + + const response = await fetch("https://api.github.com/repos/Myriad-Dreamin/tinymist/releases/latest") + if (!response.ok) { + log.error("Failed to fetch tinymist release info") + return + } + + const release = (await response.json()) as { + tag_name?: string + assets?: { name?: string; browser_download_url?: string }[] + } + + const platform = process.platform + const arch = process.arch + + const tinymistArch = arch === "arm64" ? "aarch64" : "x86_64" + let tinymistPlatform: string + let ext: string + + if (platform === "darwin") { + tinymistPlatform = "apple-darwin" + ext = "tar.gz" + } else if (platform === "win32") { + tinymistPlatform = "pc-windows-msvc" + ext = "zip" + } else { + tinymistPlatform = "unknown-linux-gnu" + ext = "tar.gz" + } + + const assetName = `tinymist-${tinymistArch}-${tinymistPlatform}.${ext}` + + const assets = release.assets ?? [] + const asset = assets.find((a) => a.name === assetName) + if (!asset?.browser_download_url) { + log.error(`Could not find asset ${assetName} in tinymist release`) + return + } + + const downloadResponse = await fetch(asset.browser_download_url) + if (!downloadResponse.ok) { + log.error("Failed to download tinymist") + return + } + + const tempPath = path.join(Global.Path.bin, assetName) + if (downloadResponse.body) await Filesystem.writeStream(tempPath, downloadResponse.body) + + if (ext === "zip") { + const ok = await Archive.extractZip(tempPath, Global.Path.bin) + .then(() => true) + .catch((error) => { + log.error("Failed to extract tinymist archive", { error }) + return false + }) + if (!ok) return + } else { + await run(["tar", "-xzf", tempPath, "--strip-components=1"], { cwd: Global.Path.bin }) + } + + await fs.rm(tempPath, { force: true }) + + bin = path.join(Global.Path.bin, "tinymist" + (platform === "win32" ? ".exe" : "")) + + if (!(await Filesystem.exists(bin))) { + log.error("Failed to extract tinymist binary") + return + } + + if (platform !== "win32") { + await fs.chmod(bin, 0o755).catch(() => {}) + } + + log.info("installed tinymist", { bin }) + } + + return { + process: spawn(bin, { cwd: root }), + } + }, +} + +export const HLS: Info = { + id: "haskell-language-server", + extensions: [".hs", ".lhs"], + root: NearestRoot(["stack.yaml", "cabal.project", "hie.yaml", "*.cabal"]), + async spawn(root) { + const bin = which("haskell-language-server-wrapper") + if (!bin) { + log.info("haskell-language-server-wrapper not found, please install haskell-language-server") + return + } + return { + process: spawn(bin, ["--lsp"], { + cwd: root, + }), + } + }, +} + +export const JuliaLS: Info = { + id: "julials", + extensions: [".jl"], + root: NearestRoot(["Project.toml", "Manifest.toml", "*.jl"]), + async spawn(root) { + const julia = which("julia") + if (!julia) { + log.info("julia not found, please install julia first (https://julialang.org/downloads/)") + return + } + return { + process: spawn(julia, ["--startup-file=no", "--history-file=no", "-e", "using LanguageServer; runserver()"], { + cwd: root, + }), + } + }, } diff --git a/packages/opencode/test/lsp/client.test.ts b/packages/opencode/test/lsp/client.test.ts index 414d11f8e7..f124fddf95 100644 --- a/packages/opencode/test/lsp/client.test.ts +++ b/packages/opencode/test/lsp/client.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test, beforeEach } from "bun:test" import path from "path" -import { LSPClient } from "../../src/lsp/client" -import { LSPServer } from "../../src/lsp/server" +import { LSPClient } from "../../src/lsp" +import { LSPServer } from "../../src/lsp" import { Instance } from "../../src/project/instance" import { Log } from "../../src/util" diff --git a/packages/opencode/test/lsp/index.test.ts b/packages/opencode/test/lsp/index.test.ts index b12a61ae3c..7419f3bf5c 100644 --- a/packages/opencode/test/lsp/index.test.ts +++ b/packages/opencode/test/lsp/index.test.ts @@ -2,7 +2,7 @@ import { describe, expect, spyOn } from "bun:test" import path from "path" import { Effect, Layer } from "effect" import { LSP } from "../../src/lsp" -import { LSPServer } from "../../src/lsp/server" +import { LSPServer } from "../../src/lsp" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" import { provideTmpdirInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" diff --git a/packages/opencode/test/lsp/lifecycle.test.ts b/packages/opencode/test/lsp/lifecycle.test.ts index a6de869fcb..fe14729736 100644 --- a/packages/opencode/test/lsp/lifecycle.test.ts +++ b/packages/opencode/test/lsp/lifecycle.test.ts @@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test" import path from "path" import { Effect, Layer } from "effect" import { LSP } from "../../src/lsp" -import { LSPServer } from "../../src/lsp/server" +import { LSPServer } from "../../src/lsp" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" import { provideTmpdirInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" From 0e2038239699b430595980be2939c08c5e4cde93 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 23:41:34 -0400 Subject: [PATCH 219/300] fix: resolve circular sibling imports causing runtime ReferenceError (#22752) --- packages/opencode/src/config/config.ts | 6 +++--- packages/opencode/src/config/tui-migrate.ts | 2 +- packages/opencode/src/config/tui-schema.ts | 2 +- packages/opencode/src/config/tui.ts | 4 ++-- packages/opencode/src/effect/app-runtime.ts | 2 +- packages/opencode/src/effect/bootstrap-runtime.ts | 2 +- packages/opencode/src/effect/instance-state.ts | 2 +- packages/opencode/src/effect/observability.ts | 2 +- packages/opencode/src/effect/run-service.ts | 2 +- packages/opencode/src/lsp/client.ts | 2 +- packages/opencode/src/lsp/lsp.ts | 4 ++-- packages/opencode/src/project/bootstrap.ts | 4 ++-- packages/opencode/src/project/instance.ts | 2 +- packages/opencode/src/provider/transform.ts | 2 +- packages/opencode/src/session/compaction.ts | 2 +- packages/opencode/src/session/processor.ts | 2 +- packages/opencode/src/session/projectors.ts | 2 +- packages/opencode/src/session/prompt.ts | 2 +- packages/opencode/src/session/revert.ts | 2 +- packages/opencode/src/session/run-state.ts | 2 +- packages/opencode/src/session/summary.ts | 2 +- packages/opencode/src/share/session.ts | 2 +- packages/opencode/src/util/archive.ts | 2 +- 23 files changed, 28 insertions(+), 28 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index d8cfd5e48f..8690dbda2d 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -19,9 +19,9 @@ import { printParseErrorCode, } from "jsonc-parser" import { Instance, type InstanceContext } from "../project/instance" -import { LSPServer } from "../lsp" +import * as LSPServer from "../lsp/server" import { Installation } from "@/installation" -import { ConfigMarkdown } from "." +import * as ConfigMarkdown from "./markdown" import { existsSync } from "fs" import { Bus } from "@/bus" import { GlobalBus } from "@/bus/global" @@ -29,7 +29,7 @@ import { Event } from "../server/event" import { Glob } from "@opencode-ai/shared/util/glob" import { Account } from "@/account" import { isRecord } from "@/util/record" -import { ConfigPaths } from "." +import * as ConfigPaths from "./paths" import type { ConsoleState } from "./console-state" import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { InstanceState } from "@/effect" diff --git a/packages/opencode/src/config/tui-migrate.ts b/packages/opencode/src/config/tui-migrate.ts index 18cee554d5..ed19474be2 100644 --- a/packages/opencode/src/config/tui-migrate.ts +++ b/packages/opencode/src/config/tui-migrate.ts @@ -2,7 +2,7 @@ import path from "path" import { type ParseError as JsoncParseError, applyEdits, modify, parse as parseJsonc } from "jsonc-parser" import { unique } from "remeda" import z from "zod" -import { ConfigPaths } from "." +import * as ConfigPaths from "./paths" import { TuiInfo, TuiOptions } from "./tui-schema" import { Instance } from "@/project/instance" import { Flag } from "@/flag/flag" diff --git a/packages/opencode/src/config/tui-schema.ts b/packages/opencode/src/config/tui-schema.ts index fd5cd8c88d..3be988370d 100644 --- a/packages/opencode/src/config/tui-schema.ts +++ b/packages/opencode/src/config/tui-schema.ts @@ -1,5 +1,5 @@ import z from "zod" -import { Config } from "." +import * as Config from "./config" const KeybindOverride = z .object( diff --git a/packages/opencode/src/config/tui.ts b/packages/opencode/src/config/tui.ts index 43f1bce460..3cde908b03 100644 --- a/packages/opencode/src/config/tui.ts +++ b/packages/opencode/src/config/tui.ts @@ -2,8 +2,8 @@ import { existsSync } from "fs" import z from "zod" import { mergeDeep, unique } from "remeda" import { Context, Effect, Fiber, Layer } from "effect" -import { Config } from "." -import { ConfigPaths } from "." +import * as Config from "./config" +import * as ConfigPaths from "./paths" import { migrateTuiConfig } from "./tui-migrate" import { TuiInfo } from "./tui-schema" import { Flag } from "@/flag/flag" diff --git a/packages/opencode/src/effect/app-runtime.ts b/packages/opencode/src/effect/app-runtime.ts index 3e28183448..0b76e96a84 100644 --- a/packages/opencode/src/effect/app-runtime.ts +++ b/packages/opencode/src/effect/app-runtime.ts @@ -1,6 +1,6 @@ import { Layer, ManagedRuntime } from "effect" import { attach, memoMap } from "./run-service" -import { Observability } from "." +import * as Observability from "./observability" import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { Bus } from "@/bus" diff --git a/packages/opencode/src/effect/bootstrap-runtime.ts b/packages/opencode/src/effect/bootstrap-runtime.ts index 208a83bf85..89cc071561 100644 --- a/packages/opencode/src/effect/bootstrap-runtime.ts +++ b/packages/opencode/src/effect/bootstrap-runtime.ts @@ -10,7 +10,7 @@ import { File } from "@/file" import { Vcs } from "@/project" import { Snapshot } from "@/snapshot" import { Bus } from "@/bus" -import { Observability } from "." +import * as Observability from "./observability" export const BootstrapLayer = Layer.mergeAll( Plugin.defaultLayer, diff --git a/packages/opencode/src/effect/instance-state.ts b/packages/opencode/src/effect/instance-state.ts index d71f82df97..7095657f5d 100644 --- a/packages/opencode/src/effect/instance-state.ts +++ b/packages/opencode/src/effect/instance-state.ts @@ -1,5 +1,5 @@ import { Effect, Fiber, ScopedCache, Scope, Context } from "effect" -import { EffectLogger } from "@/effect" +import * as EffectLogger from "./logger" import { Instance, type InstanceContext } from "@/project/instance" import { LocalContext } from "@/util" import { InstanceRef, WorkspaceRef } from "./instance-ref" diff --git a/packages/opencode/src/effect/observability.ts b/packages/opencode/src/effect/observability.ts index 4e8ae22217..2f4040113d 100644 --- a/packages/opencode/src/effect/observability.ts +++ b/packages/opencode/src/effect/observability.ts @@ -1,7 +1,7 @@ import { Effect, Layer, Logger } from "effect" import { FetchHttpClient } from "effect/unstable/http" import { OtlpLogger, OtlpSerialization } from "effect/unstable/observability" -import { EffectLogger } from "@/effect" +import * as EffectLogger from "./logger" import { Flag } from "@/flag/flag" import { CHANNEL, VERSION } from "@/installation/meta" diff --git a/packages/opencode/src/effect/run-service.ts b/packages/opencode/src/effect/run-service.ts index a9d653b108..28265f9b27 100644 --- a/packages/opencode/src/effect/run-service.ts +++ b/packages/opencode/src/effect/run-service.ts @@ -3,7 +3,7 @@ import * as Context from "effect/Context" import { Instance } from "@/project/instance" import { LocalContext } from "@/util" import { InstanceRef, WorkspaceRef } from "./instance-ref" -import { Observability } from "." +import * as Observability from "./observability" import { WorkspaceContext } from "@/control-plane/workspace-context" import type { InstanceContext } from "@/project/instance" diff --git a/packages/opencode/src/lsp/client.ts b/packages/opencode/src/lsp/client.ts index fed2bf5c99..59a64ca1ed 100644 --- a/packages/opencode/src/lsp/client.ts +++ b/packages/opencode/src/lsp/client.ts @@ -8,7 +8,7 @@ import { Log } from "../util" import { Process } from "../util" import { LANGUAGE_EXTENSIONS } from "./language" import z from "zod" -import type { LSPServer } from "." +import type * as LSPServer from "./server" import { NamedError } from "@opencode-ai/shared/util/error" import { withTimeout } from "../util/timeout" import { Instance } from "../project/instance" diff --git a/packages/opencode/src/lsp/lsp.ts b/packages/opencode/src/lsp/lsp.ts index 7f5b36313d..2c0982eca5 100644 --- a/packages/opencode/src/lsp/lsp.ts +++ b/packages/opencode/src/lsp/lsp.ts @@ -1,10 +1,10 @@ import { BusEvent } from "@/bus/bus-event" import { Bus } from "@/bus" import { Log } from "../util" -import { LSPClient } from "." +import * as LSPClient from "./client" import path from "path" import { pathToFileURL, fileURLToPath } from "url" -import { LSPServer } from "." +import * as LSPServer from "./server" import z from "zod" import { Config } from "../config" import { Instance } from "../project/instance" diff --git a/packages/opencode/src/project/bootstrap.ts b/packages/opencode/src/project/bootstrap.ts index 27ed35b7f0..a405607bea 100644 --- a/packages/opencode/src/project/bootstrap.ts +++ b/packages/opencode/src/project/bootstrap.ts @@ -3,8 +3,8 @@ import { Format } from "../format" import { LSP } from "../lsp" import { File } from "../file" import { Snapshot } from "../snapshot" -import { Project } from "." -import { Vcs } from "." +import * as Project from "./project" +import * as Vcs from "./vcs" import { Bus } from "../bus" import { Command } from "../command" import { Instance } from "./instance" diff --git a/packages/opencode/src/project/instance.ts b/packages/opencode/src/project/instance.ts index b95962ae08..056eede01b 100644 --- a/packages/opencode/src/project/instance.ts +++ b/packages/opencode/src/project/instance.ts @@ -5,7 +5,7 @@ import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { iife } from "@/util/iife" import { Log } from "@/util" import { LocalContext } from "../util" -import { Project } from "." +import * as Project from "./project" import { WorkspaceContext } from "@/control-plane/workspace-context" export interface InstanceContext { diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 7b83c245f4..a0d3e2258a 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -2,7 +2,7 @@ import type { ModelMessage } from "ai" import { mergeDeep, unique } from "remeda" import type { JSONSchema7 } from "@ai-sdk/provider" import type { JSONSchema } from "zod/v4/core" -import type { Provider } from "." +import type * as Provider from "./provider" import type { ModelsDev } from "./models" import { iife } from "@/util/iife" import { Flag } from "@/flag/flag" diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index 5ad80b6b02..3ef6977547 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -1,6 +1,6 @@ import { BusEvent } from "@/bus/bus-event" import { Bus } from "@/bus" -import { Session } from "." +import * as Session from "./session" import { SessionID, MessageID, PartID } from "./schema" import { Provider } from "../provider" import { MessageV2 } from "./message-v2" diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 72b27403bd..415639fbe5 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -6,7 +6,7 @@ import { Config } from "@/config" import { Permission } from "@/permission" import { Plugin } from "@/plugin" import { Snapshot } from "@/snapshot" -import { Session } from "." +import * as Session from "./session" import { LLM } from "./llm" import { MessageV2 } from "./message-v2" import { isOverflow } from "./overflow" diff --git a/packages/opencode/src/session/projectors.ts b/packages/opencode/src/session/projectors.ts index 9a36ef5b3b..fb8354dda1 100644 --- a/packages/opencode/src/session/projectors.ts +++ b/packages/opencode/src/session/projectors.ts @@ -1,6 +1,6 @@ import { NotFoundError, eq, and } from "../storage" import { SyncEvent } from "@/sync" -import { Session } from "." +import * as Session from "./session" import { MessageV2 } from "./message-v2" import { SessionTable, MessageTable, PartTable } from "./session.sql" import { Log } from "../util" diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 1d4bb66bc5..65fc7c8c70 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -5,7 +5,7 @@ import { SessionID, MessageID, PartID } from "./schema" import { MessageV2 } from "./message-v2" import { Log } from "../util" import { SessionRevert } from "./revert" -import { Session } from "." +import * as Session from "./session" import { Agent } from "../agent/agent" import { Provider } from "../provider" import { ModelID, ProviderID } from "../provider/schema" diff --git a/packages/opencode/src/session/revert.ts b/packages/opencode/src/session/revert.ts index 93d0e6219c..f09ccf24ad 100644 --- a/packages/opencode/src/session/revert.ts +++ b/packages/opencode/src/session/revert.ts @@ -5,7 +5,7 @@ import { Snapshot } from "../snapshot" import { Storage } from "@/storage" import { SyncEvent } from "../sync" import { Log } from "../util" -import { Session } from "." +import * as Session from "./session" import { MessageV2 } from "./message-v2" import { SessionID, MessageID, PartID } from "./schema" import { SessionRunState } from "./run-state" diff --git a/packages/opencode/src/session/run-state.ts b/packages/opencode/src/session/run-state.ts index 179f287fa8..a18e0b5732 100644 --- a/packages/opencode/src/session/run-state.ts +++ b/packages/opencode/src/session/run-state.ts @@ -1,7 +1,7 @@ import { InstanceState } from "@/effect" import { Runner } from "@/effect" import { Effect, Layer, Scope, Context } from "effect" -import { Session } from "." +import * as Session from "./session" import { MessageV2 } from "./message-v2" import { SessionID } from "./schema" import { SessionStatus } from "./status" diff --git a/packages/opencode/src/session/summary.ts b/packages/opencode/src/session/summary.ts index 21203c326b..9f8e70f162 100644 --- a/packages/opencode/src/session/summary.ts +++ b/packages/opencode/src/session/summary.ts @@ -3,7 +3,7 @@ import { Effect, Layer, Context } from "effect" import { Bus } from "@/bus" import { Snapshot } from "@/snapshot" import { Storage } from "@/storage" -import { Session } from "." +import * as Session from "./session" import { MessageV2 } from "./message-v2" import { SessionID, MessageID } from "./schema" diff --git a/packages/opencode/src/share/session.ts b/packages/opencode/src/share/session.ts index 71fa17c889..63b7670785 100644 --- a/packages/opencode/src/share/session.ts +++ b/packages/opencode/src/share/session.ts @@ -4,7 +4,7 @@ import { SyncEvent } from "@/sync" import { Effect, Layer, Scope, Context } from "effect" import { Config } from "../config" import { Flag } from "../flag/flag" -import { ShareNext } from "." +import * as ShareNext from "./share-next" export interface Interface { readonly create: (input?: Session.CreateInput) => Effect.Effect diff --git a/packages/opencode/src/util/archive.ts b/packages/opencode/src/util/archive.ts index 21d014c6a8..97fe6aefb2 100644 --- a/packages/opencode/src/util/archive.ts +++ b/packages/opencode/src/util/archive.ts @@ -1,5 +1,5 @@ import path from "path" -import { Process } from "." +import * as Process from "./process" export async function extractZip(zipPath: string, destDir: string) { if (process.platform === "win32") { From 225a769411f35a0e2dd357589374766dae77ae6a Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Thu, 16 Apr 2026 03:42:25 +0000 Subject: [PATCH 220/300] chore: generate --- packages/opencode/src/lsp/server.ts | 4 +--- packages/opencode/src/project/project.ts | 7 ++----- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index 25aaaa36a4..390c5f2428 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -457,9 +457,7 @@ export const Ty: Info = { if (!binary) { for (const venvPath of potentialVenvPaths) { const isWindows = process.platform === "win32" - const potentialTyPath = isWindows - ? path.join(venvPath, "Scripts", "ty.exe") - : path.join(venvPath, "bin", "ty") + const potentialTyPath = isWindows ? path.join(venvPath, "Scripts", "ty.exe") : path.join(venvPath, "bin", "ty") if (await Filesystem.exists(potentialTyPath)) { binary = potentialTyPath break diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index 050951a606..f838d9ab43 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -54,9 +54,7 @@ type Row = typeof ProjectTable.$inferSelect export function fromRow(row: Row): Info { const icon = - row.icon_url || row.icon_color - ? { url: row.icon_url ?? undefined, color: row.icon_color ?? undefined } - : undefined + row.icon_url || row.icon_color ? { url: row.icon_url ?? undefined, color: row.icon_color ?? undefined } : undefined return { id: row.id, worktree: row.worktree, @@ -256,8 +254,7 @@ export const layer: Layer.Layer< time: { created: Date.now(), updated: Date.now() }, } - if (Flag.OPENCODE_EXPERIMENTAL_ICON_DISCOVERY) - yield* discover(existing).pipe(Effect.ignore, Effect.forkIn(scope)) + if (Flag.OPENCODE_EXPERIMENTAL_ICON_DISCOVERY) yield* discover(existing).pipe(Effect.ignore, Effect.forkIn(scope)) const result: Info = { ...existing, From c802695ee9555ccfd8b0a6ae2215f750bccda712 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 23:44:08 -0400 Subject: [PATCH 221/300] docs: add circular import rules to namespace treeshake spec (#22754) --- .../specs/effect/namespace-treeshake.md | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/packages/opencode/specs/effect/namespace-treeshake.md b/packages/opencode/specs/effect/namespace-treeshake.md index 8a9cf94fd4..5d1fbd07e5 100644 --- a/packages/opencode/specs/effect/namespace-treeshake.md +++ b/packages/opencode/specs/effect/namespace-treeshake.md @@ -442,3 +442,58 @@ Going forward: - If a file grows large enough that it's hard to navigate, split by concern (errors.ts, schema.ts, etc.) for readability. Not for tree-shaking — the bundler handles that. + +## Circular import rules + +Barrel files (`index.ts` with `export * as`) introduce circular import risks. +These cause `ReferenceError: Cannot access 'X' before initialization` at +runtime — not caught by the type checker. + +### Rule 1: Sibling files never import through their own barrel + +Files in the same directory must import directly from the source file, never +through `"."` or `"@/"`: + +```ts +// BAD — circular: index.ts re-exports both files, so A → index → B → index → A +import { Sibling } from "." + +// GOOD — direct, no cycle +import * as Sibling from "./sibling" +``` + +### Rule 2: Cross-directory imports must not form cycles through barrels + +If `src/lsp/lsp.ts` imports `Config` from `"../config"`, and +`src/config/config.ts` imports `LSPServer` from `"../lsp"`, that's a cycle: + +``` +lsp/lsp.ts → config/index.ts → config/config.ts → lsp/index.ts → lsp/lsp.ts 💥 +``` + +Fix by importing the specific file, breaking the cycle: + +```ts +// In config/config.ts — import directly, not through the lsp barrel +import * as LSPServer from "../lsp/server" +``` + +### Why the type checker doesn't catch this + +TypeScript resolves types lazily — it doesn't evaluate module-scope +expressions. The `ReferenceError` only happens at runtime when a module-scope +`const` or function call accesses a value from a circular dependency that +hasn't finished initializing. The SDK build step (`bun run --conditions=browser +./src/index.ts generate`) is the reliable way to catch these because it +evaluates all modules eagerly. + +### How to verify + +After any namespace conversion, run: + +```bash +cd packages/opencode +bun run --conditions=browser ./src/index.ts generate +``` + +If this completes without `ReferenceError`, the module graph is safe. From 8aa0f9fe9515ba0234ab6a0a58c868068913bb05 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Apr 2026 23:50:47 -0400 Subject: [PATCH 222/300] feat: enable type-aware no-base-to-string rule, fix 56 violations (#22750) --- .oxlintrc.json | 4 ++++ .../workspace/[id]/billing/black-section.tsx | 4 ++-- .../[id]/billing/monthly-limit-section.tsx | 4 ++-- .../workspace/[id]/billing/reload-section.tsx | 22 +++++++++---------- .../routes/workspace/[id]/go/lite-section.tsx | 4 ++-- .../workspace/[id]/keys/key-section.tsx | 8 +++---- .../workspace/[id]/members/member-section.tsx | 22 +++++++++---------- .../routes/workspace/[id]/model-section.tsx | 8 +++---- .../workspace/[id]/provider-section.tsx | 17 ++++++++------ .../[id]/settings/settings-section.tsx | 4 ++-- packages/opencode/script/schema.ts | 2 +- packages/opencode/src/effect/logger.ts | 2 ++ .../src/plugin/github-copilot/copilot.ts | 2 +- packages/opencode/src/provider/transform.ts | 4 ++-- packages/opencode/src/pty/service.ts | 2 +- packages/opencode/src/snapshot/snapshot.ts | 2 +- packages/opencode/src/tool/apply_patch.ts | 10 ++++++++- packages/opencode/src/util/error.ts | 1 + packages/opencode/src/v2/session-entry.ts | 1 + packages/opencode/test/config/config.test.ts | 4 ++-- packages/shared/src/util/retry.ts | 1 + .../shared/test/util/effect-flock.test.ts | 1 + .../.storybook/mocks/app/context/language.ts | 1 + packages/ui/src/components/file.tsx | 3 +++ packages/ui/src/components/session-turn.tsx | 1 + packages/web/src/components/share/part.tsx | 8 ++++++- 26 files changed, 87 insertions(+), 55 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index e16c8408d6..a0b620649f 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -1,9 +1,13 @@ { "$schema": "https://raw.githubusercontent.com/nicolo-ribaudo/oxc-project.github.io/refs/heads/json-schema/src/public/.oxlintrc.schema.json", + "options": { + "typeAware": true + }, "categories": { "suspicious": "warn" }, "rules": { + "typescript/no-base-to-string": "warn", // Effect uses `function*` with Effect.gen/Effect.fnUntraced that don't always yield "require-yield": "off", // SolidJS uses `let ref: T | undefined` for JSX ref bindings assigned at runtime diff --git a/packages/console/app/src/routes/workspace/[id]/billing/black-section.tsx b/packages/console/app/src/routes/workspace/[id]/billing/black-section.tsx index b8f089864d..5b4389466e 100644 --- a/packages/console/app/src/routes/workspace/[id]/billing/black-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/billing/black-section.tsx @@ -116,9 +116,9 @@ const createSessionUrl = action(async (workspaceID: string, returnUrl: string) = const setUseBalance = action(async (form: FormData) => { "use server" - const workspaceID = form.get("workspaceID")?.toString() + const workspaceID = form.get("workspaceID") as string | null if (!workspaceID) return { error: formError.workspaceRequired } - const useBalance = form.get("useBalance")?.toString() === "true" + const useBalance = (form.get("useBalance") as string | null) === "true" return json( await withActor(async () => { diff --git a/packages/console/app/src/routes/workspace/[id]/billing/monthly-limit-section.tsx b/packages/console/app/src/routes/workspace/[id]/billing/monthly-limit-section.tsx index ef54b84099..7da1de2389 100644 --- a/packages/console/app/src/routes/workspace/[id]/billing/monthly-limit-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/billing/monthly-limit-section.tsx @@ -10,11 +10,11 @@ import { formError, localizeError } from "~/lib/form-error" const setMonthlyLimit = action(async (form: FormData) => { "use server" - const limit = form.get("limit")?.toString() + const limit = form.get("limit") as string | null if (!limit) return { error: formError.limitRequired } const numericLimit = parseInt(limit) if (numericLimit < 0) return { error: formError.monthlyLimitInvalid } - const workspaceID = form.get("workspaceID")?.toString() + const workspaceID = form.get("workspaceID") as string | null if (!workspaceID) return { error: formError.workspaceRequired } return json( await withActor( diff --git a/packages/console/app/src/routes/workspace/[id]/billing/reload-section.tsx b/packages/console/app/src/routes/workspace/[id]/billing/reload-section.tsx index a25963ab07..c9a72c0879 100644 --- a/packages/console/app/src/routes/workspace/[id]/billing/reload-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/billing/reload-section.tsx @@ -12,7 +12,7 @@ import { formError, formErrorReloadAmountMin, formErrorReloadTriggerMin, localiz const reload = action(async (form: FormData) => { "use server" - const workspaceID = form.get("workspaceID")?.toString() + const workspaceID = form.get("workspaceID") as string | null if (!workspaceID) return { error: formError.workspaceRequired } return json(await withActor(() => Billing.reload(), workspaceID), { revalidate: queryBillingInfo.key, @@ -21,11 +21,11 @@ const reload = action(async (form: FormData) => { const setReload = action(async (form: FormData) => { "use server" - const workspaceID = form.get("workspaceID")?.toString() + const workspaceID = form.get("workspaceID") as string | null if (!workspaceID) return { error: formError.workspaceRequired } - const reloadValue = form.get("reload")?.toString() === "true" - const amountStr = form.get("reloadAmount")?.toString() - const triggerStr = form.get("reloadTrigger")?.toString() + const reloadValue = (form.get("reload") as string | null) === "true" + const amountStr = form.get("reloadAmount") as string | null + const triggerStr = form.get("reloadTrigger") as string | null const reloadAmount = amountStr && amountStr.trim() !== "" ? parseInt(amountStr) : null const reloadTrigger = triggerStr && triggerStr.trim() !== "" ? parseInt(triggerStr) : null @@ -91,8 +91,8 @@ export function ReloadSection() { const info = billingInfo()! setStore("show", true) setStore("reload", true) - setStore("reloadAmount", info.reloadAmount.toString()) - setStore("reloadTrigger", info.reloadTrigger.toString()) + setStore("reloadAmount", String(info.reloadAmount)) + setStore("reloadTrigger", String(info.reloadTrigger)) } function hide() { @@ -152,11 +152,11 @@ export function ReloadSection() { data-component="input" name="reloadAmount" type="number" - min={billingInfo()?.reloadAmountMin.toString()} + min={String(billingInfo()?.reloadAmountMin ?? "")} step="1" value={store.reloadAmount} onInput={(e) => setStore("reloadAmount", e.currentTarget.value)} - placeholder={billingInfo()?.reloadAmount.toString()} + placeholder={String(billingInfo()?.reloadAmount ?? "")} disabled={!store.reload} />
@@ -166,11 +166,11 @@ export function ReloadSection() { data-component="input" name="reloadTrigger" type="number" - min={billingInfo()?.reloadTriggerMin.toString()} + min={String(billingInfo()?.reloadTriggerMin ?? "")} step="1" value={store.reloadTrigger} onInput={(e) => setStore("reloadTrigger", e.currentTarget.value)} - placeholder={billingInfo()?.reloadTrigger.toString()} + placeholder={String(billingInfo()?.reloadTrigger ?? "")} disabled={!store.reload} /> diff --git a/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx b/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx index 95ff7af2b9..d0f8121828 100644 --- a/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx @@ -120,9 +120,9 @@ const createSessionUrl = action(async (workspaceID: string, returnUrl: string) = const setLiteUseBalance = action(async (form: FormData) => { "use server" - const workspaceID = form.get("workspaceID")?.toString() + const workspaceID = form.get("workspaceID") as string | null if (!workspaceID) return { error: formError.workspaceRequired } - const useBalance = form.get("useBalance")?.toString() === "true" + const useBalance = (form.get("useBalance") as string | null) === "true" return json( await withActor(async () => { diff --git a/packages/console/app/src/routes/workspace/[id]/keys/key-section.tsx b/packages/console/app/src/routes/workspace/[id]/keys/key-section.tsx index 837ab743a5..cb273a422e 100644 --- a/packages/console/app/src/routes/workspace/[id]/keys/key-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/keys/key-section.tsx @@ -12,18 +12,18 @@ import { formError, localizeError } from "~/lib/form-error" const removeKey = action(async (form: FormData) => { "use server" - const id = form.get("id")?.toString() + const id = form.get("id") as string | null if (!id) return { error: formError.idRequired } - const workspaceID = form.get("workspaceID")?.toString() + const workspaceID = form.get("workspaceID") as string | null if (!workspaceID) return { error: formError.workspaceRequired } return json(await withActor(() => Key.remove({ id }), workspaceID), { revalidate: listKeys.key }) }, "key.remove") const createKey = action(async (form: FormData) => { "use server" - const name = form.get("name")?.toString().trim() + const name = (form.get("name") as string | null)?.trim() if (!name) return { error: formError.nameRequired } - const workspaceID = form.get("workspaceID")?.toString() + const workspaceID = form.get("workspaceID") as string | null if (!workspaceID) return { error: formError.workspaceRequired } return json( await withActor( diff --git a/packages/console/app/src/routes/workspace/[id]/members/member-section.tsx b/packages/console/app/src/routes/workspace/[id]/members/member-section.tsx index 5a440632f8..00edb400c9 100644 --- a/packages/console/app/src/routes/workspace/[id]/members/member-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/members/member-section.tsx @@ -24,13 +24,13 @@ const listMembers = query(async (workspaceID: string) => { const inviteMember = action(async (form: FormData) => { "use server" - const email = form.get("email")?.toString().trim() + const email = (form.get("email") as string | null)?.trim() if (!email) return { error: formError.emailRequired } - const workspaceID = form.get("workspaceID")?.toString() + const workspaceID = form.get("workspaceID") as string | null if (!workspaceID) return { error: formError.workspaceRequired } - const role = form.get("role")?.toString() as (typeof UserRole)[number] + const role = form.get("role") as (typeof UserRole)[number] | null if (!role) return { error: formError.roleRequired } - const limit = form.get("limit")?.toString() + const limit = form.get("limit") as string | null const monthlyLimit = limit && limit.trim() !== "" ? parseInt(limit) : null if (monthlyLimit !== null && monthlyLimit < 0) return { error: formError.monthlyLimitInvalid } return json( @@ -47,9 +47,9 @@ const inviteMember = action(async (form: FormData) => { const removeMember = action(async (form: FormData) => { "use server" - const id = form.get("id")?.toString() + const id = form.get("id") as string | null if (!id) return { error: formError.idRequired } - const workspaceID = form.get("workspaceID")?.toString() + const workspaceID = form.get("workspaceID") as string | null if (!workspaceID) return { error: formError.workspaceRequired } return json( await withActor( @@ -66,13 +66,13 @@ const removeMember = action(async (form: FormData) => { const updateMember = action(async (form: FormData) => { "use server" - const id = form.get("id")?.toString() + const id = form.get("id") as string | null if (!id) return { error: formError.idRequired } - const workspaceID = form.get("workspaceID")?.toString() + const workspaceID = form.get("workspaceID") as string | null if (!workspaceID) return { error: formError.workspaceRequired } - const role = form.get("role")?.toString() as (typeof UserRole)[number] + const role = form.get("role") as (typeof UserRole)[number] | null if (!role) return { error: formError.roleRequired } - const limit = form.get("limit")?.toString() + const limit = form.get("limit") as string | null const monthlyLimit = limit && limit.trim() !== "" ? parseInt(limit) : null if (monthlyLimit !== null && monthlyLimit < 0) return { error: formError.monthlyLimitInvalid } @@ -118,7 +118,7 @@ function MemberRow(props: { } setStore("editing", true) setStore("selectedRole", props.member.role) - setStore("limit", props.member.monthlyLimit?.toString() ?? "") + setStore("limit", props.member.monthlyLimit != null ? String(props.member.monthlyLimit) : "") } function hide() { diff --git a/packages/console/app/src/routes/workspace/[id]/model-section.tsx b/packages/console/app/src/routes/workspace/[id]/model-section.tsx index bf19f81cd2..b9cdf3bc3a 100644 --- a/packages/console/app/src/routes/workspace/[id]/model-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/model-section.tsx @@ -67,11 +67,11 @@ const getModelsInfo = query(async (workspaceID: string) => { const updateModel = action(async (form: FormData) => { "use server" - const model = form.get("model")?.toString() + const model = form.get("model") as string | null if (!model) return { error: formError.modelRequired } - const workspaceID = form.get("workspaceID")?.toString() + const workspaceID = form.get("workspaceID") as string | null if (!workspaceID) return { error: formError.workspaceRequired } - const enabled = form.get("enabled")?.toString() === "true" + const enabled = (form.get("enabled") as string | null) === "true" return json( withActor(async () => { if (enabled) { @@ -163,7 +163,7 @@ export function ModelSection() {
- +