Compare commits

..

1 Commits

Author SHA1 Message Date
Kit Langton 28149729f3 fix(tui): adapt logo to narrow terminals 2026-08-04 13:01:49 +00:00
46 changed files with 1015 additions and 804 deletions
@@ -1,63 +0,0 @@
---
name: rtl-aware-development
description: OpenCode Desktop should be RTL-aware. Use when implementing or reviewing RTL/LTR behavior in the web app, desktop app, CSS, menus, scrolling, resizing, icons, mixed-direction text, or Electron title bars.
---
# RTL-Aware Development
Treat direction as independent from language. Test English in both directions as well as real RTL and mixed-script content.
## Guidelines
- Set `lang` and `dir` on the document, and propagate direction through component providers used by portaled menus and popovers. Do not change the selected locale merely to force RTL.
- Keep DOM and focus order semantic. Flexbox and Grid already follow `dir`; do not add `row-reverse`, CSS `order`, or reversed markup just to mirror a layout.
- Prefer logical CSS for semantic layout. Reserve physical coordinates for pointer positions, canvas geometry, native window controls, and other genuinely physical placement.
```css
/* Avoid */
padding-left: 12px;
right: 0;
border-right: 1px solid;
text-align: left;
/* Prefer */
padding-inline-start: 12px;
inset-inline-end: 0;
border-inline-end: 1px solid;
text-align: start;
```
- Isolate mixed-direction text. Use `dir="auto"` or `<bdi>` for unknown text; keep code, URLs, IDs, and filesystem paths LTR without forcing the surrounding component LTR.
```html
<span class="file-row"><bdi dir="auto">README.md</bdi></span> <bdi dir="ltr"><code>C:\src\app.ts</code></bdi>
```
- Mirror directional meaning, not every image. Back/forward, previous/next, disclosure, indentation, and directional progress may need mirroring. Do not mirror brands, clocks, media controls, charts, or text. Reverse physical gradients, `translateX`, SVG transforms, and animation deltas explicitly.
- Map interactions through direction. `clientX` remains physical; resizing a logical edge needs an RTL-aware delta. Logical previous/next keyboard controls may swap ArrowLeft/ArrowRight. Follow the relevant WAI-ARIA widget pattern.
- Do not assume LTR scrolling. RTL `scrollLeft` can start at `0` and become negative. Prefer `scrollIntoView({ inline: "nearest" })` or a tested direction-normalizing helper.
- For Electron title bars, prefer native caption controls and use `titleBarOverlay` plus `env(titlebar-area-*)` for the safe content rectangle. Keep Windows/macOS native-control avoidance and `trafficLightPosition` physical; keep app navigation inside that rectangle logical. Mark interactive titlebar children `app-region: no-drag`.
- Verify behavior, not screenshots alone. Check computed styles, pseudo-element geometry, hit zones, focus order, keyboard behavior, submenu direction, zoom/scaling, and both LTR and RTL scroll endpoints.
## Test Matrix
- English + LTR
- English + forced RTL
- A real RTL locale + RTL
- Mixed RTL/LTR content, long labels, numbers, code, and paths
- Keyboard, pointer resize, scrolling, menus/submenus, and Electron titlebar controls in both directions
## References
- [RTL Styling 101, Ahmad Shadeed](https://rtlstyling.com/posts/rtl-styling/)
- [CSS-Tricks: RTL Styling 101](https://css-tricks.com/rtl-styling-101/)
- [CSS-Tricks: CSS Logical Properties and Values](https://css-tricks.com/css-logical-properties-and-values/)
- [W3C: Structural markup and right-to-left text](https://www.w3.org/International/questions/qa-html-dir)
- [W3C: Inline bidirectional markup](https://www.w3.org/International/articles/inline-bidi-markup/)
- [MDN: CSS logical properties and values](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Logical_properties_and_values)
- [MDN: `dir`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/dir)
- [MDN: `scrollLeft`](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollLeft)
- [web.dev: Logical properties](https://web.dev/learn/css/logical-properties/)
- [Electron: Custom title bar](https://www.electronjs.org/docs/latest/tutorial/custom-title-bar)
- [WAI-ARIA: Window splitter pattern](https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/)
- [Kobalte: I18n Provider](https://kobalte.dev/docs/core/components/i18n-provider/)
-16
View File
@@ -1,16 +0,0 @@
## Required Reading
- Before writing, changing, or reviewing E2E tests, ALWAYS read and follow Playwright's official [Best Practices](https://playwright.dev/docs/best-practices), [Auto-waiting](https://playwright.dev/docs/actionability), and [Assertions](https://playwright.dev/docs/test-assertions) guides.
- Use the official [Locators](https://playwright.dev/docs/locators), [Network](https://playwright.dev/docs/network), and [Test Isolation](https://playwright.dev/docs/browser-contexts) guides when those concerns apply.
## Test Hygiene
- Test user-visible behavior with isolated, deterministic data and scoped, unique locators.
- Prefer role, label, text, and explicit test-contract locators. Do not use `.first()` or `.last()` merely to silence strictness errors.
- Use locator actions, Playwright auto-waiting, and web-first assertions for observable readiness and outcomes.
- NEVER use `waitForTimeout`, `setTimeout`, sleeps, animation-frame counts, or other wall-clock delays to synchronize a test. Wait for the specific UI state, request, response, event, or application outcome instead.
- Do not treat navigation, a network response, DOM attachment, or visibility alone as proof that asynchronously rendered UI is ready. Assert the state the next action actually requires.
- Register event and network waits before the action that triggers them.
- Do not retry state-changing actions. Retry idempotent readiness checks, then perform the action once and assert its outcome.
- Keep action and assertion timeouts adaptive. Do not use short timeouts as readiness probes or rely on retries to hide flakes.
- Assert exact outcomes and identities so stale state, duplicate rendering, and interactions with the wrong element cannot pass.
@@ -197,9 +197,7 @@ export async function setupTimeline(
)
},
async waitForPart(partID: string) {
const part = page.locator(`[data-timeline-part-id="${partID}"]`)
await expect(part).toHaveCount(1)
await expect(part).toBeVisible()
await expect(page.locator(`[data-timeline-part-id="${partID}"]`).first()).toBeVisible()
},
}
}
@@ -18,7 +18,6 @@ test("opens the comment editor when code is clicked", async ({ page }) => {
await line.click()
await expect(review.getByRole("textbox")).toBeVisible()
await expect(review.locator('[data-slot="line-comment-editor-label"]')).toHaveText("Commenting on line 2")
})
test("opens the comment editor when a line number is clicked", async ({ page }) => {
@@ -28,7 +27,6 @@ test("opens the comment editor when a line number is clicked", async ({ page })
await lineNumber.click()
await expect(review.getByRole("textbox")).toBeVisible()
await expect(review.locator('[data-slot="line-comment-editor-label"]')).toHaveText("Commenting on line 1")
})
test("opens the comment editor for a line number range", async ({ page }) => {
@@ -38,10 +36,15 @@ test("opens the comment editor for a line number range", async ({ page }) => {
await expectAppVisible(start)
await expectAppVisible(end)
await start.dragTo(end)
const from = await start.boundingBox()
const to = await end.boundingBox()
if (!from || !to) throw new Error("Missing line number bounds")
await page.mouse.move(from.x + from.width / 2, from.y + from.height / 2)
await page.mouse.down()
await page.mouse.move(to.x + to.width / 2, to.y + to.height / 2)
await page.mouse.up()
await expect(review.getByRole("textbox")).toBeVisible()
await expect(review.locator('[data-slot="line-comment-editor-label"]')).toHaveText("Commenting on lines 1-3")
})
test("shows a comment button when a line number is hovered", async ({ page }) => {
@@ -51,38 +54,31 @@ test("shows a comment button when a line number is hovered", async ({ page }) =>
const comment = review.getByRole("button", { name: "Comment", exact: true })
await expect(async () => {
await page.mouse.move(0, 0)
await lineNumber.hover()
await expect(lineNumber).toHaveAttribute("data-hovered", "")
await expect(comment).toHaveCount(1)
await expect(comment).toHaveCSS("pointer-events", "auto")
await comment.focus()
await expect(comment).toBeFocused()
}).toPass({ timeout: 10_000 })
await comment.press("Enter")
await expect(comment).toBeVisible({ timeout: 500 })
await comment.click({ timeout: 500 })
}).toPass()
await expect(review.getByRole("textbox")).toBeVisible()
await expect(review.locator('[data-slot="line-comment-editor-label"]')).toHaveText("Commenting on line 1")
})
test("stages a submitted line comment in the prompt context", async ({ page }) => {
const requests: string[] = []
page.on("request", (request) => {
expect.soft(request.method(), `unexpected ${request.method()} ${new URL(request.url()).pathname}`).toBe("GET")
if (request.method() !== "GET") requests.push(`${request.method()} ${new URL(request.url()).pathname}`)
})
const review = page.locator('[data-component="session-review"]')
await review.getByText("export const value = 'after'", { exact: true }).click()
const textbox = review.getByRole("textbox")
await expect(textbox).toBeVisible()
await expect(review.locator('[data-slot="line-comment-editor-label"]')).toHaveText("Commenting on line 2")
await textbox.fill("Use the existing value instead")
const submit = review.locator('[data-slot="line-comment-action"][data-variant="primary"]')
await expect(submit).toBeEnabled()
await submit.click()
await review.getByRole("textbox").fill("Use the existing value instead")
await review.locator('[data-slot="line-comment-action"][data-variant="primary"]').click()
await expect(review.getByText("Use the existing value instead", { exact: true })).toBeVisible()
await page.getByRole("tab", { name: "Session" }).click()
const context = page.getByText("Use the existing value instead", { exact: true }).last()
await expect(context).toBeVisible()
await expect(context.locator("..")).toContainText("review.ts:2")
expect(requests).toEqual([])
})
async function openReview(page: Page) {
@@ -148,22 +144,15 @@ async function openReview(page: Page) {
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
await expectSessionTitle(page, title)
const changes = page.getByRole("tab", { name: "Changes" })
const diffResponse = page.waitForResponse(
(response) =>
response.request().method() === "GET" && response.ok() && new URL(response.url()).pathname === "/api/vcs/diff",
)
await changes.click()
const diffResponse = page.waitForResponse((response) => new URL(response.url()).pathname === "/api/vcs/diff")
await page.getByRole("tab", { name: "Changes" }).click()
expect((await (await diffResponse).json()).data).toHaveLength(1)
await expect(page.getByRole("tab", { selected: true })).toHaveAccessibleName(/Files Changed/)
const review = page.locator('[data-component="session-review"]')
await expectAppVisible(review)
const file = review.locator('[data-file="src/review.ts"]')
await expectAppVisible(file)
const trigger = file.getByRole("button", { expanded: false })
await expect(trigger).toHaveCount(1)
await trigger.click()
await expect(file.getByRole("button", { expanded: true })).toBeVisible()
await expect(file.getByText("export const value = 'after'", { exact: true })).toBeVisible()
await review
.getByRole("heading", { name: /review\.ts/ })
.getByRole("button")
.first()
.click()
}
@@ -1,8 +1,9 @@
import { expect, test, type Page } from "@playwright/test"
import { expect, test } from "@playwright/test"
import {
assistantMessage,
partUpdated,
setupTimeline,
status,
textPart,
userMessage,
} from "../performance/timeline-stability/fixture"
@@ -16,7 +17,7 @@ test("keeps one connection open while delivering multiple events", async ({ page
await timeline.waitForPart("prt_transport_first")
await timeline.waitForPart("prt_transport_second")
expect(first.connectionID).toBe(second.connectionID)
await expect.poll(async () => (await timeline.transport.connections()).length).toBe(1)
expect(await timeline.transport.connections()).toHaveLength(1)
expect(await timeline.transport.acknowledgements()).toHaveLength(2)
})
@@ -50,28 +51,20 @@ test("parses split JSON and a split multibyte code point", async ({ page }) => {
})
test("delivers server heartbeat without mutating the timeline", async ({ page }) => {
const sentinelID = "prt_transport_heartbeat_sentinel"
const timeline = await setupTimeline(page, {
messages: [userMessage(), assistantMessage([textPart("prt_transport_steady", "steady")])],
})
await timeline.waitForPart("prt_transport_steady")
const before = await stableTimelineRows(page)
const before = await page.locator("[data-timeline-row]").allTextContents()
await timeline.transport.writeRaw(": heartbeat\n\n")
await timeline.transport.send(partUpdated(textPart(sentinelID, "heartbeat processed")))
await timeline.waitForPart(sentinelID)
await timeline.transport.heartbeat()
await timeline.settle()
await expect
.poll(async () => {
const rows = await timelineRows(page)
return rows.filter((row) => before.some((item) => item.key === row.key))
})
.toEqual(before)
await expect.poll(async () => (await timeline.transport.connections()).length).toBe(1)
expect(await page.locator("[data-timeline-row]").allTextContents()).toEqual(before)
expect(await timeline.transport.connections()).toHaveLength(1)
})
test("reconnects after a clean close", async ({ page }) => {
const timeline = await setupTimeline(page)
const timeline = await setupTimeline(page, { eventRetry: 10 })
const first = await timeline.transport.waitForConnection()
await timeline.transport.close()
@@ -84,21 +77,20 @@ test("reconnects after a clean close", async ({ page }) => {
})
test("reconnects after a stream error", async ({ page }) => {
const timeline = await setupTimeline(page)
const timeline = await setupTimeline(page, { eventRetry: 10 })
const first = await timeline.transport.waitForConnection()
await timeline.transport.error("contract failure")
const second = await timeline.transport.waitForConnection({ after: first.id })
await timeline.transport.send(partUpdated(textPart("prt_transport_error", "after error")))
await timeline.transport.send(status("busy"))
await timeline.waitForPart("prt_transport_error")
await expect.poll(async () => (await timeline.transport.connections()).length).toBe(2)
expect(second.id).toBeGreaterThan(first.id)
expect((await timeline.transport.connections())[0]?.endedBy).toBe("error")
})
test("does not request replay when reconnecting the volatile V2 event stream", async ({ page }) => {
const timeline = await setupTimeline(page, { protocol: "v2" })
const timeline = await setupTimeline(page, { eventRetry: 10, protocol: "v2" })
const first = await timeline.transport.send(partUpdated(textPart("prt_transport_id", "event with id")), {
id: "timeline-event-7",
})
@@ -120,35 +112,5 @@ test("passes through non-event fetches", async ({ page }) => {
})
expect(health).toEqual({ healthy: true })
await expect.poll(async () => (await timeline.transport.connections()).length).toBe(1)
expect(await timeline.transport.connections()).toHaveLength(1)
})
async function stableTimelineRows(page: Page) {
let previous: Awaited<ReturnType<typeof timelineRows>> | undefined
let stable = 0
await expect
.poll(
async () => {
const next = await timelineRows(page)
stable = JSON.stringify(next) === JSON.stringify(previous) ? stable + 1 : 0
previous = next
return stable
},
{ intervals: [50, 50, 100] },
)
.toBeGreaterThanOrEqual(2)
return previous!
}
function timelineRows(page: Page) {
return page.locator("[data-timeline-key]").evaluateAll((elements) =>
elements.map((element) => ({
key: element.getAttribute("data-timeline-key"),
row: element.querySelector("[data-timeline-row]")?.getAttribute("data-timeline-row"),
parts: Array.from(element.querySelectorAll("[data-timeline-part-id]"), (part) =>
part.getAttribute("data-timeline-part-id"),
),
text: element.textContent,
})),
)
}
+5 -10
View File
@@ -247,23 +247,18 @@ export async function installSseTransport<T>(
return {
server,
async waitForConnection(input = {}) {
const connection = await page.waitForFunction(
await page.waitForFunction(
(after) => {
const transport = (window as BrowserTransport).__testSseTransport
const connections = transport?.command({ type: "connections" }) as SseConnectionRecord[] | undefined
return connections?.findLast((connection) => connection.id > after && connection.endedAt === undefined)
return connections?.some((connection) => connection.id > after)
},
input.after ?? 0,
{ timeout: input.timeout },
)
let result: SseConnectionRecord | undefined
try {
result = await connection.jsonValue()
} finally {
await connection.dispose()
}
if (!result) throw new Error("SSE transport connection disappeared while waiting")
return result
return (await command<SseConnectionRecord[]>({ type: "connections" })).findLast(
(connection) => connection.id > (input.after ?? 0),
)!
},
send(payload, eventOptions) {
return command({ type: "send", deliveries: [{ payload, options: eventOptions }], burst: false })
+1 -1
View File
@@ -656,7 +656,7 @@ function makeUsageService(sdk: OpencodeClient) {
sessionId: params.sessionID,
update: {
sessionUpdate: "usage_update",
used: UsageService.contextTokens(message),
used: message.tokens.input + message.tokens.cache.read,
size,
cost: { amount: UsageService.totalSessionCost(messages), currency: "USD" },
},
+1 -5
View File
@@ -83,10 +83,6 @@ export function messageLoaderFromSDK(sdk: SDK): MessageLoaderInterface {
export const messageLoaderLayer = (sdk: SDK) => Layer.succeed(MessageLoader, messageLoaderFromSDK(sdk))
export function contextTokens(message: AssistantTokenCost): number {
return message.tokens.input + message.tokens.cache.read + message.tokens.cache.write
}
export function buildUsage(message: AssistantTokenCost): Usage {
const cachedReadTokens = message.tokens.cache.read
const cachedWriteTokens = message.tokens.cache.write
@@ -211,7 +207,7 @@ const layer = Layer.effect(
sessionId: input.sessionID,
update: {
sessionUpdate: "usage_update",
used: contextTokens(message),
used: message.tokens.input + message.tokens.cache.read,
size,
cost: { amount: totalSessionCost(messages), currency: "USD" },
},
+290 -3
View File
@@ -1,9 +1,14 @@
import type { Hooks, PluginInput } from "@opencode-ai/plugin"
import { OAUTH_DUMMY_KEY } from "../auth"
import { createServer } from "http"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { OauthCallbackPage } from "@opencode-ai/core/oauth/page"
// Public Grok-CLI OAuth client.
// Public Grok-CLI OAuth client. xAI's auth server rejects loopback OAuth from
// non-allowlisted clients, so we reuse the Grok-CLI client_id that xAI ships
// for desktop OAuth flows. Source of truth: hermes-agent PR #26534.
const CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828"
const AUTHORIZE_URL = "https://auth.x.ai/oauth2/authorize"
const TOKEN_URL = "https://auth.x.ai/oauth2/token"
// RFC 8628 device authorization grant. Confirmed exposed by xAI's
// /.well-known/openid-configuration as `device_authorization_endpoint`
@@ -25,15 +30,51 @@ const DEVICE_CODE_SLOW_DOWN_INCREMENT_MS = 5_000
const DEVICE_CODE_DEFAULT_EXPIRES_MS = 5 * 60 * 1000
const OAUTH_POLLING_SAFETY_MARGIN_MS = 3_000
// xAI rejects redirect_uris that don't match what was registered for the
// Grok-CLI client. The host:port pair is part of the registration, so we have
// to bind the loopback server to this exact port.
const OAUTH_HOST = "127.0.0.1"
const OAUTH_PORT = 56121
const OAUTH_REDIRECT_PATH = "/callback"
const REDIRECT_URI = `http://${OAUTH_HOST}:${OAUTH_PORT}${OAUTH_REDIRECT_PATH}`
// Refresh the access token a little before it actually expires so a single
// long-running tool call doesn't have to recover from a mid-flight 401.
const ACCESS_TOKEN_REFRESH_SKEW_MS = 120_000
interface XaiAuthPluginOptions {
authorizeUrl?: string
tokenUrl?: string
deviceAuthorizationUrl?: string
}
interface PkceCodes {
verifier: string
challenge: string
}
async function generatePKCE(): Promise<PkceCodes> {
const verifier = generateRandomString(64)
const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier))
return { verifier, challenge: base64UrlEncode(hash) }
}
function generateRandomString(length: number): string {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"
return Array.from(crypto.getRandomValues(new Uint8Array(length)))
.map((b) => chars[b % chars.length])
.join("")
}
function base64UrlEncode(buffer: ArrayBuffer): string {
const binary = String.fromCharCode(...new Uint8Array(buffer))
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")
}
function generateState(): string {
return base64UrlEncode(crypto.getRandomValues(new Uint8Array(32)).buffer)
}
interface TokenResponse {
access_token: string
refresh_token: string
@@ -74,6 +115,55 @@ export function accessTokenIsExpiring(
}
}
export function buildAuthorizeUrl(
pkce: PkceCodes,
state: string,
nonce: string,
options: XaiAuthPluginOptions = {},
): string {
// `plan=generic` opts the consent screen into xAI's generic OAuth plan tier;
// without it, accounts.x.ai rejects loopback OAuth from non-allowlisted
// clients. `referrer=opencode` lets xAI attribute opencode-originated
// logins in their OAuth server logs (best-effort attribution while we
// continue to reuse the Grok-CLI client_id).
const params = new URLSearchParams({
response_type: "code",
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
scope: SCOPE,
code_challenge: pkce.challenge,
code_challenge_method: "S256",
state,
nonce,
plan: "generic",
referrer: "opencode",
})
return `${options.authorizeUrl ?? AUTHORIZE_URL}?${params.toString()}`
}
async function exchangeCodeForTokens(
code: string,
pkce: PkceCodes,
options: XaiAuthPluginOptions = {},
): Promise<TokenResponse> {
const response = await fetch(options.tokenUrl ?? TOKEN_URL, {
method: "POST",
headers: authHeaders(),
body: new URLSearchParams({
grant_type: "authorization_code",
code,
redirect_uri: REDIRECT_URI,
client_id: CLIENT_ID,
code_verifier: pkce.verifier,
}).toString(),
})
if (!response.ok) {
const detail = await response.text().catch(() => "")
throw new Error(`xAI token exchange failed (${response.status})${detail ? `: ${detail}` : ""}`)
}
return response.json() as Promise<TokenResponse>
}
async function refreshAccessToken(refreshToken: string, options: XaiAuthPluginOptions = {}): Promise<TokenResponse> {
const response = await fetch(options.tokenUrl ?? TOKEN_URL, {
method: "POST",
@@ -112,7 +202,6 @@ export async function requestDeviceCode(options: XaiAuthPluginOptions = {}): Pro
body: new URLSearchParams({
client_id: CLIENT_ID,
scope: SCOPE,
referrer: "opencode",
}).toString(),
})
if (!response.ok) {
@@ -196,6 +285,170 @@ export async function pollDeviceCodeToken(
throw new Error("xAI device authorization timed out")
}
// CORS allowlist for the loopback callback. The redirect_uri itself is
// already bound to 127.0.0.1 and gated by PKCE+state, so we only accept
// xAI's own auth origins for additional defense-in-depth on the OPTIONS
// preflight.
const CORS_ALLOWED_ORIGINS = new Set(["https://accounts.x.ai", "https://auth.x.ai"])
interface PendingOAuth {
pkce: PkceCodes
state: string
resolve: (tokens: TokenResponse) => void
reject: (error: Error) => void
}
let oauthServer: ReturnType<typeof createServer> | undefined
let pendingOAuth: PendingOAuth | undefined
async function startOAuthServer(): Promise<{ port: number; redirectUri: string }> {
if (oauthServer) return { port: OAUTH_PORT, redirectUri: REDIRECT_URI }
const server = createServer((req, res) => {
const reqUrl = req.url || "/"
const url = new URL(reqUrl, `http://${OAUTH_HOST}:${OAUTH_PORT}`)
const origin = req.headers["origin"]
const allowOrigin = typeof origin === "string" && CORS_ALLOWED_ORIGINS.has(origin) ? origin : ""
if (allowOrigin) {
res.setHeader("Access-Control-Allow-Origin", allowOrigin)
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS")
res.setHeader("Access-Control-Allow-Headers", "Content-Type")
res.setHeader("Access-Control-Allow-Private-Network", "true")
res.setHeader("Vary", "Origin")
}
if (req.method === "OPTIONS") {
res.writeHead(204)
res.end()
return
}
if (url.pathname === OAUTH_REDIRECT_PATH) {
const code = url.searchParams.get("code")
const state = url.searchParams.get("state")
const error = url.searchParams.get("error")
const errorDescription = url.searchParams.get("error_description")
if (error) {
const errorMsg = errorDescription || error
pendingOAuth?.reject(new Error(errorMsg))
pendingOAuth = undefined
res.writeHead(200, { "Content-Type": "text/html" })
res.end(OauthCallbackPage.error(errorMsg, { provider: "xAI" }))
return
}
if (!code) {
const errorMsg = "Missing authorization code"
pendingOAuth?.reject(new Error(errorMsg))
pendingOAuth = undefined
res.writeHead(400, { "Content-Type": "text/html" })
res.end(OauthCallbackPage.error(errorMsg, { provider: "xAI" }))
return
}
if (!pendingOAuth || state !== pendingOAuth.state) {
const errorMsg = "Invalid state - potential CSRF attack"
pendingOAuth?.reject(new Error(errorMsg))
pendingOAuth = undefined
res.writeHead(400, { "Content-Type": "text/html" })
res.end(OauthCallbackPage.error(errorMsg, { provider: "xAI" }))
return
}
const current = pendingOAuth
pendingOAuth = undefined
exchangeCodeForTokens(code, current.pkce)
.then((tokens) => current.resolve(tokens))
.catch((err) => current.reject(err))
res.writeHead(200, { "Content-Type": "text/html" })
res.end(OauthCallbackPage.success({ provider: "xAI" }))
return
}
if (url.pathname === "/cancel") {
pendingOAuth?.reject(new Error("Login cancelled"))
pendingOAuth = undefined
res.writeHead(200)
res.end("Login cancelled")
return
}
res.writeHead(404)
res.end("Not found")
})
// listen() failures (e.g. EADDRINUSE because Grok-CLI is bound to the same
// pinned port) must clear `oauthServer` and remove our error listener,
// otherwise the next startOAuthServer() short-circuits on the truthy check
// and returns a redirect_uri pointing at nothing.
await new Promise<void>((resolve, reject) => {
const onError = (err: Error) => {
oauthServer = undefined
reject(err)
}
server.once("error", onError)
server.listen(OAUTH_PORT, OAUTH_HOST, () => {
server.removeListener("error", onError)
// After listen() succeeds, install a permanent log-only listener so
// that subsequent server errors (e.g. accept() failures, socket-level
// errors) don't trip Node's default "unhandled error event = throw"
// behavior and crash the entire opencode process. Matches the silent-
// swallow behavior the Codex plugin gets from its permanent
// `oauthServer!.on("error", reject)`.
resolve()
})
oauthServer = server
})
return { port: OAUTH_PORT, redirectUri: REDIRECT_URI }
}
function stopOAuthServer() {
if (oauthServer) {
oauthServer.close()
oauthServer = undefined
}
}
function waitForOAuthCallback(pkce: PkceCodes, state: string): Promise<TokenResponse> {
// A previous in-flight authorize() that the user abandoned (or that is
// being superseded by a fresh attempt) still owns `pendingOAuth`. Reject
// it eagerly so its caller stops waiting on a state value that can never
// match the next callback.
if (pendingOAuth) {
pendingOAuth.reject(new Error("Superseded by a newer xAI authorize request"))
pendingOAuth = undefined
}
return new Promise((resolve, reject) => {
const timeout = setTimeout(
() => {
if (pendingOAuth) {
pendingOAuth = undefined
reject(new Error("OAuth callback timeout - authorization took too long"))
}
},
5 * 60 * 1000,
)
pendingOAuth = {
pkce,
state,
resolve: (tokens) => {
clearTimeout(timeout)
resolve(tokens)
},
reject: (error) => {
clearTimeout(timeout)
reject(error)
},
}
})
}
interface RefreshResult {
access: string
refresh: string
@@ -295,6 +548,40 @@ export async function XaiAuthPlugin(input: PluginInput, options: XaiAuthPluginOp
}
},
methods: [
{
label: "xAI Grok OAuth (SuperGrok Subscription)",
type: "oauth",
authorize: async () => {
await startOAuthServer()
const pkce = await generatePKCE()
const state = generateState()
const nonce = generateState()
const authUrl = buildAuthorizeUrl(pkce, state, nonce, options)
const callbackPromise = waitForOAuthCallback(pkce, state)
return {
url: authUrl,
instructions: "Complete authorization in your browser. This window will close automatically.",
method: "auto" as const,
callback: async () => {
try {
const tokens = await callbackPromise
return {
type: "success" as const,
refresh: tokens.refresh_token,
access: tokens.access_token,
expires: Date.now() + (tokens.expires_in ?? 3600) * 1000,
}
} catch (err) {
return { type: "failed" as const }
} finally {
stopOAuthServer()
}
},
}
},
},
{
// RFC 8628 device-code flow. The CLI prints a verification URL
// and a short user_code that the user enters in a browser on
@@ -304,7 +591,7 @@ export async function XaiAuthPlugin(input: PluginInput, options: XaiAuthPluginOp
// user's browser. Defends the only attack surface (the polling
// loop) with the standard authorization_pending / slow_down
// backoff and a hard deadline from xAI's `expires_in`.
label: "SuperGrok Subscription",
label: "xAI Grok OAuth (Headless / Remote / VPS)",
type: "oauth",
authorize: async () => {
const device = await requestDeviceCode(options)
@@ -97,29 +97,6 @@ export function http(
headers.delete("content-encoding")
headers.delete("content-length")
// An upstream 5xx from a remote workspace sandbox arrives here as an opaque
// status — its real cause (and log line) live only inside the sandbox. Buffer
// the small error body, log it locally so it shows up in the host's log, and
// forward it unchanged (preserving content-type so the client can still parse
// the structured error, e.g. its `ref`).
if (response.status >= 500) {
const body = yield* response.text.pipe(Effect.catch(() => Effect.succeed("")))
const contentType = response.headers["content-type"] ?? "application/json"
headers.delete("content-type")
yield* Effect.logError("workspace proxy upstream error", {
url: url.toString(),
method: request.method,
status: response.status,
body: body.slice(0, 2000),
})
return HttpServerResponse.text(body, {
status: response.status,
statusText: statusText(response),
headers,
contentType,
})
}
return HttpServerResponse.stream(response.stream.pipe(Stream.catchCause(() => Stream.empty)), {
status: response.status,
statusText: statusText(response),
@@ -34,12 +34,5 @@ export function workspaceProxyURL(target: string | URL, requestURL: URL) {
proxyURL.search = requestURL.search
proxyURL.hash = requestURL.hash
proxyURL.searchParams.delete("workspace")
// The `directory` param is the *host's* working directory (e.g. a Windows
// path like `F:\proj`). It is meaningless — and dangerous — on the remote:
// the sandbox would `path.resolve` it against its own cwd, producing a bogus
// path like `/home/daytona/workspace/repo/F:\proj` that does not exist and
// crashes prompt handling. Drop it so the remote falls back to its own
// project root. This mirrors ProxyUtil.headers stripping `x-opencode-directory`.
proxyURL.searchParams.delete("directory")
return proxyURL
}
+27 -26
View File
@@ -28,15 +28,6 @@ export const RETRY_BACKOFF_FACTOR = 2
export const RETRY_MAX_DELAY_NO_HEADERS = 30_000 // 30 seconds
export const RETRY_MAX_DELAY = 2_147_483_647 // max 32-bit signed integer for setTimeout
const RETRYABLE_MESSAGE_PATTERNS = [
/429|500|502|503|504|524/i,
/rate increased too quickly|rate limit|rate-limit|rate_limit|too many requests/i,
/overloaded|service unavailable|service_unavailable|service-unavailable|internal error|internal_error|internal server error|server error|server_error|server-error|provider returned error|provider_returned_error|provider-returned-error/i,
/terminated|fetch failed|failed to fetch|network error|upstream connect|connection error|connection refused|connection lost|socket connection was closed|socket hang up|reset before headers|getaddrinfo|enotfound|eai_again|econnrefused|econnreset|etimedout/i,
/^timeout$|\b(?:request|response|connection|network|stream|read) (?:timeout|timed out|time out)\b/i,
/try your request again|retry your request|resource exhausted|resource_exhausted/i,
]
function cap(ms: number) {
return Math.min(ms, RETRY_MAX_DELAY)
}
@@ -81,13 +72,7 @@ export function retryable(error: Err, provider: string) {
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) &&
!matchesRetryableMessage(error.data.message) &&
!matchesRetryableMessage(error.data.responseBody)
)
return undefined
if (!error.data.isRetryable && !(status !== undefined && status >= 500)) return undefined
if (error.data.responseBody?.includes("FreeUsageLimitError")) {
return {
message: GO_UPSELL_MESSAGE,
@@ -137,17 +122,33 @@ export function retryable(error: Err, provider: string) {
return { message: error.data.message.includes("Overloaded") ? "Provider is overloaded" : error.data.message }
}
const message = isRecord(error.data) ? error.data.message : undefined
if (typeof message !== "string") return undefined
const lower = message.toLowerCase()
if (lower.includes("too_many_requests")) return { message: "Too Many Requests" }
if (lower.includes("exhausted") || lower.includes("unavailable")) return { message: "Provider is overloaded" }
if (matchesRetryableMessage(message)) return { message }
return undefined
}
// Check for rate limit patterns in plain text error messages
const msg = isRecord(error.data) ? error.data.message : undefined
if (typeof msg === "string") {
const lower = msg.toLowerCase()
if (
lower.includes("rate increased too quickly") ||
lower.includes("rate limit") ||
lower.includes("too many requests")
) {
return { message: msg }
}
}
function matchesRetryableMessage(value: unknown) {
return typeof value === "string" && RETRYABLE_MESSAGE_PATTERNS.some((pattern) => pattern.test(value))
const json = parseJSON(msg)
if (!json || typeof json !== "object") return undefined
const code = typeof json.code === "string" ? json.code : ""
if (json.type === "error" && json.error?.type === "too_many_requests") {
return { message: "Too Many Requests" }
}
if (code.includes("exhausted") || code.includes("unavailable")) {
return { message: "Provider is overloaded" }
}
if (json.type === "error" && typeof json.error?.code === "string" && json.error.code.includes("rate_limit")) {
return { message: "Rate Limited" }
}
return undefined
}
function str(value: unknown) {
+3 -3
View File
@@ -207,7 +207,7 @@ describe("acp usage", () => {
)
})
it.effect("includes cache reads and writes in ACP context usage", () => {
it.effect("sends ACP usage_update with context size and cumulative assistant cost", () => {
const updates: SessionNotification[] = []
return Effect.gen(function* () {
const usage = yield* UsageService.Service
@@ -222,7 +222,7 @@ describe("acp usage", () => {
sessionId: "ses_1",
update: {
sessionUpdate: "usage_update",
used: 22,
used: 15,
size: 128_000,
cost: { amount: 3, currency: "USD" },
},
@@ -239,7 +239,7 @@ describe("acp usage", () => {
input: 10,
output: 20,
reasoning: 0,
cache: { read: 5, write: 7 },
cache: { read: 5, write: 0 },
},
}),
]),
+41 -6
View File
@@ -1,5 +1,11 @@
import { describe, expect, test } from "bun:test"
import { accessTokenIsExpiring, pollDeviceCodeToken, requestDeviceCode, XaiAuthPlugin } from "../../src/plugin/xai"
import {
accessTokenIsExpiring,
buildAuthorizeUrl,
pollDeviceCodeToken,
requestDeviceCode,
XaiAuthPlugin,
} from "../../src/plugin/xai"
import { OAUTH_DUMMY_KEY } from "../../src/auth"
function makeJwt(payload: object): string {
@@ -70,6 +76,32 @@ describe("plugin.xai", () => {
})
})
describe("buildAuthorizeUrl", () => {
const pkce = { verifier: "ver", challenge: "chal" }
test("includes required OAuth + PKCE + OIDC params", () => {
const url = new URL(buildAuthorizeUrl(pkce, "state-abc", "nonce-xyz"))
const params = url.searchParams
expect(url.origin + url.pathname).toBe("https://auth.x.ai/oauth2/authorize")
expect(params.get("response_type")).toBe("code")
expect(params.get("client_id")).toBe("b1a00492-073a-47ea-816f-4c329264a828")
expect(params.get("redirect_uri")).toBe("http://127.0.0.1:56121/callback")
expect(params.get("scope")).toBe("openid profile email offline_access grok-cli:access api:access")
expect(params.get("code_challenge")).toBe("chal")
expect(params.get("code_challenge_method")).toBe("S256")
expect(params.get("state")).toBe("state-abc")
expect(params.get("nonce")).toBe("nonce-xyz")
expect(params.get("plan")).toBe("generic")
expect(params.get("referrer")).toBe("opencode")
})
test("supports endpoint override for local integration tests", () => {
const url = new URL(buildAuthorizeUrl(pkce, "s", "n", { authorizeUrl: "http://127.0.0.1/oauth2/authorize" }))
expect(url.origin + url.pathname).toBe("http://127.0.0.1/oauth2/authorize")
})
})
describe("loader", () => {
test("returns no options unless stored auth is OAuth and exposes methods in order", async () => {
const hooks = await XaiAuthPlugin({} as any)
@@ -78,7 +110,8 @@ describe("plugin.xai", () => {
await hooks.auth!.loader!(async () => ({ type: "wellknown", key: "k", token: "t" }) as any, {} as any),
).toEqual({})
expect(hooks.auth!.methods.map((m) => [m.type, m.label])).toEqual([
["oauth", "SuperGrok Subscription"],
["oauth", "xAI Grok OAuth (SuperGrok Subscription)"],
["oauth", "xAI Grok OAuth (Headless / Remote / VPS)"],
["api", "Manually enter API Key"],
])
})
@@ -392,7 +425,8 @@ describe("plugin.xai", () => {
})
const hooks = await XaiAuthPlugin({} as any, serverOptions(server))
const headless = hooks.auth!.methods.find(
(m): m is Extract<typeof m, { type: "oauth" }> => m.type === "oauth" && m.label === "SuperGrok Subscription",
(m): m is Extract<typeof m, { type: "oauth" }> =>
m.type === "oauth" && m.label === "xAI Grok OAuth (Headless / Remote / VPS)",
)!
const result = await headless.authorize!()
@@ -415,7 +449,8 @@ describe("plugin.xai", () => {
return new Response("unexpected request", { status: 500 })
})
const headless = (await XaiAuthPlugin({} as any, serverOptions(server))).auth!.methods.find(
(m): m is Extract<typeof m, { type: "oauth" }> => m.type === "oauth" && m.label === "SuperGrok Subscription",
(m): m is Extract<typeof m, { type: "oauth" }> =>
m.type === "oauth" && m.label === "xAI Grok OAuth (Headless / Remote / VPS)",
)!
expect((await headless.authorize!()).url).toBe("https://x.ai/device")
})
@@ -439,7 +474,6 @@ describe("plugin.xai", () => {
expect(parsed.get("scope")).toContain("offline_access")
expect(parsed.get("scope")).toContain("grok-cli:access")
expect(parsed.get("scope")).toContain("api:access")
expect(parsed.get("referrer")).toBe("opencode")
await expect(
requestDeviceCode({ deviceAuthorizationUrl: new URL("/error", server.url).toString() }),
).rejects.toThrow(/429.*rate limited/)
@@ -577,7 +611,8 @@ describe("plugin.xai", () => {
return Response.json({ error: "access_denied" }, { status: 400 })
})
const headless = (await XaiAuthPlugin({} as any, serverOptions(server))).auth!.methods.find(
(m): m is Extract<typeof m, { type: "oauth" }> => m.type === "oauth" && m.label === "SuperGrok Subscription",
(m): m is Extract<typeof m, { type: "oauth" }> =>
m.type === "oauth" && m.label === "xAI Grok OAuth (Headless / Remote / VPS)",
)!
expect(await ((await headless.authorize!()) as any).callback()).toEqual({ type: "failed" })
})
@@ -80,13 +80,6 @@ describe("workspaceProxyURL", () => {
expect(result.searchParams.get("keep")).toBe("yes")
})
test("strips the host directory param so the remote resolves its own root", () => {
const url = new URL("http://localhost/session/abc?directory=F%3A%5Cproj&keep=yes")
const result = workspaceProxyURL("http://remote:8080/base", url)
expect(result.searchParams.get("directory")).toBeNull()
expect(result.searchParams.get("keep")).toBe("yes")
})
test("preserves hash from request", () => {
const url = new URL("http://localhost/page#section")
const result = workspaceProxyURL("http://remote:8080", url)
+2 -46
View File
@@ -118,21 +118,16 @@ describe("session.retry.delay", () => {
})
describe("session.retry.retryable", () => {
test("retries serialized too_many_requests messages", () => {
test("maps too_many_requests json messages", () => {
const error = wrap(JSON.stringify({ type: "error", error: { type: "too_many_requests" } }))
expect(SessionRetry.retryable(error, retryProvider)).toEqual({ message: "Too Many Requests" })
})
test("retries serialized overloaded provider codes", () => {
test("maps overloaded provider codes", () => {
const error = wrap(JSON.stringify({ code: "resource_exhausted" }))
expect(SessionRetry.retryable(error, retryProvider)).toEqual({ message: "Provider is overloaded" })
})
test("retries serialized rate_limit messages", () => {
const message = JSON.stringify({ type: "error", error: { code: "rate_limit_exceeded" } })
expect(SessionRetry.retryable(wrap(message), retryProvider)).toEqual({ message })
})
test("does not retry unknown json messages", () => {
const error = wrap(JSON.stringify({ error: { message: "no_kv_space" } }))
expect(SessionRetry.retryable(error, retryProvider)).toBeUndefined()
@@ -168,45 +163,6 @@ describe("session.retry.retryable", () => {
expect(SessionRetry.retryable(error, retryProvider)).toEqual({ message: msg })
})
test.each([
"Internal server error",
"internal error",
"server-error",
"Provider returned error",
"provider-returned-error",
"terminated",
"fetch failed",
"connection refused",
"connect ECONNREFUSED",
"request ETIMEDOUT",
"failed to fetch",
"EAI_AGAIN",
"response timed out",
"Please retry your request",
"try your request again",
"upstream returned status 524",
])("retries matching API error text: %s", (message) => {
expect(SessionRetry.retryable(wrap(message), retryProvider)).toEqual({ message })
})
test("retries hyphenated service-unavailable errors", () => {
expect(SessionRetry.retryable(wrap("service-unavailable"), retryProvider)).toEqual({
message: "Provider is overloaded",
})
})
test("matches retryable API response bodies", () => {
const error = Schema.decodeUnknownSync(SessionV1.APIError.Schema)(
new SessionV1.APIError({
message: "Request failed",
isRetryable: false,
statusCode: 400,
responseBody: JSON.stringify({ error: { message: "upstream connection refused" } }),
}).toObject(),
)
expect(SessionRetry.retryable(error, retryProvider)).toEqual({ message: "Request failed" })
})
test("retries transport timeout errors", () => {
const request = MessageV2.fromError(new ProviderError.HeaderTimeoutError(10000), { providerID })
expect(SessionV1.APIError.isInstance(request)).toBe(true)
@@ -204,7 +204,7 @@ export function BasicTool(props: BasicToolProps) {
>
<TextShimmer text={title().title} active={pending()} />
</span>
<Show when={!pending() || title().subtitle || title().args?.length}>
<Show when={!pending()}>
<Show when={title().subtitle}>
<span
data-slot="basic-tool-tool-subtitle"
@@ -61,7 +61,6 @@ import { TextShimmer } from "@opencode-ai/ui/text-shimmer"
import { AnimatedCountList } from "./tool-count-summary"
import { ToolStatusTitle } from "./tool-status-title"
import { patchFiles } from "./apply-patch-file"
import { partDefaultOpen } from "./part-default-open"
import { animate } from "motion"
import { attached, inline, kind, typeLabel } from "./message-file"
import { readPartText } from "./message-part-text"
@@ -719,7 +718,15 @@ export function renderable(part: PartType, showReasoningSummaries = true) {
return !!PART_MAPPING[part.type]
}
export { partDefaultOpen } from "./part-default-open"
function toolDefaultOpen(tool: string, shell = false, edit = false) {
if (tool === "bash" || tool === "shell") return shell
if (tool === "edit" || tool === "write" || tool === "patch" || tool === "apply_patch") return edit
}
export function partDefaultOpen(part: PartType, shell = false, edit = false) {
if (part.type !== "tool") return
return toolDefaultOpen(part.tool, shell, edit)
}
export function AssistantParts(props: {
messages: AssistantMessage[]
@@ -1127,10 +1134,10 @@ export function ContextToolGroup(props: {
<span data-slot="basic-tool-tool-title">
<TextShimmer text={trigger().title} active={running()} />
</span>
<Show when={trigger().subtitle}>
<Show when={!running() && trigger().subtitle}>
<span data-slot="basic-tool-tool-subtitle">{trigger().subtitle}</span>
</Show>
<Show when={trigger().args?.length}>
<Show when={!running() && trigger().args?.length}>
<For each={trigger().args}>
{(arg) => <span data-slot="basic-tool-tool-arg">{arg}</span>}
</For>
@@ -1,66 +0,0 @@
import { describe, expect, test } from "bun:test"
import type { Part as PartType } from "@opencode-ai/sdk/v2"
import { partDefaultOpen } from "./part-default-open"
describe("partDefaultOpen", () => {
test("keeps edited files expanded when enabled", () => {
expect(partDefaultOpen(tool("edit", { filediff: { additions: 1, deletions: 1 } }), false, true)).toBe(true)
})
test("collapses deletion-only edits when enabled", () => {
expect(partDefaultOpen(tool("edit", { filediff: { additions: 0, deletions: 1_200 } }), false, true)).toBe(false)
})
test("collapses patches containing only deleted files when enabled", () => {
expect(
partDefaultOpen(
tool("apply_patch", {
files: [
{ filePath: "one.ts", type: "delete" },
{ filePath: "two.ts", type: "delete" },
],
}),
false,
true,
),
).toBe(false)
})
test("keeps mixed patches expanded when enabled", () => {
expect(
partDefaultOpen(
tool("apply_patch", {
files: [
{ filePath: "one.ts", type: "delete" },
{ filePath: "two.ts", type: "update" },
],
}),
false,
true,
),
).toBe(true)
})
test("preserves shell defaults", () => {
expect(partDefaultOpen(tool("shell", {}), true, false)).toBe(true)
})
})
function tool(name: string, metadata: Record<string, unknown>): PartType {
return {
id: `part_${name}`,
sessionID: "session",
messageID: "message",
type: "tool",
callID: `call_${name}`,
tool: name,
state: {
status: "completed",
input: {},
output: "",
title: name,
metadata,
time: { start: 0, end: 1 },
},
}
}
@@ -1,26 +0,0 @@
import type { Part as PartType, ToolPart } from "@opencode-ai/sdk/v2"
function deletionOnly(part: ToolPart) {
if (!("metadata" in part.state)) return false
const metadata = part.state.metadata
if (!metadata) return false
const files = metadata.files
if (Array.isArray(files) && files.length > 0) {
return files.every((file) => !!file && typeof file === "object" && "type" in file && file.type === "delete")
}
const filediff = metadata.filediff
if (!filediff || typeof filediff !== "object") return false
if (!("additions" in filediff) || !("deletions" in filediff)) return false
return filediff.additions === 0 && typeof filediff.deletions === "number" && filediff.deletions > 0
}
export function partDefaultOpen(part: PartType, shell = false, edit = false) {
if (part.type !== "tool") return
if (part.tool === "bash" || part.tool === "shell") return shell
if (part.tool === "edit" || part.tool === "write" || part.tool === "patch" || part.tool === "apply_patch") {
if (!edit) return false
return !deletionOnly(part)
}
}
+119 -74
View File
@@ -1,17 +1,23 @@
import { Meta, Title } from "@solidjs/meta"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { geoEquirectangular, geoPath } from "d3-geo"
import { scaleSqrt } from "d3-scale"
import countryCodesSource from "i18n-iso-countries/codes.json?raw"
import { feature, mesh } from "topojson-client"
import countriesTopologySource from "world-atlas/countries-50m.json?raw"
import {
getStatsModelData,
type CountryEntry,
type ModelPeerEntry,
type ModelUsagePoint,
type StatsModelData,
type UsageRange,
} from "@opencode-ai/stats-core/domain/home"
import { createAsync, query, useParams } from "@solidjs/router"
import { createMemo, createSignal, createUniqueId, For, onMount, Show, type JSX } from "solid-js"
import { getRequestEvent } from "solid-js/web"
import type { FeatureCollection, GeometryObject, GeoJsonProperties } from "geojson"
import type { GeometryCollection, Topology } from "topojson-specification"
import { LocaleLinks } from "../../component/locale-links"
import { useI18n } from "../../context/i18n"
import { useLanguage } from "../../context/language"
@@ -19,10 +25,10 @@ import { localizedUrl } from "../../lib/language"
import {
findModelCatalogEntry,
formatCatalogLabName,
loadModelCatalog,
getModelCatalog,
type ModelCatalog,
type ModelCatalogEntry,
} from "../model-catalog"
import { geoMapHeight, geoMapWidth, worldBorderPath, worldCountryMarkers, worldCountryPaths } from "../geo-map"
import { SectionHeading } from "../section-heading"
import { runStatsEffect } from "../../stats-runtime"
import { setStatsPageCacheHeaders } from "../stats-cache"
@@ -45,41 +51,45 @@ import {
} from "../stats-shell"
const statsUnfurlPath = "banner.png"
const geoMapWidth = 960
const geoMapHeight = 430
const shortMonths = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"] as const
type IsoCountryCode = readonly [string, string, string]
type ModelCatalogOption = Pick<ModelCatalogEntry, "id" | "lab" | "slug" | "name">
type ModelPageCatalog = {
entry: ModelCatalogEntry | null
labs: { id: string; name: string }[]
labModels: ModelCatalogOption[]
}
type StatsModelPageData = Omit<StatsModelData, "country"> & { country: CountryEntry[] }
type ModelPageData = { catalog: ModelPageCatalog; stats: StatsModelPageData | null }
type WorldCountryProperties = GeoJsonProperties & { name?: string }
type WorldTopology = Topology<{ countries: GeometryCollection<WorldCountryProperties> }>
const countryNumericIds = new Map(
(JSON.parse(countryCodesSource) as IsoCountryCode[]).map((country) => [country[0], country[2]] as const),
)
const worldTopology = JSON.parse(countriesTopologySource) as WorldTopology
const worldCountryGeometries: GeometryCollection<WorldCountryProperties> = {
...worldTopology.objects.countries,
geometries: worldTopology.objects.countries.geometries.filter((country) => String(country.id ?? "") !== "010"),
}
const worldCountries = feature<WorldCountryProperties>(worldTopology, worldCountryGeometries) as FeatureCollection<
GeometryObject,
WorldCountryProperties
>
const worldProjection = geoEquirectangular().fitExtent(
[
[10, 12],
[geoMapWidth - 10, geoMapHeight - 12],
],
worldCountries,
)
const worldPath = geoPath(worldProjection)
const worldCountryPaths = worldCountries.features.map((country) => ({
id: String(country.id ?? "").padStart(3, "0"),
path: worldPath(country) ?? "",
marker: geoCountryMarker(country),
}))
const worldBorderPath = worldPath(mesh(worldTopology, worldCountryGeometries, (a, b) => a !== b)) ?? ""
const getModelPageData = query(async (labParam: string, modelParam: string) => {
const getModelData = query(async (lab: string, model: string) => {
"use server"
const catalog = await loadModelCatalog()
const entry = findModelCatalogEntry(catalog, modelParam, labParam) ?? null
const lab = entry?.lab ?? labParam
const model = entry?.slug ?? modelParam
const stats = lab && model ? await runStatsEffect(getStatsModelData(model, lab)) : null
return {
catalog: {
entry,
labs: catalog.labs.map((item) => ({ id: item.id, name: item.name })),
labModels:
catalog.labs
.find((item) => item.id === (entry?.lab ?? providerSlug(labParam)))
?.models.map((item) => ({ id: item.id, lab: item.lab, slug: item.slug, name: item.name })) ?? [],
},
stats: stats ? { ...stats, country: stats.country["2M"] } : null,
} satisfies ModelPageData
}, "getStatsModelPageData")
return runStatsEffect(getStatsModelData(model, lab))
}, "getStatsModelData")
export default function StatsModel() {
const i18n = useI18n()
@@ -89,9 +99,18 @@ export default function StatsModel() {
const params = useParams()
const labParam = createMemo(() => params.lab ?? "")
const modelParam = createMemo(() => params.model ?? "")
const page = createAsync(() => getModelPageData(labParam(), modelParam()))
const catalogEntry = createMemo(() => page()?.catalog.entry)
const stats = createMemo(() => page()?.stats)
const catalog = createAsync(() => getModelCatalog())
const catalogEntry = createMemo(() => {
const data = catalog()
if (!data) return undefined
return findModelCatalogEntry(data, modelParam(), labParam()) ?? null
})
const stats = createAsync(() => {
const entry = catalogEntry()
if (catalog() === undefined || entry === undefined) return Promise.resolve(undefined)
if (!entry && (!labParam() || !modelParam())) return Promise.resolve(null)
return getModelData(labParam(), entry?.slug ?? modelParam())
})
const githubStars = createAsync(() => getGitHubStars())
const [themePreference, setThemePreference] = createSignal<ThemePreference>("system")
const modelName = createMemo(() => catalogEntry()?.name ?? stats()?.model ?? modelParam() ?? i18n.t("model.fallback"))
@@ -160,13 +179,13 @@ export default function StatsModel() {
<Header githubStars={githubStars() ?? "150K"} links={modelHeaderLinks()} brandHref={import.meta.env.BASE_URL} />
<div data-component="container">
<div data-component="content">
<Show when={page() !== undefined} fallback={<ModelLoading />}>
<Show when={catalogEntry() || stats() !== undefined} fallback={<ModelLoading />}>
<Show when={catalogEntry() || stats()} fallback={<ModelNotFound lab={labParam()} model={modelParam()} />}>
<>
<ModelHero
data={stats() ?? null}
catalog={catalogEntry() ?? null}
catalogData={page()?.catalog ?? null}
catalogData={catalog() ?? null}
labName={labName()}
/>
<ModelOverview catalog={catalogEntry() ?? null} />
@@ -174,10 +193,10 @@ export default function StatsModel() {
<ModelUsageSection data={stats() ?? null} />
<ModelUniqueUsersSection data={stats() ?? null} />
<ModelEfficiencySection data={stats() ?? null} catalog={catalogEntry() ?? null} />
<ModelGeoBreakdownSection data={stats()?.country ?? []} />
<ModelGeoBreakdownSection data={stats()?.country ?? emptyCountryRecord()} />
<ModelPeersSection data={stats() ?? null} />
<ComparisonCardsSection
pairs={modelComparisonPairs(page()?.catalog.labModels, catalogEntry() ?? null, stats() ?? null)}
pairs={modelComparisonPairs(catalog(), catalogEntry() ?? null, stats() ?? null)}
title="Compare This Model"
description="Other models to compare with this one."
variant="featured"
@@ -252,9 +271,9 @@ function ModelNotFound(props: { lab: string; model: string }) {
}
function ModelHero(props: {
data: StatsModelPageData | null
data: StatsModelData | null
catalog: ModelCatalogEntry | null
catalogData: ModelPageCatalog | null
catalogData: ModelCatalog | null
labName: string
}) {
const i18n = useI18n()
@@ -263,7 +282,9 @@ function ModelHero(props: {
const modelName = () => props.catalog?.name ?? props.data?.model ?? i18n.t("model.fallback")
const weights = () => props.catalog?.weights[0]
const labs = () => props.catalogData?.labs ?? []
const labModels = () => props.catalogData?.labModels ?? (props.catalog ? [props.catalog] : [])
const labModels = () =>
props.catalogData?.labs.find((lab) => lab.id === providerSlug(labId()))?.models ??
(props.catalog ? [props.catalog] : [])
return (
<section id="overview" data-section="model-hero">
<nav data-component="model-hero-breadcrumb" aria-label="Data breadcrumb">
@@ -382,7 +403,7 @@ function ModelHeroActionIcon(props: { kind: "weights" | "compare" }) {
)
}
function ModelHeroSparkline(props: { data: StatsModelPageData }) {
function ModelHeroSparkline(props: { data: StatsModelData }) {
const values = () => props.data.usage.slice(-14).map((point) => point.tokens)
return (
<span data-slot="model-hero-sparkline" aria-hidden="true">
@@ -445,7 +466,7 @@ function ModelOverview(props: { catalog: ModelCatalogEntry | null }) {
)
}
function ModelMomentumSection(props: { data: StatsModelPageData | null }) {
function ModelMomentumSection(props: { data: StatsModelData | null }) {
const i18n = useI18n()
const language = useLanguage()
return (
@@ -484,7 +505,7 @@ function ModelMomentumSection(props: { data: StatsModelPageData | null }) {
)
}
function MomentumChart(props: { data: StatsModelPageData; locale: string }) {
function MomentumChart(props: { data: StatsModelData; locale: string }) {
const chart = createMemo(() => momentumChart(props.data.usage, props.data.updatedAt))
const changeState = createMemo(() => (props.data.tokenChange < 0 ? "negative" : "positive"))
return (
@@ -541,7 +562,7 @@ function MomentumMetric(props: { label: string; value: string; watermark?: strin
)
}
function ModelUsageSection(props: { data: StatsModelPageData | null }) {
function ModelUsageSection(props: { data: StatsModelData | null }) {
const i18n = useI18n()
return (
<ModelTrendSection
@@ -561,7 +582,7 @@ function ModelUsageSection(props: { data: StatsModelPageData | null }) {
)
}
function ModelUniqueUsersSection(props: { data: StatsModelPageData | null }) {
function ModelUniqueUsersSection(props: { data: StatsModelData | null }) {
const i18n = useI18n()
return (
<ModelTrendSection
@@ -585,7 +606,7 @@ function ModelUniqueUsersSection(props: { data: StatsModelPageData | null }) {
}
function ModelTrendSection(props: {
data: StatsModelPageData | null
data: StatsModelData | null
id: string
title: string
description: string
@@ -838,7 +859,7 @@ function ModelTrendSection(props: {
)
}
function ModelEfficiencySection(props: { data: StatsModelPageData | null; catalog: ModelCatalogEntry | null }) {
function ModelEfficiencySection(props: { data: StatsModelData | null; catalog: ModelCatalogEntry | null }) {
const i18n = useI18n()
return (
<section id="efficiency" data-section="model-panel">
@@ -889,11 +910,11 @@ function ModelEfficiencySection(props: { data: StatsModelPageData | null; catalo
)
}
function ModelGeoBreakdownSection(props: { data: CountryEntry[] }) {
function ModelGeoBreakdownSection(props: { data: Record<UsageRange, CountryEntry[]> }) {
const i18n = useI18n()
const language = useLanguage()
const [activeCountry, setActiveCountry] = createSignal<string>()
const data = createMemo(() => props.data)
const data = createMemo(() => props.data["2M"])
const countryById = createMemo(
() =>
new Map(
@@ -1010,30 +1031,31 @@ function GeoWorldMap(props: {
</For>
</g>
<g data-slot="geo-country-markers">
<For each={worldCountryMarkers}>
<For each={worldCountryPaths}>
{(country) => {
const entry = () => props.countryById.get(country.id)
return (
<Show when={entry()}>
<circle
cx={country.marker.x}
cy={country.marker.y}
data-country-id={country.id}
r={entry()?.country === props.activeCountry ? 3.4 : 2.4}
data-active={entry()?.country === props.activeCountry ? "true" : undefined}
style={{ "--geo-country-opacity": String(countryOpacity(entry())) } as JSX.CSSProperties}
aria-hidden="true"
onPointerEnter={() => {
const item = entry()
if (!item) return
props.onActiveCountryChange(item.country)
}}
onClick={() => {
const item = entry()
if (!item) return
props.onActiveCountryChange(item.country)
}}
/>
<Show when={country.marker && entry() ? country.marker : undefined}>
{(marker) => (
<circle
cx={marker().x}
cy={marker().y}
r={entry()?.country === props.activeCountry ? 3.4 : 2.4}
data-active={entry()?.country === props.activeCountry ? "true" : undefined}
style={{ "--geo-country-opacity": String(countryOpacity(entry())) } as JSX.CSSProperties}
aria-hidden="true"
onPointerEnter={() => {
const item = entry()
if (!item) return
props.onActiveCountryChange(item.country)
}}
onClick={() => {
const item = entry()
if (!item) return
props.onActiveCountryChange(item.country)
}}
/>
)}
</Show>
)
}}
@@ -1081,7 +1103,7 @@ function GeoCountryList(props: {
)
}
function ModelPeersSection(props: { data: StatsModelPageData | null }) {
function ModelPeersSection(props: { data: StatsModelData | null }) {
const i18n = useI18n()
return (
<section id="peers" data-section="model-panel">
@@ -1150,9 +1172,9 @@ function ModelEmptyState(props: { title: string; description: string; compact?:
}
function modelComparisonPairs(
catalogModels: ModelCatalogOption[] | undefined,
catalog: ModelCatalog | undefined,
catalogEntry: ModelCatalogEntry | null,
data: StatsModelPageData | null,
data: StatsModelData | null,
) {
const current = modelComparisonRef(catalogEntry, data)
if (!current) return []
@@ -1170,7 +1192,9 @@ function modelComparisonPairs(
},
detail: "Usage peer",
}))
const catalogPairs = (catalogEntry ? (catalogModels ?? []) : [])
const catalogPairs = (
catalogEntry && catalog ? (catalog.labs.find((lab) => lab.id === catalogEntry.lab)?.models ?? []) : []
)
.filter((model) => model.id !== catalogEntry?.id)
.slice(0, 3)
.map((model) => ({
@@ -1183,7 +1207,7 @@ function modelComparisonPairs(
function modelComparisonRef(
catalogEntry: ModelCatalogEntry | null,
data: StatsModelPageData | null,
data: StatsModelData | null,
): ComparisonModelRef | undefined {
if (catalogEntry) return modelRefFromCatalog(catalogEntry)
if (!data) return undefined
@@ -1203,10 +1227,31 @@ function getProviderIconId(author: string) {
return author.toLowerCase().replace(/[^a-z0-9]+/g, "")
}
function emptyCountryRecord(): Record<UsageRange, CountryEntry[]> {
return {
"1D": [],
"1W": [],
"2W": [],
"1M": [],
"2M": [],
"3M": [],
YTD: [],
ALL: [],
}
}
function countryNumericId(country: string) {
return countryNumericIds.get(country.toUpperCase())?.padStart(3, "0")
}
function geoCountryMarker(country: (typeof worldCountries.features)[number]) {
const bounds = worldPath.bounds(country)
const [x, y] = worldPath.centroid(country)
if (!Number.isFinite(x) || !Number.isFinite(y)) return undefined
if (bounds[1][0] - bounds[0][0] >= 3 && bounds[1][1] - bounds[0][1] >= 3) return undefined
return { x, y }
}
function formatCountryName(country: string, locale: string, i18n: ReturnType<typeof useI18n>) {
const code = country.toUpperCase()
if (code === "ZZ") return i18n.t("home.unknown")
@@ -1463,7 +1508,7 @@ function formatSparklinePoint(value: number) {
return Number(value.toFixed(2)).toString()
}
function formatModelRankMoveLabel(data: StatsModelPageData, i18n: ReturnType<typeof useI18n>) {
function formatModelRankMoveLabel(data: StatsModelData, i18n: ReturnType<typeof useI18n>) {
if (data.rank === null) return i18n.t("model.noUsageLastWeek")
if (data.previousRank === null) return i18n.t("model.newThisWeek")
const change = data.previousRank - data.rank
+37 -44
View File
@@ -6,6 +6,7 @@ import {
type LabUsageModelEntry,
type MarketDay,
type ModelUsagePoint,
type StatsHomeData,
type StatsLabData,
} from "@opencode-ai/stats-core/domain/home"
import { createAsync, query, useParams } from "@solidjs/router"
@@ -19,7 +20,7 @@ import {
catalogSlug,
findModelCatalogLab,
formatCatalogLabName,
loadModelCatalog,
getModelCatalog,
type ModelCatalogEntry,
type ModelCatalogLab,
} from "../model-catalog"
@@ -41,33 +42,15 @@ import {
const statsUnfurlPath = "banner.png"
type RelatedCatalogLab = Pick<ModelCatalogLab, "id" | "name" | "description"> & {
models: Pick<ModelCatalogEntry, "name">[]
}
type LabPageData = {
lab: ModelCatalogLab | null
labs: RelatedCatalogLab[]
market: MarketDay[]
stats: StatsLabData | null
}
const getLabPageData = query(async (labParam: string) => {
const getLabData = query(async (lab: string) => {
"use server"
const [catalog, home] = await Promise.all([loadModelCatalog(), runStatsEffect(getStatsHomeData())])
const lab = findModelCatalogLab(catalog, labParam) ?? null
return {
lab,
labs: catalog.labs.map((entry) => ({
id: entry.id,
name: entry.name,
description: entry.description,
models: entry.models.map((model) => ({ name: model.name })),
})),
market: home.market["2M"],
stats: lab ? await runStatsEffect(getStatsLabData(lab.id)) : null,
} satisfies LabPageData
}, "getStatsLabPageData")
return runStatsEffect(getStatsLabData(lab))
}, "getStatsLabData")
const getHomeData = query(async () => {
"use server"
return runStatsEffect(getStatsHomeData())
}, "getStatsHomeData")
type LabModelTooltipState = {
model: ModelCatalogEntry
@@ -84,9 +67,19 @@ export default function StatsLab() {
setStatsPageCacheHeaders(event?.response.headers)
const params = useParams()
const labParam = createMemo(() => params.lab ?? "")
const page = createAsync(() => getLabPageData(labParam()))
const lab = createMemo(() => page()?.lab)
const stats = createMemo(() => page()?.stats)
const catalog = createAsync(() => getModelCatalog())
const lab = createMemo(() => {
const data = catalog()
if (!data) return undefined
return findModelCatalogLab(data, labParam()) ?? null
})
const stats = createAsync(() => {
const entry = lab()
if (catalog() === undefined || entry === undefined) return Promise.resolve(undefined)
if (!entry) return Promise.resolve(null)
return getLabData(entry.id)
})
const homeStats = createAsync((): Promise<StatsHomeData | undefined> => getHomeData())
const githubStars = createAsync(() => getGitHubStars())
const [themePreference, setThemePreference] = createSignal<ThemePreference>("system")
const labName = createMemo(() => lab()?.name ?? formatCatalogLabName(labParam()))
@@ -144,18 +137,18 @@ export default function StatsLab() {
<Header githubStars={githubStars() ?? "150K"} links={labHeaderLinks()} brandHref={import.meta.env.BASE_URL} />
<div data-component="container">
<div data-component="content">
<Show when={page() !== undefined} fallback={<LabLoading />}>
<Show when={lab()} fallback={<LabNotFound lab={labParam()} labs={page()?.labs ?? []} />}>
<Show when={catalog() !== undefined} fallback={<LabLoading />}>
<Show when={lab()} fallback={<LabNotFound lab={labParam()} labs={catalog()?.labs ?? []} />}>
{(data) => (
<>
<LabHero lab={data()} labs={page()?.labs ?? []} />
<LabHero lab={data()} labs={catalog()?.labs ?? []} />
<LabOverview lab={data()} data={stats() ?? null} />
<LabUsageSection lab={data()} data={stats() ?? null} />
<LabModelsSection lab={data()} usage={stats()?.models ?? []} />
<LabRelatedSection
lab={data()}
labs={page()?.labs ?? []}
market={page()?.market ?? []}
labs={catalog()?.labs ?? []}
market={homeStats()?.market["2M"] ?? []}
/>
<ComparisonCardsSection
pairs={labComparisonPairs(data(), stats()?.models ?? [])}
@@ -189,7 +182,7 @@ function LabLoading() {
)
}
function LabNotFound(props: { lab: string; labs: RelatedCatalogLab[] }) {
function LabNotFound(props: { lab: string; labs: ModelCatalogLab[] }) {
const i18n = useI18n()
const labName = () => formatCatalogLabName(props.lab)
return (
@@ -201,7 +194,7 @@ function LabNotFound(props: { lab: string; labs: RelatedCatalogLab[] }) {
)
}
function LabHero(props: { lab: ModelCatalogLab; labs: RelatedCatalogLab[] }) {
function LabHero(props: { lab: ModelCatalogLab; labs: ModelCatalogLab[] }) {
return (
<section id="overview" data-section="lab-hero">
<LabHeroBreadcrumb label={props.lab.name} labs={props.labs} />
@@ -210,7 +203,7 @@ function LabHero(props: { lab: ModelCatalogLab; labs: RelatedCatalogLab[] }) {
)
}
function LabHeroBreadcrumb(props: { label: string; labs?: RelatedCatalogLab[] }) {
function LabHeroBreadcrumb(props: { label: string; labs?: ModelCatalogLab[] }) {
const language = useLanguage()
const labs = () => props.labs ?? []
const current = () => labs().find((lab) => lab.name === props.label)
@@ -675,7 +668,7 @@ function LabModelTooltip(props: { state: LabModelTooltipState }) {
)
}
function LabRelatedSection(props: { lab: ModelCatalogLab; labs: RelatedCatalogLab[]; market: MarketDay[] }) {
function LabRelatedSection(props: { lab: ModelCatalogLab; labs: ModelCatalogLab[]; market: MarketDay[] }) {
const related = createMemo(() => relatedLabs(props.lab, props.labs, props.market))
return (
<section id="related-labs" data-section="model-panel" data-variant="lab-related">
@@ -764,9 +757,9 @@ function labComparisonPairs(lab: ModelCatalogLab, usage: LabUsageModelEntry[]) {
)
}
type RelatedLabEntry = { lab: RelatedCatalogLab; share: number; tokens: number }
type RelatedLabEntry = { lab: ModelCatalogLab; share: number; tokens: number }
function relatedLabs(current: ModelCatalogLab, labs: RelatedCatalogLab[], market: MarketDay[]): RelatedLabEntry[] {
function relatedLabs(current: ModelCatalogLab, labs: ModelCatalogLab[], market: MarketDay[]): RelatedLabEntry[] {
const stats = relatedLabStats(labs, market)
return labs
.filter((lab) => lab.id !== current.id)
@@ -775,8 +768,8 @@ function relatedLabs(current: ModelCatalogLab, labs: RelatedCatalogLab[], market
.slice(0, 3)
}
function relatedLabStats(labs: RelatedCatalogLab[], market: MarketDay[]) {
const labByKey = new Map<string, RelatedCatalogLab>()
function relatedLabStats(labs: ModelCatalogLab[], market: MarketDay[]) {
const labByKey = new Map<string, ModelCatalogLab>()
labs.forEach((lab) => {
labByKey.set(lab.id, lab)
labByKey.set(catalogSlug(lab.name), lab)
@@ -801,7 +794,7 @@ function relatedLabStats(labs: RelatedCatalogLab[], market: MarketDay[]) {
)
}
function labRelatedDescription(lab: RelatedCatalogLab) {
function labRelatedDescription(lab: ModelCatalogLab) {
return lab.description ?? ""
}
@@ -17,7 +17,7 @@ export type ComparisonPair = {
description?: string
}
export function modelRefFromCatalog(entry: Pick<ModelCatalogEntry, "name" | "lab" | "slug">): ComparisonModelRef {
export function modelRefFromCatalog(entry: ModelCatalogEntry): ComparisonModelRef {
return {
name: entry.name,
lab: entry.lab,
-120
View File
@@ -1,120 +0,0 @@
import { geoEquirectangular, geoPath } from "d3-geo"
import { feature, mesh } from "topojson-client"
import countriesTopologySource from "world-atlas/countries-110m.json?raw"
import type { FeatureCollection, GeometryObject, GeoJsonProperties } from "geojson"
import type { GeometryCollection, Topology } from "topojson-specification"
export const geoMapWidth = 960
export const geoMapHeight = 430
type WorldCountryProperties = GeoJsonProperties & { name?: string }
type WorldTopology = Topology<{ countries: GeometryCollection<WorldCountryProperties> }>
const worldTopology = JSON.parse(countriesTopologySource) as WorldTopology
const worldCountryGeometries: GeometryCollection<WorldCountryProperties> = {
...worldTopology.objects.countries,
geometries: worldTopology.objects.countries.geometries.filter((country) => String(country.id ?? "") !== "010"),
}
const worldCountries = feature<WorldCountryProperties>(worldTopology, worldCountryGeometries) as FeatureCollection<
GeometryObject,
WorldCountryProperties
>
const worldProjection = geoEquirectangular().fitExtent(
[
[10, 12],
[geoMapWidth - 10, geoMapHeight - 12],
],
worldCountries,
)
const worldPath = geoPath(worldProjection)
export const worldCountryPaths = worldCountries.features.map((country) => ({
id: String(country.id ?? "").padStart(3, "0"),
path: worldPath(country) ?? "",
}))
export const worldBorderPath = worldPath(mesh(worldTopology, worldCountryGeometries, (a, b) => a !== b)) ?? ""
function geoCountryMarker(country: (typeof worldCountries.features)[number]) {
const bounds = worldPath.bounds(country)
const [x, y] = worldPath.centroid(country)
if (!Number.isFinite(x) || !Number.isFinite(y)) return undefined
if (bounds[1][0] - bounds[0][0] >= 3 && bounds[1][1] - bounds[0][1] >= 3) return undefined
return { x, y }
}
// The 110m topology omits small regions. Geographic centroids keep those countries interactive without shipping 50m paths.
const fallbackCountryMarkerCoordinates = [
["016", -170.7179, -14.3046],
["020", 1.5606, 42.542],
["028", -61.7945, 17.2762],
["048", 50.5425, 26.0417],
["052", -59.5602, 13.1811],
["060", -64.7558, 32.3131],
["086", 72.4453, -7.3312],
["092", -64.4704, 18.5276],
["132", -23.9576, 15.9551],
["136", -80.9129, 19.43],
["174", 43.6844, -11.879],
["184", -159.7871, -21.2195],
["212", -61.3576, 15.4394],
["234", -6.8808, 62.0527],
["239", -36.4863, -54.4641],
["248", 19.9528, 60.2153],
["258", -144.8045, -14.7283],
["296", -167.9217, 0.893],
["308", -61.6818, 12.1174],
["316", 144.767, 13.4406],
["334", 73.52, -53.0872],
["336", 12.4343, 41.9021],
["344", 114.1143, 22.3983],
["438", 9.5357, 47.1367],
["446", 113.509, 22.2231],
["462", 73.4573, 3.7316],
["470", 14.405, 35.9215],
["480", 57.5714, -20.2779],
["492", 7.4073, 43.7526],
["500", -62.1856, 16.7404],
["520", 166.9326, -0.5189],
["531", -68.9721, 12.1957],
["533", -69.9827, 12.521],
["534", -63.0572, 18.0509],
["570", -169.8704, -19.0489],
["574", 167.9497, -29.0516],
["580", 145.6193, 15.8288],
["583", 153.2966, 7.5361],
["584", 170.3313, 7.015],
["585", 134.4056, 7.286],
["612", -128.3167, -24.3649],
["652", -62.841, 17.8988],
["654", -9.7009, -12.3548],
["659", -62.6873, 17.2647],
["660", -63.066, 18.2243],
["662", -60.9696, 13.8946],
["663", -63.0599, 18.0888],
["666", -56.3037, 46.9187],
["670", -61.2008, 13.2251],
["674", 12.4594, 43.9415],
["678", 6.7235, 0.4434],
["690", 55.476, -4.6601],
["702", 103.817, 1.359],
["776", -174.7998, -20.4161],
["796", -71.9734, 21.8312],
["831", -2.5726, 49.4678],
["832", -2.1272, 49.2181],
["833", -4.5388, 54.224],
["850", -64.8028, 17.9555],
["876", -177.3469, -13.8898],
["882", -172.1649, -13.7536],
] as const
export const worldCountryMarkers = [
...worldCountries.features.flatMap((country) => {
const marker = geoCountryMarker(country)
return marker ? [{ id: String(country.id ?? "").padStart(3, "0"), marker }] : []
}),
...fallbackCountryMarkerCoordinates.flatMap(([id, longitude, latitude]) => {
const marker = worldProjection([longitude, latitude])
return marker ? [{ id, marker: { x: marker[0], y: marker[1] } }] : []
}),
]
+371 -76
View File
@@ -1,7 +1,10 @@
import { Link, Meta, Title } from "@solidjs/meta"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { geoEquirectangular, geoPath } from "d3-geo"
import { scaleSqrt } from "d3-scale"
import countryCodesSource from "i18n-iso-countries/codes.json?raw"
import { feature, mesh } from "topojson-client"
import countriesTopologySource from "world-atlas/countries-50m.json?raw"
import ibmPlexMonoRegularLatin1 from "@ibm/plex/IBM-Plex-Mono/fonts/split/woff2/IBMPlexMono-Regular-Latin1.woff2?url"
import ibmPlexMonoMediumLatin1 from "@ibm/plex/IBM-Plex-Mono/fonts/split/woff2/IBMPlexMono-Medium-Latin1.woff2?url"
import ibmPlexMonoSemiBoldLatin1 from "@ibm/plex/IBM-Plex-Mono/fonts/split/woff2/IBMPlexMono-SemiBold-Latin1.woff2?url"
@@ -12,6 +15,7 @@ import {
type CountryEntry,
type LeaderboardEntry,
type MarketDay,
type StatsHomeData,
type SessionCostEntry,
type TokenCostEntry,
type UsagePoint,
@@ -19,13 +23,14 @@ import {
import { createAsync, query } from "@solidjs/router"
import { createEffect, createMemo, createSignal, For, onCleanup, onMount, Show, type JSX } from "solid-js"
import { getRequestEvent } from "solid-js/web"
import type { FeatureCollection, GeometryObject, GeoJsonProperties } from "geojson"
import type { GeometryCollection, Topology } from "topojson-specification"
import { runStatsEffect } from "../stats-runtime"
import { LocaleLinks } from "../component/locale-links"
import { useI18n } from "../context/i18n"
import { useLanguage } from "../context/language"
import { localizedUrl } from "../lib/language"
import { findModelCatalogEntry, loadModelCatalog, type ModelCatalog } from "./model-catalog"
import { geoMapHeight, geoMapWidth, worldBorderPath, worldCountryMarkers, worldCountryPaths } from "./geo-map"
import { findModelCatalogEntry, getModelCatalog, type ModelCatalog } from "./model-catalog"
import { SectionHeading } from "./section-heading"
import { setStatsPageCacheHeaders } from "./stats-cache"
import { ComparisonCardsSection, uniqueComparisonPairs, type ComparisonModelRef } from "./compare-cards"
@@ -40,6 +45,9 @@ import {
type ThemePreference,
} from "./stats-shell"
const products = ["All Users", "Zen", "Go"] as const
const tokenProducts = ["Zen", "Go"] as const
const ranges = ["1D", "1W", "2W", "1M", "2M"] as const
const comparisonPairIndexes = [
[0, 1, "Top two by recent usage"],
[0, 2, "Leader vs challenger"],
@@ -61,40 +69,60 @@ const usageColors = [
"#ff6467",
]
const marketColors = ["#ed6aff", "#a684ff", "#7c86ff", "#51a2ff", "#00d3f2", "#00d5be", "#00bc7d", "#9ae600", "#ffb900"]
const geoMapWidth = 960
const geoMapHeight = 430
type UsageRange = "1D" | "1W" | "2W" | "1M" | "2M"
type UsageProduct = (typeof products)[number]
type TokenProduct = (typeof tokenProducts)[number]
type UsageRange = (typeof ranges)[number]
type IsoCountryCode = readonly [string, string, string]
type WorldCountryProperties = GeoJsonProperties & { name?: string }
type WorldTopology = Topology<{ countries: GeometryCollection<WorldCountryProperties> }>
type StatsHomePageData = {
updatedAt: string | null
usage: UsagePoint[]
users: UsagePoint[]
leaderboard: LeaderboardEntry[]
market: MarketDay[]
tokenCost: TokenCostEntry[]
cacheRatio: CacheRatioEntry[]
sessionCost: SessionCostEntry[]
country: CountryEntry[]
function productLabel(product: UsageProduct | TokenProduct, i18n: ReturnType<typeof useI18n>) {
if (product === "All Users") return i18n.t("product.allUsers")
if (product === "Zen") return i18n.t("product.zen")
return i18n.t("product.go")
}
function rangeLabel(range: UsageRange, i18n: ReturnType<typeof useI18n>) {
if (range === "1D") return i18n.t("range.1D")
if (range === "1W") return i18n.t("range.1W")
if (range === "2W") return i18n.t("range.2W")
if (range === "1M") return i18n.t("range.1M")
return i18n.t("range.2M")
}
const countryNumericIds = new Map(
(JSON.parse(countryCodesSource) as IsoCountryCode[]).map((country) => [country[0], country[2]] as const),
)
const worldTopology = JSON.parse(countriesTopologySource) as WorldTopology
const worldCountryGeometries: GeometryCollection<WorldCountryProperties> = {
...worldTopology.objects.countries,
geometries: worldTopology.objects.countries.geometries.filter((country) => String(country.id ?? "") !== "010"),
}
const worldCountries = feature<WorldCountryProperties>(worldTopology, worldCountryGeometries) as FeatureCollection<
GeometryObject,
WorldCountryProperties
>
const worldProjection = geoEquirectangular().fitExtent(
[
[10, 12],
[geoMapWidth - 10, geoMapHeight - 12],
],
worldCountries,
)
const worldPath = geoPath(worldProjection)
const worldCountryPaths = worldCountries.features.map((country) => ({
id: String(country.id ?? "").padStart(3, "0"),
path: worldPath(country) ?? "",
marker: geoCountryMarker(country),
}))
const worldBorderPath = worldPath(mesh(worldTopology, worldCountryGeometries, (a, b) => a !== b)) ?? ""
const getData = query(async () => {
"use server"
const [stats, catalog] = await Promise.all([runStatsEffect(getStatsHomeData()), loadModelCatalog()])
return {
updatedAt: stats.updatedAt,
usage: stats.usage.Go["2M"],
users: stats.users.Go["2M"],
leaderboard: stats.leaderboard.Go["2M"],
market: stats.market["2M"],
tokenCost: priceTokenCostFromCatalog(stats.tokenCost.Go, catalog),
cacheRatio: stats.cacheRatio.Go,
sessionCost: stats.sessionCost.Go,
country: stats.country["2M"],
} satisfies StatsHomePageData
return runStatsEffect(getStatsHomeData())
}, "getStatsHomeData")
export default function StatsHome() {
@@ -105,6 +133,7 @@ export default function StatsHome() {
const statsHomeUrl = localizedUrl(language.locale(), "/data/")
const statsUnfurlUrl = new URL(statsUnfurlPath, localizedUrl("en", "/data/")).toString()
const data = createAsync(() => getData())
const catalog = createAsync(() => getModelCatalog())
const githubStars = createAsync(() => getGitHubStars())
const [themePreference, setThemePreference] = createSignal<ThemePreference>("system")
const updateThemePreference = (preference: ThemePreference) => {
@@ -156,12 +185,12 @@ export default function StatsHome() {
<TopModelsSection data={stats().usage} leaderboard={stats().leaderboard} />
<UniqueUsersSection data={stats().users} />
<SessionCostSection data={stats().sessionCost} />
<TokenCostSection data={stats().tokenCost} />
<TokenCostSection data={stats().tokenCost} catalog={catalog() ?? null} />
<CacheRatioSection data={stats().cacheRatio} />
<MarketShareSection data={stats().market} />
<GeoBreakdownSection data={stats().country} />
<ComparisonCardsSection
pairs={homeComparisonPairs(stats().leaderboard)}
pairs={homeComparisonPairs(stats().leaderboard["All Users"]["2M"])}
title="Model Comparisons"
description="Popular model pairs from the leaderboard."
variant="featured"
@@ -372,9 +401,32 @@ function formatUpdatedAtLabel(value: { date: string; time: string }) {
return `${value.date}, ${value.time}`
}
function TopModelsSection(props: { data: UsagePoint[]; leaderboard: LeaderboardEntry[] }) {
function TopModelsSection(props: { data: StatsHomeData["usage"]; leaderboard: StatsHomeData["leaderboard"] }) {
const i18n = useI18n()
const [product, setProduct] = createSignal<UsageProduct>("Go")
const [range, setRange] = createSignal<UsageRange>("2M")
const [sheet, setSheet] = createSignal<"product" | "range">()
const [activeModel, setActiveModel] = createSignal<string>()
const data = createMemo(() => props.data[product()][range()])
const leaderboard = createMemo(() => props.leaderboard[product()][range()])
createEffect(() => {
if (!sheet()) return
if (typeof document === "undefined") return
const htmlOverflow = document.documentElement.style.overflow
const bodyOverflow = document.body.style.overflow
document.documentElement.style.overflow = "hidden"
document.body.style.overflow = "hidden"
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") setSheet(undefined)
}
document.addEventListener("keydown", onKeyDown)
onCleanup(() => {
document.documentElement.style.overflow = htmlOverflow
document.body.style.overflow = bodyOverflow
document.removeEventListener("keydown", onKeyDown)
})
})
return (
<section id="top-models" data-section="top-models">
@@ -386,28 +438,201 @@ function TopModelsSection(props: { data: UsagePoint[]; leaderboard: LeaderboardE
description={i18n.t("home.topModelsDescription")}
/>
<Show
when={props.data.some((item) => usageTotal(item) > 0)}
when={data().some((item) => usageTotal(item) > 0)}
fallback={<EmptyState title={i18n.t("home.noUsageTitle")} description={i18n.t("home.noUsageDescription")} />}
>
<TopModelsChart
data={props.data}
range="2M"
data={data()}
range={range()}
activeModel={activeModel()}
onActiveModelChange={setActiveModel}
/>
</Show>
<Show
when={props.leaderboard.length > 0}
when={leaderboard().length > 0}
fallback={
<EmptyState title={i18n.t("home.noLeaderboardTitle")} description={i18n.t("home.noLeaderboardDescription")} />
}
>
<Leaderboard data={props.leaderboard} activeModel={activeModel()} onActiveModelChange={setActiveModel} />
<Leaderboard data={leaderboard()} activeModel={activeModel()} onActiveModelChange={setActiveModel} />
</Show>
<div data-slot="chart-footer" hidden>
<StatsFilters product={product()} range={range()} onProductSelect={setProduct} onRangeSelect={setRange} />
<div data-slot="top-models-mobile-controls">
<MobileFilterButton
label={i18n.t("home.productFilter")}
value={productLabel(product(), i18n)}
expanded={sheet() === "product"}
onClick={() => setSheet(sheet() === "product" ? undefined : "product")}
/>
<MobileFilterButton
label={i18n.t("home.dateRange")}
value={rangeLabel(range(), i18n)}
expanded={sheet() === "range"}
onClick={() => setSheet(sheet() === "range" ? undefined : "range")}
/>
</div>
</div>
<Show when={sheet()}>
{(kind) => (
<MobileFilterSheet
kind={kind()}
product={product()}
range={range()}
onProductSelect={(value) => {
setProduct(value)
setSheet(undefined)
}}
onRangeSelect={(value) => {
setRange(value)
setSheet(undefined)
}}
onClose={() => setSheet(undefined)}
/>
)}
</Show>
</section>
)
}
function MobileFilterButton(props: { label: string; value: string; expanded: boolean; onClick: () => void }) {
return (
<button
data-slot="mobile-filter-button"
type="button"
aria-label={props.label}
aria-expanded={props.expanded ? "true" : "false"}
onClick={props.onClick}
>
<span>{props.value}</span>
<ChevronDown />
</button>
)
}
function MobileFilterSheet(props: {
kind: "product" | "range"
product: UsageProduct
range: UsageRange
onProductSelect: (product: UsageProduct) => void
onRangeSelect: (range: UsageRange) => void
onClose: () => void
}) {
const i18n = useI18n()
return (
<div data-component="mobile-filter-sheet" role="presentation" onClick={props.onClose}>
<div
data-slot="filter-sheet-panel"
role="radiogroup"
aria-label={props.kind === "product" ? i18n.t("home.productFilter") : i18n.t("home.dateRange")}
>
<Show
when={props.kind === "product"}
fallback={
<For each={ranges}>
{(item) => (
<button
type="button"
role="radio"
aria-checked={props.range === item}
data-active={props.range === item ? "true" : undefined}
onClick={(event) => {
event.stopPropagation()
props.onRangeSelect(item)
}}
>
{rangeLabel(item, i18n)}
</button>
)}
</For>
}
>
<For each={products}>
{(item) => (
<button
type="button"
role="radio"
aria-checked={props.product === item}
data-active={props.product === item ? "true" : undefined}
onClick={(event) => {
event.stopPropagation()
props.onProductSelect(item)
}}
>
{productLabel(item, i18n)}
</button>
)}
</For>
</Show>
</div>
</div>
)
}
function ChevronDown() {
return (
<svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true" fill="none">
<path d="M5 7L8 10L11 7" stroke="currentColor" />
</svg>
)
}
function StatsFilters(props: {
product: UsageProduct
range: UsageRange
onProductSelect: (product: UsageProduct) => void
onRangeSelect: (range: UsageRange) => void
}) {
const i18n = useI18n()
return (
<>
<FilterPills
items={products}
selected={props.product}
label={i18n.t("home.productFilter")}
variant="product"
formatLabel={(item) => productLabel(item, i18n)}
onSelect={props.onProductSelect}
/>
<FilterPills
items={ranges}
selected={props.range}
label={i18n.t("home.dateRange")}
variant="range"
formatLabel={(item) => rangeLabel(item, i18n)}
onSelect={props.onRangeSelect}
/>
</>
)
}
function FilterPills<T extends string>(props: {
items: readonly T[]
selected: T
label: string
variant: "product" | "range"
formatLabel?: (item: T) => string
onSelect: (item: T) => void
}) {
return (
<div data-component="usage-filter" data-variant={props.variant} role="radiogroup" aria-label={props.label}>
<For each={props.items}>
{(item) => (
<button
type="button"
role="radio"
aria-checked={props.selected === item}
data-active={props.selected === item ? "true" : undefined}
onClick={() => props.onSelect(item)}
>
{props.formatLabel ? props.formatLabel(item) : item}
</button>
)}
</For>
</div>
)
}
function TopModelsChart(props: {
data: UsagePoint[]
range: UsageRange
@@ -598,9 +823,10 @@ function TopModelsChart(props: {
)
}
function UniqueUsersSection(props: { data: UsagePoint[] }) {
function UniqueUsersSection(props: { data: StatsHomeData["users"] }) {
const i18n = useI18n()
const [activeModel, setActiveModel] = createSignal<string>()
const data = createMemo(() => props.data.Go["2M"])
return (
<section id="unique-users" data-section="unique-users">
@@ -611,13 +837,13 @@ function UniqueUsersSection(props: { data: UsagePoint[] }) {
description={i18n.t("home.uniqueUsersDescription")}
/>
<Show
when={props.data.some((item) => usageTotal(item) > 0)}
when={data().some((item) => usageTotal(item) > 0)}
fallback={
<EmptyState title={i18n.t("home.noUserDataTitle")} description={i18n.t("home.noUserDataDescription")} />
}
>
<TopModelsChart
data={props.data}
data={data()}
range="2M"
metric="users"
ariaLabel={i18n.t("home.uniqueUsersChart")}
@@ -858,14 +1084,16 @@ function formatChange(value: number | null, i18n: ReturnType<typeof useI18n>) {
return `${value}%`
}
function MarketShareSection(props: { data: MarketDay[] }) {
function MarketShareSection(props: { data: StatsHomeData["market"] }) {
const i18n = useI18n()
const [range, setRange] = createSignal<UsageRange>("2M")
const [activeIndex, setActiveIndex] = createSignal(2)
const [activeAuthor, setActiveAuthor] = createSignal<string>()
const [inspecting, setInspecting] = createSignal(false)
const authorOrder = createMemo(() => getMarketAuthorOrder(props.data))
const selectedIndex = createMemo(() => Math.min(activeIndex(), Math.max(props.data.length - 1, 0)))
const activeDay = createMemo(() => props.data[selectedIndex()])
const data = createMemo(() => props.data[range()])
const authorOrder = createMemo(() => getMarketAuthorOrder(data()))
const selectedIndex = createMemo(() => Math.min(activeIndex(), Math.max(data().length - 1, 0)))
const activeDay = createMemo(() => data()[selectedIndex()])
return (
<section
@@ -890,8 +1118,8 @@ function MarketShareSection(props: { data: MarketDay[] }) {
{(day) => (
<>
<MarketShare
data={props.data}
range="2M"
data={data()}
range={range()}
authorOrder={authorOrder()}
activeIndex={selectedIndex()}
activeAuthor={activeAuthor()}
@@ -923,9 +1151,22 @@ function MarketShareSection(props: { data: MarketDay[] }) {
<strong>
{inspecting()
? formatMarketDate(activeDay(), i18n.t("home.noData"))
: formatMarketRange(props.data, i18n.t("home.noData"))}
: formatMarketRange(data(), i18n.t("home.noData"))}
</strong>
</p>
<div hidden>
<FilterPills
items={ranges}
selected={range()}
label={i18n.t("home.dateRange")}
variant="range"
onSelect={(item) => {
setRange(item)
setActiveAuthor(undefined)
setInspecting(false)
}}
/>
</div>
</div>
</section>
)
@@ -1092,22 +1333,23 @@ function MarketShareList(props: {
)
}
function GeoBreakdownSection(props: { data: CountryEntry[] }) {
function GeoBreakdownSection(props: { data: StatsHomeData["country"] }) {
const i18n = useI18n()
const language = useLanguage()
const [activeCountry, setActiveCountry] = createSignal<string>()
const data = createMemo(() => props.data["2M"])
const countryById = createMemo(
() =>
new Map(
props.data.flatMap((country) => {
data().flatMap((country) => {
const id = countryNumericId(country.country)
return id ? [[id, country] as const] : []
}),
),
)
const maxTokens = createMemo(() => Math.max(0, ...props.data.map((country) => country.tokens)) || 1)
const topCountries = createMemo(() => props.data.slice(0, 15))
const active = createMemo(() => props.data.find((country) => country.country === activeCountry()) ?? props.data[0])
const maxTokens = createMemo(() => Math.max(0, ...data().map((country) => country.tokens)) || 1)
const topCountries = createMemo(() => data().slice(0, 15))
const active = createMemo(() => data().find((country) => country.country === activeCountry()) ?? data()[0])
return (
<section
@@ -1121,7 +1363,7 @@ function GeoBreakdownSection(props: { data: CountryEntry[] }) {
<SectionBridge label={i18n.t("nav.marketShare").toUpperCase()} href="#market-share" />
<SectionTitle id="geo-breakdown" title={i18n.t("home.geoTitle")} description={i18n.t("home.geoDescription")} />
<Show
when={props.data.length > 0}
when={data().length > 0}
fallback={<EmptyState title={i18n.t("home.noGeoTitle")} description={i18n.t("home.noGeoDescription")} />}
>
<div data-component="geo-breakdown">
@@ -1211,30 +1453,31 @@ function GeoWorldMap(props: {
</For>
</g>
<g data-slot="geo-country-markers">
<For each={worldCountryMarkers}>
<For each={worldCountryPaths}>
{(country) => {
const entry = () => props.countryById.get(country.id)
return (
<Show when={entry()}>
<circle
cx={country.marker.x}
cy={country.marker.y}
data-country-id={country.id}
r={entry()?.country === props.activeCountry ? 3.4 : 2.4}
data-active={entry()?.country === props.activeCountry ? "true" : undefined}
style={{ "--geo-country-opacity": String(countryOpacity(entry())) } as JSX.CSSProperties}
aria-hidden="true"
onPointerEnter={() => {
const item = entry()
if (!item) return
props.onActiveCountryChange(item.country)
}}
onClick={() => {
const item = entry()
if (!item) return
props.onActiveCountryChange(item.country)
}}
/>
<Show when={country.marker && entry() ? country.marker : undefined}>
{(marker) => (
<circle
cx={marker().x}
cy={marker().y}
r={entry()?.country === props.activeCountry ? 3.4 : 2.4}
data-active={entry()?.country === props.activeCountry ? "true" : undefined}
style={{ "--geo-country-opacity": String(countryOpacity(entry())) } as JSX.CSSProperties}
aria-hidden="true"
onPointerEnter={() => {
const item = entry()
if (!item) return
props.onActiveCountryChange(item.country)
}}
onClick={() => {
const item = entry()
if (!item) return
props.onActiveCountryChange(item.country)
}}
/>
)}
</Show>
)
}}
@@ -1288,6 +1531,14 @@ function countryNumericId(country: string) {
return countryNumericIds.get(country.toUpperCase())?.padStart(3, "0")
}
function geoCountryMarker(country: (typeof worldCountries.features)[number]) {
const bounds = worldPath.bounds(country)
const [x, y] = worldPath.centroid(country)
if (!Number.isFinite(x) || !Number.isFinite(y)) return undefined
if (bounds[1][0] - bounds[0][0] >= 3 && bounds[1][1] - bounds[0][1] >= 3) return undefined
return { x, y }
}
function formatCountryName(country: string, locale: string, unknown: string) {
const code = country.toUpperCase()
if (code === "ZZ") return unknown
@@ -1385,10 +1636,12 @@ function marketDateParts(label: string) {
return { start: start ?? label, end: end ?? start ?? label }
}
function TokenCostSection(props: { data: TokenCostEntry[] }) {
function TokenCostSection(props: { data: StatsHomeData["tokenCost"]; catalog: ModelCatalog | null }) {
const i18n = useI18n()
const [product, setProduct] = createSignal<TokenProduct>("Go")
const [activeIndex, setActiveIndex] = createSignal(2)
const visible = createMemo(() => props.data.slice(0, 13))
const data = createMemo(() => priceTokenCostFromCatalog(props.data[product()], props.catalog))
const visible = createMemo(() => data().slice(0, 13))
const selectedIndex = createMemo(() => Math.min(activeIndex(), Math.max(visible().length - 1, 0)))
return (
@@ -1407,6 +1660,17 @@ function TokenCostSection(props: { data: TokenCostEntry[] }) {
>
<TokenCostChart data={visible()} activeIndex={selectedIndex()} onActiveIndexChange={setActiveIndex} />
</Show>
<div data-slot="token-footer" hidden>
<FilterPills
items={tokenProducts}
selected={product()}
label={i18n.t("home.productFilter")}
variant="product"
formatLabel={(item) => productLabel(item, i18n)}
onSelect={setProduct}
/>
<LiveIndicator />
</div>
</section>
)
}
@@ -1459,10 +1723,12 @@ function TokenCostChart(props: {
)
}
function CacheRatioSection(props: { data: CacheRatioEntry[] }) {
function CacheRatioSection(props: { data: StatsHomeData["cacheRatio"] }) {
const i18n = useI18n()
const [product, setProduct] = createSignal<TokenProduct>("Go")
const [activeIndex, setActiveIndex] = createSignal(2)
const visible = createMemo(() => props.data.slice(0, 16))
const data = createMemo(() => props.data[product()])
const visible = createMemo(() => data().slice(0, 16))
const selectedIndex = createMemo(() => Math.min(activeIndex(), Math.max(visible().length - 1, 0)))
return (
@@ -1479,6 +1745,17 @@ function CacheRatioSection(props: { data: CacheRatioEntry[] }) {
>
<CacheRatioChart data={visible()} activeIndex={selectedIndex()} onActiveIndexChange={setActiveIndex} />
</Show>
<div data-slot="token-footer" hidden>
<FilterPills
items={tokenProducts}
selected={product()}
label={i18n.t("home.productFilter")}
variant="product"
formatLabel={(item) => productLabel(item, i18n)}
onSelect={setProduct}
/>
<LiveIndicator />
</div>
</section>
)
}
@@ -1576,10 +1853,12 @@ function MetricBar(props: { value: number; max: number; active: boolean }) {
)
}
function SessionCostSection(props: { data: SessionCostEntry[] }) {
function SessionCostSection(props: { data: StatsHomeData["sessionCost"] }) {
const i18n = useI18n()
const [product, setProduct] = createSignal<TokenProduct>("Go")
const [activeIndex, setActiveIndex] = createSignal(2)
const visible = createMemo(() => props.data.slice(0, 16))
const data = createMemo(() => props.data[product()])
const visible = createMemo(() => data().slice(0, 16))
const selectedIndex = createMemo(() => Math.min(activeIndex(), Math.max(visible().length - 1, 0)))
return (
@@ -1598,6 +1877,17 @@ function SessionCostSection(props: { data: SessionCostEntry[] }) {
>
<SessionCostChart data={visible()} activeIndex={selectedIndex()} onActiveIndexChange={setActiveIndex} />
</Show>
<div data-slot="token-footer" hidden>
<FilterPills
items={tokenProducts}
selected={product()}
label={i18n.t("home.productFilter")}
variant="product"
formatLabel={(item) => productLabel(item, i18n)}
onSelect={setProduct}
/>
<LiveIndicator />
</div>
</section>
)
}
@@ -1659,6 +1949,11 @@ function SessionCostChart(props: {
)
}
function LiveIndicator() {
const i18n = useI18n()
return <span data-component="live-filter">{i18n.t("chart.live")}</span>
}
function formatTokenCount(value: number) {
if (value >= 1_000_000) return `${Number((value / 1_000_000).toFixed(1))}M`
return `${Math.round(value / 1_000)}K`
+34 -9
View File
@@ -1,10 +1,13 @@
import { RGBA, TextAttributes } from "@opentui/core"
import { For, type JSX } from "solid-js"
import { useTerminalDimensions } from "@opentui/solid"
import { tint, useTheme } from "../context/theme"
import { logo } from "../logo"
import { go, logo } from "../logo"
export function Logo() {
const { theme } = useTheme()
const dimensions = useTerminalDimensions()
const variant = () => logoVariant(dimensions().width, dimensions().height)
const renderLine = (line: string, fg: RGBA, bold: boolean): JSX.Element[] => {
const shadow = tint(theme.background, fg, 0.25)
@@ -48,14 +51,36 @@ export function Logo() {
return (
<box>
<For each={logo.left}>
{(line, index) => (
<box flexDirection="row" gap={1}>
<box flexDirection="row">{renderLine(line, theme.textMuted, false)}</box>
<box flexDirection="row">{renderLine(logo.right[index()], theme.text, true)}</box>
</box>
)}
</For>
{variant() === "hidden" ? null : variant() === "compact" ? (
<For each={go.right.slice(1)}>
{(line) => <box flexDirection="row">{renderLine(line, theme.text, true)}</box>}
</For>
) : variant() === "stacked" ? (
<>
<For each={logo.left.slice(1)}>
{(line) => <box flexDirection="row">{renderLine(line, theme.textMuted, false)}</box>}
</For>
<For each={logo.right}>
{(line) => <box flexDirection="row">{renderLine(line, theme.text, true)}</box>}
</For>
</>
) : (
<For each={logo.left}>
{(line, index) => (
<box flexDirection="row" gap={1}>
<box flexDirection="row">{renderLine(line, theme.textMuted, false)}</box>
<box flexDirection="row">{renderLine(logo.right[index()], theme.text, true)}</box>
</box>
)}
</For>
)}
</box>
)
}
export function logoVariant(width: number, height: number) {
if (height < 12) return "hidden"
if (width < 22) return "compact"
if (width < 44) return "stacked"
return "full"
}
+11
View File
@@ -0,0 +1,11 @@
import { expect, test } from "bun:test"
import { logoVariant } from "../src/component/logo"
test("adapts the logo to constrained terminals", () => {
expect(logoVariant(19, 24)).toBe("compact")
expect(logoVariant(21, 24)).toBe("compact")
expect(logoVariant(22, 24)).toBe("stacked")
expect(logoVariant(43, 24)).toBe("stacked")
expect(logoVariant(44, 24)).toBe("full")
expect(logoVariant(80, 11)).toBe("hidden")
})
-4
View File
@@ -112,7 +112,6 @@ OpenCode Zen هي بوابة AI تتيح لك الوصول إلى هذه الن
| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Ling-3.0-flash Free | ling-3.0-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| LongCat-2.0 Free | longcat-2.0-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -142,7 +141,6 @@ https://opencode.ai/zen/v1/models
| MiMo-V2.5 Free | Free | Free | Free | - |
| Laguna S 2.1 Free | Free | Free | Free | - |
| Ling-3.0-flash Free | Free | Free | Free | - |
| LongCat-2.0 Free | Free | Free | Free | - |
| North Mini Code Free | Free | Free | Free | - |
| Nemotron 3 Ultra Free | Free | Free | Free | - |
| MiniMax M3 | $0.30 | $1.20 | $0.06 | - |
@@ -219,7 +217,6 @@ https://opencode.ai/zen/v1/models
- MiMo-V2.5 Free متاح على OpenCode لفترة محدودة. يستخدم الفريق هذه الفترة لجمع الملاحظات وتحسين النموذج.
- Laguna S 2.1 Free متاح على OpenCode لفترة محدودة. يستخدم الفريق هذه الفترة لجمع الملاحظات وتحسين النموذج.
- Ling-3.0-flash Free متاح على OpenCode لفترة محدودة. يستخدم الفريق هذه الفترة لجمع الملاحظات وتحسين النموذج.
- LongCat-2.0 Free متاح على OpenCode لفترة محدودة. يستخدم الفريق هذه الفترة لجمع الملاحظات وتحسين النموذج.
- North Mini Code Free متاح على OpenCode لفترة محدودة. يستخدم الفريق هذه الفترة لجمع الملاحظات وتحسين النموذج.
- Nemotron 3 Ultra Free متاح على OpenCode لفترة محدودة. يستخدم الفريق هذه الفترة لجمع الملاحظات وتحسين النموذج.
- Big Pickle نموذج خفي ومتاح مجانا على OpenCode لفترة محدودة. يستخدم الفريق هذه الفترة لجمع الملاحظات وتحسين النموذج.
@@ -278,7 +275,6 @@ https://opencode.ai/zen/v1/models
- MiMo-V2.5 Free: خلال فترته المجانية، قد تُستخدم البيانات المجمعة لتحسين النموذج.
- Laguna S 2.1 Free: خلال فترته المجانية، قد تُستخدم البيانات المجمعة لتحسين النموذج.
- Ling-3.0-flash Free: خلال فترته المجانية، قد تُستخدم البيانات المجمعة لتحسين النموذج.
- LongCat-2.0 Free: خلال فترته المجانية، قد تُستخدم البيانات المجمعة لتحسين النموذج.
- North Mini Code Free: خلال فترته المجانية، قد يُحتفَظ بالبيانات المُجمَّعة وتُستخدم لتحسين النموذج. لا تُرسل بيانات شخصية أو سرية. راجع [شروط الاستخدام](https://cohere.com/terms-of-use) و[سياسة الخصوصية](https://cohere.com/privacy).
- Nemotron 3 Ultra Free (نقاط نهاية NVIDIA المجانية): للاستخدام التجريبي فقط — لا ترسل بيانات شخصية أو سرية. يُسجَّل استخدامك لأغراض أمنية ولتحسين منتجات وخدمات NVIDIA. بيانات الجلسة المُسجَّلة لأغراض التحسين غير مرتبطة بهويتك أو بأي مُعرِّف دائم. لمزيد من المعلومات حول ممارسات معالجة البيانات لدينا، راجع [سياسة الخصوصية](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). بتفاعلك مع نقطة النهاية هذه، فإنك توافق على جمعنا لهذه المعلومات وتسجيلها واستخدامها وعلى [شروط خدمة النسخة التجريبية من واجهة NVIDIA API](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf).
- OpenAI APIs: يتم الاحتفاظ بالطلبات لمدة 30 يوما وفقا لـ [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data).
-4
View File
@@ -117,7 +117,6 @@ Našim modelima možete pristupiti i preko sljedećih API endpointa.
| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Ling-3.0-flash Free | ling-3.0-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| LongCat-2.0 Free | longcat-2.0-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -149,7 +148,6 @@ Podržavamo pay-as-you-go model. Ispod su cijene **po 1M tokena**.
| MiMo-V2.5 Free | Free | Free | Free | - |
| Laguna S 2.1 Free | Free | Free | Free | - |
| Ling-3.0-flash Free | Free | Free | Free | - |
| LongCat-2.0 Free | Free | Free | Free | - |
| North Mini Code Free | Free | Free | Free | - |
| Nemotron 3 Ultra Free | Free | Free | Free | - |
| MiniMax M3 | $0.30 | $1.20 | $0.06 | - |
@@ -226,7 +224,6 @@ Besplatni modeli:
- MiMo-V2.5 Free je dostupan na OpenCode ograničeno vrijeme. Tim koristi ovo vrijeme da prikupi povratne informacije i poboljša model.
- Laguna S 2.1 Free je dostupan na OpenCode ograničeno vrijeme. Tim koristi ovo vrijeme da prikupi povratne informacije i poboljša model.
- Ling-3.0-flash Free je dostupan na OpenCode ograničeno vrijeme. Tim koristi ovo vrijeme da prikupi povratne informacije i poboljša model.
- LongCat-2.0 Free je dostupan na OpenCode ograničeno vrijeme. Tim koristi ovo vrijeme da prikupi povratne informacije i poboljša model.
- North Mini Code Free je dostupan na OpenCode ograničeno vrijeme. Tim koristi ovo vrijeme da prikupi povratne informacije i poboljša model.
- Nemotron 3 Ultra Free je dostupan na OpenCode ograničeno vrijeme. Tim koristi ovo vrijeme da prikupi povratne informacije i poboljša model.
- Big Pickle je stealth model koji je besplatan na OpenCode ograničeno vrijeme. Tim koristi ovo vrijeme da prikupi povratne informacije i poboljša model.
@@ -290,7 +287,6 @@ i ne koriste vaše podatke za treniranje modela, uz sljedeće izuzetke:
- MiMo-V2.5 Free: Tokom besplatnog perioda, prikupljeni podaci mogu se koristiti za poboljšanje modela.
- Laguna S 2.1 Free: Tokom besplatnog perioda, prikupljeni podaci mogu se koristiti za poboljšanje modela.
- Ling-3.0-flash Free: Tokom besplatnog perioda, prikupljeni podaci mogu se koristiti za poboljšanje modela.
- LongCat-2.0 Free: Tokom besplatnog perioda, prikupljeni podaci mogu se koristiti za poboljšanje modela.
- North Mini Code Free: Tokom besplatnog perioda, prikupljeni podaci mogu biti zadržani i korišteni za poboljšanje modela. Nemojte slati lične ili povjerljive podatke. Pogledajte naše [Uslove korištenja](https://cohere.com/terms-of-use) i [Politiku privatnosti](https://cohere.com/privacy).
- Nemotron 3 Ultra Free (besplatni NVIDIA endpointi): Samo za probnu upotrebu — nemojte slati lične ili povjerljive podatke. Vaše korištenje se bilježi radi sigurnosti i poboljšanja NVIDIA proizvoda i usluga. Zabilježeni podaci sesije koji se koriste u svrhu poboljšanja nisu povezani s vašim identitetom niti bilo kojim trajnim identifikatorom. Za više informacija o našim praksama obrade podataka pogledajte našu [Politiku privatnosti](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). Interakcijom s ovim endpointom pristajete na naše prikupljanje, bilježenje i korištenje takvih informacija te na [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf).
- OpenAI APIs: Requests are retained for 30 days in accordance with [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data).
-4
View File
@@ -117,7 +117,6 @@ Du kan også få adgang til vores modeller gennem følgende API-endpoints.
| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Ling-3.0-flash Free | ling-3.0-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| LongCat-2.0 Free | longcat-2.0-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -149,7 +148,6 @@ Vi understøtter en pay-as-you-go-model. Nedenfor er priserne **pr. 1M tokens**.
| MiMo-V2.5 Free | Free | Free | Free | - |
| Laguna S 2.1 Free | Free | Free | Free | - |
| Ling-3.0-flash Free | Free | Free | Free | - |
| LongCat-2.0 Free | Free | Free | Free | - |
| North Mini Code Free | Free | Free | Free | - |
| Nemotron 3 Ultra Free | Free | Free | Free | - |
| MiniMax M3 | $0.30 | $1.20 | $0.06 | - |
@@ -226,7 +224,6 @@ De gratis modeller:
- MiMo-V2.5 Free er tilgængelig på OpenCode i en begrænset periode. Teamet bruger denne tid til at indsamle feedback og forbedre modellen.
- Laguna S 2.1 Free er tilgængelig på OpenCode i en begrænset periode. Teamet bruger denne tid til at indsamle feedback og forbedre modellen.
- Ling-3.0-flash Free er tilgængelig på OpenCode i en begrænset periode. Teamet bruger denne tid til at indsamle feedback og forbedre modellen.
- LongCat-2.0 Free er tilgængelig på OpenCode i en begrænset periode. Teamet bruger denne tid til at indsamle feedback og forbedre modellen.
- North Mini Code Free er tilgængelig på OpenCode i en begrænset periode. Teamet bruger denne tid til at indsamle feedback og forbedre modellen.
- Nemotron 3 Ultra Free er tilgængelig på OpenCode i en begrænset periode. Teamet bruger denne tid til at indsamle feedback og forbedre modellen.
- Big Pickle er en stealth-model, som er gratis på OpenCode i en begrænset periode. Teamet bruger denne tid til at indsamle feedback og forbedre modellen.
@@ -288,7 +285,6 @@ Alle vores modeller hostes i US. Vores udbydere følger en nul-opbevaringspoliti
- MiMo-V2.5 Free: I den gratis periode kan indsamlede data blive brugt til at forbedre modellen.
- Laguna S 2.1 Free: I den gratis periode kan indsamlede data blive brugt til at forbedre modellen.
- Ling-3.0-flash Free: I den gratis periode kan indsamlede data blive brugt til at forbedre modellen.
- LongCat-2.0 Free: I den gratis periode kan indsamlede data blive brugt til at forbedre modellen.
- North Mini Code Free: I gratisperioden kan indsamlede data blive opbevaret og brugt til at forbedre modellen. Indsend ikke personlige eller fortrolige oplysninger. Se vores [Brugsvilkår](https://cohere.com/terms-of-use) og [Privatlivspolitik](https://cohere.com/privacy).
- Nemotron 3 Ultra Free (gratis NVIDIA-endpoints): Kun til prøvebrug — indsend ikke personlige eller fortrolige data. Din brug logges af sikkerhedshensyn og for at forbedre NVIDIAs produkter og tjenester. De loggede sessionsdata, der bruges til forbedringsformål, er ikke knyttet til din identitet eller nogen vedvarende identifikator. For mere information om vores databehandlingspraksis, se vores [privatlivspolitik](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). Ved at interagere med dette endpoint giver du samtykke til vores indsamling, registrering og brug af sådanne oplysninger samt [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf).
- OpenAI APIs: Anmodninger opbevares i 30 dage i overensstemmelse med [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data).
-4
View File
@@ -108,7 +108,6 @@ Du kannst auch über die folgenden API-Endpunkte auf unsere Modelle zugreifen.
| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Ling-3.0-flash Free | ling-3.0-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| LongCat-2.0 Free | longcat-2.0-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -138,7 +137,6 @@ Wir unterstützen ein Pay-as-you-go-Modell. Unten findest du die Preise **pro 1M
| MiMo-V2.5 Free | Free | Free | Free | - |
| Laguna S 2.1 Free | Free | Free | Free | - |
| Ling-3.0-flash Free | Free | Free | Free | - |
| LongCat-2.0 Free | Free | Free | Free | - |
| North Mini Code Free | Free | Free | Free | - |
| Nemotron 3 Ultra Free | Free | Free | Free | - |
| MiniMax M3 | $0.30 | $1.20 | $0.06 | - |
@@ -215,7 +213,6 @@ Die kostenlosen Modelle:
- MiMo-V2.5 Free ist für begrenzte Zeit auf OpenCode verfügbar. Das Team nutzt diese Zeit, um Feedback zu sammeln und das Modell zu verbessern.
- Laguna S 2.1 Free ist für begrenzte Zeit auf OpenCode verfügbar. Das Team nutzt diese Zeit, um Feedback zu sammeln und das Modell zu verbessern.
- Ling-3.0-flash Free ist für begrenzte Zeit auf OpenCode verfügbar. Das Team nutzt diese Zeit, um Feedback zu sammeln und das Modell zu verbessern.
- LongCat-2.0 Free ist für begrenzte Zeit auf OpenCode verfügbar. Das Team nutzt diese Zeit, um Feedback zu sammeln und das Modell zu verbessern.
- North Mini Code Free ist für begrenzte Zeit auf OpenCode verfügbar. Das Team nutzt diese Zeit, um Feedback zu sammeln und das Modell zu verbessern.
- Nemotron 3 Ultra Free ist für begrenzte Zeit auf OpenCode verfügbar. Das Team nutzt diese Zeit, um Feedback zu sammeln und das Modell zu verbessern.
- Big Pickle ist ein Stealth-Modell, das für begrenzte Zeit kostenlos auf OpenCode verfügbar ist. Das Team nutzt diese Zeit, um Feedback zu sammeln und das Modell zu verbessern.
@@ -274,7 +271,6 @@ Alle unsere Modelle werden in den USA gehostet. Unsere Provider folgen einer Zer
- MiMo-V2.5 Free: Während des kostenlosen Zeitraums können gesammelte Daten zur Verbesserung des Modells verwendet werden.
- Laguna S 2.1 Free: Während des kostenlosen Zeitraums können gesammelte Daten zur Verbesserung des Modells verwendet werden.
- Ling-3.0-flash Free: Während des kostenlosen Zeitraums können gesammelte Daten zur Verbesserung des Modells verwendet werden.
- LongCat-2.0 Free: Während des kostenlosen Zeitraums können gesammelte Daten zur Verbesserung des Modells verwendet werden.
- North Mini Code Free: Während des kostenlosen Zeitraums können erhobene Daten gespeichert und zur Verbesserung des Modells verwendet werden. Übermitteln Sie keine personenbezogenen oder vertraulichen Daten. Weitere Informationen finden Sie in unseren [Nutzungsbedingungen](https://cohere.com/terms-of-use) und unserer [Datenschutzerklärung](https://cohere.com/privacy).
- Nemotron 3 Ultra Free (kostenlose NVIDIA-Endpunkte): Nur für Testzwecke — übermitteln Sie keine personenbezogenen oder vertraulichen Daten. Ihre Nutzung wird zu Sicherheitszwecken und zur Verbesserung der Produkte und Dienste von NVIDIA protokolliert. Die zu Verbesserungszwecken protokollierten Sitzungsdaten sind nicht mit Ihrer Identität oder einem dauerhaften Identifikator verknüpft. Weitere Informationen zu unseren Datenverarbeitungspraktiken finden Sie in unserer [Datenschutzrichtlinie](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). Durch die Interaktion mit diesem Endpunkt stimmen Sie unserer Erhebung, Aufzeichnung und Nutzung solcher Informationen sowie den [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf) zu.
- OpenAI APIs: Anfragen werden in Übereinstimmung mit [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data) 30 Tage lang gespeichert.
-4
View File
@@ -117,7 +117,6 @@ También puedes acceder a nuestros modelos a través de los siguientes endpoints
| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Ling-3.0-flash Free | ling-3.0-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| LongCat-2.0 Free | longcat-2.0-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -149,7 +148,6 @@ Admitimos un modelo de pago por uso. A continuación se muestran los precios **p
| MiMo-V2.5 Free | Free | Free | Free | - |
| Laguna S 2.1 Free | Free | Free | Free | - |
| Ling-3.0-flash Free | Free | Free | Free | - |
| LongCat-2.0 Free | Free | Free | Free | - |
| North Mini Code Free | Free | Free | Free | - |
| Nemotron 3 Ultra Free | Free | Free | Free | - |
| MiniMax M3 | $0.30 | $1.20 | $0.06 | - |
@@ -226,7 +224,6 @@ Los modelos gratuitos:
- MiMo-V2.5 Free está disponible en OpenCode por tiempo limitado. El equipo está usando este tiempo para recopilar comentarios y mejorar el modelo.
- Laguna S 2.1 Free está disponible en OpenCode por tiempo limitado. El equipo está usando este tiempo para recopilar comentarios y mejorar el modelo.
- Ling-3.0-flash Free está disponible en OpenCode por tiempo limitado. El equipo está usando este tiempo para recopilar comentarios y mejorar el modelo.
- LongCat-2.0 Free está disponible en OpenCode por tiempo limitado. El equipo está usando este tiempo para recopilar comentarios y mejorar el modelo.
- North Mini Code Free está disponible en OpenCode por tiempo limitado. El equipo está usando este tiempo para recopilar comentarios y mejorar el modelo.
- Nemotron 3 Ultra Free está disponible en OpenCode por tiempo limitado. El equipo está usando este tiempo para recopilar comentarios y mejorar el modelo.
- Big Pickle es un modelo stealth que es gratuito en OpenCode por tiempo limitado. El equipo está usando este tiempo para recopilar comentarios y mejorar el modelo.
@@ -288,7 +285,6 @@ Todos nuestros modelos están alojados en US. Nuestros proveedores siguen una po
- MiMo-V2.5 Free: Durante su período gratuito, los datos recopilados pueden usarse para mejorar el modelo.
- Laguna S 2.1 Free: Durante su período gratuito, los datos recopilados pueden usarse para mejorar el modelo.
- Ling-3.0-flash Free: Durante su período gratuito, los datos recopilados pueden usarse para mejorar el modelo.
- LongCat-2.0 Free: Durante su período gratuito, los datos recopilados pueden usarse para mejorar el modelo.
- North Mini Code Free: Durante el período gratuito, los datos recopilados podrán conservarse y utilizarse para mejorar el modelo. No envíes datos personales ni confidenciales. Consulta nuestros [Términos de uso](https://cohere.com/terms-of-use) y nuestra [Política de privacidad](https://cohere.com/privacy).
- Nemotron 3 Ultra Free (endpoints gratuitos de NVIDIA): Solo para uso de prueba — no envíes datos personales ni confidenciales. Tu uso se registra con fines de seguridad y para mejorar los productos y servicios de NVIDIA. Los datos de sesión registrados con fines de mejora no están vinculados a tu identidad ni a ningún identificador persistente. Para obtener más información sobre nuestras prácticas de procesamiento de datos, consulta nuestra [Política de privacidad](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). Al interactuar con este endpoint, aceptas que recopilemos, registremos y usemos dicha información, así como los [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf).
- OpenAI APIs: Las solicitudes se conservan durante 30 días de acuerdo con [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data).
-4
View File
@@ -108,7 +108,6 @@ Vous pouvez également accéder à nos modèles via les points de terminaison AP
| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Ling-3.0-flash Free | ling-3.0-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| LongCat-2.0 Free | longcat-2.0-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -138,7 +137,6 @@ Nous prenons en charge un modèle de paiement à l'utilisation. Vous trouverez c
| MiMo-V2.5 Free | Free | Free | Free | - |
| Laguna S 2.1 Free | Free | Free | Free | - |
| Ling-3.0-flash Free | Free | Free | Free | - |
| LongCat-2.0 Free | Free | Free | Free | - |
| North Mini Code Free | Free | Free | Free | - |
| Nemotron 3 Ultra Free | Free | Free | Free | - |
| MiniMax M3 | $0.30 | $1.20 | $0.06 | - |
@@ -215,7 +213,6 @@ Les modèles gratuits :
- MiMo-V2.5 Free est disponible sur OpenCode pour une durée limitée. L'équipe utilise cette période pour recueillir des retours et améliorer le modèle.
- Laguna S 2.1 Free est disponible sur OpenCode pour une durée limitée. L'équipe utilise cette période pour recueillir des retours et améliorer le modèle.
- Ling-3.0-flash Free est disponible sur OpenCode pour une durée limitée. L'équipe utilise cette période pour recueillir des retours et améliorer le modèle.
- LongCat-2.0 Free est disponible sur OpenCode pour une durée limitée. L'équipe utilise cette période pour recueillir des retours et améliorer le modèle.
- North Mini Code Free est disponible sur OpenCode pour une durée limitée. L'équipe utilise cette période pour recueillir des retours et améliorer le modèle.
- Nemotron 3 Ultra Free est disponible sur OpenCode pour une durée limitée. L'équipe utilise cette période pour recueillir des retours et améliorer le modèle.
- Big Pickle est un modèle stealth gratuit sur OpenCode pour une durée limitée. L'équipe utilise cette période pour recueillir des retours et améliorer le modèle.
@@ -274,7 +271,6 @@ Tous nos modèles sont hébergés aux US. Nos fournisseurs suivent une politique
- MiMo-V2.5 Free : Pendant sa période gratuite, les données collectées peuvent être utilisées pour améliorer le modèle.
- Laguna S 2.1 Free : Pendant sa période gratuite, les données collectées peuvent être utilisées pour améliorer le modèle.
- Ling-3.0-flash Free : Pendant sa période gratuite, les données collectées peuvent être utilisées pour améliorer le modèle.
- LongCat-2.0 Free : Pendant sa période gratuite, les données collectées peuvent être utilisées pour améliorer le modèle.
- North Mini Code Free : Pendant la période de gratuité, les données collectées peuvent être conservées et utilisées pour améliorer le modèle. Ne transmettez aucune donnée personnelle ou confidentielle. Consultez nos [Conditions dutilisation](https://cohere.com/terms-of-use) et notre [Politique de confidentialité](https://cohere.com/privacy).
- Nemotron 3 Ultra Free (endpoints NVIDIA gratuits) : Réservé à un usage d'essai — n'envoyez pas de données personnelles ou confidentielles. Votre utilisation est journalisée à des fins de sécurité et pour améliorer les produits et services de NVIDIA. Les données de session journalisées à des fins d'amélioration ne sont pas liées à votre identité ni à un quelconque identifiant persistant. Pour plus d'informations sur nos pratiques de traitement des données, consultez notre [Politique de confidentialité](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). En interagissant avec cet endpoint, vous consentez à notre collecte, à notre enregistrement et à notre utilisation de ces informations ainsi qu'aux [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf).
- OpenAI APIs : Les requêtes sont conservées pendant 30 jours conformément à [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data).
-4
View File
@@ -117,7 +117,6 @@ Puoi anche accedere ai nostri modelli tramite i seguenti endpoint API.
| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Ling-3.0-flash Free | ling-3.0-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| LongCat-2.0 Free | longcat-2.0-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -149,7 +148,6 @@ Supportiamo un modello pay-as-you-go. Qui sotto trovi i prezzi **per 1M token**.
| MiMo-V2.5 Free | Free | Free | Free | - |
| Laguna S 2.1 Free | Free | Free | Free | - |
| Ling-3.0-flash Free | Free | Free | Free | - |
| LongCat-2.0 Free | Free | Free | Free | - |
| North Mini Code Free | Free | Free | Free | - |
| Nemotron 3 Ultra Free | Free | Free | Free | - |
| MiniMax M3 | $0.30 | $1.20 | $0.06 | - |
@@ -226,7 +224,6 @@ I modelli gratuiti:
- MiMo-V2.5 Free è disponibile su OpenCode per un periodo limitato. Il team usa questo periodo per raccogliere feedback e migliorare il modello.
- Laguna S 2.1 Free è disponibile su OpenCode per un periodo limitato. Il team usa questo periodo per raccogliere feedback e migliorare il modello.
- Ling-3.0-flash Free è disponibile su OpenCode per un periodo limitato. Il team usa questo periodo per raccogliere feedback e migliorare il modello.
- LongCat-2.0 Free è disponibile su OpenCode per un periodo limitato. Il team usa questo periodo per raccogliere feedback e migliorare il modello.
- North Mini Code Free è disponibile su OpenCode per un periodo limitato. Il team usa questo periodo per raccogliere feedback e migliorare il modello.
- Nemotron 3 Ultra Free è disponibile su OpenCode per un periodo limitato. Il team usa questo periodo per raccogliere feedback e migliorare il modello.
- Big Pickle è un modello stealth che è gratuito su OpenCode per un periodo limitato. Il team usa questo periodo per raccogliere feedback e migliorare il modello.
@@ -288,7 +285,6 @@ Tutti i nostri modelli sono ospitati negli US. I nostri provider seguono una pol
- MiMo-V2.5 Free: durante il periodo gratuito, i dati raccolti possono essere usati per migliorare il modello.
- Laguna S 2.1 Free: durante il periodo gratuito, i dati raccolti possono essere usati per migliorare il modello.
- Ling-3.0-flash Free: durante il periodo gratuito, i dati raccolti possono essere usati per migliorare il modello.
- LongCat-2.0 Free: durante il periodo gratuito, i dati raccolti possono essere usati per migliorare il modello.
- North Mini Code Free: Durante il periodo gratuito, i dati raccolti possono essere conservati e utilizzati per migliorare il modello. Non inviare dati personali o riservati. Consulta i nostri [Termini di utilizzo](https://cohere.com/terms-of-use) e la nostra [Informativa sulla privacy](https://cohere.com/privacy).
- Nemotron 3 Ultra Free (endpoint NVIDIA gratuiti): solo per uso di prova — non inviare dati personali o riservati. Il tuo utilizzo viene registrato per finalità di sicurezza e per migliorare i prodotti e i servizi di NVIDIA. I dati di sessione registrati a fini di miglioramento non sono collegati alla tua identità né ad alcun identificatore persistente. Per maggiori informazioni sulle nostre pratiche di trattamento dei dati, consulta la nostra [Informativa sulla privacy](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). Interagendo con questo endpoint, acconsenti alla nostra raccolta, registrazione e utilizzo di tali informazioni e ai [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf).
- OpenAI APIs: le richieste vengono conservate per 30 giorni in conformità con [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data).
-4
View File
@@ -108,7 +108,6 @@ OpenCode Zen は、OpenCode のほかのプロバイダーと同じように動
| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Ling-3.0-flash Free | ling-3.0-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| LongCat-2.0 Free | longcat-2.0-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -138,7 +137,6 @@ https://opencode.ai/zen/v1/models
| MiMo-V2.5 Free | Free | Free | Free | - |
| Laguna S 2.1 Free | Free | Free | Free | - |
| Ling-3.0-flash Free | Free | Free | Free | - |
| LongCat-2.0 Free | Free | Free | Free | - |
| North Mini Code Free | Free | Free | Free | - |
| Nemotron 3 Ultra Free | Free | Free | Free | - |
| MiniMax M3 | $0.30 | $1.20 | $0.06 | - |
@@ -215,7 +213,6 @@ https://opencode.ai/zen/v1/models
- MiMo-V2.5 Free は期間限定で OpenCode で利用できます。チームはこの期間中にフィードバックを集め、モデルを改善しています。
- Laguna S 2.1 Free は期間限定で OpenCode で利用できます。チームはこの期間中にフィードバックを集め、モデルを改善しています。
- Ling-3.0-flash Free は期間限定で OpenCode で利用できます。チームはこの期間中にフィードバックを集め、モデルを改善しています。
- LongCat-2.0 Free は期間限定で OpenCode で利用できます。チームはこの期間中にフィードバックを集め、モデルを改善しています。
- North Mini Code Free は期間限定で OpenCode で利用できます。チームはこの期間中にフィードバックを集め、モデルを改善しています。
- Nemotron 3 Ultra Free は期間限定で OpenCode で利用できます。チームはこの期間中にフィードバックを集め、モデルを改善しています。
- Big Pickle はステルスモデルで、期間限定で OpenCode で無料提供されています。チームはこの期間中にフィードバックを集め、モデルを改善しています。
@@ -274,7 +271,6 @@ https://opencode.ai/zen/v1/models
- MiMo-V2.5 Free: 無料提供期間中、収集されたデータがモデル改善に使われる場合があります。
- Laguna S 2.1 Free: 無料提供期間中、収集されたデータがモデル改善に使われる場合があります。
- Ling-3.0-flash Free: 無料提供期間中、収集されたデータがモデル改善に使われる場合があります。
- LongCat-2.0 Free: 無料提供期間中、収集されたデータがモデル改善に使われる場合があります。
- North Mini Code Free: 無料提供期間中、収集されたデータは保持され、モデルの改善に使用される場合があります。個人情報や機密情報を送信しないでください。詳しくは、[利用規約](https://cohere.com/terms-of-use)および[プライバシーポリシー](https://cohere.com/privacy)をご覧ください。
- Nemotron 3 Ultra FreeNVIDIA の無料エンドポイント): 試用専用です — 個人情報や機密データは送信しないでください。お客様の利用は、セキュリティ目的および NVIDIA の製品とサービスの改善のために記録されます。改善目的で記録されたセッションデータは、お客様の身元や永続的な識別子とは関連付けられません。当社のデータ処理慣行の詳細については、[プライバシーポリシー](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf)をご覧ください。このエンドポイントを利用することで、お客様はそのような情報の当社による収集、記録、利用、および [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf) に同意したものとみなされます。
- OpenAI APIs: リクエストは [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data) に従って 30 日間保持されます。
-4
View File
@@ -108,7 +108,6 @@ OpenCode Zen은 OpenCode의 다른 provider와 똑같이 작동합니다.
| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Ling-3.0-flash Free | ling-3.0-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| LongCat-2.0 Free | longcat-2.0-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -138,7 +137,6 @@ https://opencode.ai/zen/v1/models
| MiMo-V2.5 Free | Free | Free | Free | - |
| Laguna S 2.1 Free | Free | Free | Free | - |
| Ling-3.0-flash Free | Free | Free | Free | - |
| LongCat-2.0 Free | Free | Free | Free | - |
| North Mini Code Free | Free | Free | Free | - |
| Nemotron 3 Ultra Free | Free | Free | Free | - |
| MiniMax M3 | $0.30 | $1.20 | $0.06 | - |
@@ -215,7 +213,6 @@ https://opencode.ai/zen/v1/models
- MiMo-V2.5 Free는 한정된 기간 동안 OpenCode에서 제공됩니다. 팀은 이 기간에 피드백을 수집하고 모델을 개선합니다.
- Laguna S 2.1 Free는 한정된 기간 동안 OpenCode에서 제공됩니다. 팀은 이 기간에 피드백을 수집하고 모델을 개선합니다.
- Ling-3.0-flash Free는 한정된 기간 동안 OpenCode에서 제공됩니다. 팀은 이 기간에 피드백을 수집하고 모델을 개선합니다.
- LongCat-2.0 Free는 한정된 기간 동안 OpenCode에서 제공됩니다. 팀은 이 기간에 피드백을 수집하고 모델을 개선합니다.
- North Mini Code Free는 한정된 기간 동안 OpenCode에서 제공됩니다. 팀은 이 기간에 피드백을 수집하고 모델을 개선합니다.
- Nemotron 3 Ultra Free는 한정된 기간 동안 OpenCode에서 제공됩니다. 팀은 이 기간에 피드백을 수집하고 모델을 개선합니다.
- Big Pickle은 한정된 기간 동안 OpenCode에서 무료로 제공되는 stealth model입니다. 팀은 이 기간에 피드백을 수집하고 모델을 개선합니다.
@@ -274,7 +271,6 @@ https://opencode.ai/zen/v1/models
- MiMo-V2.5 Free: 무료 제공 기간에는 수집된 데이터가 모델 개선에 사용될 수 있습니다.
- Laguna S 2.1 Free: 무료 제공 기간에는 수집된 데이터가 모델 개선에 사용될 수 있습니다.
- Ling-3.0-flash Free: 무료 제공 기간에는 수집된 데이터가 모델 개선에 사용될 수 있습니다.
- LongCat-2.0 Free: 무료 제공 기간에는 수집된 데이터가 모델 개선에 사용될 수 있습니다.
- North Mini Code Free: 무료 제공 기간 동안 수집된 데이터는 보관되며 모델 개선에 사용될 수 있습니다. 개인 정보나 기밀 정보를 제출하지 마세요. 자세한 내용은 [이용 약관](https://cohere.com/terms-of-use) 및 [개인정보 처리방침](https://cohere.com/privacy)을 참조하세요.
- Nemotron 3 Ultra Free(NVIDIA 무료 엔드포인트): 평가판 전용이며 — 개인 정보나 기밀 데이터는 제출하지 마세요. 사용 내역은 보안 목적과 NVIDIA 제품 및 서비스 개선을 위해 기록됩니다. 개선 목적으로 기록된 세션 데이터는 사용자의 신원이나 영구 식별자와 연결되지 않습니다. 당사의 데이터 처리 관행에 대한 자세한 내용은 [개인정보처리방침](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf)을 참조하세요. 이 엔드포인트와 상호 작용함으로써 사용자는 당사가 이러한 정보를 수집, 기록, 사용하는 것과 [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf)에 동의하게 됩니다.
- OpenAI APIs: 요청은 [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data)에 따라 30일 동안 보관됩니다.
-4
View File
@@ -117,7 +117,6 @@ Du kan også få tilgang til modellene våre gjennom følgende API-endepunkter.
| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Ling-3.0-flash Free | ling-3.0-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| LongCat-2.0 Free | longcat-2.0-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -149,7 +148,6 @@ Vi støtter en pay-as-you-go-modell. Nedenfor er prisene **per 1M tokens**.
| MiMo-V2.5 Free | Free | Free | Free | - |
| Laguna S 2.1 Free | Free | Free | Free | - |
| Ling-3.0-flash Free | Free | Free | Free | - |
| LongCat-2.0 Free | Free | Free | Free | - |
| North Mini Code Free | Free | Free | Free | - |
| Nemotron 3 Ultra Free | Free | Free | Free | - |
| MiniMax M3 | $0.30 | $1.20 | $0.06 | - |
@@ -226,7 +224,6 @@ Gratis-modellene:
- MiMo-V2.5 Free er tilgjengelig på OpenCode i en begrenset periode. Teamet bruker denne tiden til å samle inn tilbakemeldinger og forbedre modellen.
- Laguna S 2.1 Free er tilgjengelig på OpenCode i en begrenset periode. Teamet bruker denne tiden til å samle inn tilbakemeldinger og forbedre modellen.
- Ling-3.0-flash Free er tilgjengelig på OpenCode i en begrenset periode. Teamet bruker denne tiden til å samle inn tilbakemeldinger og forbedre modellen.
- LongCat-2.0 Free er tilgjengelig på OpenCode i en begrenset periode. Teamet bruker denne tiden til å samle inn tilbakemeldinger og forbedre modellen.
- North Mini Code Free er tilgjengelig på OpenCode i en begrenset periode. Teamet bruker denne tiden til å samle inn tilbakemeldinger og forbedre modellen.
- Nemotron 3 Ultra Free er tilgjengelig på OpenCode i en begrenset periode. Teamet bruker denne tiden til å samle inn tilbakemeldinger og forbedre modellen.
- Big Pickle er en stealth-modell som er gratis på OpenCode i en begrenset periode. Teamet bruker denne tiden til å samle inn tilbakemeldinger og forbedre modellen.
@@ -288,7 +285,6 @@ Alle modellene våre hostes i US. Leverandørene våre følger en policy for zer
- MiMo-V2.5 Free: I gratisperioden kan innsamlede data brukes til å forbedre modellen.
- Laguna S 2.1 Free: I gratisperioden kan innsamlede data brukes til å forbedre modellen.
- Ling-3.0-flash Free: I gratisperioden kan innsamlede data brukes til å forbedre modellen.
- LongCat-2.0 Free: I gratisperioden kan innsamlede data brukes til å forbedre modellen.
- North Mini Code Free: I gratisperioden kan innsamlede data bli oppbevart og brukt til å forbedre modellen. Ikke send inn personopplysninger eller konfidensielle opplysninger. Se våre [Vilkår for bruk](https://cohere.com/terms-of-use) og vår [Personvernerklæring](https://cohere.com/privacy).
- Nemotron 3 Ultra Free (gratis NVIDIA-endepunkter): Kun for prøvebruk — ikke send inn personopplysninger eller konfidensielle data. Bruken din logges av sikkerhetshensyn og for å forbedre NVIDIAs produkter og tjenester. Sesjonsdataene som logges for forbedringsformål, er ikke knyttet til identiteten din eller noen vedvarende identifikator. For mer informasjon om vår databehandlingspraksis, se vår [personvernerklæring](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). Ved å samhandle med dette endepunktet samtykker du til at vi samler inn, registrerer og bruker slik informasjon, samt til [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf).
- OpenAI APIs: Forespørsler lagres i 30 dager i samsvar med [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data).
-4
View File
@@ -117,7 +117,6 @@ Możesz też uzyskać dostęp do naszych modeli przez poniższe endpointy API.
| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Ling-3.0-flash Free | ling-3.0-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| LongCat-2.0 Free | longcat-2.0-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -149,7 +148,6 @@ Obsługujemy model pay-as-you-go. Poniżej znajdują się ceny **za 1M tokenów*
| MiMo-V2.5 Free | Free | Free | Free | - |
| Laguna S 2.1 Free | Free | Free | Free | - |
| Ling-3.0-flash Free | Free | Free | Free | - |
| LongCat-2.0 Free | Free | Free | Free | - |
| North Mini Code Free | Free | Free | Free | - |
| Nemotron 3 Ultra Free | Free | Free | Free | - |
| MiniMax M3 | $0.30 | $1.20 | $0.06 | - |
@@ -226,7 +224,6 @@ Darmowe modele:
- MiMo-V2.5 Free jest dostępny w OpenCode przez ograniczony czas. Zespół wykorzystuje ten czas do zbierania opinii i ulepszania modelu.
- Laguna S 2.1 Free jest dostępny w OpenCode przez ograniczony czas. Zespół wykorzystuje ten czas do zbierania opinii i ulepszania modelu.
- Ling-3.0-flash Free jest dostępny w OpenCode przez ograniczony czas. Zespół wykorzystuje ten czas do zbierania opinii i ulepszania modelu.
- LongCat-2.0 Free jest dostępny w OpenCode przez ograniczony czas. Zespół wykorzystuje ten czas do zbierania opinii i ulepszania modelu.
- North Mini Code Free jest dostępny w OpenCode przez ograniczony czas. Zespół wykorzystuje ten czas do zbierania opinii i ulepszania modelu.
- Nemotron 3 Ultra Free jest dostępny w OpenCode przez ograniczony czas. Zespół wykorzystuje ten czas do zbierania opinii i ulepszania modelu.
- Big Pickle to stealth model, który jest darmowy w OpenCode przez ograniczony czas. Zespół wykorzystuje ten czas do zbierania opinii i ulepszania modelu.
@@ -288,7 +285,6 @@ Wszystkie nasze modele są hostowane w US. Nasi dostawcy stosują politykę zero
- MiMo-V2.5 Free: W czasie darmowego okresu zebrane dane mogą być wykorzystywane do ulepszania modelu.
- Laguna S 2.1 Free: W czasie darmowego okresu zebrane dane mogą być wykorzystywane do ulepszania modelu.
- Ling-3.0-flash Free: W czasie darmowego okresu zebrane dane mogą być wykorzystywane do ulepszania modelu.
- LongCat-2.0 Free: W czasie darmowego okresu zebrane dane mogą być wykorzystywane do ulepszania modelu.
- North Mini Code Free: W okresie bezpłatnego dostępu zebrane dane mogą być przechowywane i wykorzystywane do ulepszania modelu. Nie przesyłaj danych osobowych ani poufnych. Zapoznaj się z naszym [Regulaminem korzystania](https://cohere.com/terms-of-use) i [Polityką prywatności](https://cohere.com/privacy).
- Nemotron 3 Ultra Free (darmowe endpointy NVIDIA): Tylko do użytku próbnego — nie przesyłaj danych osobowych ani poufnych. Twoje korzystanie jest rejestrowane w celach bezpieczeństwa oraz w celu ulepszania produktów i usług NVIDIA. Rejestrowane dane sesji wykorzystywane do celów ulepszania nie są powiązane z Twoją tożsamością ani żadnym trwałym identyfikatorem. Aby uzyskać więcej informacji o naszych praktykach przetwarzania danych, zapoznaj się z naszą [Polityką prywatności](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). Korzystając z tego endpointu, wyrażasz zgodę na gromadzenie, rejestrowanie i wykorzystywanie przez nas takich informacji oraz na [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf).
- OpenAI APIs: Żądania są przechowywane przez 30 dni zgodnie z [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data).
+23 -7
View File
@@ -2308,9 +2308,9 @@ Some useful routing options:
### xAI
Two ways to authenticate: a SuperGrok subscription via device-code OAuth or a pay-as-you-go API key from the xAI console.
Three ways to authenticate: a SuperGrok subscription via browser OAuth, the same SuperGrok subscription via a headless device-code flow (for VPS / SSH / Docker), or a pay-as-you-go API key from the xAI console.
#### Option A — SuperGrok subscription
#### Option A — SuperGrok OAuth (browser login)
1. Run the `/connect` command and search for **xAI**.
@@ -2318,11 +2318,9 @@ Two ways to authenticate: a SuperGrok subscription via device-code OAuth or a pa
/connect
```
2. Select **SuperGrok Subscription**. OpenCode opens xAI's verification link with the user code pre-populated when supported.
2. Select **xAI Grok OAuth (SuperGrok Subscription)**. OpenCode opens xAI's consent screen in your browser and waits for the callback on `http://127.0.0.1:56121/callback`.
3. Approve the consent screen. If xAI asks for a code, enter the user code displayed by OpenCode. OpenCode polls xAI's token endpoint and stores the resulting OAuth tokens once you approve.
4. Run the `/models` command to select a Grok model.
3. Run the `/models` command to select a Grok model.
```txt
/models
@@ -2330,7 +2328,25 @@ Two ways to authenticate: a SuperGrok subscription via device-code OAuth or a pa
OpenCode refreshes the OAuth access token automatically. Any Grok or X Premium plan that includes Grok API access works; you do not need a separate `XAI_API_KEY`.
#### Option B — API key
#### Option B — SuperGrok device-code (headless / remote server / VPS)
Use this when OpenCode is running somewhere a browser can't reach the loopback redirect: a VPS, a remote dev box over SSH, inside Docker, in CI, etc. No callback port is opened on the host running OpenCode — instead xAI hands the CLI a short code that you type into a browser on any other device (laptop, phone, …).
1. Run the `/connect` command on the remote host and search for **xAI**.
```txt
/connect
```
2. Select **xAI Grok OAuth (Headless / Remote / VPS)**. OpenCode prints a verification URL and a short user code.
```txt
Open https://x.ai/device on any device and enter code: ABCD-1234
```
3. Open the URL on a device that has a browser (your laptop or phone), enter the code, and approve the consent screen. OpenCode polls xAI's token endpoint and stores the resulting OAuth tokens once you approve. Token refresh works the same as Option A.
#### Option C — API key
1. Head over to the [xAI console](https://console.x.ai/), create an account, and generate an API key.
@@ -108,7 +108,6 @@ Você também pode acessar nossos modelos pelos seguintes endpoints de API.
| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Ling-3.0-flash Free | ling-3.0-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| LongCat-2.0 Free | longcat-2.0-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -138,7 +137,6 @@ Oferecemos um modelo pay-as-you-go. Abaixo estão os preços **por 1M tokens**.
| MiMo-V2.5 Free | Free | Free | Free | - |
| Laguna S 2.1 Free | Free | Free | Free | - |
| Ling-3.0-flash Free | Free | Free | Free | - |
| LongCat-2.0 Free | Free | Free | Free | - |
| North Mini Code Free | Free | Free | Free | - |
| Nemotron 3 Ultra Free | Free | Free | Free | - |
| MiniMax M3 | $0.30 | $1.20 | $0.06 | - |
@@ -215,7 +213,6 @@ Os modelos gratuitos:
- MiMo-V2.5 Free está disponível no OpenCode por tempo limitado. A equipe está usando esse período para coletar feedback e melhorar o modelo.
- Laguna S 2.1 Free está disponível no OpenCode por tempo limitado. A equipe está usando esse período para coletar feedback e melhorar o modelo.
- Ling-3.0-flash Free está disponível no OpenCode por tempo limitado. A equipe está usando esse período para coletar feedback e melhorar o modelo.
- LongCat-2.0 Free está disponível no OpenCode por tempo limitado. A equipe está usando esse período para coletar feedback e melhorar o modelo.
- North Mini Code Free está disponível no OpenCode por tempo limitado. A equipe está usando esse período para coletar feedback e melhorar o modelo.
- Nemotron 3 Ultra Free está disponível no OpenCode por tempo limitado. A equipe está usando esse período para coletar feedback e melhorar o modelo.
- Big Pickle é um modelo stealth que está gratuito no OpenCode por tempo limitado. A equipe está usando esse período para coletar feedback e melhorar o modelo.
@@ -274,7 +271,6 @@ Todos os nossos modelos são hospedados nos US. Nossos provedores seguem uma pol
- MiMo-V2.5 Free: Durante seu período gratuito, os dados coletados podem ser usados para melhorar o modelo.
- Laguna S 2.1 Free: Durante seu período gratuito, os dados coletados podem ser usados para melhorar o modelo.
- Ling-3.0-flash Free: Durante seu período gratuito, os dados coletados podem ser usados para melhorar o modelo.
- LongCat-2.0 Free: Durante seu período gratuito, os dados coletados podem ser usados para melhorar o modelo.
- North Mini Code Free: Durante o período gratuito, os dados coletados poderão ser retidos e usados para aprimorar o modelo. Não envie dados pessoais ou confidenciais. Consulte nossos [Termos de Uso](https://cohere.com/terms-of-use) e nossa [Política de Privacidade](https://cohere.com/privacy).
- Nemotron 3 Ultra Free (endpoints gratuitos da NVIDIA): Apenas para uso de avaliação — não envie dados pessoais ou confidenciais. Seu uso é registrado para fins de segurança e para melhorar os produtos e serviços da NVIDIA. Os dados de sessão registrados para fins de melhoria não estão vinculados à sua identidade nem a qualquer identificador persistente. Para mais informações sobre nossas práticas de processamento de dados, consulte nossa [Política de Privacidade](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). Ao interagir com este endpoint, você consente com a nossa coleta, registro e uso dessas informações e com os [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf).
- OpenAI APIs: As solicitações são retidas por 30 dias de acordo com [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data).
-4
View File
@@ -117,7 +117,6 @@ OpenCode Zen работает как любой другой провайдер
| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Ling-3.0-flash Free | ling-3.0-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| LongCat-2.0 Free | longcat-2.0-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -149,7 +148,6 @@ https://opencode.ai/zen/v1/models
| MiMo-V2.5 Free | Free | Free | Free | - |
| Laguna S 2.1 Free | Free | Free | Free | - |
| Ling-3.0-flash Free | Free | Free | Free | - |
| LongCat-2.0 Free | Free | Free | Free | - |
| North Mini Code Free | Free | Free | Free | - |
| Nemotron 3 Ultra Free | Free | Free | Free | - |
| MiniMax M3 | $0.30 | $1.20 | $0.06 | - |
@@ -226,7 +224,6 @@ https://opencode.ai/zen/v1/models
- MiMo-V2.5 Free доступна в OpenCode ограниченное время. Команда использует это время, чтобы собирать отзывы и улучшать модель.
- Laguna S 2.1 Free доступна в OpenCode ограниченное время. Команда использует это время, чтобы собирать отзывы и улучшать модель.
- Ling-3.0-flash Free доступна в OpenCode ограниченное время. Команда использует это время, чтобы собирать отзывы и улучшать модель.
- LongCat-2.0 Free доступна в OpenCode ограниченное время. Команда использует это время, чтобы собирать отзывы и улучшать модель.
- North Mini Code Free доступна в OpenCode ограниченное время. Команда использует это время, чтобы собирать отзывы и улучшать модель.
- Nemotron 3 Ultra Free доступна в OpenCode ограниченное время. Команда использует это время, чтобы собирать отзывы и улучшать модель.
- Big Pickle — это скрытая модель, которая доступна бесплатно в OpenCode ограниченное время. Команда использует это время, чтобы собирать отзывы и улучшать модель.
@@ -288,7 +285,6 @@ https://opencode.ai/zen/v1/models
- MiMo-V2.5 Free: во время бесплатного периода собранные данные могут использоваться для улучшения модели.
- Laguna S 2.1 Free: во время бесплатного периода собранные данные могут использоваться для улучшения модели.
- Ling-3.0-flash Free: во время бесплатного периода собранные данные могут использоваться для улучшения модели.
- LongCat-2.0 Free: во время бесплатного периода собранные данные могут использоваться для улучшения модели.
- North Mini Code Free: В течение бесплатного периода собранные данные могут храниться и использоваться для улучшения модели. Не отправляйте персональные или конфиденциальные данные. Ознакомьтесь с нашими [Условиями использования](https://cohere.com/terms-of-use) и [Политикой конфиденциальности](https://cohere.com/privacy).
- Nemotron 3 Ultra Free (бесплатные эндпоинты NVIDIA): только для пробного использования — не отправляйте персональные или конфиденциальные данные. Использование логируется в целях безопасности и для улучшения продуктов и сервисов NVIDIA. Логируемые данные сессии, используемые в целях улучшения, не связаны с вашей личностью или каким-либо постоянным идентификатором. Подробнее о наших практиках обработки данных см. в нашей [Политике конфиденциальности](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). Взаимодействуя с этим эндпоинтом, вы соглашаетесь на сбор, запись и использование нами такой информации, а также с [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf).
- OpenAI APIs: запросы хранятся 30 дней в соответствии с [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data).
-4
View File
@@ -110,7 +110,6 @@ OpenCode Zen ทำงานเหมือน provider อื่น ๆ ใน
| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Ling-3.0-flash Free | ling-3.0-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| LongCat-2.0 Free | longcat-2.0-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -140,7 +139,6 @@ https://opencode.ai/zen/v1/models
| MiMo-V2.5 Free | Free | Free | Free | - |
| Laguna S 2.1 Free | Free | Free | Free | - |
| Ling-3.0-flash Free | Free | Free | Free | - |
| LongCat-2.0 Free | Free | Free | Free | - |
| North Mini Code Free | Free | Free | Free | - |
| Nemotron 3 Ultra Free | Free | Free | Free | - |
| MiniMax M3 | $0.30 | $1.20 | $0.06 | - |
@@ -217,7 +215,6 @@ https://opencode.ai/zen/v1/models
- MiMo-V2.5 Free เปิดให้ใช้บน OpenCode ในช่วงเวลาจำกัด ทีมกำลังใช้ช่วงเวลานี้เพื่อเก็บ feedback และปรับปรุงโมเดล
- Laguna S 2.1 Free เปิดให้ใช้บน OpenCode ในช่วงเวลาจำกัด ทีมกำลังใช้ช่วงเวลานี้เพื่อเก็บ feedback และปรับปรุงโมเดล
- Ling-3.0-flash Free เปิดให้ใช้บน OpenCode ในช่วงเวลาจำกัด ทีมกำลังใช้ช่วงเวลานี้เพื่อเก็บ feedback และปรับปรุงโมเดล
- LongCat-2.0 Free เปิดให้ใช้บน OpenCode ในช่วงเวลาจำกัด ทีมกำลังใช้ช่วงเวลานี้เพื่อเก็บ feedback และปรับปรุงโมเดล
- North Mini Code Free เปิดให้ใช้บน OpenCode ในช่วงเวลาจำกัด ทีมกำลังใช้ช่วงเวลานี้เพื่อเก็บ feedback และปรับปรุงโมเดล
- Nemotron 3 Ultra Free เปิดให้ใช้บน OpenCode ในช่วงเวลาจำกัด ทีมกำลังใช้ช่วงเวลานี้เพื่อเก็บ feedback และปรับปรุงโมเดล
- Big Pickle เป็น stealth model ที่ใช้งานฟรีบน OpenCode ในช่วงเวลาจำกัด ทีมกำลังใช้ช่วงเวลานี้เพื่อเก็บ feedback และปรับปรุงโมเดล
@@ -276,7 +273,6 @@ https://opencode.ai/zen/v1/models
- MiMo-V2.5 Free: ระหว่างช่วงที่เปิดให้ใช้ฟรี ข้อมูลที่เก็บรวบรวมอาจถูกนำไปใช้เพื่อปรับปรุงโมเดล
- Laguna S 2.1 Free: ระหว่างช่วงที่เปิดให้ใช้ฟรี ข้อมูลที่เก็บรวบรวมอาจถูกนำไปใช้เพื่อปรับปรุงโมเดล
- Ling-3.0-flash Free: ระหว่างช่วงที่เปิดให้ใช้ฟรี ข้อมูลที่เก็บรวบรวมอาจถูกนำไปใช้เพื่อปรับปรุงโมเดล
- LongCat-2.0 Free: ระหว่างช่วงที่เปิดให้ใช้ฟรี ข้อมูลที่เก็บรวบรวมอาจถูกนำไปใช้เพื่อปรับปรุงโมเดล
- North Mini Code Free: ในช่วงที่เปิดให้ใช้ฟรี ข้อมูลที่เก็บรวบรวมอาจถูกเก็บรักษาและนำไปใช้เพื่อปรับปรุงโมเดล โปรดอย่าส่งข้อมูลส่วนบุคคลหรือข้อมูลที่เป็นความลับ ดู[ข้อกำหนดการใช้งาน](https://cohere.com/terms-of-use)และ[นโยบายความเป็นส่วนตัว](https://cohere.com/privacy)ของเรา
- Nemotron 3 Ultra Free (endpoint ฟรีของ NVIDIA): ใช้สำหรับการทดลองเท่านั้น — โปรดอย่าส่งข้อมูลส่วนบุคคลหรือข้อมูลลับ การใช้งานของคุณจะถูกบันทึกเพื่อวัตถุประสงค์ด้านความปลอดภัยและเพื่อปรับปรุงผลิตภัณฑ์และบริการของ NVIDIA ข้อมูลเซสชันที่บันทึกไว้เพื่อวัตถุประสงค์ในการปรับปรุงจะไม่เชื่อมโยงกับตัวตนของคุณหรือตัวระบุถาวรใด ๆ สำหรับข้อมูลเพิ่มเติมเกี่ยวกับแนวปฏิบัติในการประมวลผลข้อมูลของเรา โปรดดู [นโยบายความเป็นส่วนตัว](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf) ของเรา การโต้ตอบกับ endpoint นี้ถือว่าคุณยินยอมให้เราเก็บรวบรวม บันทึก และใช้ข้อมูลดังกล่าว รวมถึง [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf)
- OpenAI APIs: คำขอจะถูกเก็บไว้เป็นเวลา 30 วันตาม [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data).
-4
View File
@@ -108,7 +108,6 @@ Modellerimize aşağıdaki API uç noktaları aracılığıyla da erişebilirsin
| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Ling-3.0-flash Free | ling-3.0-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| LongCat-2.0 Free | longcat-2.0-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -138,7 +137,6 @@ Kullandıkça öde modelini destekliyoruz. Aşağıda **1M token başına** fiya
| MiMo-V2.5 Free | Free | Free | Free | - |
| Laguna S 2.1 Free | Free | Free | Free | - |
| Ling-3.0-flash Free | Free | Free | Free | - |
| LongCat-2.0 Free | Free | Free | Free | - |
| North Mini Code Free | Free | Free | Free | - |
| Nemotron 3 Ultra Free | Free | Free | Free | - |
| MiniMax M3 | $0.30 | $1.20 | $0.06 | - |
@@ -215,7 +213,6 @@ Kredi kartı ücretleri maliyet üzerinden yansıtılır (%4.4 + işlem başına
- MiMo-V2.5 Free, sınırlı bir süre için OpenCode'da ücretsizdir. Ekip bu süreyi geri bildirim toplamak ve modeli iyileştirmek için kullanıyor.
- Laguna S 2.1 Free, sınırlı bir süre için OpenCode'da ücretsizdir. Ekip bu süreyi geri bildirim toplamak ve modeli iyileştirmek için kullanıyor.
- Ling-3.0-flash Free, sınırlı bir süre için OpenCode'da ücretsizdir. Ekip bu süreyi geri bildirim toplamak ve modeli iyileştirmek için kullanıyor.
- LongCat-2.0 Free, sınırlı bir süre için OpenCode'da ücretsizdir. Ekip bu süreyi geri bildirim toplamak ve modeli iyileştirmek için kullanıyor.
- North Mini Code Free, sınırlı bir süre için OpenCode'da ücretsizdir. Ekip bu süreyi geri bildirim toplamak ve modeli iyileştirmek için kullanıyor.
- Nemotron 3 Ultra Free, sınırlı bir süre için OpenCode'da ücretsizdir. Ekip bu süreyi geri bildirim toplamak ve modeli iyileştirmek için kullanıyor.
- Big Pickle, sınırlı bir süre için OpenCode'da ücretsiz olan gizli bir modeldir. Ekip bu süreyi geri bildirim toplamak ve modeli iyileştirmek için kullanıyor.
@@ -274,7 +271,6 @@ Tüm modellerimiz US'de barındırılıyor. Sağlayıcılarımız zero-retention
- MiMo-V2.5 Free: Ücretsiz döneminde toplanan veriler modeli iyileştirmek için kullanılabilir.
- Laguna S 2.1 Free: Ücretsiz döneminde toplanan veriler modeli iyileştirmek için kullanılabilir.
- Ling-3.0-flash Free: Ücretsiz döneminde toplanan veriler modeli iyileştirmek için kullanılabilir.
- LongCat-2.0 Free: Ücretsiz döneminde toplanan veriler modeli iyileştirmek için kullanılabilir.
- North Mini Code Free: Ücretsiz kullanım süresi boyunca toplanan veriler saklanabilir ve modeli geliştirmek için kullanılabilir. Kişisel veya gizli veriler göndermeyin. [Kullanım Koşullarımıza](https://cohere.com/terms-of-use) ve [Gizlilik Politikamıza](https://cohere.com/privacy) bakın.
- Nemotron 3 Ultra Free (ücretsiz NVIDIA uç noktaları): Yalnızca deneme amaçlıdır — kişisel veya gizli veri göndermeyin. Kullanımınız güvenlik amacıyla ve NVIDIA ürünlerini ve hizmetlerini geliştirmek için kaydedilir. Geliştirme amacıyla kaydedilen oturum verileri kimliğinizle veya herhangi bir kalıcı tanımlayıcıyla ilişkilendirilmez. Veri işleme uygulamalarımız hakkında daha fazla bilgi için [Gizlilik Politikamıza](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf) bakın. Bu uç noktayla etkileşime geçerek, bu tür bilgileri toplamamıza, kaydetmemize ve kullanmamıza ve [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf) koşullarına onay vermiş olursunuz.
- OpenAI APIs: İstekler [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data) uyarınca 30 gün boyunca saklanır.
-4
View File
@@ -117,7 +117,6 @@ You can also access our models through the following API endpoints.
| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Ling-3.0-flash Free | ling-3.0-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| LongCat-2.0 Free | longcat-2.0-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -149,7 +148,6 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**.
| MiMo-V2.5 Free | Free | Free | Free | - |
| Laguna S 2.1 Free | Free | Free | Free | - |
| Ling-3.0-flash Free | Free | Free | Free | - |
| LongCat-2.0 Free | Free | Free | Free | - |
| North Mini Code Free | Free | Free | Free | - |
| Nemotron 3 Ultra Free | Free | Free | Free | - |
| MiniMax M3 | $0.30 | $1.20 | $0.06 | - |
@@ -226,7 +224,6 @@ The free models:
- MiMo-V2.5 Free is available on OpenCode for a limited time. The team is using this time to collect feedback and improve the model.
- Laguna S 2.1 Free is available on OpenCode for a limited time. The team is using this time to collect feedback and improve the model.
- Ling-3.0-flash Free is available on OpenCode for a limited time. The team is using this time to collect feedback and improve the model.
- LongCat-2.0 Free is available on OpenCode for a limited time. The team is using this time to collect feedback and improve the model.
- North Mini Code Free is available on OpenCode for a limited time. The team is using this time to collect feedback and improve the model.
- Nemotron 3 Ultra Free is available on OpenCode for a limited time. The team is using this time to collect feedback and improve the model.
- Big Pickle is a stealth model that's free on OpenCode for a limited time. The team is using this time to collect feedback and improve the model.
@@ -288,7 +285,6 @@ All our models are hosted in the US. Our providers follow a zero-retention polic
- MiMo-V2.5 Free: During its free period, collected data may be used to improve the model.
- Laguna S 2.1 Free: During its free period, collected data may be used to improve the model.
- Ling-3.0-flash Free: During its free period, collected data may be used to improve the model.
- LongCat-2.0 Free: During its free period, collected data may be used to improve the model.
- North Mini Code Free: During its free period, collected data may be retained and used to improve the model. Do not submit personal or confidential data. See our [Terms of Use](https://cohere.com/terms-of-use) and [Privacy Policy](https://cohere.com/privacy).
- Nemotron 3 Ultra Free (NVIDIA free endpoints): Trial use only — do not submit personal or confidential data. Your use is logged for security purposes and to improve NVIDIA products and services. The logged session data for improvement purposes is not linked to your identity or any persistent identifier. For more information about our data processing practices, seeour[Privacy Policy](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). By interacting with this endpoint, you consent to our collection, recording, and use of such information and the[NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf).
- OpenAI APIs: Requests are retained for 30 days in accordance with [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data).
@@ -108,7 +108,6 @@ OpenCode Zen 的工作方式与 OpenCode 中的任何其他提供商相同。
| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Ling-3.0-flash Free | ling-3.0-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| LongCat-2.0 Free | longcat-2.0-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -138,7 +137,6 @@ https://opencode.ai/zen/v1/models
| MiMo-V2.5 Free | Free | Free | Free | - |
| Laguna S 2.1 Free | Free | Free | Free | - |
| Ling-3.0-flash Free | Free | Free | Free | - |
| LongCat-2.0 Free | Free | Free | Free | - |
| North Mini Code Free | Free | Free | Free | - |
| Nemotron 3 Ultra Free | Free | Free | Free | - |
| MiniMax M3 | $0.30 | $1.20 | $0.06 | - |
@@ -215,7 +213,6 @@ https://opencode.ai/zen/v1/models
- MiMo-V2.5 Free 目前在 OpenCode 上限时免费提供。团队正在利用这段时间收集反馈并改进模型。
- Laguna S 2.1 Free 目前在 OpenCode 上限时免费提供。团队正在利用这段时间收集反馈并改进模型。
- Ling-3.0-flash Free 目前在 OpenCode 上限时免费提供。团队正在利用这段时间收集反馈并改进模型。
- LongCat-2.0 Free 目前在 OpenCode 上限时免费提供。团队正在利用这段时间收集反馈并改进模型。
- North Mini Code Free 目前在 OpenCode 上限时免费提供。团队正在利用这段时间收集反馈并改进模型。
- Nemotron 3 Ultra Free 目前在 OpenCode 上限时免费提供。团队正在利用这段时间收集反馈并改进模型。
- Big Pickle 是一个隐身模型,目前在 OpenCode 上限时免费提供。团队正在利用这段时间收集反馈并改进模型。
@@ -274,7 +271,6 @@ https://opencode.ai/zen/v1/models
- MiMo-V2.5 Free:在免费期间,收集的数据可能会被用于改进模型。
- Laguna S 2.1 Free:在免费期间,收集的数据可能会被用于改进模型。
- Ling-3.0-flash Free:在免费期间,收集的数据可能会被用于改进模型。
- LongCat-2.0 Free:在免费期间,收集的数据可能会被用于改进模型。
- North Mini Code Free:免费期间,所收集的数据可能会被保留并用于改进模型。请勿提交个人或机密数据。请参阅我们的[使用条款](https://cohere.com/terms-of-use)和[隐私政策](https://cohere.com/privacy)。
- Nemotron 3 Ultra FreeNVIDIA 免费端点):仅供试用 — 请勿提交个人或机密数据。出于安全目的以及为改进 NVIDIA 产品和服务,系统会记录你的使用情况。出于改进目的而记录的会话数据不会与你的身份或任何持久标识符相关联。有关我们数据处理实践的更多信息,请参阅我们的[隐私政策](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf)。与此端点进行交互,即表示你同意我们收集、记录和使用此类信息,并同意 [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf)。
- OpenAI APIs:请求会根据 [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data) 保留 30 天。
@@ -112,7 +112,6 @@ OpenCode Zen 的運作方式和 OpenCode 中的其他供應商一樣。
| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Ling-3.0-flash Free | ling-3.0-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| LongCat-2.0 Free | longcat-2.0-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -143,7 +142,6 @@ https://opencode.ai/zen/v1/models
| MiMo-V2.5 Free | Free | Free | Free | - |
| Laguna S 2.1 Free | Free | Free | Free | - |
| Ling-3.0-flash Free | Free | Free | Free | - |
| LongCat-2.0 Free | Free | Free | Free | - |
| North Mini Code Free | Free | Free | Free | - |
| Nemotron 3 Ultra Free | Free | Free | Free | - |
| MiniMax M3 | $0.30 | $1.20 | $0.06 | - |
@@ -220,7 +218,6 @@ https://opencode.ai/zen/v1/models
- MiMo-V2.5 Free 在 OpenCode 上限時提供。團隊正在利用這段時間收集回饋並改進模型。
- Laguna S 2.1 Free 在 OpenCode 上限時提供。團隊正在利用這段時間收集回饋並改進模型。
- Ling-3.0-flash Free 在 OpenCode 上限時提供。團隊正在利用這段時間收集回饋並改進模型。
- LongCat-2.0 Free 在 OpenCode 上限時提供。團隊正在利用這段時間收集回饋並改進模型。
- North Mini Code Free 在 OpenCode 上限時提供。團隊正在利用這段時間收集回饋並改進模型。
- Nemotron 3 Ultra Free 在 OpenCode 上限時提供。團隊正在利用這段時間收集回饋並改進模型。
- Big Pickle 是一個隱身模型,在 OpenCode 上限時免費提供。團隊正在利用這段時間收集回饋並改進模型。
@@ -280,7 +277,6 @@ https://opencode.ai/zen/v1/models
- MiMo-V2.5 Free: 在免費期間,收集到的資料可能會用於改進模型。
- Laguna S 2.1 Free: 在免費期間,收集到的資料可能會用於改進模型。
- Ling-3.0-flash Free: 在免費期間,收集到的資料可能會用於改進模型。
- LongCat-2.0 Free: 在免費期間,收集到的資料可能會用於改進模型。
- North Mini Code Free:免費期間,所收集的資料可能會被保留並用於改進模型。請勿提交個人或機密資料。請參閱我們的[使用條款](https://cohere.com/terms-of-use)和[隱私權政策](https://cohere.com/privacy)。
- Nemotron 3 Ultra FreeNVIDIA 免費端點):僅供試用 — 請勿提交個人或機密資料。基於安全目的以及為了改進 NVIDIA 產品與服務,系統會記錄你的使用情況。基於改進目的而記錄的工作階段資料不會與你的身分或任何持久識別碼相關聯。有關我們資料處理實務的更多資訊,請參閱我們的[隱私政策](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf)。與此端點進行互動,即表示你同意我們收集、記錄與使用此類資訊,並同意 [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf)。
- OpenAI APIs: 請求會依據 [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data) 保留 30 天。