mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-12 12:45:07 -04:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e0353cca9 | |||
| 959c8bd498 | |||
| ca3df21b7f | |||
| 8571a922db | |||
| d92d1e654b | |||
| 46a14e685a |
+13
-1
@@ -181,6 +181,16 @@ const statsSyncConfig = new sst.Linkable("StatsSyncConfig", {
|
||||
},
|
||||
})
|
||||
|
||||
const r2SqlAuthToken = new sst.Secret("R2SqlAuthToken")
|
||||
const r2Sql = new sst.Linkable("R2Sql", {
|
||||
properties: {
|
||||
accountId: "15d29c8639fd3733b1b5486a2acfd968",
|
||||
bucket: `platform-${$app.stage}-lake`,
|
||||
namespace: "inference",
|
||||
table: "generation",
|
||||
},
|
||||
})
|
||||
|
||||
export const statSync = new sst.aws.Service("StatsSyncService", {
|
||||
cluster: lakeCluster,
|
||||
architecture: "arm64",
|
||||
@@ -193,7 +203,9 @@ export const statSync = new sst.aws.Service("StatsSyncService", {
|
||||
dockerfile: "packages/stats/server/Dockerfile",
|
||||
},
|
||||
command: ["bun", "src/stat-sync.ts"],
|
||||
link: [database, inferenceEvent, statsSyncConfig],
|
||||
// Keep the legacy Athena link and IAM permissions during the first R2-backed
|
||||
// release so reverting the application code remains a one-deploy rollback.
|
||||
link: [database, inferenceEvent, r2Sql, r2SqlAuthToken, statsSyncConfig],
|
||||
permissions: lakeQueryPermissions,
|
||||
scaling: {
|
||||
min: 1,
|
||||
|
||||
@@ -6,10 +6,14 @@ import { ChildProcess } 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
|
||||
|
||||
|
||||
@@ -84,6 +84,8 @@ function sdkKey(npm: string): string | undefined {
|
||||
return "gateway"
|
||||
case "@openrouter/ai-sdk-provider":
|
||||
return "openrouter"
|
||||
case "merge-gateway-ai-sdk-provider":
|
||||
return "mergeGateway"
|
||||
case "ai-gateway-provider":
|
||||
// ai-gateway-provider/unified wraps createOpenAICompatible({ name: "Unified" }),
|
||||
// and @ai-sdk/openai-compatible parses compatibleOptions from one of
|
||||
@@ -1772,6 +1774,7 @@ function reasoningEffort(model: Provider.Model, effort: string) {
|
||||
case "@ai-sdk/togetherai":
|
||||
case "venice-ai-sdk-provider":
|
||||
case "ai-gateway-provider":
|
||||
case "merge-gateway-ai-sdk-provider":
|
||||
return { reasoningEffort: effort }
|
||||
case "@ai-sdk/cohere":
|
||||
case "@ai-sdk/perplexity":
|
||||
|
||||
@@ -22,11 +22,9 @@ export type FileDiff = typeof FileDiff.Type
|
||||
|
||||
const prune = "7.days"
|
||||
const limit = 2 * 1024 * 1024
|
||||
// Operational flags are safe for both repositories. Working-tree semantics are
|
||||
// resolved from the source repository and persisted in the private snapshot repository.
|
||||
const operational = ["-c", "core.longpaths=true"]
|
||||
const quote = [...operational, "-c", "core.quotepath=false"]
|
||||
const mirrored = ["core.autocrlf", "core.symlinks"]
|
||||
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
|
||||
@@ -101,25 +99,27 @@ const layer: Layer.Layer<Service, never, FSUtil.Service | AppProcess.Service | C
|
||||
),
|
||||
)
|
||||
|
||||
const required = Effect.fnUntraced(function* (
|
||||
cmd: string[],
|
||||
opts?: { cwd?: string; env?: Record<string, string>; stdin?: string },
|
||||
) {
|
||||
const result = yield* git(cmd, opts)
|
||||
if (result.code === 0) return result
|
||||
return yield* Effect.die(
|
||||
new Error(`git ${cmd.join(" ")} failed with exit code ${result.code}: ${result.stderr.trim()}`),
|
||||
)
|
||||
})
|
||||
|
||||
const ignore = Effect.fnUntraced(function* (files: string[]) {
|
||||
if (!files.length) return new Set<string>()
|
||||
// check-ignore treats a leading colon as pathspec magic but accepts and echoes a protective ./ prefix.
|
||||
const checkIgnorePaths = files.map((item) => (item.startsWith(":") ? `./${item}` : item))
|
||||
const check = yield* git([...quote, "check-ignore", "--no-index", "--stdin", "-z"], {
|
||||
cwd: state.worktree,
|
||||
stdin: encodeNulTerminatedPaths(checkIgnorePaths),
|
||||
})
|
||||
const check = yield* git(
|
||||
[
|
||||
...quote,
|
||||
"--git-dir",
|
||||
path.join(state.worktree, ".git"),
|
||||
"--work-tree",
|
||||
state.worktree,
|
||||
"check-ignore",
|
||||
"--no-index",
|
||||
"--stdin",
|
||||
"-z",
|
||||
],
|
||||
{
|
||||
cwd: state.worktree,
|
||||
stdin: encodeNulTerminatedPaths(checkIgnorePaths),
|
||||
},
|
||||
)
|
||||
if (check.code !== 0 && check.code !== 1) return new Set<string>()
|
||||
return new Set(
|
||||
check.text
|
||||
@@ -131,9 +131,9 @@ const layer: Layer.Layer<Service, never, FSUtil.Service | AppProcess.Service | C
|
||||
|
||||
const drop = Effect.fnUntraced(function* (files: string[]) {
|
||||
if (!files.length) return
|
||||
yield* required(
|
||||
yield* git(
|
||||
[
|
||||
...operational,
|
||||
...cfg,
|
||||
...args(["rm", "--cached", "-f", "--ignore-unmatch", "--pathspec-from-file=-", "--pathspec-file-nul"]),
|
||||
],
|
||||
{
|
||||
@@ -145,13 +145,18 @@ const layer: Layer.Layer<Service, never, FSUtil.Service | AppProcess.Service | C
|
||||
|
||||
const stage = Effect.fnUntraced(function* (files: string[]) {
|
||||
if (!files.length) return
|
||||
yield* required(
|
||||
[...operational, ...args(["add", "--all", "--sparse", "--pathspec-from-file=-", "--pathspec-file-nul"])],
|
||||
const result = yield* git(
|
||||
[...cfg, ...args(["add", "--all", "--sparse", "--pathspec-from-file=-", "--pathspec-file-nul"])],
|
||||
{
|
||||
cwd: state.worktree,
|
||||
stdin: encodeTopLevelLiteralPathspecs(files),
|
||||
},
|
||||
)
|
||||
if (result.code === 0) return
|
||||
yield* Effect.logWarning("failed to add snapshot files", {
|
||||
exitCode: result.code,
|
||||
stderr: result.stderr,
|
||||
})
|
||||
})
|
||||
|
||||
const exists = (file: string) => fs.exists(file).pipe(Effect.orDie)
|
||||
@@ -221,62 +226,12 @@ const layer: Layer.Layer<Service, never, FSUtil.Service | AppProcess.Service | C
|
||||
|
||||
// Seed the index from the source repo so already-hashed entries are reused.
|
||||
// Best-effort: a missing/incompatible index just falls back to a full add.
|
||||
const index = yield* git(["rev-parse", "--path-format=absolute", "--git-path", "index"], {
|
||||
cwd: state.worktree,
|
||||
})
|
||||
const sourceIndex = index.text.trim()
|
||||
const sourceIndex = path.join(source, "index")
|
||||
if (yield* exists(sourceIndex)) {
|
||||
yield* fs.copyFile(sourceIndex, path.join(state.gitdir, "index")).pipe(Effect.catch(() => Effect.void))
|
||||
}
|
||||
})
|
||||
|
||||
const prepare = Effect.fnUntraced(function* () {
|
||||
const existed = yield* exists(state.gitdir)
|
||||
yield* fs.ensureDir(state.gitdir).pipe(Effect.orDie)
|
||||
if (!(yield* exists(path.join(state.gitdir, "config")))) {
|
||||
yield* required(["init"], {
|
||||
env: { GIT_DIR: state.gitdir, GIT_WORK_TREE: state.worktree },
|
||||
})
|
||||
}
|
||||
|
||||
const semantics = yield* Effect.forEach(mirrored, (key) =>
|
||||
git(["config", "--get", key], { cwd: state.worktree }).pipe(
|
||||
Effect.map((result) => {
|
||||
if (result.code === 0) return [key, result.text.trim()] as const
|
||||
if (result.code === 1) return [key, key === "core.autocrlf" ? "false" : "true"] as const
|
||||
throw new Error(`failed to resolve ${key}: ${result.stderr.trim()}`)
|
||||
}),
|
||||
),
|
||||
)
|
||||
const fingerprint = `v1;${semantics.map(([key, value]) => `${key}=${value}`).join(";")}`
|
||||
const previous = yield* git([
|
||||
"--git-dir",
|
||||
state.gitdir,
|
||||
"config",
|
||||
"--local",
|
||||
"--get",
|
||||
"opencode.snapshotSemantics",
|
||||
])
|
||||
if (previous.text.trim() === fingerprint) return
|
||||
const config = [
|
||||
...semantics,
|
||||
["core.longpaths", "true"],
|
||||
["core.fsmonitor", "false"],
|
||||
["feature.manyFiles", "true"],
|
||||
["index.version", "4"],
|
||||
["index.threads", "true"],
|
||||
["core.untrackedCache", "true"],
|
||||
] as const
|
||||
yield* Effect.forEach(
|
||||
config,
|
||||
([key, value]) => required(["--git-dir", state.gitdir, "config", "--local", key, value]),
|
||||
{ discard: true },
|
||||
)
|
||||
yield* seed()
|
||||
yield* required(["--git-dir", state.gitdir, "config", "--local", "opencode.snapshotSemantics", fingerprint])
|
||||
if (!existed) yield* Effect.logInfo("initialized")
|
||||
})
|
||||
|
||||
const add = Effect.fnUntraced(function* () {
|
||||
yield* sync()
|
||||
const [diff, other] = yield* Effect.all(
|
||||
@@ -291,11 +246,13 @@ const layer: Layer.Layer<Service, never, FSUtil.Service | AppProcess.Service | C
|
||||
{ concurrency: 2 },
|
||||
)
|
||||
if (diff.code !== 0 || other.code !== 0) {
|
||||
return yield* Effect.die(
|
||||
new Error(
|
||||
`failed to list snapshot files: diff-files=${diff.code} ${diff.stderr.trim()} ls-files=${other.code} ${other.stderr.trim()}`,
|
||||
),
|
||||
)
|
||||
yield* Effect.logWarning("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)
|
||||
@@ -362,9 +319,26 @@ const layer: Layer.Layer<Service, never, FSUtil.Service | AppProcess.Service | C
|
||||
return yield* locked(
|
||||
Effect.gen(function* () {
|
||||
if (!(yield* enabled())) return
|
||||
yield* prepare()
|
||||
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"])
|
||||
// Tuning for very large worktrees so the first add stays bounded.
|
||||
yield* git(["--git-dir", state.gitdir, "config", "feature.manyFiles", "true"])
|
||||
yield* git(["--git-dir", state.gitdir, "config", "index.version", "4"])
|
||||
yield* git(["--git-dir", state.gitdir, "config", "index.threads", "true"])
|
||||
yield* git(["--git-dir", state.gitdir, "config", "core.untrackedCache", "true"])
|
||||
yield* seed()
|
||||
yield* Effect.logInfo("initialized")
|
||||
}
|
||||
yield* add()
|
||||
const result = yield* required(args(["write-tree"]), { cwd: state.directory })
|
||||
const result = yield* git(args(["write-tree"]), { cwd: state.directory })
|
||||
const hash = result.text.trim()
|
||||
yield* Effect.logInfo("tracking", { hash, cwd: state.directory, git: state.gitdir })
|
||||
return hash
|
||||
@@ -375,7 +349,6 @@ const layer: Layer.Layer<Service, never, FSUtil.Service | AppProcess.Service | C
|
||||
const patch = Effect.fnUntraced(function* (hash: string) {
|
||||
return yield* locked(
|
||||
Effect.gen(function* () {
|
||||
yield* prepare()
|
||||
yield* add()
|
||||
const result = yield* git(
|
||||
[...quote, ...args(["diff", "--cached", "--no-ext-diff", "--name-only", hash, "--", "."])],
|
||||
@@ -409,11 +382,24 @@ const layer: Layer.Layer<Service, never, FSUtil.Service | AppProcess.Service | C
|
||||
const restore = Effect.fnUntraced(function* (snapshot: string) {
|
||||
return yield* locked(
|
||||
Effect.gen(function* () {
|
||||
yield* prepare()
|
||||
yield* Effect.logInfo("restore", { commit: snapshot })
|
||||
yield* required([...operational, ...args(["read-tree", snapshot])], { cwd: state.worktree })
|
||||
yield* required([...operational, ...args(["checkout-index", "-a", "-f"])], {
|
||||
cwd: state.worktree,
|
||||
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
|
||||
yield* Effect.logError("failed to restore snapshot", {
|
||||
snapshot,
|
||||
exitCode: checkout.code,
|
||||
stderr: checkout.stderr,
|
||||
})
|
||||
return
|
||||
}
|
||||
yield* Effect.logError("failed to restore snapshot", {
|
||||
snapshot,
|
||||
exitCode: result.code,
|
||||
stderr: result.stderr,
|
||||
})
|
||||
}),
|
||||
)
|
||||
@@ -422,7 +408,6 @@ const layer: Layer.Layer<Service, never, FSUtil.Service | AppProcess.Service | C
|
||||
const revert = Effect.fnUntraced(function* (patches: Patch[]) {
|
||||
return yield* locked(
|
||||
Effect.gen(function* () {
|
||||
yield* prepare()
|
||||
const ops: { hash: string; file: string; rel: string }[] = []
|
||||
const seen = new Set<string>()
|
||||
for (const item of patches) {
|
||||
@@ -439,21 +424,20 @@ const layer: Layer.Layer<Service, never, FSUtil.Service | AppProcess.Service | C
|
||||
|
||||
const single = Effect.fnUntraced(function* (op: (typeof ops)[number]) {
|
||||
yield* Effect.logInfo("reverting", { file: op.file, hash: op.hash })
|
||||
const result = yield* git([...operational, ...args(["checkout", op.hash, "--", op.file])], {
|
||||
const result = yield* git([...core, ...args(["checkout", op.hash, "--", op.file])], {
|
||||
cwd: state.worktree,
|
||||
})
|
||||
if (result.code === 0) return
|
||||
const tree = yield* git([...operational, ...args(["ls-tree", op.hash, "--", op.rel])], {
|
||||
const tree = yield* git([...core, ...args(["ls-tree", op.hash, "--", op.rel])], {
|
||||
cwd: state.worktree,
|
||||
})
|
||||
if (tree.code !== 0)
|
||||
yield* Effect.die(
|
||||
new Error(`failed to inspect ${op.file} in snapshot ${op.hash}: ${tree.stderr.trim()}`),
|
||||
)
|
||||
if (tree.text.trim())
|
||||
yield* Effect.die(
|
||||
new Error(`failed to restore ${op.file} from snapshot ${op.hash}: ${result.stderr.trim()}`),
|
||||
)
|
||||
if (tree.code === 0 && tree.text.trim()) {
|
||||
yield* Effect.logInfo("file existed in snapshot but checkout failed, keeping", {
|
||||
file: op.file,
|
||||
hash: op.hash,
|
||||
})
|
||||
return
|
||||
}
|
||||
yield* Effect.logInfo("file did not exist in snapshot, deleting", { file: op.file, hash: op.hash })
|
||||
yield* remove(op.file)
|
||||
})
|
||||
@@ -480,10 +464,7 @@ const layer: Layer.Layer<Service, never, FSUtil.Service | AppProcess.Service | C
|
||||
}
|
||||
|
||||
const tree = yield* git(
|
||||
[
|
||||
...operational,
|
||||
...args(["ls-tree", "--name-only", first.hash, "--", ...run.map((item) => item.rel)]),
|
||||
],
|
||||
[...core, ...args(["ls-tree", "--name-only", first.hash, "--", ...run.map((item) => item.rel)])],
|
||||
{
|
||||
cwd: state.worktree,
|
||||
},
|
||||
@@ -512,7 +493,7 @@ const layer: Layer.Layer<Service, never, FSUtil.Service | AppProcess.Service | C
|
||||
if (list.length) {
|
||||
yield* Effect.logInfo("reverting", { hash: first.hash, files: list.length })
|
||||
const result = yield* git(
|
||||
[...operational, ...args(["checkout", first.hash, "--", ...list.map((item) => item.file)])],
|
||||
[...core, ...args(["checkout", first.hash, "--", ...list.map((item) => item.file)])],
|
||||
{
|
||||
cwd: state.worktree,
|
||||
},
|
||||
@@ -545,7 +526,6 @@ const layer: Layer.Layer<Service, never, FSUtil.Service | AppProcess.Service | C
|
||||
const diff = Effect.fnUntraced(function* (hash: string) {
|
||||
return yield* locked(
|
||||
Effect.gen(function* () {
|
||||
yield* prepare()
|
||||
yield* add()
|
||||
const result = yield* git([...quote, ...args(["diff", "--cached", "--no-ext-diff", hash, "--", "."])], {
|
||||
cwd: state.worktree,
|
||||
@@ -566,7 +546,6 @@ const layer: Layer.Layer<Service, never, FSUtil.Service | AppProcess.Service | C
|
||||
const diffFull = Effect.fnUntraced(function* (from: string, to: string) {
|
||||
return yield* locked(
|
||||
Effect.gen(function* () {
|
||||
yield* prepare()
|
||||
type Row = {
|
||||
file: string
|
||||
status: "added" | "deleted" | "modified"
|
||||
@@ -586,14 +565,12 @@ const layer: Layer.Layer<Service, never, FSUtil.Service | AppProcess.Service | C
|
||||
if (row.status === "added") {
|
||||
return [
|
||||
"",
|
||||
yield* git([...operational, ...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") {
|
||||
return [
|
||||
yield* git([...operational, ...args(["show", `${from}:${row.file}`])]).pipe(
|
||||
yield* git([...cfg, ...args(["show", `${from}:${row.file}`])]).pipe(
|
||||
Effect.map((item) => item.text),
|
||||
),
|
||||
"",
|
||||
@@ -601,10 +578,8 @@ const layer: Layer.Layer<Service, never, FSUtil.Service | AppProcess.Service | C
|
||||
}
|
||||
return yield* Effect.all(
|
||||
[
|
||||
git([...operational, ...args(["show", `${from}:${row.file}`])]).pipe(
|
||||
Effect.map((item) => item.text),
|
||||
),
|
||||
git([...operational, ...args(["show", `${to}:${row.file}`])]).pipe(Effect.map((item) => item.text)),
|
||||
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 },
|
||||
)
|
||||
@@ -627,7 +602,7 @@ const layer: Layer.Layer<Service, never, FSUtil.Service | AppProcess.Service | C
|
||||
if (!refs.length) return new Map<string, { before: string; after: string }>()
|
||||
|
||||
const batch = yield* appProcess.run(
|
||||
ChildProcess.make("git", [...operational, ...args(["cat-file", "--batch"])], {
|
||||
ChildProcess.make("git", [...cfg, ...args(["cat-file", "--batch"])], {
|
||||
cwd: state.directory,
|
||||
extendEnv: true,
|
||||
}),
|
||||
|
||||
@@ -115,39 +115,6 @@ describe("Git", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("respects repository line ending configuration", () =>
|
||||
Effect.gen(function* () {
|
||||
const tmp = yield* scopedTmpdir({ git: true })
|
||||
yield* Effect.promise(() => $`git config core.autocrlf true`.cwd(tmp.path).quiet())
|
||||
yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, "line-endings.txt"), "before\n", "utf-8"))
|
||||
yield* Effect.promise(() => $`git add line-endings.txt`.cwd(tmp.path).quiet())
|
||||
yield* Effect.promise(() => $`git commit --no-gpg-sign -m "add line endings"`.cwd(tmp.path).quiet())
|
||||
yield* Effect.promise(() => fs.rm(path.join(tmp.path, "line-endings.txt")))
|
||||
yield* Effect.promise(() => $`git checkout -- line-endings.txt`.cwd(tmp.path).quiet())
|
||||
|
||||
const git = yield* Git.Service
|
||||
expect(yield* git.status(tmp.path)).toEqual([])
|
||||
expect(yield* git.diff(tmp.path, "HEAD")).toEqual([])
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("respects repository symlink configuration", () =>
|
||||
Effect.gen(function* () {
|
||||
const tmp = yield* scopedTmpdir({ git: true })
|
||||
const blob = yield* Effect.promise(() => $`echo -n target.txt | git hash-object -w --stdin`.cwd(tmp.path).text())
|
||||
yield* Effect.promise(() =>
|
||||
$`git update-index --add --cacheinfo 120000,${blob.trim()},link.txt`.cwd(tmp.path).quiet(),
|
||||
)
|
||||
yield* Effect.promise(() => $`git commit --no-gpg-sign -m "add symlink"`.cwd(tmp.path).quiet())
|
||||
yield* Effect.promise(() => $`git config core.symlinks false`.cwd(tmp.path).quiet())
|
||||
yield* Effect.promise(() => $`git checkout-index -f link.txt`.cwd(tmp.path).quiet())
|
||||
|
||||
const git = yield* Git.Service
|
||||
expect(yield* git.status(tmp.path)).toEqual([])
|
||||
expect(yield* git.diff(tmp.path, "HEAD")).toEqual([])
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("patch() returns capped native patch output", () =>
|
||||
Effect.gen(function* () {
|
||||
const tmp = yield* scopedTmpdir({ git: true })
|
||||
|
||||
@@ -1548,6 +1548,33 @@ test("models.dev reasoning options replace generated variants and unsupported to
|
||||
expect(models["gemini-3-pro-fast"].variants).toEqual(models.override.variants)
|
||||
})
|
||||
|
||||
test("MERGE Gateway exposes declared effort variants without model-specific handling", () => {
|
||||
const provider = {
|
||||
id: "merge-gateway",
|
||||
name: "MERGE Gateway",
|
||||
env: ["MERGE_GATEWAY_API_KEY"],
|
||||
npm: "merge-gateway-ai-sdk-provider",
|
||||
models: {
|
||||
"openai/gpt-5.6-sol": {
|
||||
id: "openai/gpt-5.6-sol",
|
||||
name: "GPT-5.6 Sol",
|
||||
reasoning: true,
|
||||
reasoning_options: [{ type: "effort", values: ["none", "low", "medium", "high", "xhigh", "max"] }],
|
||||
limit: { context: 128_000, output: 64_000 },
|
||||
},
|
||||
},
|
||||
} as unknown as ModelsDev.Provider
|
||||
|
||||
expect(Provider.fromModelsDevProvider(provider).models["openai/gpt-5.6-sol"].variants).toEqual({
|
||||
none: { reasoningEffort: "none" },
|
||||
low: { reasoningEffort: "low" },
|
||||
medium: { reasoningEffort: "medium" },
|
||||
high: { reasoningEffort: "high" },
|
||||
xhigh: { reasoningEffort: "xhigh" },
|
||||
max: { reasoningEffort: "max" },
|
||||
})
|
||||
})
|
||||
|
||||
test("public provider info omits invalid models", () => {
|
||||
const provider = Provider.fromModelsDevProvider({
|
||||
id: "test",
|
||||
|
||||
@@ -3370,6 +3370,7 @@ describe("ProviderTransform.reasoningVariants", () => {
|
||||
["@ai-sdk/togetherai", { reasoningEffort: "high" }],
|
||||
["venice-ai-sdk-provider", { reasoningEffort: "high" }],
|
||||
["ai-gateway-provider", { reasoningEffort: "high" }],
|
||||
["merge-gateway-ai-sdk-provider", { reasoningEffort: "high" }],
|
||||
["@ai-sdk/amazon-bedrock", { reasoningConfig: { type: "enabled", maxReasoningEffort: "high" } }],
|
||||
])("converts effort for %s", (npm, expected, ...args) => {
|
||||
const id = args[0] as string | undefined
|
||||
@@ -5555,6 +5556,25 @@ describe("ProviderTransform.providerOptions - ai-gateway-provider", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("ProviderTransform.providerOptions - merge-gateway-ai-sdk-provider", () => {
|
||||
const model = {
|
||||
id: "merge-gateway/openai/gpt-5.6-sol",
|
||||
providerID: "merge-gateway",
|
||||
api: {
|
||||
id: "openai/gpt-5.6-sol",
|
||||
url: "https://api-gateway.merge.dev/v1/ai-sdk",
|
||||
npm: "merge-gateway-ai-sdk-provider",
|
||||
},
|
||||
capabilities: { reasoning: true },
|
||||
} as any
|
||||
|
||||
test("routes normalized effort under the adapter's mergeGateway key", () => {
|
||||
expect(ProviderTransform.providerOptions(model, { reasoningEffort: "high" })).toEqual({
|
||||
mergeGateway: { reasoningEffort: "high" },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("ProviderTransform.options - kimi family adaptive thinking", () => {
|
||||
const createModel = (overrides: Record<string, any> = {}) =>
|
||||
({
|
||||
|
||||
@@ -5,7 +5,7 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { Effect, Exit, Fiber, Layer } from "effect"
|
||||
import { Effect, Fiber, Layer } from "effect"
|
||||
import { Snapshot } from "../../src/snapshot"
|
||||
import {
|
||||
disposeAllInstances,
|
||||
@@ -117,68 +117,6 @@ it.instance(
|
||||
{ git: true },
|
||||
)
|
||||
|
||||
it.instance(
|
||||
"uses source repository line ending semantics",
|
||||
Effect.gen(function* () {
|
||||
const tmp = yield* TestInstance
|
||||
const file = path.join(tmp.directory, "line-endings.txt")
|
||||
yield* exec(tmp.directory, ["git", "config", "core.autocrlf", "true"])
|
||||
yield* write(file, "before\n")
|
||||
yield* exec(tmp.directory, ["git", "add", "line-endings.txt"])
|
||||
yield* exec(tmp.directory, ["git", "commit", "--no-gpg-sign", "-m", "add line endings"])
|
||||
yield* rm(file)
|
||||
yield* exec(tmp.directory, ["git", "checkout", "--", "line-endings.txt"])
|
||||
|
||||
const snapshot = yield* Snapshot.Service
|
||||
const before = yield* snapshot.track()
|
||||
yield* write(file, "before\n")
|
||||
const after = yield* snapshot.track()
|
||||
expect(after).toBe(before)
|
||||
|
||||
yield* exec(tmp.directory, ["git", "config", "core.autocrlf", "false"])
|
||||
yield* write(file, "before\r\n")
|
||||
expect(yield* snapshot.track()).not.toBe(before)
|
||||
|
||||
yield* exec(tmp.directory, ["git", "config", "core.autocrlf", "true"])
|
||||
expect(yield* snapshot.track()).toBe(before)
|
||||
|
||||
yield* exec(tmp.directory, ["git", "config", "core.autocrlf", "input"])
|
||||
yield* write(file, "changed\n")
|
||||
yield* snapshot.restore(before!)
|
||||
expect(yield* readText(file)).toBe("before\n")
|
||||
|
||||
yield* exec(tmp.directory, ["git", "config", "core.autocrlf", "true"])
|
||||
yield* write(file, "changed\n")
|
||||
yield* snapshot.restore(before!)
|
||||
expect(yield* readText(file)).toBe("before\r\n")
|
||||
}),
|
||||
{ git: true },
|
||||
)
|
||||
|
||||
it.instance(
|
||||
"uses source repository symlink semantics",
|
||||
Effect.gen(function* () {
|
||||
const tmp = yield* TestInstance
|
||||
const blob = yield* Effect.promise(() =>
|
||||
$`printf target.txt | git hash-object -w --stdin`.cwd(tmp.directory).text(),
|
||||
)
|
||||
yield* exec(tmp.directory, ["git", "update-index", "--add", "--cacheinfo", `120000,${blob.trim()},link.txt`])
|
||||
yield* exec(tmp.directory, ["git", "commit", "--no-gpg-sign", "-m", "add symlink"])
|
||||
yield* exec(tmp.directory, ["git", "config", "core.symlinks", "false"])
|
||||
yield* exec(tmp.directory, ["git", "checkout-index", "-f", "link.txt"])
|
||||
|
||||
const snapshot = yield* Snapshot.Service
|
||||
const before = yield* snapshot.track()
|
||||
|
||||
yield* exec(tmp.directory, ["git", "config", "core.symlinks", "true"])
|
||||
expect(yield* snapshot.track()).not.toBe(before)
|
||||
|
||||
yield* exec(tmp.directory, ["git", "config", "core.symlinks", "false"])
|
||||
expect(yield* snapshot.track()).toBe(before)
|
||||
}),
|
||||
{ git: true },
|
||||
)
|
||||
|
||||
it.instance(
|
||||
"revert should remove new files",
|
||||
withTrackedSnapshot(({ tmp, snapshot, before }) =>
|
||||
@@ -349,20 +287,6 @@ it.instance(
|
||||
{ git: true },
|
||||
)
|
||||
|
||||
it.instance(
|
||||
"revert preserves files when snapshot lookup fails",
|
||||
withTrackedSnapshot(({ tmp, snapshot }) =>
|
||||
Effect.gen(function* () {
|
||||
const file = path.join(tmp.path, "protected.txt")
|
||||
yield* write(file, "keep")
|
||||
const result = yield* Effect.exit(snapshot.revert([{ hash: "invalid-hash", files: [file] }]))
|
||||
expect(Exit.isFailure(result)).toBe(true)
|
||||
expect(yield* readText(file)).toBe("keep")
|
||||
}),
|
||||
),
|
||||
{ git: true },
|
||||
)
|
||||
|
||||
it.instance(
|
||||
"unicode filenames",
|
||||
withTrackedSnapshot(({ tmp, snapshot, before }) =>
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"./database": "./src/database.ts",
|
||||
"./database/*": "./src/database/*.ts",
|
||||
"./domain/*": "./src/domain/*.ts",
|
||||
"./r2-sql": "./src/r2-sql.ts",
|
||||
"./runtime": "./src/runtime.ts",
|
||||
"./stat-sync": "./src/stat-sync.ts"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { toGeoAggregate, toModelAggregate, toProviderAggregate } from "./inference"
|
||||
import { buildStatsQueries, toGeoAggregate, toModelAggregate, toProviderAggregate } from "./inference"
|
||||
import { modelAuthor, normalizeInferenceModel, statModel, statProvider } from "./model-normalization"
|
||||
|
||||
describe("inference stat normalization", () => {
|
||||
@@ -82,6 +82,65 @@ describe("inference stat normalization", () => {
|
||||
}),
|
||||
).toMatchObject([{ period_key: "2026-W20" }])
|
||||
})
|
||||
|
||||
test("builds bounded R2 SQL queries for each day and week", () => {
|
||||
const queries = buildStatsQueries(new Date("2026-08-10T00:00:00.000Z"), new Date("2026-08-12T12:00:00.000Z"), {
|
||||
namespace: "inference",
|
||||
table: "generation",
|
||||
dataset: "zen",
|
||||
})
|
||||
|
||||
expect(queries).toHaveLength(8)
|
||||
expect(queries[0]).toContain("'week' AS grain")
|
||||
expect(queries[0]).toContain("'2026-W33' AS period_key")
|
||||
expect(queries[2]).toContain("'2026-08-10' AS period_key")
|
||||
expect(queries[6]).toContain("'2026-08-12' AS period_key")
|
||||
expect(queries[0]).toContain('FROM "inference"."generation"')
|
||||
expect(queries[0]).toContain("event_type = 'generation.completed'")
|
||||
expect(queries[0]).toContain("product = 'go'")
|
||||
expect(queries[0]).toContain("LIMIT 10000")
|
||||
expect(queries[0]).toContain("approx_distinct(session) AS sessions")
|
||||
expect(queries[1]).toContain("'geo_model' ELSE 'geo'")
|
||||
expect(queries[1]).toContain("0 AS sessions")
|
||||
})
|
||||
|
||||
test("aligns periods to UTC calendar boundaries", () => {
|
||||
const queries = buildStatsQueries(
|
||||
new Date("2026-06-17T15:56:00.000Z"),
|
||||
new Date("2026-06-19T15:56:00.000Z"),
|
||||
{
|
||||
namespace: "inference",
|
||||
table: "generation",
|
||||
dataset: "zen",
|
||||
},
|
||||
)
|
||||
|
||||
expect(queries).toHaveLength(8)
|
||||
expect(queries[0]).toContain("'2026-W25' AS period_key")
|
||||
expect(queries[0]).toContain("started_at >= '2026-06-15T00:00:00.000Z'")
|
||||
expect(queries[2]).toContain("'2026-06-17' AS period_key")
|
||||
expect(queries[2]).toContain("started_at >= '2026-06-17T00:00:00.000Z'")
|
||||
expect(queries[2]).toContain("started_at < '2026-06-18T00:00:00.000Z'")
|
||||
expect(queries[6]).toContain("'2026-06-19' AS period_key")
|
||||
expect(queries[6]).toContain("started_at < '2026-06-19T15:56:00.000Z'")
|
||||
})
|
||||
|
||||
test("uses an exclusive live and legacy source handoff", () => {
|
||||
const [query] = buildStatsQueries(
|
||||
new Date("2026-08-11T00:00:00.000Z"),
|
||||
new Date("2026-08-12T00:00:00.000Z"),
|
||||
{
|
||||
namespace: "inference",
|
||||
table: "generation",
|
||||
dataset: "zen",
|
||||
},
|
||||
)
|
||||
|
||||
expect(query).toContain(
|
||||
"(source = 'inference-legacy' AND started_at < '2026-08-11T10:57:48.186Z')",
|
||||
)
|
||||
expect(query).toContain("(source = 'inference' AND started_at >= '2026-08-11T10:57:48.186Z')")
|
||||
})
|
||||
})
|
||||
|
||||
function aggregate(model: string, provider: string) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Resource } from "sst/resource"
|
||||
import type { AthenaData } from "../athena"
|
||||
import type { R2SqlData } from "../r2-sql"
|
||||
import type { GeoStatAggregate } from "./geo"
|
||||
import type { ModelStatAggregate } from "./model"
|
||||
import {
|
||||
@@ -10,25 +10,80 @@ import {
|
||||
statProvider,
|
||||
} from "./model-normalization"
|
||||
import type { ProviderStatAggregate } from "./provider"
|
||||
import { normalizeCountry, normalizeTier, type StatBaseAggregate } from "./stat"
|
||||
import {
|
||||
normalizeCountry,
|
||||
normalizeTier,
|
||||
periodKeyFor,
|
||||
startOfIsoWeek,
|
||||
startOfUtcDay,
|
||||
type StatBaseAggregate,
|
||||
} from "./stat"
|
||||
|
||||
export type StatDimension = "model" | "provider" | "geo" | "geo_model"
|
||||
export type StatsQuerySource = { namespace: string; table: string; dataset: string }
|
||||
type StatsQueryFamily = "usage" | "geo"
|
||||
|
||||
// All stat dimensions and both grains are computed in one query via GROUPING SETS so
|
||||
// the source table is scanned once per sync pass; separate queries per dimension (and
|
||||
// the previous weekly/daily UNION ALL) each re-scanned the same events.
|
||||
export function buildStatsQuery(periodStart: Date, periodEnd: Date) {
|
||||
const periodStartValue = sqlString(periodStart.toISOString())
|
||||
const periodEndValue = sqlString(periodEnd.toISOString())
|
||||
const periodStartDateValue = sqlString(periodStart.toISOString().slice(0, 10))
|
||||
const periodEndDateValue = sqlString(periodEnd.toISOString().slice(0, 10))
|
||||
const sourceTable = [Resource.InferenceEvent.catalog, Resource.InferenceEvent.database, Resource.InferenceEvent.table]
|
||||
.map(sqlIdentifier)
|
||||
.join(".")
|
||||
const DAY_MS = 86_400_000
|
||||
const WEEK_MS = 7 * DAY_MS
|
||||
// The typed production stream began before the legacy backfill's original end
|
||||
// boundary. Use one exclusive handoff so the overlapping rows are never counted
|
||||
// from both sources.
|
||||
const LIVE_SOURCE_START = "2026-08-11T10:57:48.186Z"
|
||||
|
||||
// R2 SQL limits result sets to 10,000 rows and does not support OFFSET. Two
|
||||
// queries per day/week keep each result bounded and avoid combining the costly
|
||||
// distinct user/session aggregates with the high-cardinality geo dimensions.
|
||||
export function buildStatsQueries(periodStart: Date, periodEnd: Date, input?: StatsQuerySource) {
|
||||
const source = input ?? {
|
||||
namespace: Resource.R2Sql.namespace,
|
||||
table: Resource.R2Sql.table,
|
||||
dataset: Resource.StatsSyncConfig.dataset,
|
||||
}
|
||||
return [...statPeriods("week", periodStart, periodEnd), ...statPeriods("day", periodStart, periodEnd)].flatMap(
|
||||
(period) => [buildStatsQuery(period, source, "usage"), buildStatsQuery(period, source, "geo")],
|
||||
)
|
||||
}
|
||||
|
||||
function buildStatsQuery(
|
||||
period: { grain: "day" | "week"; key: string; start: Date; end: Date },
|
||||
source: StatsQuerySource,
|
||||
family: StatsQueryFamily,
|
||||
) {
|
||||
const periodStartValue = sqlString(period.start.toISOString())
|
||||
const periodEndValue = sqlString(period.end.toISOString())
|
||||
const ingestEndValue = sqlString(new Date(period.end.getTime() + DAY_MS).toISOString())
|
||||
const sourceTable = [source.namespace, source.table].map(sqlIdentifier).join(".")
|
||||
const dimensions =
|
||||
family === "usage"
|
||||
? `CASE WHEN grouping(model) = 0 THEN 'model' ELSE 'provider' END AS dimension,
|
||||
tier,
|
||||
provider,
|
||||
CASE WHEN grouping(model) = 0 THEN model END AS model,
|
||||
CASE WHEN grouping(model) = 0 THEN COALESCE(MAX(NULLIF(provider_model, '')), '') END AS provider_model,
|
||||
null AS country,
|
||||
null AS continent`
|
||||
: `CASE WHEN grouping(model) = 0 THEN 'geo_model' ELSE 'geo' END AS dimension,
|
||||
tier,
|
||||
CASE WHEN grouping(model) = 0 THEN provider ELSE 'all' END AS provider,
|
||||
CASE WHEN grouping(model) = 0 THEN model ELSE 'all' END AS model,
|
||||
null AS provider_model,
|
||||
country,
|
||||
COALESCE(MAX(NULLIF(continent, '')), '') AS continent`
|
||||
const distinctColumns =
|
||||
family === "usage"
|
||||
? `approx_distinct(session) AS sessions,
|
||||
approx_distinct(user_key) AS unique_users`
|
||||
: `0 AS sessions,
|
||||
0 AS unique_users`
|
||||
const groupingSets =
|
||||
family === "usage"
|
||||
? `(tier, provider, model),
|
||||
(tier, provider)`
|
||||
: `(tier, country),
|
||||
(tier, provider, model, country)`
|
||||
const aggregateColumns = `
|
||||
COUNT(DISTINCT session) AS sessions,
|
||||
${distinctColumns},
|
||||
COUNT(*) AS requests,
|
||||
COUNT(DISTINCT user_key) AS unique_users,
|
||||
COALESCE(SUM(tokens_input), 0) AS input_tokens,
|
||||
COALESCE(SUM(tokens_output), 0) AS output_tokens,
|
||||
COALESCE(SUM(tokens_reasoning), 0) AS reasoning_tokens,
|
||||
@@ -38,65 +93,61 @@ export function buildStatsQuery(periodStart: Date, periodEnd: Date) {
|
||||
COALESCE(SUM(cost_output_microcents), 0) AS output_cost_microcents,
|
||||
COALESCE(SUM(cost_total_microcents), 0) AS total_cost_microcents,
|
||||
AVG(duration_ms) AS avg_duration_ms,
|
||||
approx_percentile(CAST(duration_ms AS double), 0.5) AS p50_duration_ms,
|
||||
approx_percentile(CAST(duration_ms AS double), 0.95) AS p95_duration_ms,
|
||||
null AS p50_duration_ms,
|
||||
null AS p95_duration_ms,
|
||||
AVG(ttfb_ms) AS avg_ttfb_ms,
|
||||
approx_percentile(CAST(ttfb_ms AS double), 0.5) AS p50_ttfb_ms,
|
||||
approx_percentile(CAST(ttfb_ms AS double), 0.95) AS p95_ttfb_ms,
|
||||
null AS p50_ttfb_ms,
|
||||
null AS p95_ttfb_ms,
|
||||
AVG(output_tps) AS avg_output_tps,
|
||||
SUM(CASE WHEN status >= 200 AND status < 400 THEN 1 ELSE 0 END) AS success_count,
|
||||
SUM(CASE WHEN status >= 400 THEN 1 ELSE 0 END) AS error_count,
|
||||
SUM(CASE WHEN outcome = 'succeeded' THEN 1 ELSE 0 END) AS success_count,
|
||||
SUM(CASE WHEN outcome = 'failed' THEN 1 ELSE 0 END) AS error_count,
|
||||
COUNT(*) AS sample_count`
|
||||
|
||||
return `
|
||||
WITH normalized AS (
|
||||
SELECT
|
||||
from_iso8601_timestamp(event_timestamp) AS event_time,
|
||||
model AS raw_model,
|
||||
${statModelSql("model", "provider_model")} AS model,
|
||||
COALESCE(NULLIF(provider_model, ''), '') AS provider_model,
|
||||
COALESCE(NULLIF(provider, ''), '') AS raw_provider,
|
||||
UPPER(COALESCE(NULLIF(cf_country, ''), 'ZZ')) AS country,
|
||||
COALESCE(NULLIF(cf_continent, ''), '') AS continent,
|
||||
session,
|
||||
COALESCE(NULLIF(workspace, ''), '') AS workspace,
|
||||
COALESCE(NULLIF(api_key, ''), '') AS api_key,
|
||||
model_requested AS raw_model,
|
||||
${statModelSql("model_requested", "route_model")} AS model,
|
||||
COALESCE(NULLIF(route_model, ''), '') AS provider_model,
|
||||
COALESCE(NULLIF(provider_id, ''), '') AS raw_provider,
|
||||
UPPER(COALESCE(NULLIF(country, ''), 'ZZ')) AS country,
|
||||
COALESCE(NULLIF(continent, ''), '') AS continent,
|
||||
session_id AS session,
|
||||
COALESCE(NULLIF(workspace_id, ''), '') AS workspace,
|
||||
COALESCE(NULLIF(service_api_key_id, ''), '') AS api_key,
|
||||
COALESCE(NULLIF(user_id, ''), '') AS user_id,
|
||||
status,
|
||||
duration AS duration_ms,
|
||||
time_to_first_byte AS ttfb_ms,
|
||||
timestamp_first_byte,
|
||||
timestamp_last_byte,
|
||||
outcome,
|
||||
duration_ms,
|
||||
time_to_first_token_ms AS ttfb_ms,
|
||||
CASE
|
||||
WHEN first_token_at IS NULL OR last_token_at IS NULL THEN null
|
||||
ELSE date_part('epoch', last_token_at) - date_part('epoch', first_token_at)
|
||||
END AS output_seconds,
|
||||
tokens_input,
|
||||
tokens_output,
|
||||
tokens_reasoning,
|
||||
tokens_cache_read,
|
||||
tokens_cache_write_5m,
|
||||
tokens_cache_write_1h,
|
||||
cost_input_microcents,
|
||||
cost_output_microcents,
|
||||
cost_total_microcents,
|
||||
cost_input,
|
||||
cost_output,
|
||||
cost_total,
|
||||
source
|
||||
tokens_cache_write,
|
||||
cost_input AS cost_input_microcents,
|
||||
cost_output AS cost_output_microcents,
|
||||
cost_total AS cost_total_microcents
|
||||
FROM ${sourceTable}
|
||||
WHERE event_type = 'completions'
|
||||
AND model IS NOT NULL
|
||||
AND model <> ''
|
||||
AND source = 'lite'
|
||||
AND event_date >= ${periodStartDateValue}
|
||||
AND event_date <= ${periodEndDateValue}
|
||||
AND event_timestamp >= ${periodStartValue}
|
||||
AND event_timestamp < ${periodEndValue}
|
||||
WHERE event_type = 'generation.completed'
|
||||
AND source IN ('inference', 'inference-legacy')
|
||||
AND (
|
||||
(source = 'inference-legacy' AND started_at < ${sqlString(LIVE_SOURCE_START)})
|
||||
OR (source = 'inference' AND started_at >= ${sqlString(LIVE_SOURCE_START)})
|
||||
)
|
||||
AND product = 'go'
|
||||
AND model_requested IS NOT NULL
|
||||
AND model_requested <> ''
|
||||
AND __ingest_ts >= ${periodStartValue}
|
||||
AND __ingest_ts < ${ingestEndValue}
|
||||
AND started_at >= ${periodStartValue}
|
||||
AND started_at < ${periodEndValue}
|
||||
), filtered AS (
|
||||
SELECT
|
||||
event_time,
|
||||
CASE
|
||||
WHEN source = 'lite' THEN 'Go'
|
||||
WHEN raw_model IN ('gpt-5-nano', 'grok-code', 'big-pickle') OR regexp_like(raw_model, '-free(:global)?$') THEN 'Free'
|
||||
ELSE 'Paid'
|
||||
END AS tier,
|
||||
'Go' AS tier,
|
||||
${statProviderSql("model", "provider_model", "raw_provider")} AS provider,
|
||||
provider_model,
|
||||
model,
|
||||
@@ -104,63 +155,39 @@ WITH normalized AS (
|
||||
continent,
|
||||
session,
|
||||
COALESCE(NULLIF(user_id, ''), NULLIF(workspace, ''), NULLIF(api_key, '')) AS user_key,
|
||||
status,
|
||||
outcome,
|
||||
duration_ms,
|
||||
ttfb_ms,
|
||||
CASE
|
||||
WHEN timestamp_last_byte - timestamp_first_byte < 100 THEN null
|
||||
ELSE CAST(tokens_output AS double) / (timestamp_last_byte - timestamp_first_byte) * 1000
|
||||
WHEN output_seconds < 0.1 THEN null
|
||||
ELSE CAST(tokens_output AS double) / output_seconds
|
||||
END AS output_tps,
|
||||
tokens_input,
|
||||
tokens_output,
|
||||
tokens_reasoning,
|
||||
tokens_cache_read,
|
||||
COALESCE(tokens_cache_read, 0) + COALESCE(tokens_cache_write_5m, 0) + COALESCE(tokens_cache_write_1h, 0) + COALESCE(tokens_input, 0) + COALESCE(tokens_output, 0) AS tokens_total,
|
||||
COALESCE(cost_input_microcents, cost_input * 1000000) AS cost_input_microcents,
|
||||
COALESCE(cost_output_microcents, cost_output * 1000000) AS cost_output_microcents,
|
||||
COALESCE(cost_total_microcents, cost_total * 1000000) AS cost_total_microcents
|
||||
COALESCE(tokens_cache_read, 0) + COALESCE(tokens_cache_write, 0) + COALESCE(tokens_input, 0) + COALESCE(tokens_output, 0) AS tokens_total,
|
||||
cost_input_microcents,
|
||||
cost_output_microcents,
|
||||
cost_total_microcents
|
||||
FROM normalized
|
||||
WHERE lower(model) NOT IN (${[...EXCLUDED_MODELS].map(sqlString).join(", ")})
|
||||
), periods AS (
|
||||
SELECT
|
||||
concat(CAST(year_of_week(event_time) AS varchar), '-W', lpad(CAST(week(event_time) AS varchar), 2, '0')) AS week_key,
|
||||
substr(to_iso8601(date_trunc('day', event_time)), 1, 10) AS day_key,
|
||||
*
|
||||
FROM filtered
|
||||
)
|
||||
SELECT
|
||||
CASE WHEN grouping(week_key) = 0 THEN 'week' ELSE 'day' END AS grain,
|
||||
COALESCE(week_key, day_key) AS period_key,
|
||||
${sqlString(Resource.StatsSyncConfig.dataset)} AS dataset,
|
||||
CASE
|
||||
WHEN grouping(country) = 0 AND grouping(model) = 0 THEN 'geo_model'
|
||||
WHEN grouping(country) = 0 THEN 'geo'
|
||||
WHEN grouping(model) = 0 THEN 'model'
|
||||
ELSE 'provider'
|
||||
END AS dimension,
|
||||
tier,
|
||||
CASE WHEN grouping(provider) = 0 THEN provider ELSE 'all' END AS provider,
|
||||
CASE WHEN grouping(model) = 0 THEN model WHEN grouping(country) = 0 THEN 'all' END AS model,
|
||||
CASE WHEN grouping(model) = 0 AND grouping(country) = 1 THEN COALESCE(MAX(NULLIF(provider_model, '')), '') END AS provider_model,
|
||||
CASE WHEN grouping(country) = 0 THEN country END AS country,
|
||||
CASE WHEN grouping(country) = 0 THEN COALESCE(MAX(NULLIF(continent, '')), '') END AS continent,
|
||||
${sqlString(period.grain)} AS grain,
|
||||
${sqlString(period.key)} AS period_key,
|
||||
${sqlString(source.dataset)} AS dataset,
|
||||
${dimensions},
|
||||
${aggregateColumns}
|
||||
FROM periods
|
||||
FROM filtered
|
||||
GROUP BY GROUPING SETS (
|
||||
(week_key, tier, provider, model),
|
||||
(week_key, tier, provider),
|
||||
(week_key, tier, country),
|
||||
(week_key, tier, provider, model, country),
|
||||
(day_key, tier, provider, model),
|
||||
(day_key, tier, provider),
|
||||
(day_key, tier, country),
|
||||
(day_key, tier, provider, model, country)
|
||||
${groupingSets}
|
||||
)
|
||||
ORDER BY grain, period_key, total_tokens DESC
|
||||
LIMIT 10000
|
||||
`
|
||||
}
|
||||
|
||||
export function toModelAggregate(data: AthenaData): ModelStatAggregate[] {
|
||||
export function toModelAggregate(data: R2SqlData): ModelStatAggregate[] {
|
||||
const model = statModel(data.model, data.provider_model)
|
||||
const provider = statProvider(model, data.provider_model, data.provider)
|
||||
if (!provider) return []
|
||||
@@ -170,13 +197,13 @@ export function toModelAggregate(data: AthenaData): ModelStatAggregate[] {
|
||||
])
|
||||
}
|
||||
|
||||
export function toProviderAggregate(data: AthenaData): ProviderStatAggregate[] {
|
||||
export function toProviderAggregate(data: R2SqlData): ProviderStatAggregate[] {
|
||||
return toStatBaseAggregate(data).flatMap((base) => [
|
||||
{ ...base, provider: statProvider(data.model, data.provider_model, data.provider) || "unknown" },
|
||||
])
|
||||
}
|
||||
|
||||
export function toGeoAggregate(data: AthenaData): GeoStatAggregate[] {
|
||||
export function toGeoAggregate(data: R2SqlData): GeoStatAggregate[] {
|
||||
return toStatBaseAggregate(data).flatMap((base) => [
|
||||
{
|
||||
...base,
|
||||
@@ -188,7 +215,7 @@ export function toGeoAggregate(data: AthenaData): GeoStatAggregate[] {
|
||||
])
|
||||
}
|
||||
|
||||
function toStatBaseAggregate(data: AthenaData): StatBaseAggregate[] {
|
||||
function toStatBaseAggregate(data: R2SqlData): StatBaseAggregate[] {
|
||||
const grain = data.grain === "day" || data.grain === "week" ? data.grain : undefined
|
||||
if (!grain || !data.period_key) return []
|
||||
|
||||
@@ -223,21 +250,21 @@ function toStatBaseAggregate(data: AthenaData): StatBaseAggregate[] {
|
||||
]
|
||||
}
|
||||
|
||||
function integer(data: AthenaData, key: string) {
|
||||
function integer(data: R2SqlData, key: string) {
|
||||
return Math.round(number(data, key))
|
||||
}
|
||||
|
||||
function nullableNumber(data: AthenaData, key: string) {
|
||||
function nullableNumber(data: R2SqlData, key: string) {
|
||||
if (data[key] === undefined || data[key] === "") return null
|
||||
return Number(number(data, key).toFixed(2))
|
||||
}
|
||||
|
||||
function nullableInteger(data: AthenaData, key: string) {
|
||||
function nullableInteger(data: R2SqlData, key: string) {
|
||||
if (data[key] === undefined || data[key] === "") return null
|
||||
return Math.round(number(data, key))
|
||||
}
|
||||
|
||||
function number(data: AthenaData, key: string) {
|
||||
function number(data: R2SqlData, key: string) {
|
||||
const value = Number(data[key])
|
||||
return Number.isFinite(value) ? value : 0
|
||||
}
|
||||
@@ -250,6 +277,21 @@ function sqlString(value: string) {
|
||||
return `'${value.replace(/'/g, "''")}'`
|
||||
}
|
||||
|
||||
function statPeriods(grain: "day" | "week", periodStart: Date, periodEnd: Date) {
|
||||
const interval = grain === "day" ? DAY_MS : WEEK_MS
|
||||
const first = grain === "day" ? startOfUtcDay(periodStart) : startOfIsoWeek(periodStart)
|
||||
const count = Math.max(0, Math.ceil((periodEnd.getTime() - first.getTime()) / interval))
|
||||
return Array.from({ length: count }, (_, index) => {
|
||||
const start = new Date(first.getTime() + index * interval)
|
||||
return {
|
||||
grain,
|
||||
key: periodKeyFor(grain, start),
|
||||
start,
|
||||
end: new Date(Math.min(start.getTime() + interval, periodEnd.getTime())),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function statModelSql(model: string, providerModel: string) {
|
||||
return `COALESCE(NULLIF(regexp_replace(CASE
|
||||
WHEN lower(${model}) = 'big-pickle' THEN NULLIF(${providerModel}, '')
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import { Context, Effect, Layer, Schema } from "effect"
|
||||
import { Resource } from "sst/resource"
|
||||
|
||||
const R2_SQL_MAX_ROWS = 10_000
|
||||
const R2SqlValue = Schema.Union([Schema.String, Schema.Number, Schema.Boolean, Schema.Null])
|
||||
const R2SqlResponse = Schema.Struct({
|
||||
success: Schema.Boolean,
|
||||
result: Schema.optional(
|
||||
Schema.NullOr(
|
||||
Schema.Struct({
|
||||
request_id: Schema.String,
|
||||
rows: Schema.Array(Schema.Record(Schema.String, R2SqlValue)),
|
||||
}),
|
||||
),
|
||||
),
|
||||
errors: Schema.Array(Schema.Unknown),
|
||||
})
|
||||
const decodeResponse = Schema.decodeUnknownEffect(Schema.fromJsonString(R2SqlResponse))
|
||||
|
||||
export type R2SqlData = Record<string, string>
|
||||
|
||||
export class R2SqlQueryError extends Error {
|
||||
readonly _tag = "R2SqlQueryError"
|
||||
readonly requestId?: string
|
||||
readonly status?: number
|
||||
|
||||
constructor(input: { message: string; requestId?: string; status?: number; cause?: unknown }) {
|
||||
super(input.message, { cause: input.cause })
|
||||
this.name = "R2SqlQueryError"
|
||||
this.requestId = input.requestId
|
||||
this.status = input.status
|
||||
}
|
||||
}
|
||||
|
||||
export declare namespace R2Sql {
|
||||
export interface Service {
|
||||
readonly query: (query: string) => Effect.Effect<R2SqlData[], R2SqlQueryError>
|
||||
}
|
||||
}
|
||||
|
||||
export class R2Sql extends Context.Service<R2Sql, R2Sql.Service>()("@opencode/stats/R2Sql") {
|
||||
static readonly layer: Layer.Layer<R2Sql> = Layer.succeed(
|
||||
R2Sql,
|
||||
R2Sql.of({
|
||||
query: Effect.fn("R2Sql.query")(function* (query: string) {
|
||||
const response = yield* Effect.tryPromise({
|
||||
try: () =>
|
||||
Bun.fetch(
|
||||
`https://api.sql.cloudflarestorage.com/api/v1/accounts/${Resource.R2Sql.accountId}/r2-sql/query/${Resource.R2Sql.bucket}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${Resource.R2SqlAuthToken.value}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ query }),
|
||||
},
|
||||
),
|
||||
catch: (cause) => new R2SqlQueryError({ message: "Failed to run R2 SQL stats query", cause }),
|
||||
})
|
||||
const body = yield* Effect.tryPromise({
|
||||
try: () => response.text(),
|
||||
catch: (cause) =>
|
||||
new R2SqlQueryError({ message: "Failed to read R2 SQL stats response", status: response.status, cause }),
|
||||
})
|
||||
const decoded = yield* decodeResponse(body).pipe(
|
||||
Effect.mapError(
|
||||
(cause) =>
|
||||
new R2SqlQueryError({
|
||||
message: "R2 SQL returned an invalid stats response",
|
||||
status: response.status,
|
||||
cause,
|
||||
}),
|
||||
),
|
||||
)
|
||||
if (!response.ok || !decoded.success || !decoded.result)
|
||||
return yield* Effect.fail(
|
||||
new R2SqlQueryError({
|
||||
message: `R2 SQL stats query failed: ${JSON.stringify(decoded.errors)}`,
|
||||
requestId: decoded.result?.request_id,
|
||||
status: response.status,
|
||||
}),
|
||||
)
|
||||
|
||||
// R2 SQL has no OFFSET support and caps LIMIT at 10,000. Each stats
|
||||
// query is scoped to one day or week, and reaching the cap is treated as
|
||||
// an error so a newly high-cardinality period can never be truncated.
|
||||
if (decoded.result.rows.length >= R2_SQL_MAX_ROWS)
|
||||
return yield* Effect.fail(
|
||||
new R2SqlQueryError({
|
||||
message: `R2 SQL stats query reached the ${R2_SQL_MAX_ROWS} row limit`,
|
||||
requestId: decoded.result.request_id,
|
||||
status: response.status,
|
||||
}),
|
||||
)
|
||||
|
||||
return decoded.result.rows.map((row) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(row).flatMap(([key, value]) => (value === null ? [] : [[key, String(value)]])),
|
||||
),
|
||||
)
|
||||
}),
|
||||
}),
|
||||
)
|
||||
}
|
||||
+11
@@ -11,6 +11,17 @@ declare module "sst/resource" {
|
||||
type: "sst.sst.Linkable"
|
||||
workgroup: string
|
||||
}
|
||||
R2Sql: {
|
||||
accountId: string
|
||||
bucket: string
|
||||
namespace: string
|
||||
table: string
|
||||
type: "sst.sst.Linkable"
|
||||
}
|
||||
R2SqlAuthToken: {
|
||||
type: "sst.sst.Secret"
|
||||
value: string
|
||||
}
|
||||
StatsSyncConfig: {
|
||||
dataset: string
|
||||
type: "sst.sst.Linkable"
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { DateTime, Effect } from "effect"
|
||||
import { Resource } from "sst/resource"
|
||||
import { Athena, AthenaQueryError, AthenaQueryTimeoutError } from "./athena"
|
||||
import { DatabaseError } from "./database"
|
||||
import { GeoStatRepo, rowsFromAggregates as geoRowsFromAggregates } from "./domain/geo"
|
||||
import { buildStatsQuery, toGeoAggregate, toModelAggregate, toProviderAggregate } from "./domain/inference"
|
||||
import { buildStatsQueries, toGeoAggregate, toModelAggregate, toProviderAggregate } from "./domain/inference"
|
||||
import { ModelStatRepo, rowsFromAggregates as modelRowsFromAggregates } from "./domain/model"
|
||||
import { ProviderStatRepo, rowsFromAggregates as providerRowsFromAggregates } from "./domain/provider"
|
||||
import { startOfIsoWeek } from "./domain/stat"
|
||||
import { R2Sql, R2SqlQueryError } from "./r2-sql"
|
||||
|
||||
const DATALAKE_INGESTION_LAG_MS = 5 * 60_000
|
||||
const STATS_DATA_START_MS = new Date("2026-05-28T00:00:00.000Z").getTime()
|
||||
@@ -18,23 +18,25 @@ const DISPLAY_WINDOW_MS = 56 * 86_400_000
|
||||
const INCREMENTAL_LOOKBACK_MS = 2 * 3_600_000
|
||||
|
||||
export type SyncStatsResult = { ok: true; rows: number; startedAt: string; periodStart: string; periodEnd: string }
|
||||
export type SyncStatsError = AthenaQueryError | AthenaQueryTimeoutError | DatabaseError
|
||||
export type SyncStatsError = R2SqlQueryError | DatabaseError
|
||||
|
||||
export const syncStats: (options?: {
|
||||
full?: boolean
|
||||
}) => Effect.Effect<SyncStatsResult, SyncStatsError, Athena | ModelStatRepo | ProviderStatRepo | GeoStatRepo> =
|
||||
}) => Effect.Effect<SyncStatsResult, SyncStatsError, R2Sql | ModelStatRepo | ProviderStatRepo | GeoStatRepo> =
|
||||
Effect.fn("StatSync.sync")(function* (options?: { full?: boolean }) {
|
||||
const startedAt = yield* DateTime.nowAsDate
|
||||
const periodEnd = new Date(Math.floor((startedAt.getTime() - DATALAKE_INGESTION_LAG_MS) / 60_000) * 60_000)
|
||||
const periodStart = options?.full ? fullPeriodStart(periodEnd) : incrementalPeriodStart(periodEnd)
|
||||
const athena = yield* Athena
|
||||
const r2Sql = yield* R2Sql
|
||||
const modelStats = yield* ModelStatRepo
|
||||
const providerStats = yield* ProviderStatRepo
|
||||
const geoStats = yield* GeoStatRepo
|
||||
|
||||
yield* logRuntimeCheck()
|
||||
|
||||
const rows = yield* athena.query(buildStatsQuery(periodStart, periodEnd))
|
||||
const rows = yield* Effect.forEach(buildStatsQueries(periodStart, periodEnd), r2Sql.query, {
|
||||
concurrency: 4,
|
||||
}).pipe(Effect.map((batches) => batches.flat()))
|
||||
const modelRows = modelRowsFromAggregates(rows.filter((row) => row.dimension === "model").flatMap(toModelAggregate))
|
||||
const providerRows = providerRowsFromAggregates(
|
||||
rows.filter((row) => row.dimension === "provider").flatMap(toProviderAggregate),
|
||||
@@ -77,7 +79,7 @@ export const syncStats: (options?: {
|
||||
}
|
||||
})
|
||||
|
||||
// May 27 was partial, so keep Athena stats anchored at the first complete day.
|
||||
// May 27 was partial, so keep stats anchored at the first complete day.
|
||||
function fullPeriodStart(periodEnd: Date) {
|
||||
return new Date(
|
||||
Math.max(
|
||||
@@ -99,13 +101,12 @@ function incrementalPeriodStart(periodEnd: Date) {
|
||||
|
||||
function logRuntimeCheck() {
|
||||
return Effect.logInfo(
|
||||
`athena stats runtime check ${JSON.stringify({
|
||||
catalog: Resource.InferenceEvent.catalog,
|
||||
database: Resource.InferenceEvent.database,
|
||||
`r2 sql stats runtime check ${JSON.stringify({
|
||||
accountId: Resource.R2Sql.accountId,
|
||||
bucket: Resource.R2Sql.bucket,
|
||||
dataset: Resource.StatsSyncConfig.dataset,
|
||||
table: Resource.InferenceEvent.table,
|
||||
workgroup: Resource.InferenceEvent.workgroup,
|
||||
region: Resource.InferenceEvent.region,
|
||||
namespace: Resource.R2Sql.namespace,
|
||||
table: Resource.R2Sql.table,
|
||||
stage: Resource.App.stage,
|
||||
})}`,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as NodeRuntime from "@effect/platform-node/NodeRuntime"
|
||||
import { Athena } from "@opencode-ai/stats-core/athena"
|
||||
import { ModelStatRepo } from "@opencode-ai/stats-core/domain/model"
|
||||
import { R2Sql } from "@opencode-ai/stats-core/r2-sql"
|
||||
import { layer as statsLayer } from "@opencode-ai/stats-core/runtime"
|
||||
import { syncStats } from "@opencode-ai/stats-core/stat-sync"
|
||||
import { Cause, Duration, Effect, Layer, Schedule } from "effect"
|
||||
@@ -8,7 +8,7 @@ import { Cause, Duration, Effect, Layer, Schedule } from "effect"
|
||||
const SYNC_INTERVAL = "1 hour"
|
||||
const SYNC_INTERVAL_MS = 3_600_000
|
||||
|
||||
const runtimeLayer = Layer.mergeAll(statsLayer, Athena.layer)
|
||||
const runtimeLayer = Layer.mergeAll(statsLayer, R2Sql.layer)
|
||||
|
||||
const daemon = Effect.gen(function* () {
|
||||
yield* Effect.logInfo("stats sync daemon started")
|
||||
@@ -40,9 +40,9 @@ const daemon = Effect.gen(function* () {
|
||||
yield* pass.pipe(Effect.repeat(Schedule.fixed(SYNC_INTERVAL)))
|
||||
}).pipe(Effect.forkScoped)
|
||||
|
||||
// A restarted daemon must not immediately re-run the expensive Athena pass; resume
|
||||
// the hourly cadence from the last completed sync instead. This caps the Athena
|
||||
// spend of a crash loop at one pass per interval.
|
||||
// A restarted daemon must not immediately re-run the R2 SQL pass; resume the
|
||||
// hourly cadence from the last completed sync instead. This caps the query spend
|
||||
// of a crash loop at one pass per interval.
|
||||
const initialDelay = Effect.fnUntraced(function* () {
|
||||
const modelStats = yield* ModelStatRepo
|
||||
const lastSynced = yield* modelStats.lastSyncedAt().pipe(Effect.catchCause(() => Effect.succeed(null)))
|
||||
|
||||
@@ -90,6 +90,7 @@ OpenCode Zen هي بوابة AI تتيح لك الوصول إلى هذه الن
|
||||
| Gemini 3.5 Flash Lite | gemini-3.5-flash-lite | `https://opencode.ai/zen/v1/models/gemini-3.5-flash-lite` | `@ai-sdk/google` |
|
||||
| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` |
|
||||
| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` |
|
||||
| Grok 4.6 | grok-4.6 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
||||
@@ -178,6 +179,8 @@ https://opencode.ai/zen/v1/models
|
||||
| Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
|
||||
| Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
|
||||
| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - |
|
||||
| Grok 4.6 (≤ 200K tokens) | $2.00 | $6.00 | $0.50 | - |
|
||||
| Grok 4.6 (> 200K tokens) | $4.00 | $12.00 | $1.00 | - |
|
||||
| Grok 4.5 (≤ 200K tokens) | $2.00 | $6.00 | $0.30 | - |
|
||||
| Grok 4.5 (> 200K tokens) | $4.00 | $12.00 | $0.60 | - |
|
||||
| Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - |
|
||||
|
||||
@@ -95,6 +95,7 @@ Našim modelima možete pristupiti i preko sljedećih API endpointa.
|
||||
| Gemini 3.5 Flash Lite | gemini-3.5-flash-lite | `https://opencode.ai/zen/v1/models/gemini-3.5-flash-lite` | `@ai-sdk/google` |
|
||||
| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` |
|
||||
| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` |
|
||||
| Grok 4.6 | grok-4.6 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
||||
@@ -185,6 +186,8 @@ Podržavamo pay-as-you-go model. Ispod su cijene **po 1M tokena**.
|
||||
| Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
|
||||
| Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
|
||||
| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - |
|
||||
| Grok 4.6 (≤ 200K tokens) | $2.00 | $6.00 | $0.50 | - |
|
||||
| Grok 4.6 (> 200K tokens) | $4.00 | $12.00 | $1.00 | - |
|
||||
| Grok 4.5 (≤ 200K tokens) | $2.00 | $6.00 | $0.30 | - |
|
||||
| Grok 4.5 (> 200K tokens) | $4.00 | $12.00 | $0.60 | - |
|
||||
| Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - |
|
||||
|
||||
@@ -95,6 +95,7 @@ Du kan også få adgang til vores modeller gennem følgende API-endpoints.
|
||||
| Gemini 3.5 Flash Lite | gemini-3.5-flash-lite | `https://opencode.ai/zen/v1/models/gemini-3.5-flash-lite` | `@ai-sdk/google` |
|
||||
| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` |
|
||||
| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` |
|
||||
| Grok 4.6 | grok-4.6 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
||||
@@ -185,6 +186,8 @@ Vi understøtter en pay-as-you-go-model. Nedenfor er priserne **pr. 1M tokens**.
|
||||
| Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
|
||||
| Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
|
||||
| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - |
|
||||
| Grok 4.6 (≤ 200K tokens) | $2.00 | $6.00 | $0.50 | - |
|
||||
| Grok 4.6 (> 200K tokens) | $4.00 | $12.00 | $1.00 | - |
|
||||
| Grok 4.5 (≤ 200K tokens) | $2.00 | $6.00 | $0.30 | - |
|
||||
| Grok 4.5 (> 200K tokens) | $4.00 | $12.00 | $0.60 | - |
|
||||
| Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - |
|
||||
|
||||
@@ -86,6 +86,7 @@ Du kannst auch über die folgenden API-Endpunkte auf unsere Modelle zugreifen.
|
||||
| Gemini 3.5 Flash Lite | gemini-3.5-flash-lite | `https://opencode.ai/zen/v1/models/gemini-3.5-flash-lite` | `@ai-sdk/google` |
|
||||
| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` |
|
||||
| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` |
|
||||
| Grok 4.6 | grok-4.6 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
||||
@@ -174,6 +175,8 @@ Wir unterstützen ein Pay-as-you-go-Modell. Unten findest du die Preise **pro 1M
|
||||
| Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
|
||||
| Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
|
||||
| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - |
|
||||
| Grok 4.6 (≤ 200K tokens) | $2.00 | $6.00 | $0.50 | - |
|
||||
| Grok 4.6 (> 200K tokens) | $4.00 | $12.00 | $1.00 | - |
|
||||
| Grok 4.5 (≤ 200K tokens) | $2.00 | $6.00 | $0.30 | - |
|
||||
| Grok 4.5 (> 200K tokens) | $4.00 | $12.00 | $0.60 | - |
|
||||
| Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - |
|
||||
|
||||
@@ -17,7 +17,7 @@ You can also check out [awesome-opencode](https://github.com/awesome-opencode/aw
|
||||
|
||||
| Name | Description |
|
||||
| -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
|
||||
| [opencode-daytona](https://github.com/daytonaio/daytona/tree/main/libs/opencode-plugin) | Automatically run OpenCode sessions in isolated Daytona sandboxes with git sync and live previews |
|
||||
| [opencode-daytona](https://github.com/daytona/integrations/tree/main/packages/opencode-plugin) | Automatically run OpenCode sessions in isolated Daytona sandboxes with git sync and live previews |
|
||||
| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | Automatically inject Helicone session headers for request grouping |
|
||||
| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | Auto-inject TypeScript/Svelte types into file reads with lookup tools |
|
||||
| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | Use your ChatGPT Plus/Pro subscription instead of API credits |
|
||||
|
||||
@@ -95,6 +95,7 @@ También puedes acceder a nuestros modelos a través de los siguientes endpoints
|
||||
| Gemini 3.5 Flash Lite | gemini-3.5-flash-lite | `https://opencode.ai/zen/v1/models/gemini-3.5-flash-lite` | `@ai-sdk/google` |
|
||||
| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` |
|
||||
| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` |
|
||||
| Grok 4.6 | grok-4.6 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
||||
@@ -185,6 +186,8 @@ Admitimos un modelo de pago por uso. A continuación se muestran los precios **p
|
||||
| Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
|
||||
| Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
|
||||
| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - |
|
||||
| Grok 4.6 (≤ 200K tokens) | $2.00 | $6.00 | $0.50 | - |
|
||||
| Grok 4.6 (> 200K tokens) | $4.00 | $12.00 | $1.00 | - |
|
||||
| Grok 4.5 (≤ 200K tokens) | $2.00 | $6.00 | $0.30 | - |
|
||||
| Grok 4.5 (> 200K tokens) | $4.00 | $12.00 | $0.60 | - |
|
||||
| Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - |
|
||||
|
||||
@@ -86,6 +86,7 @@ Vous pouvez également accéder à nos modèles via les points de terminaison AP
|
||||
| Gemini 3.5 Flash Lite | gemini-3.5-flash-lite | `https://opencode.ai/zen/v1/models/gemini-3.5-flash-lite` | `@ai-sdk/google` |
|
||||
| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` |
|
||||
| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` |
|
||||
| Grok 4.6 | grok-4.6 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
||||
@@ -174,6 +175,8 @@ Nous prenons en charge un modèle de paiement à l'utilisation. Vous trouverez c
|
||||
| Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
|
||||
| Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
|
||||
| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - |
|
||||
| Grok 4.6 (≤ 200K tokens) | $2.00 | $6.00 | $0.50 | - |
|
||||
| Grok 4.6 (> 200K tokens) | $4.00 | $12.00 | $1.00 | - |
|
||||
| Grok 4.5 (≤ 200K tokens) | $2.00 | $6.00 | $0.30 | - |
|
||||
| Grok 4.5 (> 200K tokens) | $4.00 | $12.00 | $0.60 | - |
|
||||
| Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - |
|
||||
|
||||
@@ -97,7 +97,7 @@ Or you can set it up manually.
|
||||
issues: write
|
||||
```
|
||||
|
||||
You can also use a [personal access tokens](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens)(PAT) if preferred.
|
||||
You can also use a [personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens)(PAT) if preferred.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -95,6 +95,7 @@ Puoi anche accedere ai nostri modelli tramite i seguenti endpoint API.
|
||||
| Gemini 3.5 Flash Lite | gemini-3.5-flash-lite | `https://opencode.ai/zen/v1/models/gemini-3.5-flash-lite` | `@ai-sdk/google` |
|
||||
| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` |
|
||||
| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` |
|
||||
| Grok 4.6 | grok-4.6 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
||||
@@ -185,6 +186,8 @@ Supportiamo un modello pay-as-you-go. Qui sotto trovi i prezzi **per 1M token**.
|
||||
| Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
|
||||
| Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
|
||||
| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - |
|
||||
| Grok 4.6 (≤ 200K tokens) | $2.00 | $6.00 | $0.50 | - |
|
||||
| Grok 4.6 (> 200K tokens) | $4.00 | $12.00 | $1.00 | - |
|
||||
| Grok 4.5 (≤ 200K tokens) | $2.00 | $6.00 | $0.30 | - |
|
||||
| Grok 4.5 (> 200K tokens) | $4.00 | $12.00 | $0.60 | - |
|
||||
| Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - |
|
||||
|
||||
@@ -86,6 +86,7 @@ OpenCode Zen は、OpenCode のほかのプロバイダーと同じように動
|
||||
| Gemini 3.5 Flash Lite | gemini-3.5-flash-lite | `https://opencode.ai/zen/v1/models/gemini-3.5-flash-lite` | `@ai-sdk/google` |
|
||||
| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` |
|
||||
| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` |
|
||||
| Grok 4.6 | grok-4.6 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
||||
@@ -174,6 +175,8 @@ https://opencode.ai/zen/v1/models
|
||||
| Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
|
||||
| Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
|
||||
| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - |
|
||||
| Grok 4.6 (≤ 200K tokens) | $2.00 | $6.00 | $0.50 | - |
|
||||
| Grok 4.6 (> 200K tokens) | $4.00 | $12.00 | $1.00 | - |
|
||||
| Grok 4.5 (≤ 200K tokens) | $2.00 | $6.00 | $0.30 | - |
|
||||
| Grok 4.5 (> 200K tokens) | $4.00 | $12.00 | $0.60 | - |
|
||||
| Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - |
|
||||
|
||||
@@ -86,6 +86,7 @@ OpenCode Zen은 OpenCode의 다른 provider와 똑같이 작동합니다.
|
||||
| Gemini 3.5 Flash Lite | gemini-3.5-flash-lite | `https://opencode.ai/zen/v1/models/gemini-3.5-flash-lite` | `@ai-sdk/google` |
|
||||
| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` |
|
||||
| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` |
|
||||
| Grok 4.6 | grok-4.6 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
||||
@@ -174,6 +175,8 @@ https://opencode.ai/zen/v1/models
|
||||
| Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
|
||||
| Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
|
||||
| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - |
|
||||
| Grok 4.6 (≤ 200K tokens) | $2.00 | $6.00 | $0.50 | - |
|
||||
| Grok 4.6 (> 200K tokens) | $4.00 | $12.00 | $1.00 | - |
|
||||
| Grok 4.5 (≤ 200K tokens) | $2.00 | $6.00 | $0.30 | - |
|
||||
| Grok 4.5 (> 200K tokens) | $4.00 | $12.00 | $0.60 | - |
|
||||
| Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - |
|
||||
|
||||
@@ -95,6 +95,7 @@ Du kan også få tilgang til modellene våre gjennom følgende API-endepunkter.
|
||||
| Gemini 3.5 Flash Lite | gemini-3.5-flash-lite | `https://opencode.ai/zen/v1/models/gemini-3.5-flash-lite` | `@ai-sdk/google` |
|
||||
| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` |
|
||||
| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` |
|
||||
| Grok 4.6 | grok-4.6 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
||||
@@ -185,6 +186,8 @@ Vi støtter en pay-as-you-go-modell. Nedenfor er prisene **per 1M tokens**.
|
||||
| Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
|
||||
| Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
|
||||
| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - |
|
||||
| Grok 4.6 (≤ 200K tokens) | $2.00 | $6.00 | $0.50 | - |
|
||||
| Grok 4.6 (> 200K tokens) | $4.00 | $12.00 | $1.00 | - |
|
||||
| Grok 4.5 (≤ 200K tokens) | $2.00 | $6.00 | $0.30 | - |
|
||||
| Grok 4.5 (> 200K tokens) | $4.00 | $12.00 | $0.60 | - |
|
||||
| Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - |
|
||||
|
||||
@@ -95,6 +95,7 @@ Możesz też uzyskać dostęp do naszych modeli przez poniższe endpointy API.
|
||||
| Gemini 3.5 Flash Lite | gemini-3.5-flash-lite | `https://opencode.ai/zen/v1/models/gemini-3.5-flash-lite` | `@ai-sdk/google` |
|
||||
| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` |
|
||||
| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` |
|
||||
| Grok 4.6 | grok-4.6 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
||||
@@ -185,6 +186,8 @@ Obsługujemy model pay-as-you-go. Poniżej znajdują się ceny **za 1M tokenów*
|
||||
| Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
|
||||
| Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
|
||||
| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - |
|
||||
| Grok 4.6 (≤ 200K tokens) | $2.00 | $6.00 | $0.50 | - |
|
||||
| Grok 4.6 (> 200K tokens) | $4.00 | $12.00 | $1.00 | - |
|
||||
| Grok 4.5 (≤ 200K tokens) | $2.00 | $6.00 | $0.30 | - |
|
||||
| Grok 4.5 (> 200K tokens) | $4.00 | $12.00 | $0.60 | - |
|
||||
| Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - |
|
||||
|
||||
@@ -759,7 +759,7 @@ Cloudflare Workers AI lets you run AI models on Cloudflare's global network dire
|
||||
|
||||
### DigitalOcean
|
||||
|
||||
DigitalOcean's [Inference Engine](https://docs.digitalocean.com/products/inference/) provides access to open models like GPT-OSS, Llama, Qwen, and DeepSeek, plus custom [Inference Routers](https://docs.digitalocean.com/products/genai-platform/concepts/inference-routers/) that route each request to the cheapest, fastest, or best-fit model for a task.
|
||||
DigitalOcean's [Inference Engine](https://docs.digitalocean.com/products/inference/) provides access to open models like GPT-OSS, Llama, Qwen, and DeepSeek, plus custom [Inference Routers](https://docs.digitalocean.com/products/inference/how-to/use-inference-router/) that route each request to the cheapest, fastest, or best-fit model for a task.
|
||||
|
||||
OpenCode supports two authentication methods:
|
||||
|
||||
@@ -2487,7 +2487,7 @@ You can use any OpenAI-compatible provider with opencode. Most modern AI provide
|
||||
"provider": {
|
||||
"myprovider": {
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"name": "My AI ProviderDisplay Name",
|
||||
"name": "My AI Provider Display Name",
|
||||
"options": {
|
||||
"baseURL": "https://api.myprovider.com/v1"
|
||||
},
|
||||
@@ -2525,7 +2525,7 @@ Here's an example setting the `apiKey`, `headers`, and model `limit` options.
|
||||
"provider": {
|
||||
"myprovider": {
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"name": "My AI ProviderDisplay Name",
|
||||
"name": "My AI Provider Display Name",
|
||||
"options": {
|
||||
"baseURL": "https://api.myprovider.com/v1",
|
||||
"apiKey": "{env:ANTHROPIC_API_KEY}",
|
||||
|
||||
@@ -86,6 +86,7 @@ Você também pode acessar nossos modelos pelos seguintes endpoints de API.
|
||||
| Gemini 3.5 Flash Lite | gemini-3.5-flash-lite | `https://opencode.ai/zen/v1/models/gemini-3.5-flash-lite` | `@ai-sdk/google` |
|
||||
| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` |
|
||||
| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` |
|
||||
| Grok 4.6 | grok-4.6 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
||||
@@ -174,6 +175,8 @@ Oferecemos um modelo pay-as-you-go. Abaixo estão os preços **por 1M tokens**.
|
||||
| Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
|
||||
| Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
|
||||
| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - |
|
||||
| Grok 4.6 (≤ 200K tokens) | $2.00 | $6.00 | $0.50 | - |
|
||||
| Grok 4.6 (> 200K tokens) | $4.00 | $12.00 | $1.00 | - |
|
||||
| Grok 4.5 (≤ 200K tokens) | $2.00 | $6.00 | $0.30 | - |
|
||||
| Grok 4.5 (> 200K tokens) | $4.00 | $12.00 | $0.60 | - |
|
||||
| Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - |
|
||||
|
||||
@@ -95,6 +95,7 @@ OpenCode Zen работает как любой другой провайдер
|
||||
| Gemini 3.5 Flash Lite | gemini-3.5-flash-lite | `https://opencode.ai/zen/v1/models/gemini-3.5-flash-lite` | `@ai-sdk/google` |
|
||||
| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` |
|
||||
| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` |
|
||||
| Grok 4.6 | grok-4.6 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
||||
@@ -185,6 +186,8 @@ https://opencode.ai/zen/v1/models
|
||||
| Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
|
||||
| Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
|
||||
| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - |
|
||||
| Grok 4.6 (≤ 200K tokens) | $2.00 | $6.00 | $0.50 | - |
|
||||
| Grok 4.6 (> 200K tokens) | $4.00 | $12.00 | $1.00 | - |
|
||||
| Grok 4.5 (≤ 200K tokens) | $2.00 | $6.00 | $0.30 | - |
|
||||
| Grok 4.5 (> 200K tokens) | $4.00 | $12.00 | $0.60 | - |
|
||||
| Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - |
|
||||
|
||||
@@ -88,6 +88,7 @@ OpenCode Zen ทำงานเหมือน provider อื่น ๆ ใน
|
||||
| Gemini 3.5 Flash Lite | gemini-3.5-flash-lite | `https://opencode.ai/zen/v1/models/gemini-3.5-flash-lite` | `@ai-sdk/google` |
|
||||
| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` |
|
||||
| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` |
|
||||
| Grok 4.6 | grok-4.6 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
||||
@@ -176,6 +177,8 @@ https://opencode.ai/zen/v1/models
|
||||
| Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
|
||||
| Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
|
||||
| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - |
|
||||
| Grok 4.6 (≤ 200K tokens) | $2.00 | $6.00 | $0.50 | - |
|
||||
| Grok 4.6 (> 200K tokens) | $4.00 | $12.00 | $1.00 | - |
|
||||
| Grok 4.5 (≤ 200K tokens) | $2.00 | $6.00 | $0.30 | - |
|
||||
| Grok 4.5 (> 200K tokens) | $4.00 | $12.00 | $0.60 | - |
|
||||
| Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - |
|
||||
|
||||
@@ -86,6 +86,7 @@ Modellerimize aşağıdaki API uç noktaları aracılığıyla da erişebilirsin
|
||||
| Gemini 3.5 Flash Lite | gemini-3.5-flash-lite | `https://opencode.ai/zen/v1/models/gemini-3.5-flash-lite` | `@ai-sdk/google` |
|
||||
| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` |
|
||||
| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` |
|
||||
| Grok 4.6 | grok-4.6 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
||||
@@ -174,6 +175,8 @@ Kullandıkça öde modelini destekliyoruz. Aşağıda **1M token başına** fiya
|
||||
| Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
|
||||
| Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
|
||||
| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - |
|
||||
| Grok 4.6 (≤ 200K tokens) | $2.00 | $6.00 | $0.50 | - |
|
||||
| Grok 4.6 (> 200K tokens) | $4.00 | $12.00 | $1.00 | - |
|
||||
| Grok 4.5 (≤ 200K tokens) | $2.00 | $6.00 | $0.30 | - |
|
||||
| Grok 4.5 (> 200K tokens) | $4.00 | $12.00 | $0.60 | - |
|
||||
| Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - |
|
||||
|
||||
@@ -95,6 +95,7 @@ You can also access our models through the following API endpoints.
|
||||
| Gemini 3.5 Flash Lite | gemini-3.5-flash-lite | `https://opencode.ai/zen/v1/models/gemini-3.5-flash-lite` | `@ai-sdk/google` |
|
||||
| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` |
|
||||
| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` |
|
||||
| Grok 4.6 | grok-4.6 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
||||
@@ -185,6 +186,8 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**.
|
||||
| Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
|
||||
| Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
|
||||
| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - |
|
||||
| Grok 4.6 (≤ 200K tokens) | $2.00 | $6.00 | $0.50 | - |
|
||||
| Grok 4.6 (> 200K tokens) | $4.00 | $12.00 | $1.00 | - |
|
||||
| Grok 4.5 (≤ 200K tokens) | $2.00 | $6.00 | $0.30 | - |
|
||||
| Grok 4.5 (> 200K tokens) | $4.00 | $12.00 | $0.60 | - |
|
||||
| Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - |
|
||||
|
||||
@@ -86,6 +86,7 @@ OpenCode Zen 的工作方式与 OpenCode 中的任何其他提供商相同。
|
||||
| Gemini 3.5 Flash Lite | gemini-3.5-flash-lite | `https://opencode.ai/zen/v1/models/gemini-3.5-flash-lite` | `@ai-sdk/google` |
|
||||
| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` |
|
||||
| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` |
|
||||
| Grok 4.6 | grok-4.6 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
||||
@@ -174,6 +175,8 @@ https://opencode.ai/zen/v1/models
|
||||
| Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
|
||||
| Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
|
||||
| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - |
|
||||
| Grok 4.6 (≤ 200K tokens) | $2.00 | $6.00 | $0.50 | - |
|
||||
| Grok 4.6 (> 200K tokens) | $4.00 | $12.00 | $1.00 | - |
|
||||
| Grok 4.5 (≤ 200K tokens) | $2.00 | $6.00 | $0.30 | - |
|
||||
| Grok 4.5 (> 200K tokens) | $4.00 | $12.00 | $0.60 | - |
|
||||
| Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - |
|
||||
|
||||
@@ -90,6 +90,7 @@ OpenCode Zen 的運作方式和 OpenCode 中的其他供應商一樣。
|
||||
| Gemini 3.5 Flash Lite | gemini-3.5-flash-lite | `https://opencode.ai/zen/v1/models/gemini-3.5-flash-lite` | `@ai-sdk/google` |
|
||||
| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` |
|
||||
| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` |
|
||||
| Grok 4.6 | grok-4.6 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||
| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
||||
@@ -179,6 +180,8 @@ https://opencode.ai/zen/v1/models
|
||||
| Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
|
||||
| Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
|
||||
| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - |
|
||||
| Grok 4.6 (≤ 200K tokens) | $2.00 | $6.00 | $0.50 | - |
|
||||
| Grok 4.6 (> 200K tokens) | $4.00 | $12.00 | $1.00 | - |
|
||||
| Grok 4.5 (≤ 200K tokens) | $2.00 | $6.00 | $0.30 | - |
|
||||
| Grok 4.5 (> 200K tokens) | $4.00 | $12.00 | $0.60 | - |
|
||||
| Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - |
|
||||
|
||||
Reference in New Issue
Block a user