Compare commits

..

1 Commits

Author SHA1 Message Date
Brendan Allan 28dc3cab22 refactor(app): split server management controllers 2026-07-29 14:35:22 +08:00
317 changed files with 5501 additions and 6798 deletions
+10 -7
View File
@@ -2,12 +2,15 @@ import type { Context } from "../../../packages/plugin/src/tui/context"
export default { export default {
id: "test.tui-discovery-smoke", id: "test.tui-discovery-smoke",
setup(_context: Context) { setup(context: Context) {
// context.ui.toast.show({ const timer = setTimeout(() => {
// title: "TUI plugin discovery works", context.ui.toast.show({
// message: "Loaded .opencode/plugins/tui/discovery-smoke.ts", title: "TUI plugin discovery works",
// variant: "success", message: "Loaded .opencode/plugins/tui/discovery-smoke.ts",
// duration: 30_000, variant: "success",
// }) duration: 30_000,
})
}, 1_000)
return () => clearTimeout(timer)
}, },
} }
-3
View File
@@ -600,7 +600,6 @@
"zod": "catalog:", "zod": "catalog:",
}, },
"devDependencies": { "devDependencies": {
"@opencode-ai/theme": "workspace:*",
"@opentui/core": "catalog:", "@opentui/core": "catalog:",
"@opentui/keymap": "catalog:", "@opentui/keymap": "catalog:",
"@opentui/solid": "catalog:", "@opentui/solid": "catalog:",
@@ -612,14 +611,12 @@
"typescript": "catalog:", "typescript": "catalog:",
}, },
"peerDependencies": { "peerDependencies": {
"@opencode-ai/theme": "workspace:*",
"@opentui/core": ">=0.4.5", "@opentui/core": ">=0.4.5",
"@opentui/keymap": ">=0.4.5", "@opentui/keymap": ">=0.4.5",
"@opentui/solid": ">=0.4.5", "@opentui/solid": ">=0.4.5",
"solid-js": ">=1.9.0", "solid-js": ">=1.9.0",
}, },
"optionalPeers": [ "optionalPeers": [
"@opencode-ai/theme",
"@opentui/core", "@opentui/core",
"@opentui/keymap", "@opentui/keymap",
"@opentui/solid", "@opentui/solid",
-118
View File
@@ -1,118 +0,0 @@
# V1 to V2 Database Migration
## Approach
- Use the `dev` branch database schema and migration registry as the V1 baseline.
- Remove migrations that exist only on the V2 branch.
- Generate one canonical migration from the `dev` schema to the final V2 schema.
- Add explicit data operations to that migration where generated DDL is insufficient.
- Test the migration against a populated database at the exact `dev` schema.
## Preserve
The canonical V1 data remains in its existing tables. In particular, preserve `session`, `message`, and `part` rows.
Preserve `workspace` rows and existing `session.workspace_id` values unchanged. The migration must not clear or rebuild
workspace relationships.
Keep the `todo` table and its data unchanged. V2 does not currently migrate todos into another representation, and the
generated migration must not drop the table.
## Truncate
Truncate these pre-launch V2 tables before applying schema changes:
- `event`
- `event_sequence`
- `session_message`
These rows are not canonical V1 data. Truncating `event` before adding the required `event.created` column means the
column needs neither a backfill nor a default. After truncation, rebuild `session_message` from canonical V1 `message`
and `part` rows rather than retaining its pre-launch V2 contents.
## Message Backfill
Backfill canonical V1 history from `message` and `part` into `session_message`. This is the main data transformation in
the migration. Preserving the V1 tables alone keeps the data safe but does not make existing history visible through the
V2 session APIs, which read `session_message`.
Reuse each V1 `message.id` as the corresponding `session_message.id`. Stable IDs keep the migration deterministic and
avoid rewriting other persisted state that may refer to a message.
Within each session, order V1 messages by `time_created` and then `id`, matching the existing V1 message index. Assign
contiguous `session_message.seq` values starting at `0`.
Map ordinary V1 messages one-to-one by role. Each ordinary V1 user message becomes one V2 `user` row, and each ordinary
V1 assistant message becomes one V2 `assistant` row. Fold the source message's ordered V1 parts into that row's V2
payload.
Handle semantic marker parts before applying the ordinary mapping. In particular, a V1 user message containing a
`compaction` part and its paired assistant summary represent one compaction operation, not two ordinary messages. Special
part mappings must be decided explicitly before implementing the backfill.
V1 synthetic content is represented by user text parts with `synthetic: true`, not by a separate message role. A V1 user
message whose visible text parts are all synthetic should become a V2 `synthetic` message. If a V1 user message mixes
ordinary and synthetic content, preserve the ordinary content in the V2 `user` row and emit the synthetic content as an
adjacent V2 `synthetic` row. Ignore text parts marked `ignored`, matching V1 model-history behavior.
Use the V1 compaction user message ID as the ID of the collapsed V2 compaction message. This matches V2's use of the
admitted compaction input ID and preserves references to the initiating message.
For a completed compaction, create one V2 `compaction` row with `status: "completed"`. Set `reason` from the V1
compaction part's `auto` flag, join the paired summary assistant's nonempty text parts with blank lines for `summary`, and
serialize the retained V1 tail beginning at `tail_start_id` for `recent`. Use an empty `recent` value when no tail was
retained, and use the compaction user message creation time. Do not emit the paired summary assistant as a separate V2
assistant row.
After rebuilding `session_message`, seed `event_sequence` with one row per migrated session. Set its watermark to that
session's maximum backfilled `session_message.seq`. This prevents new V2 events from reusing sequence numbers or sorting
before migrated history. The `event` table remains empty.
## Drop
Drop these pre-launch V2 tables without preserving or transforming their rows:
- `session_input`
- `session_context_epoch`
Do not transfer `session_input` rows into `session_pending`.
## Create Empty
Let the generated migration create these tables empty:
- `instruction_blob`
- `instruction_entry`
- `instruction_state`
- `session_pending`
- `kv`
V1 has no canonical data to backfill into these tables. V2 initializes their state as it runs.
## Fork Storage
V1 has no fork-boundary state to backfill. New V2 forks use a required message boundary and persist it in
`session.fork_boundary`. The durable fork event contains no parent sequence. Its resolved boundary is one of:
- `before`: copy messages before the identified message.
- `through`: copy messages through the identified message.
Forking an empty session is not supported. `session.fork_seq` and `session.fork_message_id` are not part of the final V2
schema.
New nullable session columns, including `fork_session_id`, `fork_boundary`, and `time_suspended`, require no explicit
backfill. Existing rows naturally receive `NULL` when the generated migration adds the columns.
## Verification
The canonical migration test should seed representative V1 sessions, messages, parts, todos, projects, accounts,
credentials, permissions, shares, and workspaces. After migration, it should verify:
- Preserved rows and encoded values remain unchanged.
- Todo rows remain available in the unchanged `todo` table.
- `event` is empty, and stale pre-launch rows are absent from the rebuilt projections.
- Backfilled `session_message` rows represent the canonical V1 `message` and `part` history.
- Each migrated session's `event_sequence` watermark matches its maximum backfilled message sequence.
- Dropped tables no longer exist.
- New tables exist and are empty.
- The final schema has no ungenerated changes.
+3 -3
View File
@@ -46,7 +46,7 @@ const response = yield * LLMClient.generate(request)
`LLM.request(...)` builds an `LLMRequest`. `LLMClient.generate(...)` reads the executable route carried by `request.model.route`, builds the provider-native body, asks the route's transport for a real `HttpClientRequest.HttpClientRequest`, sends it through `RequestExecutor.Service`, parses the provider stream into common `LLMEvent`s, and finally returns an `LLMResponse`. `LLM.request(...)` builds an `LLMRequest`. `LLMClient.generate(...)` reads the executable route carried by `request.model.route`, builds the provider-native body, asks the route's transport for a real `HttpClientRequest.HttpClientRequest`, sends it through `RequestExecutor.Service`, parses the provider stream into common `LLMEvent`s, and finally returns an `LLMResponse`.
Use `LLMClient.stream(request)` when callers want incremental `LLMEvent`s. Use `LLMClient.generate(request)` when callers want those same events collected into an `LLMResponse`. Use `LLMClient.stream(request)` when callers want incremental `LLMEvent`s. Use `LLMClient.generate(request)` when callers want those same events collected into an `LLMResponse`. Use `LLMClient.prepare<Body>(request)` to compile a request through the route pipeline without sending it — the optional `Body` type argument narrows `.body` to the route's native shape (e.g. `prepare<OpenAIChatBody>(...)` returns a `PreparedRequestOf<OpenAIChatBody>`). The runtime body is identical; the generic is a type-level assertion.
Filter or narrow `LLMEvent` streams with `LLMEvent.is.*` (camelCase guards, e.g. `events.filter(LLMEvent.is.toolCall)`). The kebab-case `LLMEvent.guards["tool-call"]` form also works but prefer `is.*` in new code. Filter or narrow `LLMEvent` streams with `LLMEvent.is.*` (camelCase guards, e.g. `events.filter(LLMEvent.is.toolCall)`). The kebab-case `LLMEvent.guards["tool-call"]` form also works but prefer `is.*` in new code.
@@ -138,13 +138,13 @@ packages/ai/src/
ids.ts branded IDs, literal types, ProviderMetadata ids.ts branded IDs, literal types, ProviderMetadata
options.ts Generation/Provider/Http options, Limits, Model, cache policy options.ts Generation/Provider/Http options, Limits, Model, cache policy
messages.ts content parts, Message, ToolDefinition, LLMRequest messages.ts content parts, Message, ToolDefinition, LLMRequest
events.ts Usage, individual events, LLMEvent, LLMResponse events.ts Usage, individual events, LLMEvent, PreparedRequest, LLMResponse
errors.ts error reasons, LLMError, ToolFailure errors.ts error reasons, LLMError, ToolFailure
index.ts barrel index.ts barrel
llm.ts request constructors and convenience helpers llm.ts request constructors and convenience helpers
route/ route/
index.ts @opencode-ai/ai/route advanced barrel index.ts @opencode-ai/ai/route advanced barrel
client.ts Route.make + LLMClient.stream/generate client.ts Route.make + LLMClient.prepare/stream/generate
executor.ts RequestExecutor service + transport error mapping executor.ts RequestExecutor service + transport error mapping
protocol.ts Protocol type + Protocol.make protocol.ts Protocol type + Protocol.make
endpoint.ts Endpoint type + Endpoint.path endpoint.ts Endpoint type + Endpoint.path
+1
View File
@@ -196,6 +196,7 @@ The hosted result is represented as a provider-executed tool call and tool resul
- **`LLM.generate` / `LLM.stream`** — re-exported from `LLMClient` for one-import use. - **`LLM.generate` / `LLM.stream`** — re-exported from `LLMClient` for one-import use.
- **`Message.user(...)` / `Message.assistant(...)` / `Message.tool(...)`** — message constructors from the canonical schema model. - **`Message.user(...)` / `Message.assistant(...)` / `Message.tool(...)`** — message constructors from the canonical schema model.
- **`Model.make(...)` / `ToolCallPart.make(...)` / `ToolResultPart.make(...)` / `ToolDefinition.make(...)`** — model and tool-related constructors from the canonical schema model. - **`Model.make(...)` / `ToolCallPart.make(...)` / `ToolResultPart.make(...)` / `ToolDefinition.make(...)`** — model and tool-related constructors from the canonical schema model.
- **`LLMClient.prepare(request)`** — compile a request through protocol body construction, validation, and HTTP preparation without sending. Useful for inspection and testing.
- **`LLMEvent.is.*`** — typed guards (`is.textDelta`, `is.toolCall`, `is.finish`, …) for filtering streams. - **`LLMEvent.is.*`** — typed guards (`is.textDelta`, `is.toolCall`, `is.finish`, …) for filtering streams.
- **`Image.generate({...})`** — generate images through a provider-neutral image request and response model. - **`Image.generate({...})`** — generate images through a provider-neutral image request and response model.
- **`ImageClient`** — Effect service and layer for image execution, parallel to `LLMClient`. - **`ImageClient`** — Effect service and layer for image execution, parallel to `LLMClient`.
+1 -1
View File
@@ -568,7 +568,7 @@ App boundary = explicit durable-config -> typed-provider call
calling `.model(...)`. calling `.model(...)`.
- [x] Remove request-shaping defaults from `Model`; selected models now carry only - [x] Remove request-shaping defaults from `Model`; selected models now carry only
id, provider, and configured route while defaults live on routes or requests. id, provider, and configured route while defaults live on routes or requests.
- [x] Rework `LLMClient.stream` / `generate` to read - [x] Rework `LLMClient.prepare` / `stream` / `generate` to read
`request.model.route` directly instead of calling `registeredRoute(...)`. `request.model.route` directly instead of calling `registeredRoute(...)`.
- [x] Remove `Route.make(...)` global registration from the normal execution - [x] Remove `Route.make(...)` global registration from the normal execution
path; keep route ids only as diagnostics/provider API labels. path; keep route ids only as diagnostics/provider API labels.
+32 -2
View File
@@ -50,6 +50,18 @@ const request = LLM.request({
}, },
}) })
// `http` is intentionally not needed for normal calls. This shows the shape for
// newly released provider fields before they deserve a typed provider option.
const rawOverlayExample = LLM.request({
model,
prompt: "Show the final HTTP overlay shape.",
http: {
body: { metadata: { example: "tutorial" } },
headers: { "x-opencode-tutorial": "1" },
query: { debug: "1" },
},
})
// 3. `generate` sends the request and collects the event stream into one // 3. `generate` sends the request and collects the event stream into one
// response object. `response.text` is the collected text output. // response object. `response.text` is the collected text output.
const generateOnce = Effect.gen(function* () { const generateOnce = Effect.gen(function* () {
@@ -210,15 +222,33 @@ const FakeEcho = {
}), }),
} }
// `LLMClient.prepare` is the lower-level inspection hook: it compiles through
// body conversion, validation, endpoint, auth, and HTTP construction without
// sending anything over the network.
const inspectFakeProvider = Effect.gen(function* () {
const prepared = yield* LLMClient.prepare(
LLM.request({
model: FakeEcho.configure().model("tiny-echo"),
prompt: "Show me the provider pipeline.",
}),
)
console.log("\n== fake provider prepare ==")
console.log("route:", prepared.route)
console.log("body:", Formatter.formatJson(prepared.body, { space: 2 }))
})
// Provide the LLM runtime and the HTTP request executor once. Keep one path // Provide the LLM runtime and the HTTP request executor once. Keep one path
// enabled at a time so the tutorial can demonstrate generate, stream, or // enabled at a time so the tutorial can demonstrate generate, prepare, stream,
// tool-loop behavior without spending tokens on every example. // or tool-loop behavior without spending tokens on every example.
const requestExecutorLayer = RequestExecutor.fetchLayer const requestExecutorLayer = RequestExecutor.fetchLayer
const llmDeps = Layer.mergeAll(requestExecutorLayer, WebSocketExecutor.layer) const llmDeps = Layer.mergeAll(requestExecutorLayer, WebSocketExecutor.layer)
const llmClientLayer = LLMClient.layer.pipe(Layer.provide(llmDeps)) const llmClientLayer = LLMClient.layer.pipe(Layer.provide(llmDeps))
const program = Effect.gen(function* () { const program = Effect.gen(function* () {
// yield* generateOnce // yield* generateOnce
// yield* inspectFakeProvider
// yield* LLMClient.prepare(rawOverlayExample).pipe(Effect.andThen((prepared) => Effect.sync(() => console.log(prepared.body))))
// yield* streamText // yield* streamText
// yield* generateStructuredObject // yield* generateStructuredObject
// yield* generateDynamicObject.pipe(Effect.andThen((response) => Effect.sync(() => console.log(response.object)))) // yield* generateDynamicObject.pipe(Effect.andThen((response) => Effect.sync(() => console.log(response.object))))
+11 -16
View File
@@ -9,28 +9,25 @@ import {
LLMRequest, LLMRequest,
LLMResponse, LLMResponse,
Message, Message,
Model,
SystemPart, SystemPart,
ToolChoice, ToolChoice,
ToolDefinition, ToolDefinition,
type ContentPart, type ContentPart,
type ModelProviderOptions,
} from "./schema" } from "./schema"
import { make as makeTool, toDefinitions, type ToolSchema } from "./tool" import { make as makeTool, toDefinitions, type ToolSchema } from "./tool"
/** Input accepted by `LLM.request`, normalized into the canonical `LLMRequest` class. */ /** Input accepted by `LLM.request`, normalized into the canonical `LLMRequest` class. */
export type RequestInput<SelectedModel extends Model = Model> = Omit< export type RequestInput = Omit<
ConstructorParameters<typeof LLMRequest>[0], ConstructorParameters<typeof LLMRequest>[0],
"model" | "system" | "messages" | "tools" | "toolChoice" | "generation" | "http" | "providerOptions" "system" | "messages" | "tools" | "toolChoice" | "generation" | "http" | "providerOptions"
> & { > & {
readonly model: SelectedModel
readonly system?: string | SystemPart | ReadonlyArray<SystemPart> readonly system?: string | SystemPart | ReadonlyArray<SystemPart>
readonly prompt?: string | ContentPart | ReadonlyArray<ContentPart> readonly prompt?: string | ContentPart | ReadonlyArray<ContentPart>
readonly messages?: ReadonlyArray<Message | Message.Input> readonly messages?: ReadonlyArray<Message | Message.Input>
readonly tools?: ReadonlyArray<ToolDefinition.Input> readonly tools?: ReadonlyArray<ToolDefinition.Input>
readonly toolChoice?: ToolChoice.Input readonly toolChoice?: ToolChoice.Input
readonly generation?: GenerationOptions.Input readonly generation?: GenerationOptions.Input
readonly providerOptions?: NoInfer<ModelProviderOptions<SelectedModel>> readonly providerOptions?: ConstructorParameters<typeof LLMRequest>[0]["providerOptions"]
readonly http?: HttpOptions.Input readonly http?: HttpOptions.Input
} }
@@ -38,7 +35,7 @@ export const generate = LLMClient.generate
export const stream = LLMClient.stream export const stream = LLMClient.stream
export const request = <const SelectedModel extends Model>(input: RequestInput<SelectedModel>) => { export const request = (input: RequestInput) => {
const { const {
system: requestSystem, system: requestSystem,
prompt, prompt,
@@ -66,7 +63,7 @@ const GENERATE_OBJECT_TOOL_NAME = "generate_object"
const GENERATE_OBJECT_TOOL_DESCRIPTION = "Return the structured result by calling this tool." const GENERATE_OBJECT_TOOL_DESCRIPTION = "Return the structured result by calling this tool."
type GenerateObjectBase<SelectedModel extends Model = Model> = Omit<RequestInput<SelectedModel>, "tools" | "toolChoice"> type GenerateObjectBase = Omit<RequestInput, "tools" | "toolChoice">
export class GenerateObjectResponse<T> { export class GenerateObjectResponse<T> {
constructor( constructor(
@@ -83,13 +80,11 @@ export class GenerateObjectResponse<T> {
} }
} }
export interface GenerateObjectOptions<S extends ToolSchema<any>, SelectedModel extends Model = Model> export interface GenerateObjectOptions<S extends ToolSchema<any>> extends GenerateObjectBase {
extends GenerateObjectBase<SelectedModel> {
readonly schema: S readonly schema: S
} }
export interface GenerateObjectDynamicOptions<SelectedModel extends Model = Model> export interface GenerateObjectDynamicOptions extends GenerateObjectBase {
extends GenerateObjectBase<SelectedModel> {
/** Raw JSON Schema object describing the expected output shape. */ /** Raw JSON Schema object describing the expected output shape. */
readonly jsonSchema: JsonSchema.JsonSchema readonly jsonSchema: JsonSchema.JsonSchema
} }
@@ -142,11 +137,11 @@ const runGenerateObject = Effect.fn("LLM.generateObject")(function* (
* 2. `jsonSchema: JsonSchema.JsonSchema` — `.object` is `unknown`. Use when * 2. `jsonSchema: JsonSchema.JsonSchema` — `.object` is `unknown`. Use when
* the schema is only available at runtime (MCP, plugin manifests). Caller validates. * the schema is only available at runtime (MCP, plugin manifests). Caller validates.
*/ */
export function generateObject<const SelectedModel extends Model, S extends ToolSchema<any>>( export function generateObject<S extends ToolSchema<any>>(
options: GenerateObjectOptions<S, SelectedModel>, options: GenerateObjectOptions<S>,
): Effect.Effect<GenerateObjectResponse<Schema.Schema.Type<S>>, LLMError> ): Effect.Effect<GenerateObjectResponse<Schema.Schema.Type<S>>, LLMError>
export function generateObject<const SelectedModel extends Model>( export function generateObject(
options: GenerateObjectDynamicOptions<SelectedModel>, options: GenerateObjectDynamicOptions,
): Effect.Effect<GenerateObjectResponse<unknown>, LLMError> ): Effect.Effect<GenerateObjectResponse<unknown>, LLMError>
export function generateObject(options: GenerateObjectOptions<ToolSchema<any>> | GenerateObjectDynamicOptions) { export function generateObject(options: GenerateObjectOptions<ToolSchema<any>> | GenerateObjectDynamicOptions) {
if ("schema" in options) { if ("schema" in options) {
+3 -6
View File
@@ -1,4 +1,4 @@
import type { Model, ProviderOptions } from "./schema" import type { Model } from "./schema"
export interface Settings extends Readonly<Record<string, unknown>> { export interface Settings extends Readonly<Record<string, unknown>> {
readonly headers?: Readonly<Record<string, string>> readonly headers?: Readonly<Record<string, string>>
@@ -9,11 +9,8 @@ export interface Settings extends Readonly<Record<string, unknown>> {
} }
} }
export interface Definition< export interface Definition<ProviderSettings extends Settings = Settings> {
ProviderSettings extends Settings = Settings, readonly model: (modelID: string, settings: ProviderSettings) => Model
Options extends ProviderOptions = ProviderOptions,
> {
readonly model: (modelID: string, settings: ProviderSettings) => Model<Options>
} }
export * as ProviderPackage from "./provider-package" export * as ProviderPackage from "./provider-package"
@@ -47,7 +47,7 @@ export const configure = (input: Config) => {
}) })
return { return {
id: ProviderID.make(provider), id: ProviderID.make(provider),
model: (modelID: string | ModelID) => route.model<AnthropicMessages.ProviderOptionsInput>({ id: modelID }), model: (modelID: string | ModelID) => route.model({ id: modelID }),
configure, configure,
} }
} }
@@ -57,10 +57,7 @@ export const provider = {
configure, configure,
} }
export const model: ProviderPackage.Definition<Settings, AnthropicMessages.ProviderOptionsInput>["model"] = ( export const model: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) => {
modelID,
settings,
) => {
if (settings.apiKey !== undefined && settings.authToken !== undefined) if (settings.apiKey !== undefined && settings.authToken !== undefined)
throw new Error("Anthropic-compatible apiKey cannot be combined with authToken") throw new Error("Anthropic-compatible apiKey cannot be combined with authToken")
return configure({ return configure({
+1 -4
View File
@@ -52,10 +52,7 @@ export const configure = (input: Config = {}) => {
} }
export const provider = configure() export const provider = configure()
export const model: ProviderPackage.Definition<Settings, AnthropicMessages.ProviderOptionsInput>["model"] = ( export const model: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) => {
modelID,
settings,
) => {
if (settings.apiKey !== undefined && settings.authToken !== undefined) if (settings.apiKey !== undefined && settings.authToken !== undefined)
throw new Error("Anthropic apiKey cannot be combined with authToken") throw new Error("Anthropic apiKey cannot be combined with authToken")
return configure({ return configure({
+6 -14
View File
@@ -99,14 +99,10 @@ export const configure = (input: Config) => {
const modelDefaults = defaults(input) const modelDefaults = defaults(input)
const responses = (modelID: string | ModelID) => const responses = (modelID: string | ModelID) =>
configuredResponsesRoute configuredResponsesRoute.with(withOpenAIOptions(modelID, modelDefaults)).model({ id: modelID })
.with(withOpenAIOptions(modelID, modelDefaults))
.model<OpenAIProviderOptionsInput>({ id: modelID })
const chat = (modelID: string | ModelID) => const chat = (modelID: string | ModelID) =>
configuredChatRoute configuredChatRoute.with(withOpenAIOptions(modelID, modelDefaults)).model({ id: modelID })
.with(withOpenAIOptions(modelID, modelDefaults))
.model<OpenAIProviderOptionsInput>({ id: modelID })
return { return {
id, id,
@@ -137,12 +133,8 @@ const config = (settings: Settings): Config => {
throw new Error("Azure requires resourceName or baseURL") throw new Error("Azure requires resourceName or baseURL")
} }
export const responsesModel: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = ( export const responsesModel: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) =>
modelID, configure(config(settings)).responses(modelID)
settings, export const chatModel: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) =>
) => configure(config(settings)).responses(modelID) configure(config(settings)).chat(modelID)
export const chatModel: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (
modelID,
settings,
) => configure(config(settings)).chat(modelID)
export const model = responsesModel export const model = responsesModel
+4 -10
View File
@@ -4,7 +4,6 @@ import { Auth } from "../route/auth"
import { AuthOptions, type AtLeastOne, type ProviderAuthOption } from "../route/auth-options" import { AuthOptions, type AtLeastOne, type ProviderAuthOption } from "../route/auth-options"
import type { RouteDefaultsInput } from "../route/client" import type { RouteDefaultsInput } from "../route/client"
import { ProviderID, type ModelID } from "../schema" import { ProviderID, type ModelID } from "../schema"
import type { OpenAIProviderOptionsInput } from "./openai-options"
export const aiGatewayID = ProviderID.make("cloudflare-ai-gateway") export const aiGatewayID = ProviderID.make("cloudflare-ai-gateway")
export const workersAIID = ProviderID.make("cloudflare-workers-ai") export const workersAIID = ProviderID.make("cloudflare-workers-ai")
@@ -21,11 +20,10 @@ type GatewayURL = AtLeastOne<{
} }
export type AIGatewayOptions = GatewayURL & export type AIGatewayOptions = GatewayURL &
Omit<RouteDefaultsInput, "providerOptions"> & RouteDefaultsInput &
ProviderAuthOption<"optional"> & { ProviderAuthOption<"optional"> & {
/** Cloudflare AI Gateway authentication token. Sent as `cf-aig-authorization`. */ /** Cloudflare AI Gateway authentication token. Sent as `cf-aig-authorization`. */
readonly gatewayApiKey?: CloudflareSecret readonly gatewayApiKey?: CloudflareSecret
readonly providerOptions?: OpenAIProviderOptionsInput
} }
type WorkersAIURL = AtLeastOne<{ type WorkersAIURL = AtLeastOne<{
@@ -33,11 +31,7 @@ type WorkersAIURL = AtLeastOne<{
readonly baseURL: string readonly baseURL: string
}> }>
export type WorkersAIOptions = WorkersAIURL & export type WorkersAIOptions = WorkersAIURL & RouteDefaultsInput & ProviderAuthOption<"optional">
Omit<RouteDefaultsInput, "providerOptions"> &
ProviderAuthOption<"optional"> & {
readonly providerOptions?: OpenAIProviderOptionsInput
}
export const aiGatewayBaseURL = (input: GatewayURL) => { export const aiGatewayBaseURL = (input: GatewayURL) => {
if (input.baseURL) return input.baseURL if (input.baseURL) return input.baseURL
@@ -104,7 +98,7 @@ const configureAIGateway = (options: AIGatewayOptions) => {
}) })
return { return {
id: aiGatewayID, id: aiGatewayID,
model: (modelID: string | ModelID) => route.model<OpenAIProviderOptionsInput>({ id: modelID }), model: (modelID: string | ModelID) => route.model({ id: modelID }),
configure: configureAIGateway, configure: configureAIGateway,
} }
} }
@@ -117,7 +111,7 @@ const configureWorkersAI = (options: WorkersAIOptions) => {
}) })
return { return {
id: workersAIID, id: workersAIID,
model: (modelID: string | ModelID) => route.model<OpenAIProviderOptionsInput>({ id: modelID }), model: (modelID: string | ModelID) => route.model({ id: modelID }),
configure: configureWorkersAI, configure: configureWorkersAI,
} }
} }
+2 -4
View File
@@ -50,11 +50,9 @@ export const configure = (options: ModelOptions) => {
const responsesRoute = configuredResponsesRoute(options) const responsesRoute = configuredResponsesRoute(options)
const chatRoute = configuredChatRoute(options) const chatRoute = configuredChatRoute(options)
const responses = (modelID: string | ModelID) => const responses = (modelID: string | ModelID) =>
responsesRoute responsesRoute.with(withOpenAIOptions(modelID, defaults(options))).model({ id: modelID })
.with(withOpenAIOptions(modelID, defaults(options)))
.model<OpenAIProviderOptionsInput>({ id: modelID })
const chat = (modelID: string | ModelID) => const chat = (modelID: string | ModelID) =>
chatRoute.with(withOpenAIOptions(modelID, defaults(options))).model<OpenAIProviderOptionsInput>({ id: modelID }) chatRoute.with(withOpenAIOptions(modelID, defaults(options))).model({ id: modelID })
return { return {
id, id,
model: (modelID: string | ModelID) => model: (modelID: string | ModelID) =>
@@ -1,9 +1,8 @@
import type { ProviderPackage } from "../provider-package" import type { ProviderPackage } from "../provider-package"
import { OpenAICompatibleChat } from "../protocols/openai-compatible-chat" import { OpenAICompatibleChat } from "../protocols/openai-compatible-chat"
import type { RouteDefaultsInput } from "../route/client" import type { RouteDefaultsInput } from "../route/client"
import { ProviderID, type ModelID } from "../schema" import { ProviderID, type ModelID, type ProviderOptions } from "../schema"
import { GoogleVertexShared } from "./google-vertex-shared" import { GoogleVertexShared } from "./google-vertex-shared"
import type { OpenAIProviderOptionsInput } from "./openai-options"
export const id = ProviderID.make("google-vertex") export const id = ProviderID.make("google-vertex")
@@ -12,7 +11,6 @@ export type Config = RouteDefaultsInput &
readonly baseURL?: string readonly baseURL?: string
readonly location?: string readonly location?: string
readonly project?: string readonly project?: string
readonly providerOptions?: OpenAIProviderOptionsInput
} }
export interface Settings extends ProviderPackage.Settings { export interface Settings extends ProviderPackage.Settings {
@@ -21,7 +19,7 @@ export interface Settings extends ProviderPackage.Settings {
readonly baseURL?: string readonly baseURL?: string
readonly location?: string readonly location?: string
readonly project?: string readonly project?: string
readonly providerOptions?: OpenAIProviderOptionsInput readonly providerOptions?: ProviderOptions
} }
const route = OpenAICompatibleChat.route.with({ const route = OpenAICompatibleChat.route.with({
@@ -58,7 +56,7 @@ export const configure = (input: Config = {}) => {
const route = configuredRoute(input) const route = configuredRoute(input)
return { return {
id, id,
model: (modelID: string | ModelID) => route.model<OpenAIProviderOptionsInput>({ id: modelID }), model: (modelID: string | ModelID) => route.model({ id: modelID }),
configure, configure,
} }
} }
@@ -68,7 +66,7 @@ export const provider = {
configure, configure,
} }
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (modelID, settings) => { export const model: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) => {
if (settings.apiKey !== undefined) throw new Error("Google Vertex Chat does not support API keys") if (settings.apiKey !== undefined) throw new Error("Google Vertex Chat does not support API keys")
return configure({ return configure({
accessToken: settings.accessToken, accessToken: settings.accessToken,
@@ -91,7 +91,7 @@ export const configure = (input: Config = {}) => {
const route = configuredRoute(input) const route = configuredRoute(input)
return { return {
id, id,
model: (modelID: string | ModelID) => route.model<AnthropicMessages.ProviderOptionsInput>({ id: modelID }), model: (modelID: string | ModelID) => route.model({ id: modelID }),
configure, configure,
} }
} }
@@ -101,10 +101,7 @@ export const provider = {
configure, configure,
} }
export const model: ProviderPackage.Definition<Settings, AnthropicMessages.ProviderOptionsInput>["model"] = ( export const model: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) => {
modelID,
settings,
) => {
if (settings.apiKey !== undefined) throw new Error("Google Vertex Messages does not support API keys") if (settings.apiKey !== undefined) throw new Error("Google Vertex Messages does not support API keys")
return configure({ return configure({
accessToken: settings.accessToken, accessToken: settings.accessToken,
@@ -1,9 +1,8 @@
import type { ProviderPackage } from "../provider-package" import type { ProviderPackage } from "../provider-package"
import { OpenAICompatibleResponses } from "../protocols/openai-compatible-responses" import { OpenAICompatibleResponses } from "../protocols/openai-compatible-responses"
import type { RouteDefaultsInput } from "../route/client" import type { RouteDefaultsInput } from "../route/client"
import { ProviderID, type ModelID } from "../schema" import { ProviderID, type ModelID, type ProviderOptions } from "../schema"
import { GoogleVertexShared } from "./google-vertex-shared" import { GoogleVertexShared } from "./google-vertex-shared"
import type { OpenResponsesProviderOptionsInput } from "./open-responses-options"
export const id = ProviderID.make("google-vertex") export const id = ProviderID.make("google-vertex")
@@ -12,7 +11,6 @@ export type Config = RouteDefaultsInput &
readonly baseURL?: string readonly baseURL?: string
readonly location?: string readonly location?: string
readonly project?: string readonly project?: string
readonly providerOptions?: OpenResponsesProviderOptionsInput
} }
export interface Settings extends ProviderPackage.Settings { export interface Settings extends ProviderPackage.Settings {
@@ -21,7 +19,7 @@ export interface Settings extends ProviderPackage.Settings {
readonly baseURL?: string readonly baseURL?: string
readonly location?: string readonly location?: string
readonly project?: string readonly project?: string
readonly providerOptions?: OpenResponsesProviderOptionsInput readonly providerOptions?: ProviderOptions
} }
const route = OpenAICompatibleResponses.route.with({ const route = OpenAICompatibleResponses.route.with({
@@ -60,7 +58,7 @@ export const configure = (input: Config = {}) => {
const route = configuredRoute(input) const route = configuredRoute(input)
return { return {
id, id,
model: (modelID: string | ModelID) => route.model<OpenResponsesProviderOptionsInput>({ id: modelID }), model: (modelID: string | ModelID) => route.model({ id: modelID }),
configure, configure,
} }
} }
@@ -70,10 +68,7 @@ export const provider = {
configure, configure,
} }
export const model: ProviderPackage.Definition<Settings, OpenResponsesProviderOptionsInput>["model"] = ( export const model: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) => {
modelID,
settings,
) => {
if (settings.apiKey !== undefined) throw new Error("Google Vertex Responses does not support API keys") if (settings.apiKey !== undefined) throw new Error("Google Vertex Responses does not support API keys")
return configure({ return configure({
accessToken: settings.accessToken, accessToken: settings.accessToken,
+2 -6
View File
@@ -77,8 +77,7 @@ const configuredRoute = (input: Config, modelID: string | ModelID) => {
export const configure = (input: Config = {}) => { export const configure = (input: Config = {}) => {
return { return {
id, id,
model: (modelID: string | ModelID) => model: (modelID: string | ModelID) => configuredRoute(input, modelID).model({ id: modelID }),
configuredRoute(input, modelID).model<Gemini.ProviderOptionsInput>({ id: modelID }),
configure, configure,
} }
} }
@@ -87,10 +86,7 @@ export const provider = {
id, id,
configure, configure,
} }
export const model: ProviderPackage.Definition<Settings, Gemini.ProviderOptionsInput>["model"] = ( export const model: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) => {
modelID,
settings,
) => {
if (settings.apiKey !== undefined && settings.accessToken !== undefined) if (settings.apiKey !== undefined && settings.accessToken !== undefined)
throw new Error("Google Vertex apiKey cannot be combined with accessToken or auth") throw new Error("Google Vertex apiKey cannot be combined with accessToken or auth")
return configure({ return configure({
+2 -2
View File
@@ -50,14 +50,14 @@ export const configure = (input: Config = {}) => {
}) })
return { return {
id, id,
model: (modelID: string | ModelID) => route.model<Gemini.ProviderOptionsInput>({ id: modelID }), model: (modelID: string | ModelID) => route.model({ id: modelID }),
image, image,
configure, configure,
} }
} }
export const provider = configure() export const provider = configure()
export const model: ProviderPackage.Definition<Settings, Gemini.ProviderOptionsInput>["model"] = (modelID, settings) => export const model: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) =>
configure({ configure({
apiKey: settings.apiKey, apiKey: settings.apiKey,
baseURL: settings.baseURL, baseURL: settings.baseURL,
@@ -36,7 +36,7 @@ export const configure = (input: Config) => {
}) })
return { return {
id: ProviderID.make(provider), id: ProviderID.make(provider),
model: (modelID: string | ModelID) => route.model<OpenResponsesProviderOptionsInput>({ id: modelID }), model: (modelID: string | ModelID) => route.model({ id: modelID }),
configure, configure,
} }
} }
@@ -46,10 +46,7 @@ export const provider = {
configure, configure,
} }
export const model: ProviderPackage.Definition<Settings, OpenResponsesProviderOptionsInput>["model"] = ( export const model: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) =>
modelID,
settings,
) =>
configure({ configure({
apiKey: settings.apiKey, apiKey: settings.apiKey,
baseURL: settings.baseURL, baseURL: settings.baseURL,
@@ -4,15 +4,13 @@ import type { RouteDefaultsInput } from "../route/client"
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options" import { AuthOptions, type ProviderAuthOption } from "../route/auth-options"
import type { ProviderPackage } from "../provider-package" import type { ProviderPackage } from "../provider-package"
import { profiles, type OpenAICompatibleProfile } from "./openai-compatible-profile" import { profiles, type OpenAICompatibleProfile } from "./openai-compatible-profile"
import type { OpenAIProviderOptionsInput } from "./openai-options"
export const id = ProviderID.make("openai-compatible") export const id = ProviderID.make("openai-compatible")
type GenericModelOptions = Omit<RouteDefaultsInput, "providerOptions"> & type GenericModelOptions = RouteDefaultsInput &
ProviderAuthOption<"optional"> & { ProviderAuthOption<"optional"> & {
readonly provider?: string readonly provider?: string
readonly baseURL: string readonly baseURL: string
readonly providerOptions?: OpenAIProviderOptionsInput
} }
export interface Settings extends ProviderPackage.Settings { export interface Settings extends ProviderPackage.Settings {
@@ -21,10 +19,9 @@ export interface Settings extends ProviderPackage.Settings {
readonly provider?: string readonly provider?: string
} }
export type FamilyModelOptions = Omit<RouteDefaultsInput, "providerOptions"> & export type FamilyModelOptions = RouteDefaultsInput &
ProviderAuthOption<"optional"> & { ProviderAuthOption<"optional"> & {
readonly baseURL?: string readonly baseURL?: string
readonly providerOptions?: OpenAIProviderOptionsInput
} }
export const routes = [OpenAICompatibleChat.route] export const routes = [OpenAICompatibleChat.route]
@@ -40,8 +37,7 @@ export const configure = (input: GenericModelOptions) => {
}) })
return { return {
id: ProviderID.make(provider), id: ProviderID.make(provider),
model: (modelID: string | ModelID) => model: (modelID: string | ModelID) => route.model({ id: modelID, provider: ProviderID.make(provider) }),
route.model<OpenAIProviderOptionsInput>({ id: modelID, provider: ProviderID.make(provider) }),
configure, configure,
} }
} }
@@ -67,7 +63,7 @@ export const provider = {
configure, configure,
} }
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (modelID, settings) => export const model: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) =>
configure({ configure({
apiKey: settings.apiKey, apiKey: settings.apiKey,
baseURL: settings.baseURL, baseURL: settings.baseURL,
+6 -13
View File
@@ -86,15 +86,10 @@ export const configure = (input: Config = {}) => {
const chatRoute = configuredRoute(OpenAIChat.route, input) const chatRoute = configuredRoute(OpenAIChat.route, input)
const modelDefaults = defaults(input) const modelDefaults = defaults(input)
const responses = (id: string | ModelID) => const responses = (id: string | ModelID) =>
responsesRoute responsesRoute.with(withOpenAIOptions(id, modelDefaults, { textVerbosity: true })).model({ id })
.with(withOpenAIOptions(id, modelDefaults, { textVerbosity: true }))
.model<OpenAIProviderOptionsInput>({ id })
const responsesWebSocket = (id: string | ModelID) => const responsesWebSocket = (id: string | ModelID) =>
responsesWebSocketRoute responsesWebSocketRoute.with(withOpenAIOptions(id, modelDefaults, { textVerbosity: true })).model({ id })
.with(withOpenAIOptions(id, modelDefaults, { textVerbosity: true })) const chat = (id: string | ModelID) => chatRoute.with(withOpenAIOptions(id, modelDefaults)).model({ id })
.model<OpenAIProviderOptionsInput>({ id })
const chat = (id: string | ModelID) =>
chatRoute.with(withOpenAIOptions(id, modelDefaults)).model<OpenAIProviderOptionsInput>({ id })
const image = (modelID: string | ModelID) => const image = (modelID: string | ModelID) =>
OpenAIImages.model({ OpenAIImages.model({
id: modelID, id: modelID,
@@ -137,17 +132,15 @@ const config = (settings: Settings): Config => {
} }
} }
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (modelID, settings) => { export const model: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) => {
const configured = configure(config(settings)) const configured = configure(config(settings))
if (settings.transport === undefined || settings.transport === "http") return configured.responses(modelID) if (settings.transport === undefined || settings.transport === "http") return configured.responses(modelID)
if (settings.transport === "websocket") return configured.responsesWebSocket(modelID) if (settings.transport === "websocket") return configured.responsesWebSocket(modelID)
throw new Error(`Unsupported OpenAI Responses transport: ${String(settings.transport)}`) throw new Error(`Unsupported OpenAI Responses transport: ${String(settings.transport)}`)
} }
export const chatModel: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = ( export const chatModel: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) =>
modelID, configure(config(settings)).chat(modelID)
settings,
) => configure(config(settings)).chat(modelID)
export const responses = provider.responses export const responses = provider.responses
export const responsesWebSocket = provider.responsesWebSocket export const responsesWebSocket = provider.responsesWebSocket
export const chat = provider.chat export const chat = provider.chat
+1 -1
View File
@@ -107,7 +107,7 @@ export const configure = (input: ModelOptions = {}) => {
const route = configuredRoute(input) const route = configuredRoute(input)
return { return {
id, id,
model: (modelID: string | ModelID) => route.model<OpenRouterProviderOptionsInput>({ id: modelID }), model: (modelID: string | ModelID) => route.model({ id: modelID }),
configure, configure,
} }
} }
+3 -5
View File
@@ -5,14 +5,12 @@ import * as OpenAICompatibleProfiles from "./openai-compatible-profile"
import * as OpenAICompatibleChat from "../protocols/openai-compatible-chat" import * as OpenAICompatibleChat from "../protocols/openai-compatible-chat"
import * as OpenAIResponses from "../protocols/openai-responses" import * as OpenAIResponses from "../protocols/openai-responses"
import { XAIImages } from "../protocols/xai-images" import { XAIImages } from "../protocols/xai-images"
import type { OpenAIProviderOptionsInput } from "./openai-options"
export const id = ProviderID.make("xai") export const id = ProviderID.make("xai")
export type ModelOptions = Omit<RouteDefaultsInput, "providerOptions"> & export type ModelOptions = RouteDefaultsInput &
ProviderAuthOption<"optional"> & { ProviderAuthOption<"optional"> & {
readonly baseURL?: string readonly baseURL?: string
readonly providerOptions?: OpenAIProviderOptionsInput
} }
export type { XAIImageOptions } from "../protocols/xai-images" export type { XAIImageOptions } from "../protocols/xai-images"
@@ -44,8 +42,8 @@ const configuredChatRoute = (input: ModelOptions) => {
export const configure = (input: ModelOptions = {}) => { export const configure = (input: ModelOptions = {}) => {
const responsesRoute = configuredResponsesRoute(input) const responsesRoute = configuredResponsesRoute(input)
const chatRoute = configuredChatRoute(input) const chatRoute = configuredChatRoute(input)
const responses = (modelID: string | ModelID) => responsesRoute.model<OpenAIProviderOptionsInput>({ id: modelID }) const responses = (modelID: string | ModelID) => responsesRoute.model({ id: modelID })
const chat = (modelID: string | ModelID) => chatRoute.model<OpenAIProviderOptionsInput>({ id: modelID }) const chat = (modelID: string | ModelID) => chatRoute.model({ id: modelID })
const image = (modelID: string | ModelID) => const image = (modelID: string | ModelID) =>
XAIImages.model({ XAIImages.model({
id: modelID, id: modelID,
+29 -11
View File
@@ -10,7 +10,7 @@ import { WebSocketExecutor } from "./transport"
import type { Protocol } from "./protocol" import type { Protocol } from "./protocol"
import { applyCachePolicy } from "../cache-policy" import { applyCachePolicy } from "../cache-policy"
import * as ProviderShared from "../protocols/shared" import * as ProviderShared from "../protocols/shared"
import type { LLMError, ProtocolID, ProviderOptions } from "../schema" import type { LLMError, PreparedRequestOf, ProtocolID, ProviderOptions } from "../schema"
import { import {
GenerationOptions, GenerationOptions,
HttpOptions, HttpOptions,
@@ -20,6 +20,7 @@ import {
ModelLimits, ModelLimits,
LLMError as LLMErrorClass, LLMError as LLMErrorClass,
LLMEvent, LLMEvent,
PreparedRequest,
ProviderID, ProviderID,
mergeGenerationOptions, mergeGenerationOptions,
mergeHttpOptions, mergeHttpOptions,
@@ -45,7 +46,7 @@ export interface Route<Body, Prepared = unknown> {
readonly defaults: RouteDefaults readonly defaults: RouteDefaults
readonly body: RouteBody<Body> readonly body: RouteBody<Body>
readonly with: (patch: RoutePatch<Body, Prepared>) => Route<Body, Prepared> readonly with: (patch: RoutePatch<Body, Prepared>) => Route<Body, Prepared>
readonly model: <Options extends ProviderOptions = ProviderOptions>(input: RouteMappedModelInput) => Model<Options> readonly model: (input: RouteMappedModelInput) => Model
readonly prepareTransport: (body: Body, request: LLMRequest) => Effect.Effect<Prepared, LLMError> readonly prepareTransport: (body: Body, request: LLMRequest) => Effect.Effect<Prepared, LLMError>
readonly streamPrepared: ( readonly streamPrepared: (
prepared: Prepared, prepared: Prepared,
@@ -92,12 +93,12 @@ export interface RoutePatch<Body, Prepared> extends RouteDefaultsInput {
type RouteMappedModelInput = RouteModelInput | RouteRoutedModelInput type RouteMappedModelInput = RouteModelInput | RouteRoutedModelInput
const makeRouteModel = <Options extends ProviderOptions = ProviderOptions>(route: AnyRoute, mapped: RouteMappedModelInput) => { const makeRouteModel = (route: AnyRoute, mapped: RouteMappedModelInput) => {
const provider = route.provider ?? ("provider" in mapped ? mapped.provider : undefined) const provider = route.provider ?? ("provider" in mapped ? mapped.provider : undefined)
if (!provider) throw new Error(`Route.model(${route.id}) requires a provider`) if (!provider) throw new Error(`Route.model(${route.id}) requires a provider`)
if (!endpointBaseURL(route.endpoint)) if (!endpointBaseURL(route.endpoint))
throw new Error(`Route.model(${route.id}) requires an endpoint baseURL — configure it on the route first`) throw new Error(`Route.model(${route.id}) requires an endpoint baseURL — configure it on the route first`)
return Model.make<Options>({ return Model.make({
...mapped, ...mapped,
provider, provider,
route, route,
@@ -141,6 +142,17 @@ export const httpOptions = (input: HttpOptionsInput | undefined) => {
} }
export interface Interface { export interface Interface {
/**
* Compile a request through protocol body construction, validation, and HTTP
* preparation without sending it. Returns the prepared request including the
* provider-native body.
*
* Pass a `Body` type argument to statically expose the route's body
* shape (e.g. `prepare<OpenAIChatBody>(...)`) — the runtime body is
* identical, so this is a type-level assertion the caller makes about which
* route the request will resolve to.
*/
readonly prepare: <Body = unknown>(request: LLMRequest) => Effect.Effect<PreparedRequestOf<Body>, LLMError>
readonly stream: StreamMethod readonly stream: StreamMethod
readonly generate: GenerateMethod readonly generate: GenerateMethod
} }
@@ -284,8 +296,7 @@ function makeFromTransport<Body, Prepared, Frame, Event, State>(
defaults: mergeRouteDefaults(route.defaults, defaults), defaults: mergeRouteDefaults(route.defaults, defaults),
}) })
}, },
model: <Options extends ProviderOptions = ProviderOptions>(input: RouteMappedModelInput) => model: (input) => makeRouteModel(route, input),
makeRouteModel<Options>(route, input),
prepareTransport: (body, request) => prepareTransport: (body, request) =>
routeInput.transport.prepare({ routeInput.transport.prepare({
body, body,
@@ -359,6 +370,9 @@ export function make<Body, Prepared, Frame, Event, State>(
}) })
} }
// `compile` is the important boundary: it turns a common `LLMRequest` into a
// validated provider body plus transport-private prepared data, but does not
// execute transport.
const compile = Effect.fn("LLM.compile")(function* (request: LLMRequest) { const compile = Effect.fn("LLM.compile")(function* (request: LLMRequest) {
const resolved = applyCachePolicy(resolveRequestOptions(request)) const resolved = applyCachePolicy(resolveRequestOptions(request))
const route = resolved.model.route const route = resolved.model.route
@@ -376,17 +390,17 @@ const compile = Effect.fn("LLM.compile")(function* (request: LLMRequest) {
} }
}) })
/** @internal Test-only projection of the execution compiler; not exported from package barrels. */ const prepareWith = Effect.fn("LLMClient.prepare")(function* (request: LLMRequest) {
export const compileRequest = Effect.fn("LLM.compileRequest")(function* (request: LLMRequest) {
const compiled = yield* compile(request) const compiled = yield* compile(request)
return {
return new PreparedRequest({
id: compiled.request.id ?? "request", id: compiled.request.id ?? "request",
route: compiled.route.id, route: compiled.route.id,
protocol: compiled.route.protocol, protocol: compiled.route.protocol,
model: compiled.request.model, model: compiled.request.model,
body: compiled.body, body: compiled.body,
metadata: { transport: compiled.route.transport.id }, metadata: { transport: compiled.route.transport.id },
} })
}) })
const streamRequestWith = (runtime: TransportRuntime) => (request: LLMRequest) => const streamRequestWith = (runtime: TransportRuntime) => (request: LLMRequest) =>
@@ -408,6 +422,9 @@ const generateWith = (stream: Interface["stream"]) =>
) )
}) })
export const prepare = <Body = unknown>(request: LLMRequest) =>
prepareWith(request) as Effect.Effect<PreparedRequestOf<Body>, LLMError>
export function stream(request: LLMRequest): Stream.Stream<LLMEvent, LLMError> { export function stream(request: LLMRequest): Stream.Stream<LLMEvent, LLMError> {
return Stream.unwrap( return Stream.unwrap(
Effect.gen(function* () { Effect.gen(function* () {
@@ -436,7 +453,7 @@ export const layer: Layer.Layer<Service, never, RequestExecutor.Service> = Layer
http: yield* RequestExecutor.Service, http: yield* RequestExecutor.Service,
webSocket: Option.getOrUndefined(yield* Effect.serviceOption(WebSocketExecutor.Service)), webSocket: Option.getOrUndefined(yield* Effect.serviceOption(WebSocketExecutor.Service)),
}) })
return Service.of({ stream, generate: generateWith(stream) }) return Service.of({ prepare: prepareWith as Interface["prepare"], stream, generate: generateWith(stream) })
}), }),
) )
@@ -445,6 +462,7 @@ export const Route = { make } as const
export const LLMClient = { export const LLMClient = {
Service, Service,
layer, layer,
prepare,
stream, stream,
generate, generate,
} as const } as const
+25 -1
View File
@@ -1,5 +1,6 @@
import { Schema } from "effect" import { Schema } from "effect"
import { ContentBlockID, FinishReason, ProviderMetadata, ToolCallID } from "./ids" import { ContentBlockID, FinishReason, ProtocolID, ProviderMetadata, RouteID, ToolCallID } from "./ids"
import { ModelSchema } from "./options"
import { Message, ToolCallPart, ToolOutput, ToolResultPart, ToolResultValue, type ContentPart } from "./messages" import { Message, ToolCallPart, ToolOutput, ToolResultPart, ToolResultValue, type ContentPart } from "./messages"
import { ProviderFailureClassification } from "./errors" import { ProviderFailureClassification } from "./errors"
@@ -313,6 +314,29 @@ export const LLMEvent = Object.assign(llmEventTagged, {
}) })
export type LLMEvent = Schema.Schema.Type<typeof llmEventTagged> export type LLMEvent = Schema.Schema.Type<typeof llmEventTagged>
export class PreparedRequest extends Schema.Class<PreparedRequest>("LLM.PreparedRequest")({
id: Schema.String,
route: RouteID,
protocol: ProtocolID,
model: ModelSchema,
body: Schema.Unknown,
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
}) {}
/**
* A `PreparedRequest` whose `body` is typed as `Body`. Use with the generic
* on `LLMClient.prepare<Body>(...)` when the caller knows which route their
* request will resolve to and wants its native shape statically exposed
* (debug UIs, request previews, plan rendering).
*
* The runtime body is identical — the route still emits `body: unknown` — so
* this is a type-level assertion the caller makes about what they expect to
* find. The prepare runtime does not validate the assertion.
*/
export type PreparedRequestOf<Body> = Omit<PreparedRequest, "body"> & {
readonly body: Body
}
const responseText = (events: ReadonlyArray<LLMEvent>) => const responseText = (events: ReadonlyArray<LLMEvent>) =>
events events
.filter(LLMEvent.is.textDelta) .filter(LLMEvent.is.textDelta)
+6 -9
View File
@@ -178,8 +178,7 @@ export namespace ModelCompatibility {
export const make = (input: Input) => (input instanceof ModelCompatibility ? input : new ModelCompatibility(input)) export const make = (input: Input) => (input instanceof ModelCompatibility ? input : new ModelCompatibility(input))
} }
export class Model<Options extends ProviderOptions = ProviderOptions> { export class Model {
declare protected readonly _ProviderOptions: Options
readonly id: ModelID readonly id: ModelID
readonly provider: ProviderID readonly provider: ProviderID
readonly route: AnyRoute readonly route: AnyRoute
@@ -194,8 +193,8 @@ export class Model<Options extends ProviderOptions = ProviderOptions> {
this.compatibility = input.compatibility this.compatibility = input.compatibility
} }
static make<Options extends ProviderOptions = ProviderOptions>(input: Model.Input) { static make(input: Model.Input) {
return new Model<Options>({ return new Model({
id: ModelID.make(input.id), id: ModelID.make(input.id),
provider: ProviderID.make(input.provider), provider: ProviderID.make(input.provider),
route: input.route, route: input.route,
@@ -204,7 +203,7 @@ export class Model<Options extends ProviderOptions = ProviderOptions> {
}) })
} }
static input<Options extends ProviderOptions>(model: Model<Options>): Model.ConstructorInput { static input(model: Model): Model.ConstructorInput {
return { return {
id: model.id, id: model.id,
provider: model.provider, provider: model.provider,
@@ -214,9 +213,9 @@ export class Model<Options extends ProviderOptions = ProviderOptions> {
} }
} }
static update<Options extends ProviderOptions>(model: Model<Options>, patch: Partial<Model.Input>) { static update(model: Model, patch: Partial<Model.Input>) {
if (Object.keys(patch).length === 0) return model if (Object.keys(patch).length === 0) return model
return Model.make<Options>({ return Model.make({
...Model.input(model), ...Model.input(model),
...patch, ...patch,
}) })
@@ -242,8 +241,6 @@ export namespace Model {
export type ModelInput = Model.Input export type ModelInput = Model.Input
export type ModelProviderOptions<SelectedModel> = SelectedModel extends Model<infer Options> ? Options : never
export const ModelSchema = Schema.declare((value): value is Model => value instanceof Model, { expected: "LLM.Model" }) export const ModelSchema = Schema.declare((value): value is Model => value instanceof Model, { expected: "LLM.Model" })
export class CacheHint extends Schema.Class<CacheHint>("LLM.CacheHint")({ export class CacheHint extends Schema.Class<CacheHint>("LLM.CacheHint")({
+1
View File
@@ -99,6 +99,7 @@ export const layer = (options: LayerOptions = {}) =>
) )
}) as LLMClientShape["stream"] }) as LLMClientShape["stream"]
const client = LLMClient.Service.of({ const client = LLMClient.Service.of({
prepare: () => Effect.die("TestLLM does not prepare provider-native requests"),
stream, stream,
generate: (request) => generate: (request) =>
stream(request).pipe( stream(request).pipe(
+3 -3
View File
@@ -2,7 +2,6 @@ import { describe, expect } from "bun:test"
import { Effect, Schema, Stream } from "effect" import { Effect, Schema, Stream } from "effect"
import { LLM, LLMRequest, LLMResponse } from "../src" import { LLM, LLMRequest, LLMResponse } from "../src"
import { Route, Endpoint, LLMClient, Protocol, type FramingDef } from "../src/route" import { Route, Endpoint, LLMClient, Protocol, type FramingDef } from "../src/route"
import { compileRequest } from "../src/route/client"
import { Model } from "../src/schema" import { Model } from "../src/schema"
import { testEffect } from "./lib/effect" import { testEffect } from "./lib/effect"
import { dynamicResponse } from "./lib/http" import { dynamicResponse } from "./lib/http"
@@ -140,7 +139,8 @@ describe("llm route", () => {
it.effect("selects routes by model route value", () => it.effect("selects routes by model route value", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const llm = yield* LLMClient.Service
const prepared = yield* llm.prepare(
LLMRequest.update(request, { model: updateModel(request.model, { route: configuredGemini }) }), LLMRequest.update(request, { model: updateModel(request.model, { route: configuredGemini }) }),
) )
@@ -173,7 +173,7 @@ describe("llm route", () => {
framing: fakeFraming, framing: fakeFraming,
}) })
const prepared = yield* compileRequest( const prepared = yield* (yield* LLMClient.Service).prepare(
LLMRequest.update(request, { model: updateModel(request.model, { route: duplicate }) }), LLMRequest.update(request, { model: updateModel(request.model, { route: duplicate }) }),
) )
+13 -14
View File
@@ -1,8 +1,7 @@
import { describe, expect, test } from "bun:test" import { describe, expect, test } from "bun:test"
import { Effect } from "effect" import { Effect } from "effect"
import { CacheHint, LLM, Message } from "../src" import { CacheHint, LLM, Message } from "../src"
import { Auth } from "../src/route" import { Auth, LLMClient } from "../src/route"
import { compileRequest } from "../src/route/client"
import { AmazonBedrock } from "../src/providers" import { AmazonBedrock } from "../src/providers"
import * as AnthropicMessages from "../src/protocols/anthropic-messages" import * as AnthropicMessages from "../src/protocols/anthropic-messages"
import * as Gemini from "../src/protocols/gemini" import * as Gemini from "../src/protocols/gemini"
@@ -32,7 +31,7 @@ const geminiModel = Gemini.route
describe("applyCachePolicy", () => { describe("applyCachePolicy", () => {
it.effect("undefined cache resolves to 'auto' (the recommended default)", () => it.effect("undefined cache resolves to 'auto' (the recommended default)", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLM.request({ LLM.request({
model: anthropicModel, model: anthropicModel,
system: "You are concise.", system: "You are concise.",
@@ -51,7 +50,7 @@ describe("applyCachePolicy", () => {
it.effect("'auto' marks the last tool, first and last system parts, and final message boundary on Anthropic", () => it.effect("'auto' marks the last tool, first and last system parts, and final message boundary on Anthropic", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLM.request({ LLM.request({
model: anthropicModel, model: anthropicModel,
system: [ system: [
@@ -88,7 +87,7 @@ describe("applyCachePolicy", () => {
it.effect("'auto' is a no-op on OpenAI (implicit caching protocol)", () => it.effect("'auto' is a no-op on OpenAI (implicit caching protocol)", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLM.request({ LLM.request({
model: openaiModel, model: openaiModel,
system: "Sys", system: "Sys",
@@ -107,7 +106,7 @@ describe("applyCachePolicy", () => {
it.effect("'auto' is a no-op on Gemini (out-of-band caching protocol)", () => it.effect("'auto' is a no-op on Gemini (out-of-band caching protocol)", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLM.request({ LLM.request({
model: geminiModel, model: geminiModel,
system: "Sys", system: "Sys",
@@ -124,7 +123,7 @@ describe("applyCachePolicy", () => {
it.effect("'auto' on Bedrock emits cachePoint markers in the right places", () => it.effect("'auto' on Bedrock emits cachePoint markers in the right places", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLM.request({ LLM.request({
model: bedrockModel, model: bedrockModel,
system: [ system: [
@@ -158,7 +157,7 @@ describe("applyCachePolicy", () => {
it.effect("'none' disables auto placement even when manual hints exist", () => it.effect("'none' disables auto placement even when manual hints exist", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLM.request({ LLM.request({
model: anthropicModel, model: anthropicModel,
system: "Sys", system: "Sys",
@@ -177,7 +176,7 @@ describe("applyCachePolicy", () => {
it.effect("granular object form: tools-only marks just tools", () => it.effect("granular object form: tools-only marks just tools", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLM.request({ LLM.request({
model: anthropicModel, model: anthropicModel,
system: "Sys", system: "Sys",
@@ -196,7 +195,7 @@ describe("applyCachePolicy", () => {
it.effect("auto policy preserves manual CacheHints on other parts", () => it.effect("auto policy preserves manual CacheHints on other parts", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLM.request({ LLM.request({
model: anthropicModel, model: anthropicModel,
system: [ system: [
@@ -242,7 +241,7 @@ describe("applyCachePolicy", () => {
expect("cache" in tail ? tail.cache : undefined).toBeUndefined() expect("cache" in tail ? tail.cache : undefined).toBeUndefined()
expect(applyCachePolicy(applied)).toBe(applied) expect(applyCachePolicy(applied)).toBe(applied)
const prepared = yield* compileRequest(request) const prepared = yield* LLMClient.prepare(request)
const body = prepared.body as { const body = prepared.body as {
tools: Array<{ cache_control?: unknown }> tools: Array<{ cache_control?: unknown }>
@@ -262,7 +261,7 @@ describe("applyCachePolicy", () => {
it.effect("ttlSeconds in the policy flows through to wire markers", () => it.effect("ttlSeconds in the policy flows through to wire markers", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLM.request({ LLM.request({
model: anthropicModel, model: anthropicModel,
system: "Sys", system: "Sys",
@@ -279,7 +278,7 @@ describe("applyCachePolicy", () => {
it.effect("messages: { tail: 2 } marks the last 2 message boundaries", () => it.effect("messages: { tail: 2 } marks the last 2 message boundaries", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLM.request({ LLM.request({
model: anthropicModel, model: anthropicModel,
messages: [Message.user("u1"), Message.assistant("a1"), Message.user("u2"), Message.assistant("a2")], messages: [Message.user("u1"), Message.assistant("a1"), Message.user("u2"), Message.assistant("a2")],
@@ -297,7 +296,7 @@ describe("applyCachePolicy", () => {
it.effect("'latest-assistant' marks the last assistant message", () => it.effect("'latest-assistant' marks the last assistant message", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLM.request({ LLM.request({
model: anthropicModel, model: anthropicModel,
messages: [Message.user("u1"), Message.assistant("a1"), Message.user("u2")], messages: [Message.user("u1"), Message.assistant("a1"), Message.user("u2")],
@@ -1,47 +0,0 @@
import { Schema } from "effect"
import { LLM, type Model, type ModelProviderOptions, type ProviderOptions } from "../src"
import { OpenAIChat } from "../src/protocols"
interface ExampleOptions {
readonly [key: string]: unknown
readonly mode?: "fast" | "thorough"
}
type ExampleProviderOptions = ProviderOptions & {
readonly example?: ExampleOptions
}
const model = OpenAIChat.route
.with({ endpoint: { baseURL: "https://example.com/v1" } })
.model<ExampleProviderOptions>({ id: "example" })
LLM.request({ model, prompt: "Hello", providerOptions: { example: { mode: "fast" } } })
LLM.request({ model, prompt: "Hello", providerOptions: { future: { option: true } } })
LLM.request({
model,
prompt: "Hello",
// @ts-expect-error Known provider options preserve their value types.
providerOptions: { example: { mode: "slow" } },
})
LLM.generateObject({
model,
prompt: "Hello",
schema: Schema.Struct({ answer: Schema.String }),
providerOptions: { example: { mode: "thorough" } },
})
LLM.generateObject({
model,
prompt: "Hello",
jsonSchema: { type: "object" },
// @ts-expect-error Dynamic object generation uses the selected model's provider options.
providerOptions: { example: { mode: false } },
})
declare const generic: Model
LLM.request({ model: generic, prompt: "Hello", providerOptions: { arbitrary: { option: true } } })
const options: ModelProviderOptions<typeof model> = { example: { mode: "fast" } }
void options
@@ -4,7 +4,6 @@ import { HttpClientRequest } from "effect/unstable/http"
import { LLM, mergeProviderOptions } from "../src" import { LLM, mergeProviderOptions } from "../src"
import { AnthropicMessages, OpenAIChat } from "../src/protocols" import { AnthropicMessages, OpenAIChat } from "../src/protocols"
import { Auth, LLMClient } from "../src/route" import { Auth, LLMClient } from "../src/route"
import { compileRequest } from "../src/route/client"
import { it } from "./lib/effect" import { it } from "./lib/effect"
import { dynamicResponse } from "./lib/http" import { dynamicResponse } from "./lib/http"
import { deltaChunk } from "./lib/openai-chunks" import { deltaChunk } from "./lib/openai-chunks"
@@ -45,7 +44,7 @@ describe("request option precedence", () => {
}) })
}) })
it.effect("compiles bodies with route defaults, model defaults, and call options in order", () => it.effect("prepares bodies with route defaults, model defaults, and call options in order", () =>
Effect.gen(function* () { Effect.gen(function* () {
const route = OpenAIChat.route.with({ const route = OpenAIChat.route.with({
endpoint: { baseURL: "https://api.openai.test/v1/" }, endpoint: { baseURL: "https://api.openai.test/v1/" },
@@ -60,7 +59,7 @@ describe("request option precedence", () => {
providerOptions: { openai: { reasoningEffort: "medium" } }, providerOptions: { openai: { reasoningEffort: "medium" } },
}, },
}) })
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
LLM.request({ LLM.request({
model, model,
prompt: "Say hello.", prompt: "Say hello.",
@@ -142,7 +141,7 @@ describe("request option precedence", () => {
const model = OpenAIChat.route const model = OpenAIChat.route
.with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") }) .with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") })
.model({ id: "gpt-4o-mini" }) .model({ id: "gpt-4o-mini" })
const error = yield* compileRequest( const error = yield* LLMClient.prepare(
LLM.request({ LLM.request({
model, model,
prompt: "Say hello.", prompt: "Say hello.",
@@ -165,8 +164,10 @@ describe("request option precedence", () => {
limits: { output: 128 }, limits: { output: 128 },
}) })
const model = route.model({ id: "claude-sonnet-4-5", defaults: { limits: { output: 64 } } }) const model = route.model({ id: "claude-sonnet-4-5", defaults: { limits: { output: 64 } } })
const withoutMaxTokens = yield* compileRequest(LLM.request({ model, prompt: "Say hello.", cache: "none" })) const withoutMaxTokens = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
const withMaxTokens = yield* compileRequest( LLM.request({ model, prompt: "Say hello.", cache: "none" }),
)
const withMaxTokens = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
LLM.request({ model, prompt: "Say hello.", cache: "none", generation: { maxTokens: 32 } }), LLM.request({ model, prompt: "Say hello.", cache: "none", generation: { maxTokens: 32 } }),
) )
@@ -1,13 +0,0 @@
import { LLM } from "../../src"
import { AnthropicCompatible } from "../../src/providers"
const model = AnthropicCompatible.configure({ baseURL: "https://example.com" }).model("claude")
LLM.request({ model, prompt: "Hello", providerOptions: { anthropic: { effort: "high" } } })
LLM.request({
model,
prompt: "Hello",
// @ts-expect-error Anthropic effort must be a string.
providerOptions: { anthropic: { effort: 1 } },
})
@@ -1,13 +0,0 @@
import { LLM } from "../../src"
import { Anthropic } from "../../src/providers"
const model = Anthropic.provider.model("claude-sonnet-4-5")
LLM.request({ model, prompt: "Hello", providerOptions: { anthropic: { thinking: { type: "adaptive" } } } })
LLM.request({
model,
prompt: "Hello",
// @ts-expect-error Anthropic thinking modes are a fixed union.
providerOptions: { anthropic: { thinking: { type: "automatic" } } },
})
@@ -1,13 +0,0 @@
import { LLM } from "../../src"
import { Azure } from "../../src/providers"
const model = Azure.configure({ resourceName: "example" }).responses("deployment")
LLM.request({ model, prompt: "Hello", providerOptions: { openai: { store: false } } })
LLM.request({
model,
prompt: "Hello",
// @ts-expect-error Azure OpenAI store must be boolean.
providerOptions: { openai: { store: "false" } },
})
@@ -1,13 +0,0 @@
import { LLM } from "../../src"
import { CloudflareWorkersAI } from "../../src/providers"
const model = CloudflareWorkersAI.configure({ accountId: "account", apiKey: "test" }).model("model")
LLM.request({ model, prompt: "Hello", providerOptions: { openai: { promptCacheKey: "cache" } } })
LLM.request({
model,
prompt: "Hello",
// @ts-expect-error Cloudflare's OpenAI-compatible prompt cache key must be a string.
providerOptions: { openai: { promptCacheKey: 1 } },
})
@@ -1,13 +0,0 @@
import { LLM } from "../../src"
import { GitHubCopilot } from "../../src/providers"
const model = GitHubCopilot.configure({ baseURL: "https://example.com" }).model("gpt-5")
LLM.request({ model, prompt: "Hello", providerOptions: { openai: { reasoningSummary: "auto" } } })
LLM.request({
model,
prompt: "Hello",
// @ts-expect-error Copilot reasoning summaries use the OpenAI union.
providerOptions: { openai: { reasoningSummary: "full" } },
})
@@ -1,13 +0,0 @@
import { LLM } from "../../src"
import { GoogleVertexChat } from "../../src/providers"
const model = GoogleVertexChat.configure({ accessToken: "test", project: "project" }).model("gemini")
LLM.request({ model, prompt: "Hello", providerOptions: { openai: { serviceTier: "priority" } } })
LLM.request({
model,
prompt: "Hello",
// @ts-expect-error Vertex OpenAI-compatible service tiers use the OpenAI union.
providerOptions: { openai: { serviceTier: "premium" } },
})
@@ -1,13 +0,0 @@
import { LLM } from "../../src"
import { GoogleVertexMessages } from "../../src/providers"
const model = GoogleVertexMessages.configure({ accessToken: "test", project: "project" }).model("claude")
LLM.request({ model, prompt: "Hello", providerOptions: { anthropic: { effort: "medium" } } })
LLM.request({
model,
prompt: "Hello",
// @ts-expect-error Vertex Anthropic effort must be a string.
providerOptions: { anthropic: { effort: false } },
})
@@ -1,13 +0,0 @@
import { LLM } from "../../src"
import { GoogleVertexResponses } from "../../src/providers"
const model = GoogleVertexResponses.configure({ accessToken: "test", project: "project" }).model("gemini")
LLM.request({ model, prompt: "Hello", providerOptions: { openresponses: { textVerbosity: "high" } } })
LLM.request({
model,
prompt: "Hello",
// @ts-expect-error Vertex Responses verbosity uses the Open Responses union.
providerOptions: { openresponses: { textVerbosity: "verbose" } },
})
@@ -1,17 +0,0 @@
import { LLM } from "../../src"
import { GoogleVertex } from "../../src/providers"
const model = GoogleVertex.provider.configure({ apiKey: "test" }).model("gemini-2.5-pro")
LLM.request({
model,
prompt: "Hello",
providerOptions: { gemini: { thinkingConfig: { includeThoughts: true } } },
})
LLM.request({
model,
prompt: "Hello",
// @ts-expect-error Vertex Gemini includeThoughts must be boolean.
providerOptions: { gemini: { thinkingConfig: { includeThoughts: "yes" } } },
})
@@ -1,17 +0,0 @@
import { LLM } from "../../src"
import { Google } from "../../src/providers"
const model = Google.provider.model("gemini-2.5-pro")
LLM.request({
model,
prompt: "Hello",
providerOptions: { gemini: { thinkingConfig: { thinkingBudget: 1024 } } },
})
LLM.request({
model,
prompt: "Hello",
// @ts-expect-error Gemini thinking budgets must be numeric.
providerOptions: { gemini: { thinkingConfig: { thinkingBudget: "large" } } },
})
@@ -1,13 +0,0 @@
import { LLM } from "../../src"
import { OpenAICompatibleResponses } from "../../src/providers"
const model = OpenAICompatibleResponses.configure({ baseURL: "https://example.com" }).model("model")
LLM.request({ model, prompt: "Hello", providerOptions: { openresponses: { reasoningSummary: "detailed" } } })
LLM.request({
model,
prompt: "Hello",
// @ts-expect-error Open Responses reasoning summaries use a fixed union.
providerOptions: { openresponses: { reasoningSummary: "full" } },
})
@@ -1,13 +0,0 @@
import { LLM } from "../../src"
import { OpenAICompatible } from "../../src/providers"
const model = OpenAICompatible.deepseek.model("deepseek-chat")
LLM.request({ model, prompt: "Hello", providerOptions: { openai: { store: false } } })
LLM.request({
model,
prompt: "Hello",
// @ts-expect-error OpenAI-compatible store must be boolean.
providerOptions: { openai: { store: "false" } },
})
@@ -1,13 +0,0 @@
import { LLM } from "../../src"
import { OpenAI } from "../../src/providers"
const model = OpenAI.responses("gpt-5")
LLM.request({ model, prompt: "Hello", providerOptions: { openai: { reasoningEffort: "high" } } })
LLM.request({
model,
prompt: "Hello",
// @ts-expect-error OpenAI reasoning effort must be a string.
providerOptions: { openai: { reasoningEffort: 1 } },
})
@@ -1,13 +0,0 @@
import { LLM } from "../../src"
import { OpenRouter } from "../../src/providers"
const model = OpenRouter.provider.model("anthropic/claude-sonnet-4.5")
LLM.request({ model, prompt: "Hello", providerOptions: { openrouter: { usage: true } } })
LLM.request({
model,
prompt: "Hello",
// @ts-expect-error OpenRouter usage must be boolean or an option record.
providerOptions: { openrouter: { usage: "yes" } },
})
@@ -1,13 +0,0 @@
import { LLM } from "../../src"
import { XAI } from "../../src/providers"
const model = XAI.provider.model("grok-4")
LLM.request({ model, prompt: "Hello", providerOptions: { openai: { reasoningEffort: "high" } } })
LLM.request({
model,
prompt: "Hello",
// @ts-expect-error xAI's OpenAI-compatible reasoning effort must be a string.
providerOptions: { openai: { reasoningEffort: true } },
})
@@ -3,7 +3,6 @@ import { Effect } from "effect"
import { HttpClientRequest } from "effect/unstable/http" import { HttpClientRequest } from "effect/unstable/http"
import { CacheHint, LLM, LLMError, LLMRequest, Message, ToolCallPart, ToolDefinition, Usage } from "../../src" import { CacheHint, LLM, LLMError, LLMRequest, Message, ToolCallPart, ToolDefinition, Usage } from "../../src"
import { Auth, LLMClient } from "../../src/route" import { Auth, LLMClient } from "../../src/route"
import { compileRequest } from "../../src/route/client"
import * as AnthropicMessages from "../../src/protocols/anthropic-messages" import * as AnthropicMessages from "../../src/protocols/anthropic-messages"
import { continuationRequest, nativeAnthropicMessagesContinuation } from "../continuation-scenarios" import { continuationRequest, nativeAnthropicMessagesContinuation } from "../continuation-scenarios"
import { it } from "../lib/effect" import { it } from "../lib/effect"
@@ -45,7 +44,7 @@ const expectToolResult = (body: AnthropicMessages.AnthropicMessagesBody): Anthro
describe("Anthropic Messages route", () => { describe("Anthropic Messages route", () => {
it.effect("prepares Anthropic Messages target", () => it.effect("prepares Anthropic Messages target", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest(request) const prepared = yield* LLMClient.prepare(request)
expect(prepared.body).toEqual({ expect(prepared.body).toEqual({
model: "claude-sonnet-4-5", model: "claude-sonnet-4-5",
@@ -60,7 +59,7 @@ describe("Anthropic Messages route", () => {
it.effect("lowers adaptive thinking settings with effort", () => it.effect("lowers adaptive thinking settings with effort", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
LLMRequest.update(request, { LLMRequest.update(request, {
providerOptions: { providerOptions: {
anthropic: { thinking: { type: "adaptive", display: "summarized" }, effort: "low" }, anthropic: { thinking: { type: "adaptive", display: "summarized" }, effort: "low" },
@@ -77,17 +76,17 @@ describe("Anthropic Messages route", () => {
it.effect("normalizes enabled and disabled thinking settings", () => it.effect("normalizes enabled and disabled thinking settings", () =>
Effect.gen(function* () { Effect.gen(function* () {
const enabled = yield* compileRequest( const enabled = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
LLMRequest.update(request, { LLMRequest.update(request, {
providerOptions: { anthropic: { thinking: { type: "enabled", budgetTokens: 1_024 } } }, providerOptions: { anthropic: { thinking: { type: "enabled", budgetTokens: 1_024 } } },
}), }),
) )
const legacy = yield* compileRequest( const legacy = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
LLMRequest.update(request, { LLMRequest.update(request, {
providerOptions: { anthropic: { thinking: { type: "enabled", budget_tokens: 2_048 } } }, providerOptions: { anthropic: { thinking: { type: "enabled", budget_tokens: 2_048 } } },
}), }),
) )
const disabled = yield* compileRequest( const disabled = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
LLMRequest.update(request, { LLMRequest.update(request, {
providerOptions: { anthropic: { thinking: { type: "disabled" } } }, providerOptions: { anthropic: { thinking: { type: "disabled" } } },
}), }),
@@ -101,7 +100,7 @@ describe("Anthropic Messages route", () => {
it.effect("rejects enabled thinking without a budget", () => it.effect("rejects enabled thinking without a budget", () =>
Effect.gen(function* () { Effect.gen(function* () {
const error = yield* compileRequest( const error = yield* LLMClient.prepare(
LLMRequest.update(request, { LLMRequest.update(request, {
providerOptions: { anthropic: { thinking: { type: "enabled" } } }, providerOptions: { anthropic: { thinking: { type: "enabled" } } },
}), }),
@@ -113,7 +112,7 @@ describe("Anthropic Messages route", () => {
it.effect("lowers chronological system updates natively for Claude Opus 4.8 with cache hints", () => it.effect("lowers chronological system updates natively for Claude Opus 4.8 with cache hints", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
LLM.request({ LLM.request({
model: opus48, model: opus48,
messages: [ messages: [
@@ -138,7 +137,7 @@ describe("Anthropic Messages route", () => {
it.effect("lowers chronological system updates to wrapped user text for unsupported Anthropic models", () => it.effect("lowers chronological system updates to wrapped user text for unsupported Anthropic models", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
LLM.request({ LLM.request({
model, model,
messages: [ messages: [
@@ -165,7 +164,7 @@ describe("Anthropic Messages route", () => {
it.effect("rejects non-text chronological system update content before send", () => it.effect("rejects non-text chronological system update content before send", () =>
Effect.gen(function* () { Effect.gen(function* () {
const error = yield* compileRequest( const error = yield* LLMClient.prepare(
LLM.request({ LLM.request({
model: opus48, model: opus48,
messages: [ messages: [
@@ -182,7 +181,7 @@ describe("Anthropic Messages route", () => {
it.effect("falls back for unsupported native chronological system update placement", () => it.effect("falls back for unsupported native chronological system update placement", () =>
Effect.gen(function* () { Effect.gen(function* () {
expect( expect(
(yield* compileRequest( (yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
LLM.request({ LLM.request({
model: opus48, model: opus48,
messages: [Message.assistant("Plain."), Message.system("After plain assistant.")], messages: [Message.assistant("Plain."), Message.system("After plain assistant.")],
@@ -197,11 +196,12 @@ describe("Anthropic Messages route", () => {
}, },
]) ])
expect( expect(
(yield* compileRequest(LLM.request({ model: opus48, messages: [Message.system("First.")], cache: "none" }))) (yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
.body.messages, LLM.request({ model: opus48, messages: [Message.system("First.")], cache: "none" }),
)).body.messages,
).toEqual([{ role: "user", content: [{ type: "text", text: "<system-update>\nFirst.\n</system-update>" }] }]) ).toEqual([{ role: "user", content: [{ type: "text", text: "<system-update>\nFirst.\n</system-update>" }] }])
expect( expect(
(yield* compileRequest( (yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
LLM.request({ LLM.request({
model: opus48, model: opus48,
messages: [Message.user("Before."), Message.system("One."), Message.system("Two.")], messages: [Message.user("Before."), Message.system("One."), Message.system("Two.")],
@@ -223,7 +223,7 @@ describe("Anthropic Messages route", () => {
it.effect("rejects a system update between a local tool call and its result", () => it.effect("rejects a system update between a local tool call and its result", () =>
Effect.gen(function* () { Effect.gen(function* () {
const error = yield* compileRequest( const error = yield* LLMClient.prepare(
LLM.request({ LLM.request({
model: opus48, model: opus48,
messages: [ messages: [
@@ -242,7 +242,7 @@ describe("Anthropic Messages route", () => {
it.effect("prepares tool call and tool result messages", () => it.effect("prepares tool call and tool result messages", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
LLM.request({ LLM.request({
id: "req_tool_result", id: "req_tool_result",
model, model,
@@ -273,7 +273,7 @@ describe("Anthropic Messages route", () => {
it.effect("keeps tools and sends tool_choice none", () => it.effect("keeps tools and sends tool_choice none", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
LLM.request({ LLM.request({
id: "req_tool_choice_none", id: "req_tool_choice_none",
model, model,
@@ -303,7 +303,7 @@ describe("Anthropic Messages route", () => {
// not JSON-stringified into `tool_result.content`. // not JSON-stringified into `tool_result.content`.
it.effect("lowers media tool-result content as structured blocks", () => it.effect("lowers media tool-result content as structured blocks", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
LLM.request({ LLM.request({
id: "req_tool_result_image", id: "req_tool_result_image",
model, model,
@@ -335,7 +335,7 @@ describe("Anthropic Messages route", () => {
it.effect("lowers single-image tool-result content as a structured image block", () => it.effect("lowers single-image tool-result content as a structured image block", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
LLM.request({ LLM.request({
id: "req_tool_result_image_only", id: "req_tool_result_image_only",
model, model,
@@ -360,7 +360,7 @@ describe("Anthropic Messages route", () => {
it.effect("rejects unsupported media in tool-result content with a clear error", () => it.effect("rejects unsupported media in tool-result content with a clear error", () =>
Effect.gen(function* () { Effect.gen(function* () {
const error = yield* compileRequest( const error = yield* LLMClient.prepare(
LLM.request({ LLM.request({
id: "req_tool_result_unsupported_media", id: "req_tool_result_unsupported_media",
model, model,
@@ -384,7 +384,7 @@ describe("Anthropic Messages route", () => {
it.effect("prepares the composed native continuation request", () => it.effect("prepares the composed native continuation request", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
continuationRequest({ continuationRequest({
id: "req_native_continuation_anthropic", id: "req_native_continuation_anthropic",
model, model,
@@ -428,7 +428,7 @@ describe("Anthropic Messages route", () => {
it.effect("lowers preserved Anthropic reasoning signature metadata", () => it.effect("lowers preserved Anthropic reasoning signature metadata", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLM.request({ LLM.request({
model, model,
messages: [ messages: [
@@ -447,7 +447,7 @@ describe("Anthropic Messages route", () => {
it.effect("round-trips redacted thinking as redacted_thinking blocks", () => it.effect("round-trips redacted thinking as redacted_thinking blocks", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLM.request({ LLM.request({
model, model,
messages: [ messages: [
@@ -628,7 +628,9 @@ describe("Anthropic Messages route", () => {
{ type: "reasoning", text: "", providerMetadata: { anthropic: { signature: "sig_1" } } }, { type: "reasoning", text: "", providerMetadata: { anthropic: { signature: "sig_1" } } },
]) ])
const prepared = yield* compileRequest(LLM.request({ model, messages: [response.message], cache: "none" })) const prepared = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
LLM.request({ model, messages: [response.message], cache: "none" }),
)
expect(prepared.body.messages).toEqual([ expect(prepared.body.messages).toEqual([
{ role: "assistant", content: [{ type: "thinking", thinking: "", signature: "sig_1" }] }, { role: "assistant", content: [{ type: "thinking", thinking: "", signature: "sig_1" }] },
]) ])
@@ -771,7 +773,7 @@ describe("Anthropic Messages route", () => {
), ),
), ),
) )
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
LLM.request({ LLM.request({
model, model,
messages: [ messages: [
@@ -1131,7 +1133,7 @@ describe("Anthropic Messages route", () => {
it.effect("round-trips provider-executed assistant content into server tool blocks", () => it.effect("round-trips provider-executed assistant content into server tool blocks", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLM.request({ LLM.request({
id: "req_round_trip", id: "req_round_trip",
model, model,
@@ -1182,7 +1184,7 @@ describe("Anthropic Messages route", () => {
it.effect("rejects round-trip for unknown server tool names", () => it.effect("rejects round-trip for unknown server tool names", () =>
Effect.gen(function* () { Effect.gen(function* () {
const error = yield* compileRequest( const error = yield* LLMClient.prepare(
LLM.request({ LLM.request({
id: "req_unknown_server_tool", id: "req_unknown_server_tool",
model, model,
@@ -1259,7 +1261,7 @@ describe("Anthropic Messages route", () => {
it.effect("maps ttlSeconds >= 3600 to cache_control ttl: '1h'", () => it.effect("maps ttlSeconds >= 3600 to cache_control ttl: '1h'", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLM.request({ LLM.request({
model, model,
system: { type: "text", text: "system", cache: new CacheHint({ type: "ephemeral", ttlSeconds: 3600 }) }, system: { type: "text", text: "system", cache: new CacheHint({ type: "ephemeral", ttlSeconds: 3600 }) },
@@ -1275,7 +1277,7 @@ describe("Anthropic Messages route", () => {
it.effect("emits cache_control on tool definitions and tool-result blocks", () => it.effect("emits cache_control on tool definitions and tool-result blocks", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLM.request({ LLM.request({
model, model,
tools: [ tools: [
@@ -1316,7 +1318,7 @@ describe("Anthropic Messages route", () => {
it.effect("drops cache_control breakpoints past the 4-per-request cap", () => it.effect("drops cache_control breakpoints past the 4-per-request cap", () =>
Effect.gen(function* () { Effect.gen(function* () {
const hint = new CacheHint({ type: "ephemeral" }) const hint = new CacheHint({ type: "ephemeral" })
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLM.request({ LLM.request({
model, model,
system: [ system: [
@@ -1342,7 +1344,7 @@ describe("Anthropic Messages route", () => {
it.effect("spends breakpoint budget on tools before system before messages", () => it.effect("spends breakpoint budget on tools before system before messages", () =>
Effect.gen(function* () { Effect.gen(function* () {
const hint = new CacheHint({ type: "ephemeral" }) const hint = new CacheHint({ type: "ephemeral" })
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLM.request({ LLM.request({
model, model,
tools: [ tools: [
@@ -13,7 +13,6 @@ import {
ToolDefinition, ToolDefinition,
} from "../../src" } from "../../src"
import { LLMClient } from "../../src/route" import { LLMClient } from "../../src/route"
import { compileRequest } from "../../src/route/client"
import { AmazonBedrock } from "../../src/providers" import { AmazonBedrock } from "../../src/providers"
import * as BedrockConverse from "../../src/protocols/bedrock-converse" import * as BedrockConverse from "../../src/protocols/bedrock-converse"
import { it } from "../lib/effect" import { it } from "../lib/effect"
@@ -102,7 +101,7 @@ const baseRequest = LLM.request({
describe("Bedrock Converse route", () => { describe("Bedrock Converse route", () => {
it.effect("prepares Converse target with system, inference config, and messages", () => it.effect("prepares Converse target with system, inference config, and messages", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest(baseRequest) const prepared = yield* LLMClient.prepare(baseRequest)
expect(prepared.body).toEqual({ expect(prepared.body).toEqual({
modelId: "anthropic.claude-3-5-sonnet-20240620-v1:0", modelId: "anthropic.claude-3-5-sonnet-20240620-v1:0",
@@ -115,7 +114,7 @@ describe("Bedrock Converse route", () => {
it.effect("passes topK through additionalModelRequestFields as top_k", () => it.effect("passes topK through additionalModelRequestFields as top_k", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<BedrockConverse.BedrockConverseBody>(
LLMRequest.update(baseRequest, { LLMRequest.update(baseRequest, {
generation: GenerationOptions.make({ maxTokens: 64, temperature: 0, topK: 40 }), generation: GenerationOptions.make({ maxTokens: 64, temperature: 0, topK: 40 }),
}), }),
@@ -130,14 +129,14 @@ describe("Bedrock Converse route", () => {
it.effect("omits additionalModelRequestFields when topK is unset", () => it.effect("omits additionalModelRequestFields when topK is unset", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest(baseRequest) const prepared = yield* LLMClient.prepare<BedrockConverse.BedrockConverseBody>(baseRequest)
expect(prepared.body.additionalModelRequestFields).toBeUndefined() expect(prepared.body.additionalModelRequestFields).toBeUndefined()
}), }),
) )
it.effect("lowers chronological system updates to wrapped user text in order", () => it.effect("lowers chronological system updates to wrapped user text in order", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<BedrockConverse.BedrockConverseBody>(
LLM.request({ LLM.request({
model, model,
messages: [Message.user("Before."), Message.system("Update."), Message.assistant("After.")], messages: [Message.user("Before."), Message.system("Update."), Message.assistant("After.")],
@@ -154,7 +153,7 @@ describe("Bedrock Converse route", () => {
it.effect("prepares tool config with toolSpec and toolChoice", () => it.effect("prepares tool config with toolSpec and toolChoice", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLMRequest.update(baseRequest, { LLMRequest.update(baseRequest, {
tools: [ tools: [
ToolDefinition.make({ ToolDefinition.make({
@@ -188,7 +187,7 @@ describe("Bedrock Converse route", () => {
it.effect("keeps tools and omits the unsupported choice when tool choice is none", () => it.effect("keeps tools and omits the unsupported choice when tool choice is none", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLMRequest.update(baseRequest, { LLMRequest.update(baseRequest, {
tools: [ tools: [
ToolDefinition.make({ ToolDefinition.make({
@@ -218,7 +217,7 @@ describe("Bedrock Converse route", () => {
it.effect("lowers assistant tool-call + tool-result message history", () => it.effect("lowers assistant tool-call + tool-result message history", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLM.request({ LLM.request({
id: "req_history", id: "req_history",
model, model,
@@ -257,7 +256,7 @@ describe("Bedrock Converse route", () => {
it.effect("lowers image content in tool-result messages", () => it.effect("lowers image content in tool-result messages", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLM.request({ LLM.request({
id: "req_tool_image", id: "req_tool_image",
model, model,
@@ -492,7 +491,7 @@ describe("Bedrock Converse route", () => {
providerMetadata: { bedrock: { signature: "sig_1" } }, providerMetadata: { bedrock: { signature: "sig_1" } },
}) })
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<BedrockConverse.BedrockConverseBody>(
LLM.request({ LLM.request({
model, model,
messages: [ messages: [
@@ -547,7 +546,9 @@ describe("Bedrock Converse route", () => {
}, },
]) ])
const prepared = yield* compileRequest(LLM.request({ model, messages: [response.message], cache: "none" })) const prepared = yield* LLMClient.prepare<BedrockConverse.BedrockConverseBody>(
LLM.request({ model, messages: [response.message], cache: "none" }),
)
expect(prepared.body.messages).toEqual([ expect(prepared.body.messages).toEqual([
{ {
role: "assistant", role: "assistant",
@@ -638,7 +639,7 @@ describe("Bedrock Converse route", () => {
text: "", text: "",
providerMetadata: { bedrock: { redactedData } }, providerMetadata: { bedrock: { redactedData } },
}) })
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<BedrockConverse.BedrockConverseBody>(
LLM.request({ LLM.request({
model, model,
messages: [ messages: [
@@ -753,7 +754,7 @@ describe("Bedrock Converse route", () => {
secretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", secretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
}, },
}).model("anthropic.claude-3-5-sonnet-20240620-v1:0") }).model("anthropic.claude-3-5-sonnet-20240620-v1:0")
const prepared = yield* compileRequest(LLMRequest.update(baseRequest, { model: signed })) const prepared = yield* LLMClient.prepare(LLMRequest.update(baseRequest, { model: signed }))
expect(prepared.route).toBe("bedrock-converse") expect(prepared.route).toBe("bedrock-converse")
expect(prepared.model).toBe(signed) expect(prepared.model).toBe(signed)
@@ -763,7 +764,7 @@ describe("Bedrock Converse route", () => {
it.effect("emits cachePoint markers after system, user-text, and assistant-text with cache hints", () => it.effect("emits cachePoint markers after system, user-text, and assistant-text with cache hints", () =>
Effect.gen(function* () { Effect.gen(function* () {
const cache = new CacheHint({ type: "ephemeral" }) const cache = new CacheHint({ type: "ephemeral" })
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLM.request({ LLM.request({
id: "req_cache", id: "req_cache",
model, model,
@@ -795,7 +796,7 @@ describe("Bedrock Converse route", () => {
it.effect("does not emit cachePoint when no cache hint is set", () => it.effect("does not emit cachePoint when no cache hint is set", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest(baseRequest) const prepared = yield* LLMClient.prepare(baseRequest)
expect(prepared.body).toMatchObject({ expect(prepared.body).toMatchObject({
system: [{ text: "You are concise." }], system: [{ text: "You are concise." }],
messages: [{ role: "user", content: [{ text: "Say hello." }] }], messages: [{ role: "user", content: [{ text: "Say hello." }] }],
@@ -805,7 +806,7 @@ describe("Bedrock Converse route", () => {
it.effect("lowers image media into Bedrock image blocks", () => it.effect("lowers image media into Bedrock image blocks", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLM.request({ LLM.request({
id: "req_image", id: "req_image",
model, model,
@@ -842,7 +843,7 @@ describe("Bedrock Converse route", () => {
it.effect("base64-encodes Uint8Array image bytes", () => it.effect("base64-encodes Uint8Array image bytes", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLM.request({ LLM.request({
id: "req_image_bytes", id: "req_image_bytes",
model, model,
@@ -864,7 +865,7 @@ describe("Bedrock Converse route", () => {
it.effect("lowers document media into Bedrock document blocks with format and name", () => it.effect("lowers document media into Bedrock document blocks with format and name", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLM.request({ LLM.request({
id: "req_doc", id: "req_doc",
model, model,
@@ -896,7 +897,7 @@ describe("Bedrock Converse route", () => {
it.effect("requires names for document media", () => it.effect("requires names for document media", () =>
Effect.gen(function* () { Effect.gen(function* () {
const error = yield* compileRequest( const error = yield* LLMClient.prepare(
LLM.request({ LLM.request({
model, model,
messages: [Message.user({ type: "media", mediaType: "application/pdf", data: "UERGREFUQQ==" })], messages: [Message.user({ type: "media", mediaType: "application/pdf", data: "UERGREFUQQ==" })],
@@ -909,7 +910,7 @@ describe("Bedrock Converse route", () => {
it.effect("passes named document-only messages through for provider validation", () => it.effect("passes named document-only messages through for provider validation", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<BedrockConverse.BedrockConverseBody>(
LLM.request({ LLM.request({
model, model,
cache: "none", cache: "none",
@@ -935,7 +936,7 @@ describe("Bedrock Converse route", () => {
it.effect("lowers document media in tool results", () => it.effect("lowers document media in tool results", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<BedrockConverse.BedrockConverseBody>(
LLM.request({ LLM.request({
model, model,
cache: "none", cache: "none",
@@ -987,7 +988,7 @@ describe("Bedrock Converse route", () => {
it.effect("rejects unsupported image media types", () => it.effect("rejects unsupported image media types", () =>
Effect.gen(function* () { Effect.gen(function* () {
const error = yield* compileRequest( const error = yield* LLMClient.prepare(
LLM.request({ LLM.request({
id: "req_bad_image", id: "req_bad_image",
model, model,
@@ -1001,7 +1002,7 @@ describe("Bedrock Converse route", () => {
it.effect("rejects unsupported document media types", () => it.effect("rejects unsupported document media types", () =>
Effect.gen(function* () { Effect.gen(function* () {
const error = yield* compileRequest( const error = yield* LLMClient.prepare(
LLM.request({ LLM.request({
id: "req_bad_doc", id: "req_bad_doc",
model, model,
@@ -1016,7 +1017,7 @@ describe("Bedrock Converse route", () => {
it.effect("maps ttlSeconds >= 3600 to cachePoint ttl: '1h'", () => it.effect("maps ttlSeconds >= 3600 to cachePoint ttl: '1h'", () =>
Effect.gen(function* () { Effect.gen(function* () {
const cache = new CacheHint({ type: "ephemeral", ttlSeconds: 3600 }) const cache = new CacheHint({ type: "ephemeral", ttlSeconds: 3600 })
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLM.request({ LLM.request({
model, model,
system: [{ type: "text", text: "system", cache }], system: [{ type: "text", text: "system", cache }],
@@ -1033,7 +1034,7 @@ describe("Bedrock Converse route", () => {
it.effect("appends cachePoint after marked tool definitions and tool-result blocks", () => it.effect("appends cachePoint after marked tool definitions and tool-result blocks", () =>
Effect.gen(function* () { Effect.gen(function* () {
const cache = new CacheHint({ type: "ephemeral" }) const cache = new CacheHint({ type: "ephemeral" })
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLM.request({ LLM.request({
model, model,
tools: [{ name: "lookup", description: "lookup", inputSchema: { type: "object", properties: {} }, cache }], tools: [{ name: "lookup", description: "lookup", inputSchema: { type: "object", properties: {} }, cache }],
@@ -1065,7 +1066,7 @@ describe("Bedrock Converse route", () => {
it.effect("drops cachePoint markers past the 4-per-request cap", () => it.effect("drops cachePoint markers past the 4-per-request cap", () =>
Effect.gen(function* () { Effect.gen(function* () {
const cache = new CacheHint({ type: "ephemeral" }) const cache = new CacheHint({ type: "ephemeral" })
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLM.request({ LLM.request({
model, model,
system: [ system: [
+5 -5
View File
@@ -3,7 +3,7 @@ import { ConfigProvider, Effect, Schema } from "effect"
import { HttpClientRequest } from "effect/unstable/http" import { HttpClientRequest } from "effect/unstable/http"
import { LLM, LLMEvent } from "../../src" import { LLM, LLMEvent } from "../../src"
import { CloudflareAIGateway, CloudflareWorkersAI } from "../../src/providers/cloudflare" import { CloudflareAIGateway, CloudflareWorkersAI } from "../../src/providers/cloudflare"
import { compileRequest } from "../../src/route/client" import { LLMClient } from "../../src/route"
import { it } from "../lib/effect" import { it } from "../lib/effect"
import { dynamicResponse } from "../lib/http" import { dynamicResponse } from "../lib/http"
import { sseEvents } from "../lib/sse" import { sseEvents } from "../lib/sse"
@@ -34,7 +34,7 @@ describe("Cloudflare", () => {
}) })
expect(model.route.endpoint.baseURL).toBe("https://gateway.ai.cloudflare.com/v1/test-account/test-gateway/compat") expect(model.route.endpoint.baseURL).toBe("https://gateway.ai.cloudflare.com/v1/test-account/test-gateway/compat")
const prepared = yield* compileRequest(LLM.request({ model, prompt: "Say hello." })) const prepared = yield* LLMClient.prepare(LLM.request({ model, prompt: "Say hello." }))
expect(prepared.route).toBe("cloudflare-ai-gateway") expect(prepared.route).toBe("cloudflare-ai-gateway")
expect(prepared.body).toMatchObject({ expect(prepared.body).toMatchObject({
@@ -129,7 +129,7 @@ describe("Cloudflare", () => {
openai: { reasoningField: "reasoning", reasoningDetails: merged }, openai: { reasoningField: "reasoning", reasoningDetails: merged },
}) })
const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] })) const replay = yield* LLMClient.prepare(LLM.request({ model, messages: [response.message] }))
expect(replay.body.messages).toEqual([ expect(replay.body.messages).toEqual([
{ role: "assistant", content: "Hello", reasoning: "Thinking", reasoning_details: merged }, { role: "assistant", content: "Hello", reasoning: "Thinking", reasoning_details: merged },
]) ])
@@ -180,7 +180,7 @@ describe("Cloudflare", () => {
it.effect("allows a fully configured baseURL override", () => it.effect("allows a fully configured baseURL override", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLM.request({ LLM.request({
model: CloudflareAIGateway.configure({ model: CloudflareAIGateway.configure({
baseURL: "https://gateway.proxy.test/v1/custom/compat", baseURL: "https://gateway.proxy.test/v1/custom/compat",
@@ -208,7 +208,7 @@ describe("Cloudflare", () => {
}) })
expect(model.route.endpoint.baseURL).toBe("https://api.cloudflare.com/client/v4/accounts/test-account/ai/v1") expect(model.route.endpoint.baseURL).toBe("https://api.cloudflare.com/client/v4/accounts/test-account/ai/v1")
const prepared = yield* compileRequest(LLM.request({ model, prompt: "Say hello." })) const prepared = yield* LLMClient.prepare(LLM.request({ model, prompt: "Say hello." }))
expect(prepared.route).toBe("cloudflare-workers-ai") expect(prepared.route).toBe("cloudflare-workers-ai")
expect(prepared.body).toMatchObject({ expect(prepared.body).toMatchObject({
+13 -14
View File
@@ -2,7 +2,6 @@ import { describe, expect } from "bun:test"
import { Effect } from "effect" import { Effect } from "effect"
import { LLM, LLMError, LLMRequest, Message, ToolCallPart, ToolDefinition, Usage } from "../../src" import { LLM, LLMError, LLMRequest, Message, ToolCallPart, ToolDefinition, Usage } from "../../src"
import { Auth, LLMClient } from "../../src/route" import { Auth, LLMClient } from "../../src/route"
import { compileRequest } from "../../src/route/client"
import * as Gemini from "../../src/protocols/gemini" import * as Gemini from "../../src/protocols/gemini"
import { ProviderShared } from "../../src/protocols/shared" import { ProviderShared } from "../../src/protocols/shared"
import { it } from "../lib/effect" import { it } from "../lib/effect"
@@ -27,7 +26,7 @@ const request = LLM.request({
describe("Gemini route", () => { describe("Gemini route", () => {
it.effect("prepares Gemini target", () => it.effect("prepares Gemini target", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest(request) const prepared = yield* LLMClient.prepare(request)
expect(prepared.body).toEqual({ expect(prepared.body).toEqual({
contents: [{ role: "user", parts: [{ text: "Say hello." }] }], contents: [{ role: "user", parts: [{ text: "Say hello." }] }],
@@ -39,12 +38,12 @@ describe("Gemini route", () => {
it.effect("normalizes Gemini thinking options", () => it.effect("normalizes Gemini thinking options", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<Gemini.GeminiBody>(
LLMRequest.update(request, { LLMRequest.update(request, {
providerOptions: { gemini: { thinkingConfig: { thinkingBudget: 0, includeThoughts: false } } }, providerOptions: { gemini: { thinkingConfig: { thinkingBudget: 0, includeThoughts: false } } },
}), }),
) )
const filtered = yield* compileRequest( const filtered = yield* LLMClient.prepare<Gemini.GeminiBody>(
LLMRequest.update(request, { LLMRequest.update(request, {
providerOptions: { gemini: { thinkingConfig: { thinkingBudget: "invalid", includeThoughts: false } } }, providerOptions: { gemini: { thinkingConfig: { thinkingBudget: "invalid", includeThoughts: false } } },
}), }),
@@ -60,7 +59,7 @@ describe("Gemini route", () => {
it.effect("lowers chronological system updates to wrapped user text in order", () => it.effect("lowers chronological system updates to wrapped user text in order", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<Gemini.GeminiBody>(
LLM.request({ LLM.request({
model, model,
messages: [Message.user("Before."), Message.system("Update."), Message.assistant("After.")], messages: [Message.user("Before."), Message.system("Update."), Message.assistant("After.")],
@@ -76,7 +75,7 @@ describe("Gemini route", () => {
it.effect("prepares multimodal user input and tool history", () => it.effect("prepares multimodal user input and tool history", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLM.request({ LLM.request({
id: "req_tool_result", id: "req_tool_result",
model, model,
@@ -144,7 +143,7 @@ describe("Gemini route", () => {
it.effect("continues media tool results as inline model input without base64 text", () => it.effect("continues media tool results as inline model input without base64 text", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<Gemini.GeminiBody>(
LLM.request({ LLM.request({
model, model,
messages: [ messages: [
@@ -189,7 +188,7 @@ describe("Gemini route", () => {
it.effect("strips matching data URLs to raw base64 inlineData", () => it.effect("strips matching data URLs to raw base64 inlineData", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<Gemini.GeminiBody>(
LLM.request({ LLM.request({
model, model,
messages: [ messages: [
@@ -230,7 +229,7 @@ describe("Gemini route", () => {
] as const) ] as const)
it.effect(`rejects ${name}`, () => it.effect(`rejects ${name}`, () =>
Effect.gen(function* () { Effect.gen(function* () {
const error = yield* compileRequest( const error = yield* LLMClient.prepare(
LLM.request({ model, messages: [Message.user({ type: "media", ...media })] }), LLM.request({ model, messages: [Message.user({ type: "media", ...media })] }),
).pipe(Effect.flip) ).pipe(Effect.flip)
expect(error.message).toMatch(/does not support|does not match|valid base64/) expect(error.message).toMatch(/does not support|does not match|valid base64/)
@@ -239,7 +238,7 @@ describe("Gemini route", () => {
it.effect("rejects oversized image input", () => it.effect("rejects oversized image input", () =>
Effect.gen(function* () { Effect.gen(function* () {
const error = yield* compileRequest( const error = yield* LLMClient.prepare(
LLM.request({ LLM.request({
model, model,
messages: [ messages: [
@@ -257,7 +256,7 @@ describe("Gemini route", () => {
it.effect("keeps tools and sends function calling mode NONE", () => it.effect("keeps tools and sends function calling mode NONE", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLM.request({ LLM.request({
id: "req_tool_choice_none", id: "req_tool_choice_none",
model, model,
@@ -277,7 +276,7 @@ describe("Gemini route", () => {
it.effect("sanitizes integer enums, dangling required, untyped arrays, and scalar object keys", () => it.effect("sanitizes integer enums, dangling required, untyped arrays, and scalar object keys", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLM.request({ LLM.request({
id: "req_schema_patch", id: "req_schema_patch",
model, model,
@@ -458,7 +457,7 @@ describe("Gemini route", () => {
response.events.findIndex((event) => event.type === "tool-call"), response.events.findIndex((event) => event.type === "tool-call"),
) )
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<Gemini.GeminiBody>(
LLM.request({ LLM.request({
model, model,
messages: [ messages: [
@@ -692,7 +691,7 @@ describe("Gemini route", () => {
it.effect("rejects unsupported assistant media content", () => it.effect("rejects unsupported assistant media content", () =>
Effect.gen(function* () { Effect.gen(function* () {
const error = yield* compileRequest( const error = yield* LLMClient.prepare(
LLM.request({ LLM.request({
id: "req_media", id: "req_media",
model, model,
@@ -4,7 +4,6 @@ import { HttpClientRequest } from "effect/unstable/http"
import { LLM } from "../../src" import { LLM } from "../../src"
import { GoogleVertex, GoogleVertexChat, GoogleVertexMessages, GoogleVertexResponses } from "../../src/providers" import { GoogleVertex, GoogleVertexChat, GoogleVertexMessages, GoogleVertexResponses } from "../../src/providers"
import { LLMClient } from "../../src/route" import { LLMClient } from "../../src/route"
import { compileRequest } from "../../src/route/client"
import { it } from "../lib/effect" import { it } from "../lib/effect"
import { dynamicResponse } from "../lib/http" import { dynamicResponse } from "../lib/http"
import { deltaChunk, finishChunk } from "../lib/openai-chunks" import { deltaChunk, finishChunk } from "../lib/openai-chunks"
@@ -183,7 +182,7 @@ describe("Google Vertex providers", () => {
it.effect("protects the Vertex Messages API version from body overlays", () => it.effect("protects the Vertex Messages API version from body overlays", () =>
Effect.gen(function* () { Effect.gen(function* () {
const error = yield* compileRequest( const error = yield* LLMClient.prepare(
LLM.request({ LLM.request({
model: GoogleVertexMessages.configure({ model: GoogleVertexMessages.configure({
accessToken: "vertex-token", accessToken: "vertex-token",
@@ -5,7 +5,6 @@ import { OpenAIChat } from "../../src/protocols/openai-chat"
import * as OpenAICompatible from "../../src/providers/openai-compatible" import * as OpenAICompatible from "../../src/providers/openai-compatible"
import * as OpenRouter from "../../src/providers/openrouter" import * as OpenRouter from "../../src/providers/openrouter"
import { LLMClient } from "../../src/route" import { LLMClient } from "../../src/route"
import { compileRequest } from "../../src/route/client"
import { recordedTests } from "../recorded-test" import { recordedTests } from "../recorded-test"
import { expectWeatherToolLoop, goldenWeatherToolLoopRequest, runWeatherToolLoop } from "../recorded-scenarios" import { expectWeatherToolLoop, goldenWeatherToolLoopRequest, runWeatherToolLoop } from "../recorded-scenarios"
@@ -85,7 +84,9 @@ for (const item of cases) {
), ),
).toBe(true) ).toBe(true)
const replay = yield* compileRequest(LLM.request({ model: item.model, messages: [response.message] })) const replay = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
LLM.request({ model: item.model, messages: [response.message] }),
)
expect(replay.body.messages).toMatchObject([ expect(replay.body.messages).toMatchObject([
{ role: "assistant", content: response.text, reasoning: response.reasoning }, { role: "assistant", content: response.text, reasoning: response.reasoning },
]) ])
+48 -29
View File
@@ -18,7 +18,6 @@ import * as OpenAI from "../../src/providers/openai"
import * as OpenAIChat from "../../src/protocols/openai-chat" import * as OpenAIChat from "../../src/protocols/openai-chat"
import { ProviderShared } from "../../src/protocols/shared" import { ProviderShared } from "../../src/protocols/shared"
import { Auth, LLMClient } from "../../src/route" import { Auth, LLMClient } from "../../src/route"
import { compileRequest } from "../../src/route/client"
import { it } from "../lib/effect" import { it } from "../lib/effect"
import { dynamicResponse, fixedResponse, truncatedStream } from "../lib/http" import { dynamicResponse, fixedResponse, truncatedStream } from "../lib/http"
import { deltaChunk, usageChunk } from "../lib/openai-chunks" import { deltaChunk, usageChunk } from "../lib/openai-chunks"
@@ -43,7 +42,11 @@ const request = LLM.request({
describe("OpenAI Chat route", () => { describe("OpenAI Chat route", () => {
it.effect("prepares OpenAI Chat payload", () => it.effect("prepares OpenAI Chat payload", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest(request) // Pass the OpenAIChat payload type so `prepared.body` is statically
// typed to the route's native shape — the assertions below read field
// names without `unknown` casts.
const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(request)
const _typed: { readonly model: string; readonly stream: true } = prepared.body
expect(prepared.body).toEqual({ expect(prepared.body).toEqual({
model: "gpt-4o-mini", model: "gpt-4o-mini",
@@ -61,7 +64,7 @@ describe("OpenAI Chat route", () => {
it.effect("lowers chronological system updates to escaped user wrappers in order", () => it.effect("lowers chronological system updates to escaped user wrappers in order", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
LLM.request({ LLM.request({
model, model,
messages: [ messages: [
@@ -84,7 +87,7 @@ describe("OpenAI Chat route", () => {
it.effect("replays canonical reasoning as OpenAI-compatible reasoning_content", () => it.effect("replays canonical reasoning as OpenAI-compatible reasoning_content", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
LLM.request({ LLM.request({
model, model,
messages: [ messages: [
@@ -102,7 +105,7 @@ describe("OpenAI Chat route", () => {
it.effect("writes reasoning to a configured custom field on every assistant message", () => it.effect("writes reasoning to a configured custom field on every assistant message", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
LLM.request({ LLM.request({
model: Model.update(model, { compatibility: { reasoningField: "vendor_reasoning" } }), model: Model.update(model, { compatibility: { reasoningField: "vendor_reasoning" } }),
messages: [ messages: [
@@ -128,7 +131,7 @@ describe("OpenAI Chat route", () => {
it.effect("rejects reasoning fields that conflict with assistant message fields", () => it.effect("rejects reasoning fields that conflict with assistant message fields", () =>
Effect.gen(function* () { Effect.gen(function* () {
const error = yield* compileRequest( const error = yield* LLMClient.prepare(
LLM.request({ LLM.request({
model: Model.update(model, { compatibility: { reasoningField: "content" } }), model: Model.update(model, { compatibility: { reasoningField: "content" } }),
messages: [Message.assistant([{ type: "reasoning", text: "thinking" }])], messages: [Message.assistant([{ type: "reasoning", text: "thinking" }])],
@@ -141,7 +144,7 @@ describe("OpenAI Chat route", () => {
it.effect("maps OpenAI provider options to Chat options", () => it.effect("maps OpenAI provider options to Chat options", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
LLM.request({ LLM.request({
model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).chat("gpt-4o-mini"), model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).chat("gpt-4o-mini"),
prompt: "think", prompt: "think",
@@ -156,7 +159,7 @@ describe("OpenAI Chat route", () => {
it.effect("passes through custom OpenAI-compatible reasoning effort strings", () => it.effect("passes through custom OpenAI-compatible reasoning effort strings", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
LLM.request({ LLM.request({
model, model,
prompt: "think", prompt: "think",
@@ -250,7 +253,7 @@ describe("OpenAI Chat route", () => {
it.effect("prepares assistant tool-call and tool-result messages", () => it.effect("prepares assistant tool-call and tool-result messages", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLM.request({ LLM.request({
id: "req_tool_result", id: "req_tool_result",
model, model,
@@ -288,7 +291,7 @@ describe("OpenAI Chat route", () => {
it.effect("preserves structured tool errors for the model", () => it.effect("preserves structured tool errors for the model", () =>
Effect.gen(function* () { Effect.gen(function* () {
const error = { error: { type: "unknown", message: "Tool execution interrupted" } } const error = { error: { type: "unknown", message: "Tool execution interrupted" } }
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
LLM.request({ LLM.request({
model, model,
messages: [ messages: [
@@ -308,7 +311,7 @@ describe("OpenAI Chat route", () => {
it.effect("continues image tool results as vision input without base64 text", () => it.effect("continues image tool results as vision input without base64 text", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
LLM.request({ LLM.request({
model, model,
messages: [ messages: [
@@ -352,7 +355,7 @@ describe("OpenAI Chat route", () => {
it.effect("orders parallel tool responses before one aggregated vision message", () => it.effect("orders parallel tool responses before one aggregated vision message", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
LLM.request({ LLM.request({
model, model,
messages: [ messages: [
@@ -402,7 +405,7 @@ describe("OpenAI Chat route", () => {
it.effect("aggregates consecutive tool images with a following system update", () => it.effect("aggregates consecutive tool images with a following system update", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
LLM.request({ LLM.request({
model, model,
messages: [ messages: [
@@ -443,7 +446,7 @@ describe("OpenAI Chat route", () => {
it.effect("appends system updates without replacing multipart user content", () => it.effect("appends system updates without replacing multipart user content", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
LLM.request({ LLM.request({
model, model,
messages: [ messages: [
@@ -471,7 +474,7 @@ describe("OpenAI Chat route", () => {
] as const) ] as const)
it.effect(`rejects ${name}`, () => it.effect(`rejects ${name}`, () =>
Effect.gen(function* () { Effect.gen(function* () {
const error = yield* compileRequest( const error = yield* LLMClient.prepare(
LLM.request({ model, messages: [Message.user({ type: "media", ...media })] }), LLM.request({ model, messages: [Message.user({ type: "media", ...media })] }),
).pipe(Effect.flip) ).pipe(Effect.flip)
expect(error.message).toMatch(/does not support|does not match|valid base64/) expect(error.message).toMatch(/does not support|does not match|valid base64/)
@@ -480,7 +483,7 @@ describe("OpenAI Chat route", () => {
it.effect("rejects oversized image input", () => it.effect("rejects oversized image input", () =>
Effect.gen(function* () { Effect.gen(function* () {
const error = yield* compileRequest( const error = yield* LLMClient.prepare(
LLM.request({ LLM.request({
model, model,
messages: [ messages: [
@@ -498,7 +501,7 @@ describe("OpenAI Chat route", () => {
it.effect("prepares raw and data URL image media as vision input", () => it.effect("prepares raw and data URL image media as vision input", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
LLM.request({ LLM.request({
id: "req_media", id: "req_media",
model, model,
@@ -525,7 +528,7 @@ describe("OpenAI Chat route", () => {
it.effect("lowers reasoning-only assistant history", () => it.effect("lowers reasoning-only assistant history", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
LLM.request({ LLM.request({
id: "req_reasoning", id: "req_reasoning",
model, model,
@@ -616,7 +619,9 @@ describe("OpenAI Chat route", () => {
openai: { reasoningField: field }, openai: { reasoningField: field },
}) })
const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] })) const replay = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
LLM.request({ model, messages: [response.message] }),
)
expect(replay.body.messages).toEqual([{ role: "assistant", content: "Hello", [field]: "thinking" }]) expect(replay.body.messages).toEqual([{ role: "assistant", content: "Hello", [field]: "thinking" }])
} }
}), }),
@@ -642,7 +647,9 @@ describe("OpenAI Chat route", () => {
openai: { reasoningField: "vendor_reasoning" }, openai: { reasoningField: "vendor_reasoning" },
}) })
const replay = yield* compileRequest(LLM.request({ model: custom, messages: [response.message] })) const replay = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
LLM.request({ model: custom, messages: [response.message] }),
)
expect(replay.body.messages).toEqual([{ role: "assistant", content: "Hello", vendor_reasoning: "thinking" }]) expect(replay.body.messages).toEqual([{ role: "assistant", content: "Hello", vendor_reasoning: "thinking" }])
}), }),
) )
@@ -685,7 +692,9 @@ describe("OpenAI Chat route", () => {
openai: { reasoningField: "reasoning", reasoningDetails: details }, openai: { reasoningField: "reasoning", reasoningDetails: details },
}) })
const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] })) const replay = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
LLM.request({ model, messages: [response.message] }),
)
expect(replay.body.messages).toEqual([ expect(replay.body.messages).toEqual([
{ {
role: "assistant", role: "assistant",
@@ -728,7 +737,9 @@ describe("OpenAI Chat route", () => {
openai: { reasoningDetails: details }, openai: { reasoningDetails: details },
}) })
const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] })) const replay = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
LLM.request({ model, messages: [response.message] }),
)
expect(replay.body.messages).toEqual([{ role: "assistant", content: "Hello", reasoning_details: details }]) expect(replay.body.messages).toEqual([{ role: "assistant", content: "Hello", reasoning_details: details }])
}), }),
) )
@@ -753,7 +764,9 @@ describe("OpenAI Chat route", () => {
openai: { reasoningField: "reasoning", reasoningDetails: details }, openai: { reasoningField: "reasoning", reasoningDetails: details },
}) })
const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] })) const replay = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
LLM.request({ model, messages: [response.message] }),
)
expect(replay.body.messages).toEqual([ expect(replay.body.messages).toEqual([
{ role: "assistant", content: "Hello", reasoning: "thinking", reasoning_details: details }, { role: "assistant", content: "Hello", reasoning: "thinking", reasoning_details: details },
]) ])
@@ -826,7 +839,9 @@ describe("OpenAI Chat route", () => {
openai: { reasoningDetails: [] }, openai: { reasoningDetails: [] },
}) })
const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] })) const replay = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
LLM.request({ model, messages: [response.message] }),
)
expect(replay.body.messages).toEqual([{ role: "assistant", content: "Hello", reasoning_details: [] }]) expect(replay.body.messages).toEqual([{ role: "assistant", content: "Hello", reasoning_details: [] }])
}), }),
) )
@@ -874,7 +889,9 @@ describe("OpenAI Chat route", () => {
response.events.findIndex(LLMEvent.is.textStart), response.events.findIndex(LLMEvent.is.textStart),
) )
const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] })) const replay = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
LLM.request({ model, messages: [response.message] }),
)
expect(replay.body.messages).toEqual([ expect(replay.body.messages).toEqual([
{ role: "assistant", content: "Hello", reasoning: "thinking", reasoning_details: merged }, { role: "assistant", content: "Hello", reasoning: "thinking", reasoning_details: merged },
]) ])
@@ -901,7 +918,9 @@ describe("OpenAI Chat route", () => {
expect(response.events.filter(LLMEvent.is.reasoningStart)).toHaveLength(1) expect(response.events.filter(LLMEvent.is.reasoningStart)).toHaveLength(1)
expect(response.events.filter(LLMEvent.is.reasoningEnd)).toHaveLength(1) expect(response.events.filter(LLMEvent.is.reasoningEnd)).toHaveLength(1)
const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] })) const replay = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
LLM.request({ model, messages: [response.message] }),
)
expect(replay.body.messages).toEqual([{ role: "assistant", content: null, reasoning_details: details }]) expect(replay.body.messages).toEqual([{ role: "assistant", content: null, reasoning_details: details }])
}), }),
) )
@@ -931,7 +950,7 @@ describe("OpenAI Chat route", () => {
Effect.gen(function* () { Effect.gen(function* () {
const first = { type: "reasoning.text", text: "first", signature: "signed-0", index: 0 } const first = { type: "reasoning.text", text: "first", signature: "signed-0", index: 0 }
const second = { type: "reasoning.text", text: "second", signature: "signed-1", index: 1 } const second = { type: "reasoning.text", text: "second", signature: "signed-1", index: 1 }
const replay = yield* compileRequest( const replay = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
LLM.request({ LLM.request({
model, model,
messages: [ messages: [
@@ -960,7 +979,7 @@ describe("OpenAI Chat route", () => {
it.effect("retains scalar replay for mixed structured reasoning parts", () => it.effect("retains scalar replay for mixed structured reasoning parts", () =>
Effect.gen(function* () { Effect.gen(function* () {
const detail = { type: "reasoning.encrypted", data: "opaque", index: 0 } const detail = { type: "reasoning.encrypted", data: "opaque", index: 0 }
const replay = yield* compileRequest( const replay = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
LLM.request({ LLM.request({
model, model,
messages: [ messages: [
@@ -985,7 +1004,7 @@ describe("OpenAI Chat route", () => {
it.effect("replays native scalar reasoning alongside native details", () => it.effect("replays native scalar reasoning alongside native details", () =>
Effect.gen(function* () { Effect.gen(function* () {
const details = [{ type: "reasoning.encrypted", data: "opaque", index: 0 }] const details = [{ type: "reasoning.encrypted", data: "opaque", index: 0 }]
const replay = yield* compileRequest( const replay = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
LLM.request({ LLM.request({
model, model,
messages: [ messages: [
@@ -3,7 +3,6 @@ import { Effect, Schema } from "effect"
import { HttpClientRequest } from "effect/unstable/http" import { HttpClientRequest } from "effect/unstable/http"
import { LLM, LLMRequest, Message, ToolCallPart, ToolChoice, ToolDefinition } from "../../src" import { LLM, LLMRequest, Message, ToolCallPart, ToolChoice, ToolDefinition } from "../../src"
import { Auth, LLMClient } from "../../src/route" import { Auth, LLMClient } from "../../src/route"
import { compileRequest } from "../../src/route/client"
import * as OpenAICompatible from "../../src/providers/openai-compatible" import * as OpenAICompatible from "../../src/providers/openai-compatible"
import * as OpenAICompatibleChat from "../../src/protocols/openai-compatible-chat" import * as OpenAICompatibleChat from "../../src/protocols/openai-compatible-chat"
import { it } from "../lib/effect" import { it } from "../lib/effect"
@@ -53,7 +52,7 @@ const providerFamilies = [
describe("OpenAI-compatible Chat route", () => { describe("OpenAI-compatible Chat route", () => {
it.effect("prepares generic Chat target", () => it.effect("prepares generic Chat target", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLMRequest.update(request, { LLMRequest.update(request, {
tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })], tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })],
toolChoice: ToolChoice.make({ type: "required" }), toolChoice: ToolChoice.make({ type: "required" }),
@@ -128,7 +127,7 @@ describe("OpenAI-compatible Chat route", () => {
it.effect("matches AI SDK compatible basic request body fixture", () => it.effect("matches AI SDK compatible basic request body fixture", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest(request) const prepared = yield* LLMClient.prepare(request)
expect(prepared.body).toEqual({ expect(prepared.body).toEqual({
model: "deepseek-chat", model: "deepseek-chat",
@@ -146,7 +145,7 @@ describe("OpenAI-compatible Chat route", () => {
it.effect("matches AI SDK compatible tool request body fixture", () => it.effect("matches AI SDK compatible tool request body fixture", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLM.request({ LLM.request({
id: "req_tool_parity", id: "req_tool_parity",
model, model,
@@ -7,7 +7,6 @@ import { OpenResponses } from "../../src/protocols/open-responses"
import { OpenAICompatibleResponses } from "../../src/protocols/openai-compatible-responses" import { OpenAICompatibleResponses } from "../../src/protocols/openai-compatible-responses"
import { OpenAIResponses } from "../../src/protocols/openai-responses" import { OpenAIResponses } from "../../src/protocols/openai-responses"
import { LLMClient } from "../../src/route" import { LLMClient } from "../../src/route"
import { compileRequest } from "../../src/route/client"
import { it } from "../lib/effect" import { it } from "../lib/effect"
import { fixedResponse } from "../lib/http" import { fixedResponse } from "../lib/http"
import { sseEvents } from "../lib/sse" import { sseEvents } from "../lib/sse"
@@ -24,7 +23,7 @@ describe("Open Responses-compatible route", () => {
baseURL: "https://responses.example.test/v1", baseURL: "https://responses.example.test/v1",
provider: "example", provider: "example",
}).model("example-model") }).model("example-model")
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLM.request({ LLM.request({
model, model,
system: "You are concise.", system: "You are concise.",
@@ -62,7 +61,7 @@ describe("Open Responses-compatible route", () => {
apiKey: "test-key", apiKey: "test-key",
baseURL: "https://responses.example.test/v1", baseURL: "https://responses.example.test/v1",
}).model("example-model") }).model("example-model")
const error = yield* compileRequest( const error = yield* LLMClient.prepare(
LLM.request({ model, prompt: "Draw.", tools: [OpenAI.imageGeneration()] }), LLM.request({ model, prompt: "Draw.", tools: [OpenAI.imageGeneration()] }),
).pipe(Effect.flip) ).pipe(Effect.flip)
@@ -77,7 +76,7 @@ describe("Open Responses-compatible route", () => {
apiKey: "test-key", apiKey: "test-key",
baseURL: "https://responses.example.test/v1", baseURL: "https://responses.example.test/v1",
}).model("example-model") }).model("example-model")
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLM.request({ LLM.request({
model, model,
messages: [ messages: [
@@ -103,7 +102,7 @@ describe("Open Responses-compatible route", () => {
baseURL: "https://responses.example.test/v1", baseURL: "https://responses.example.test/v1",
providerOptions: { openresponses: { reasoningEffort: "low", store: true } }, providerOptions: { openresponses: { reasoningEffort: "low", store: true } },
}).model("example-model") }).model("example-model")
const prepared = yield* compileRequest(LLM.request({ model, prompt: "Think." })) const prepared = yield* LLMClient.prepare(LLM.request({ model, prompt: "Think." }))
expect(prepared.body).toMatchObject({ expect(prepared.body).toMatchObject({
reasoning: { effort: "low" }, reasoning: { effort: "low" },
@@ -14,7 +14,6 @@ import {
Usage, Usage,
} from "../../src" } from "../../src"
import { Auth, LLMClient, RequestExecutor, WebSocketExecutor } from "../../src/route" import { Auth, LLMClient, RequestExecutor, WebSocketExecutor } from "../../src/route"
import { compileRequest } from "../../src/route/client"
import * as Azure from "../../src/providers/azure" import * as Azure from "../../src/providers/azure"
import * as OpenAI from "../../src/providers/openai" import * as OpenAI from "../../src/providers/openai"
import * as XAI from "../../src/providers/xai" import * as XAI from "../../src/providers/xai"
@@ -57,7 +56,7 @@ const expectToolOutput = (body: OpenAIResponses.OpenAIResponsesBody): OpenAITool
describe("OpenAI Responses route", () => { describe("OpenAI Responses route", () => {
it.effect("prepares OpenAI Responses target", () => it.effect("prepares OpenAI Responses target", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest(request) const prepared = yield* LLMClient.prepare(request)
expect(prepared.body).toEqual({ expect(prepared.body).toEqual({
model: "gpt-4.1-mini", model: "gpt-4.1-mini",
@@ -75,7 +74,7 @@ describe("OpenAI Responses route", () => {
it.effect("lowers the hosted OpenAI image generation tool", () => it.effect("lowers the hosted OpenAI image generation tool", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({ LLM.request({
model, model,
prompt: "Show me a rooftop garden.", prompt: "Show me a rooftop garden.",
@@ -93,7 +92,7 @@ describe("OpenAI Responses route", () => {
it.effect("rejects invalid hosted image generation options locally", () => it.effect("rejects invalid hosted image generation options locally", () =>
Effect.gen(function* () { Effect.gen(function* () {
const error = yield* compileRequest( const error = yield* LLMClient.prepare(
LLM.request({ LLM.request({
model, model,
prompt: "Show me a rooftop garden.", prompt: "Show me a rooftop garden.",
@@ -110,7 +109,7 @@ describe("OpenAI Responses route", () => {
Effect.gen(function* () { Effect.gen(function* () {
const input = LLMRequest.update(request, { providerOptions: { openai: { serviceTier: "priority" } } }) const input = LLMRequest.update(request, { providerOptions: { openai: { serviceTier: "priority" } } })
expect(input.providerOptions).toEqual({ openai: { serviceTier: "priority" } }) expect(input.providerOptions).toEqual({ openai: { serviceTier: "priority" } })
const prepared = yield* compileRequest(input) const prepared = yield* LLMClient.prepare(input)
expect(prepared.body).toMatchObject({ service_tier: "priority" }) expect(prepared.body).toMatchObject({ service_tier: "priority" })
expect(prepared.body).not.toHaveProperty("serviceTier") expect(prepared.body).not.toHaveProperty("serviceTier")
@@ -119,7 +118,7 @@ describe("OpenAI Responses route", () => {
it.effect("passes through custom OpenAI reasoning effort strings", () => it.effect("passes through custom OpenAI reasoning effort strings", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLMRequest.update(request, { providerOptions: { openai: { reasoningEffort: "experimental" } } }), LLMRequest.update(request, { providerOptions: { openai: { reasoningEffort: "experimental" } } }),
) )
@@ -129,7 +128,7 @@ describe("OpenAI Responses route", () => {
it.effect("omits unsupported semantic service tiers", () => it.effect("omits unsupported semantic service tiers", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLMRequest.update(request, { providerOptions: { openai: { serviceTier: "unsupported" } } }), LLMRequest.update(request, { providerOptions: { openai: { serviceTier: "unsupported" } } }),
) )
@@ -139,7 +138,7 @@ describe("OpenAI Responses route", () => {
it.effect("flattens top-level object unions in function schemas", () => it.effect("flattens top-level object unions in function schemas", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLMRequest.update(request, { LLMRequest.update(request, {
tools: [ tools: [
ToolDefinition.make({ ToolDefinition.make({
@@ -192,7 +191,7 @@ describe("OpenAI Responses route", () => {
it.effect("lowers chronological system updates to escaped user wrappers in order", () => it.effect("lowers chronological system updates to escaped user wrappers in order", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({ LLM.request({
model, model,
messages: [ messages: [
@@ -218,7 +217,7 @@ describe("OpenAI Responses route", () => {
it.effect("prepares OpenAI Responses WebSocket target", () => it.effect("prepares OpenAI Responses WebSocket target", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLMRequest.update(request, { LLMRequest.update(request, {
model: OpenAIResponses.webSocketRoute model: OpenAIResponses.webSocketRoute
.with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") }) .with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") })
@@ -396,7 +395,7 @@ describe("OpenAI Responses route", () => {
it.effect("prepares function call and function output input items", () => it.effect("prepares function call and function output input items", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLM.request({ LLM.request({
id: "req_tool_result", id: "req_tool_result",
model, model,
@@ -433,7 +432,7 @@ describe("OpenAI Responses route", () => {
content: [], content: [],
structured: {}, structured: {},
} }
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({ LLM.request({
model, model,
messages: [ messages: [
@@ -454,7 +453,7 @@ describe("OpenAI Responses route", () => {
it.effect("keeps primitive tool errors as plain text", () => it.effect("keeps primitive tool errors as plain text", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({ LLM.request({
model, model,
messages: [ messages: [
@@ -470,7 +469,7 @@ describe("OpenAI Responses route", () => {
it.effect("keeps non-JSON tool errors as plain text", () => it.effect("keeps non-JSON tool errors as plain text", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({ LLM.request({
model, model,
messages: [ messages: [
@@ -488,7 +487,7 @@ describe("OpenAI Responses route", () => {
// image data is not JSON-stringified into `function_call_output.output`. // image data is not JSON-stringified into `function_call_output.output`.
it.effect("lowers image tool-result content as structured input_image items", () => it.effect("lowers image tool-result content as structured input_image items", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({ LLM.request({
id: "req_tool_result_image", id: "req_tool_result_image",
model, model,
@@ -517,7 +516,7 @@ describe("OpenAI Responses route", () => {
it.effect("lowers single-image tool-result content as structured input_image array", () => it.effect("lowers single-image tool-result content as structured input_image array", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({ LLM.request({
id: "req_tool_result_image_only", id: "req_tool_result_image_only",
model, model,
@@ -541,7 +540,7 @@ describe("OpenAI Responses route", () => {
it.effect("lowers PDF tool-result content as structured input_file array", () => it.effect("lowers PDF tool-result content as structured input_file array", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({ LLM.request({
id: "req_tool_result_pdf", id: "req_tool_result_pdf",
model, model,
@@ -576,7 +575,7 @@ describe("OpenAI Responses route", () => {
it.effect("uses xAI inline file encoding for PDF tool results", () => it.effect("uses xAI inline file encoding for PDF tool results", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({ LLM.request({
model: xaiModel, model: xaiModel,
messages: [ messages: [
@@ -611,7 +610,7 @@ describe("OpenAI Responses route", () => {
it.effect("rejects unsupported media in tool-result content with a clear error", () => it.effect("rejects unsupported media in tool-result content with a clear error", () =>
Effect.gen(function* () { Effect.gen(function* () {
const error = yield* compileRequest( const error = yield* LLMClient.prepare(
LLM.request({ LLM.request({
id: "req_tool_result_unsupported_media", id: "req_tool_result_unsupported_media",
model, model,
@@ -634,7 +633,7 @@ describe("OpenAI Responses route", () => {
it.effect("prepares the composed native continuation request", () => it.effect("prepares the composed native continuation request", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
continuationRequest({ continuationRequest({
id: "req_native_continuation_openai", id: "req_native_continuation_openai",
model, model,
@@ -676,7 +675,7 @@ describe("OpenAI Responses route", () => {
it.effect("maps OpenAI provider options to Responses options", () => it.effect("maps OpenAI provider options to Responses options", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({ LLM.request({
model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).model("gpt-5.2"), model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).model("gpt-5.2"),
prompt: "think", prompt: "think",
@@ -701,7 +700,7 @@ describe("OpenAI Responses route", () => {
it.effect("accepts the full ResponseIncludable union", () => it.effect("accepts the full ResponseIncludable union", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({ LLM.request({
model, model,
prompt: "hi", prompt: "hi",
@@ -723,7 +722,7 @@ describe("OpenAI Responses route", () => {
it.effect("filters unknown includable values out of the include array", () => it.effect("filters unknown includable values out of the include array", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({ LLM.request({
model, model,
prompt: "hi", prompt: "hi",
@@ -740,7 +739,7 @@ describe("OpenAI Responses route", () => {
it.effect("treats an explicit empty include as no include at all", () => it.effect("treats an explicit empty include as no include at all", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({ model, prompt: "hi", providerOptions: { openai: { include: [] } } }), LLM.request({ model, prompt: "hi", providerOptions: { openai: { include: [] } } }),
) )
@@ -750,7 +749,7 @@ describe("OpenAI Responses route", () => {
it.effect("treats an all-invalid include as no include at all", () => it.effect("treats an all-invalid include as no include at all", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({ model, prompt: "hi", providerOptions: { openai: { include: ["bogus.thing"] } } }), LLM.request({ model, prompt: "hi", providerOptions: { openai: { include: ["bogus.thing"] } } }),
) )
@@ -760,7 +759,7 @@ describe("OpenAI Responses route", () => {
it.effect("omits include when no include is set", () => it.effect("omits include when no include is set", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({ model, prompt: "hi", providerOptions: { openai: { store: false } } }), LLM.request({ model, prompt: "hi", providerOptions: { openai: { store: false } } }),
) )
@@ -774,7 +773,7 @@ describe("OpenAI Responses route", () => {
// reasoningSummary: "auto" by default. Without `include`, a follow-up // reasoningSummary: "auto" by default. Without `include`, a follow-up
// turn cannot replay reasoning state, so the facade also opts into // turn cannot replay reasoning state, so the facade also opts into
// `reasoning.encrypted_content` automatically. // `reasoning.encrypted_content` automatically.
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({ LLM.request({
model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).responses("gpt-5.2"), model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).responses("gpt-5.2"),
prompt: "hi", prompt: "hi",
@@ -789,7 +788,7 @@ describe("OpenAI Responses route", () => {
it.effect("lets callers opt out of the GPT-5 default include", () => it.effect("lets callers opt out of the GPT-5 default include", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({ LLM.request({
model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).responses("gpt-5.2"), model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).responses("gpt-5.2"),
prompt: "hi", prompt: "hi",
@@ -803,7 +802,7 @@ describe("OpenAI Responses route", () => {
it.effect("request OpenAI provider options override route defaults", () => it.effect("request OpenAI provider options override route defaults", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({ LLM.request({
model: OpenAI.configure({ model: OpenAI.configure({
baseURL: "https://api.openai.test/v1/", baseURL: "https://api.openai.test/v1/",
@@ -935,7 +934,9 @@ describe("OpenAI Responses route", () => {
}, },
]) ])
const prepared = yield* compileRequest(LLM.request({ model, messages: [response.message] })) const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({ model, messages: [response.message] }),
)
expect(prepared.body.input).toEqual([ expect(prepared.body.input).toEqual([
{ {
role: "assistant", role: "assistant",
@@ -1269,7 +1270,7 @@ describe("OpenAI Responses route", () => {
it.effect("preserves assistant content order around reasoning items", () => it.effect("preserves assistant content order around reasoning items", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({ LLM.request({
id: "req_reasoning_order", id: "req_reasoning_order",
model, model,
@@ -1307,7 +1308,7 @@ describe("OpenAI Responses route", () => {
it.effect("references stored reasoning items by id", () => it.effect("references stored reasoning items by id", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({ LLM.request({
model, model,
messages: [ messages: [
@@ -1329,7 +1330,7 @@ describe("OpenAI Responses route", () => {
it.effect("references stored provider-executed hosted tool results by id", () => it.effect("references stored provider-executed hosted tool results by id", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({ LLM.request({
model, model,
messages: [ messages: [
@@ -1366,7 +1367,7 @@ describe("OpenAI Responses route", () => {
it.effect("continues stateless hosted image generation with the generated image", () => it.effect("continues stateless hosted image generation with the generated image", () =>
Effect.gen(function* () { Effect.gen(function* () {
const imageTool = OpenAI.imageGeneration({ action: "edit" }) const imageTool = OpenAI.imageGeneration({ action: "edit" })
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({ LLM.request({
model, model,
messages: [ messages: [
@@ -1407,7 +1408,7 @@ describe("OpenAI Responses route", () => {
it.effect("joins streamed summary blocks into one continuation reasoning item", () => it.effect("joins streamed summary blocks into one continuation reasoning item", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({ LLM.request({
id: "req_multi_summary_continuation", id: "req_multi_summary_continuation",
model, model,
@@ -1444,7 +1445,7 @@ describe("OpenAI Responses route", () => {
it.effect("skips non-persisted reasoning ids without encrypted state", () => it.effect("skips non-persisted reasoning ids without encrypted state", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLM.request({ LLM.request({
id: "req_reasoning_without_encrypted_state", id: "req_reasoning_without_encrypted_state",
model, model,
@@ -1761,7 +1762,7 @@ describe("OpenAI Responses route", () => {
it.effect("lowers user image and PDF content", () => it.effect("lowers user image and PDF content", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({ LLM.request({
id: "req_media", id: "req_media",
model, model,
@@ -1792,7 +1793,7 @@ describe("OpenAI Responses route", () => {
it.effect("uses xAI inline file encoding for user PDFs", () => it.effect("uses xAI inline file encoding for user PDFs", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({ LLM.request({
model: xaiModel, model: xaiModel,
messages: [ messages: [
@@ -1824,7 +1825,7 @@ describe("OpenAI Responses route", () => {
it.effect("rejects unsupported user media content", () => it.effect("rejects unsupported user media content", () =>
Effect.gen(function* () { Effect.gen(function* () {
const error = yield* compileRequest( const error = yield* LLMClient.prepare(
LLM.request({ LLM.request({
id: "req_media", id: "req_media",
model, model,
+6 -7
View File
@@ -2,7 +2,6 @@ import { describe, expect } from "bun:test"
import { Effect } from "effect" import { Effect } from "effect"
import { LLM, Message } from "../../src" import { LLM, Message } from "../../src"
import { LLMClient } from "../../src/route" import { LLMClient } from "../../src/route"
import { compileRequest } from "../../src/route/client"
import * as OpenRouter from "../../src/providers/openrouter" import * as OpenRouter from "../../src/providers/openrouter"
import { it } from "../lib/effect" import { it } from "../lib/effect"
import { fixedResponse } from "../lib/http" import { fixedResponse } from "../lib/http"
@@ -20,7 +19,7 @@ describe("OpenRouter", () => {
}) })
expect(model.route.endpoint.baseURL).toBe("https://openrouter.ai/api/v1") expect(model.route.endpoint.baseURL).toBe("https://openrouter.ai/api/v1")
const prepared = yield* compileRequest(LLM.request({ model, prompt: "Say hello." })) const prepared = yield* LLMClient.prepare(LLM.request({ model, prompt: "Say hello." }))
expect(prepared.route).toBe("openrouter") expect(prepared.route).toBe("openrouter")
expect(prepared.body).toMatchObject({ expect(prepared.body).toMatchObject({
@@ -33,7 +32,7 @@ describe("OpenRouter", () => {
it.effect("applies OpenRouter payload options from the model helper", () => it.effect("applies OpenRouter payload options from the model helper", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare(
LLM.request({ LLM.request({
model: OpenRouter.configure({ model: OpenRouter.configure({
apiKey: "test-key", apiKey: "test-key",
@@ -101,7 +100,7 @@ describe("OpenRouter", () => {
{ type: "reasoning.text", signature: "signed", format: "anthropic-claude-v1", index: 0 }, { type: "reasoning.text", signature: "signed", format: "anthropic-claude-v1", index: 0 },
{ type: "reasoning.encrypted", data: "opaque", format: "openai-responses-v1", index: 1 }, { type: "reasoning.encrypted", data: "opaque", format: "openai-responses-v1", index: 1 },
] ]
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenRouter.OpenRouterBody>(
LLM.request({ LLM.request({
model: OpenRouter.configure({ apiKey: "test-key" }).model("anthropic/claude-sonnet-4.6"), model: OpenRouter.configure({ apiKey: "test-key" }).model("anthropic/claude-sonnet-4.6"),
messages: [ messages: [
@@ -134,7 +133,7 @@ describe("OpenRouter", () => {
{ type: "reasoning.encrypted", id: "state", data: "opaque" }, { type: "reasoning.encrypted", id: "state", data: "opaque" },
{ type: "reasoning.encrypted", id: "state", data: "opaque" }, { type: "reasoning.encrypted", id: "state", data: "opaque" },
] ]
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenRouter.OpenRouterBody>(
LLM.request({ LLM.request({
model: OpenRouter.configure({ apiKey: "test-key" }).model("anthropic/claude-sonnet-4.6"), model: OpenRouter.configure({ apiKey: "test-key" }).model("anthropic/claude-sonnet-4.6"),
messages: [ messages: [
@@ -159,7 +158,7 @@ describe("OpenRouter", () => {
{ type: "reasoning.text", id: "first", index: 0, text: "A", opaque: "first" }, { type: "reasoning.text", id: "first", index: 0, text: "A", opaque: "first" },
{ type: "reasoning.text", id: "second", index: 1, text: "B", opaque: "second" }, { type: "reasoning.text", id: "second", index: 1, text: "B", opaque: "second" },
] ]
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenRouter.OpenRouterBody>(
LLM.request({ LLM.request({
model: OpenRouter.configure({ apiKey: "test-key" }).model("anthropic/claude-sonnet-4.6"), model: OpenRouter.configure({ apiKey: "test-key" }).model("anthropic/claude-sonnet-4.6"),
messages: [ messages: [
@@ -180,7 +179,7 @@ describe("OpenRouter", () => {
it.effect("omits scalar reasoning without continuation details", () => it.effect("omits scalar reasoning without continuation details", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenRouter.OpenRouterBody>(
LLM.request({ LLM.request({
model: OpenRouter.configure({ apiKey: "test-key" }).model("anthropic/claude-sonnet-4.6"), model: OpenRouter.configure({ apiKey: "test-key" }).model("anthropic/claude-sonnet-4.6"),
messages: [Message.assistant({ type: "reasoning", text: "Thinking" })], messages: [Message.assistant({ type: "reasoning", text: "Thinking" })],
@@ -3,8 +3,7 @@ import { Effect } from "effect"
import { LLM } from "../src" import { LLM } from "../src"
import { OpenAIChat } from "../src/protocols" import { OpenAIChat } from "../src/protocols"
import { ToolSchemaProjection } from "../src/protocols/utils/tool-schema" import { ToolSchemaProjection } from "../src/protocols/utils/tool-schema"
import { Auth } from "../src/route" import { Auth, LLMClient } from "../src/route"
import { compileRequest } from "../src/route/client"
import { it } from "./lib/effect" import { it } from "./lib/effect"
describe("tool schema projections", () => { describe("tool schema projections", () => {
@@ -80,7 +79,7 @@ describe("tool schema projections", () => {
const model = OpenAIChat.route const model = OpenAIChat.route
.with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") }) .with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") })
.model({ id: "kimi-k2", compatibility: { toolSchema: "moonshot" } }) .model({ id: "kimi-k2", compatibility: { toolSchema: "moonshot" } })
const prepared = yield* compileRequest( const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
LLM.request({ LLM.request({
model, model,
prompt: "Use the tool.", prompt: "Use the tool.",
+1 -1
View File
@@ -69,7 +69,7 @@ export const DialogFork: Component = () => {
const dir = base64Encode(sdk().directory) const dir = base64Encode(sdk().directory)
sdk() sdk()
.api.session.fork({ sessionID, boundary: { type: "before", messageID: item.id } }) .api.session.fork({ sessionID, messageID: item.id })
.then((forked) => { .then((forked) => {
dialog.close() dialog.close()
prompt.set(restored, undefined, { dir, id: forked.id }) prompt.set(restored, undefined, { dir, id: forked.id })
@@ -6,22 +6,17 @@ import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button" import { IconButton } from "@opencode-ai/ui/icon-button"
import { List } from "@opencode-ai/ui/list" import { List } from "@opencode-ai/ui/list"
import { TextField } from "@opencode-ai/ui/text-field" import { TextField } from "@opencode-ai/ui/text-field"
import { useMutation } from "@tanstack/solid-query" import { Show } from "solid-js"
import { showToast } from "@/utils/toast"
import { useNavigate } from "@solidjs/router"
import { createEffect, createMemo, createResource, Show } from "solid-js"
import { createStore } from "solid-js/store"
import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row" import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row"
import { useGlobal } from "@/context/global"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { usePlatform } from "@/context/platform" import { ServerConnection } from "@/context/server"
import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
import { detectServerProtocol } from "@/utils/server-protocol"
import { type ServerHealth, useCheckServerHealth } from "@/utils/server-health"
import { useSettings } from "@/context/settings" import { useSettings } from "@/context/settings"
import { useTabs } from "@/context/tabs" import {
type ServerDomainController,
const DEFAULT_USERNAME = "opencode" type ServerFormController,
useServerDomainController,
useServerFormController,
} from "@/components/server/server-management-controller"
interface ServerFormProps { interface ServerFormProps {
value: string value: string
@@ -40,76 +35,6 @@ interface ServerFormProps {
onBack: () => void onBack: () => void
} }
function showRequestError(language: ReturnType<typeof useLanguage>, err: unknown) {
showToast({
variant: "error",
title: language.t("common.requestFailed"),
description: err instanceof Error ? err.message : String(err),
})
}
function useDefaultServer() {
const language = useLanguage()
const platform = usePlatform()
const [defaultKey, defaultUrlActions] = createResource(
async () => {
try {
const key = await platform.getDefaultServer?.()
if (!key) return null
return key
} catch (err) {
showRequestError(language, err)
return null
}
},
{ initialValue: null },
)
const canDefault = createMemo(() => !!platform.getDefaultServer && !!platform.setDefaultServer)
const setDefault = async (key: ServerConnection.Key | null) => {
try {
await platform.setDefaultServer?.(key)
defaultUrlActions.mutate(key)
} catch (err) {
showRequestError(language, err)
}
}
return { defaultKey: () => defaultKey.latest, canDefault, setDefault }
}
function useServerPreview() {
const checkServerHealth = useCheckServerHealth()
const looksComplete = (value: string) => {
const normalized = normalizeServerUrl(value)
if (!normalized) return false
const host = normalized.replace(/^https?:\/\//, "").split("/")[0]
if (!host) return false
if (host.includes("localhost") || host.startsWith("127.0.0.1")) return true
return host.includes(".") || host.includes(":")
}
const previewStatus = async (
value: string,
username: string,
password: string,
setStatus: (value: boolean | undefined) => void,
) => {
setStatus(undefined)
if (!looksComplete(value)) return
const normalized = normalizeServerUrl(value)
if (!normalized) return
const http: ServerConnection.HttpBase = { url: normalized }
if (username) http.username = username
if (password) http.password = password
const result = await checkServerHealth(http)
setStatus(result.healthy)
}
return { previewStatus }
}
function ServerForm(props: ServerFormProps) { function ServerForm(props: ServerFormProps) {
const language = useLanguage() const language = useLanguage()
const keyDown = (event: KeyboardEvent) => { const keyDown = (event: KeyboardEvent) => {
@@ -177,401 +102,40 @@ function ServerForm(props: ServerFormProps) {
export function DialogSelectServer() { export function DialogSelectServer() {
const dialog = useDialog() const dialog = useDialog()
const controller = useServerManagementController({ onSelect: dialog.close }) const language = useLanguage()
const domain = useServerDomainController({ onSelect: () => dialog.close() })
const form = useServerFormController({ onSelect: () => dialog.close() })
const title = () => {
if (!form.state.open()) return language.t("dialog.server.title")
return (
<div class="flex items-center gap-2 -ml-2">
<IconButton icon="arrow-left" variant="ghost" onClick={form.reset} aria-label={language.t("common.goBack")} />
<span>
{form.state.adding() ? language.t("dialog.server.add.title") : language.t("dialog.server.edit.title")}
</span>
</div>
)
}
return ( return (
<Dialog title={controller.formTitle()}> <Dialog title={title()}>
<div class="flex flex-1 min-h-0 flex-col px-5"> <div class="flex flex-1 min-h-0 flex-col px-5">
<Show when={controller.isFormMode()} fallback={<ServerConnectionList controller={controller} />}> <Show
<ServerConnectionForm controller={controller} /> when={form.state.open()}
fallback={<ServerConnectionList domain={domain} onAdd={form.start.add} onEdit={form.start.edit} />}
>
<ServerConnectionForm form={form} />
</Show> </Show>
</div> </div>
</Dialog> </Dialog>
) )
} }
export function useServerManagementController(options: { onSelect?: () => void; navigateOnAdd?: boolean } = {}) { export function ServerConnectionList(props: {
const navigate = useNavigate() domain: ServerDomainController
const server = useServer() onAdd: () => void
const tabs = useTabs() onEdit: (server: ServerConnection.Http) => void
const global = useGlobal() }) {
const platform = usePlatform()
const language = useLanguage()
const { defaultKey, canDefault, setDefault } = useDefaultServer()
const { previewStatus } = useServerPreview()
const checkServerHealth = useCheckServerHealth()
const [store, setStore] = createStore({
addServer: {
url: "",
name: "",
username: DEFAULT_USERNAME,
password: "",
error: "",
showForm: false,
status: undefined as boolean | undefined,
},
editServer: {
id: undefined as string | undefined,
value: "",
name: "",
username: "",
password: "",
error: "",
status: undefined as boolean | undefined,
},
})
const resetAdd = () => {
setStore("addServer", {
url: "",
name: "",
username: DEFAULT_USERNAME,
password: "",
error: "",
showForm: false,
status: undefined,
})
}
const resetEdit = () => {
setStore("editServer", {
id: undefined,
value: "",
name: "",
username: "",
password: "",
error: "",
status: undefined,
})
}
const addMutation = useMutation(() => ({
mutationFn: async (value: string) => {
const normalized = normalizeServerUrl(value)
if (!normalized) {
resetAdd()
return
}
const conn: ServerConnection.Http = {
type: "http",
http: { url: normalized },
}
if (store.addServer.name.trim()) conn.displayName = store.addServer.name.trim()
if (store.addServer.password) conn.http.password = store.addServer.password
if (store.addServer.password && store.addServer.username) conn.http.username = store.addServer.username
const result = await checkServerHealth(conn.http)
if (!result.healthy) {
setStore("addServer", { error: language.t("dialog.server.add.error") })
return
}
if (
!settings.general.newLayoutDesigns() &&
(await detectServerProtocol(conn.http, platform.fetch ?? globalThis.fetch)) === "v2"
) {
setStore("addServer", { error: language.t("dialog.server.add.error") })
return
}
resetAdd()
if (options.navigateOnAdd === false) {
server.add(conn)
options.onSelect?.()
return
}
await select(conn, true)
},
}))
const editMutation = useMutation(() => ({
mutationFn: async (input: { original: ServerConnection.Any; value: string }) => {
if (input.original.type !== "http") return
const normalized = normalizeServerUrl(input.value)
if (!normalized) {
resetEdit()
return
}
const name = store.editServer.name.trim() || undefined
const username = store.editServer.username || undefined
const password = store.editServer.password || undefined
const existingName = input.original.displayName
if (
normalized === input.original.http.url &&
name === existingName &&
username === input.original.http.username &&
password === input.original.http.password
) {
resetEdit()
return
}
const conn: ServerConnection.Http = {
type: "http",
displayName: name,
http: { url: normalized, username, password },
}
const result = await checkServerHealth(conn.http)
if (!result.healthy) {
setStore("editServer", { error: language.t("dialog.server.add.error") })
return
}
if (
!settings.general.newLayoutDesigns() &&
(await detectServerProtocol(conn.http, platform.fetch ?? globalThis.fetch)) === "v2"
) {
setStore("editServer", { error: language.t("dialog.server.add.error") })
return
}
if (normalized === input.original.http.url) {
server.add(conn)
} else {
replaceServer(input.original, conn)
}
resetEdit()
},
}))
const replaceServer = (original: ServerConnection.Http, next: ServerConnection.Http) => {
const originalKey = ServerConnection.key(original)
const active = server.key
tabs.removeServer(originalKey)
const newConn = server.add(next)
if (!newConn) return
const nextActive = active === originalKey ? ServerConnection.key(newConn) : active
if (nextActive) server.setActive(nextActive)
server.remove(originalKey)
}
const items = createMemo(() => {
const current = server.current
const list = server.list
if (!current) return list
if (!list.includes(current)) return [current, ...list]
return [current, ...list.filter((x) => x !== current)]
})
const settings = useSettings()
const current = createMemo<ServerConnection.Any | undefined>(() =>
settings.general.newLayoutDesigns()
? undefined
: (items().find((x) => ServerConnection.key(x) === server.key) ?? items()[0]),
)
const sortedItems = createMemo(() => {
const raw = items()
const list = settings.general.newLayoutDesigns()
? raw
: raw.filter((x) => global.ensureServerCtx(x).sdk.protocolKind() !== "v2")
if (!list.length) return list
const active = current()
const order = new Map(list.map((url, index) => [url, index] as const))
const rank = (value?: ServerHealth) => {
if (value?.healthy === true) return 0
if (value?.healthy === false) return 2
return 1
}
return list.slice().sort((a, b) => {
if (a === active) return -1
if (b === active) return 1
const diff =
rank(global.servers.health[ServerConnection.key(a)]) - rank(global.servers.health[ServerConnection.key(b)])
if (diff !== 0) return diff
return (order.get(a) ?? 0) - (order.get(b) ?? 0)
})
})
async function select(conn: ServerConnection.Any, persist?: boolean) {
if (!persist && global.servers.health[ServerConnection.key(conn)]?.healthy === false) return
options.onSelect?.()
if (persist && conn.type === "http") {
server.add(conn)
navigate("/")
return
}
navigate("/")
queueMicrotask(() => server.setActive(ServerConnection.key(conn)))
}
const handleAddChange = (value: string) => {
if (addMutation.isPending) return
setStore("addServer", { url: value, error: "" })
void previewStatus(value, store.addServer.username, store.addServer.password, (next) =>
setStore("addServer", { status: next }),
)
}
const handleAddNameChange = (value: string) => {
if (addMutation.isPending) return
setStore("addServer", { name: value, error: "" })
}
const handleAddUsernameChange = (value: string) => {
if (addMutation.isPending) return
setStore("addServer", { username: value, error: "" })
void previewStatus(store.addServer.url, value, store.addServer.password, (next) =>
setStore("addServer", { status: next }),
)
}
const handleAddPasswordChange = (value: string) => {
if (addMutation.isPending) return
setStore("addServer", { password: value, error: "" })
void previewStatus(store.addServer.url, store.addServer.username, value, (next) =>
setStore("addServer", { status: next }),
)
}
const handleEditChange = (value: string) => {
if (editMutation.isPending) return
setStore("editServer", { value, error: "" })
void previewStatus(value, store.editServer.username, store.editServer.password, (next) =>
setStore("editServer", { status: next }),
)
}
const handleEditNameChange = (value: string) => {
if (editMutation.isPending) return
setStore("editServer", { name: value, error: "" })
}
const handleEditUsernameChange = (value: string) => {
if (editMutation.isPending) return
setStore("editServer", { username: value, error: "" })
void previewStatus(store.editServer.value, value, store.editServer.password, (next) =>
setStore("editServer", { status: next }),
)
}
const handleEditPasswordChange = (value: string) => {
if (editMutation.isPending) return
setStore("editServer", { password: value, error: "" })
void previewStatus(store.editServer.value, store.editServer.username, value, (next) =>
setStore("editServer", { status: next }),
)
}
const mode = createMemo<"list" | "add" | "edit">(() => {
if (store.editServer.id) return "edit"
if (store.addServer.showForm) return "add"
return "list"
})
const editing = createMemo(() => {
if (!store.editServer.id) return
return items().find((x) => x.type === "http" && x.http.url === store.editServer.id)
})
const resetForm = () => {
resetAdd()
resetEdit()
}
const startAdd = () => {
resetEdit()
setStore("addServer", {
showForm: true,
url: "",
name: "",
username: DEFAULT_USERNAME,
password: "",
error: "",
status: undefined,
})
}
const startEdit = (conn: ServerConnection.Http) => {
resetAdd()
setStore("editServer", {
id: conn.http.url,
value: conn.http.url,
name: conn.displayName ?? "",
username: conn.http.username ?? "",
password: conn.http.password ?? "",
error: "",
status: global.servers.health[ServerConnection.key(conn)]?.healthy,
})
}
const submitForm = () => {
if (mode() === "add") {
if (addMutation.isPending) return
setStore("addServer", { error: "" })
addMutation.mutate(store.addServer.url)
return
}
const original = editing()
if (!original) return
if (editMutation.isPending) return
setStore("editServer", { error: "" })
editMutation.mutate({ original, value: store.editServer.value })
}
const isFormMode = createMemo(() => mode() !== "list")
const isAddMode = createMemo(() => mode() === "add")
const formBusy = createMemo(() => (isAddMode() ? addMutation.isPending : editMutation.isPending))
const formTitle = createMemo(() => {
if (!isFormMode()) return language.t("dialog.server.title")
return (
<div class="flex items-center gap-2 -ml-2">
<IconButton icon="arrow-left" variant="ghost" onClick={resetForm} aria-label={language.t("common.goBack")} />
<span>{isAddMode() ? language.t("dialog.server.add.title") : language.t("dialog.server.edit.title")}</span>
</div>
)
})
createEffect(() => {
if (!store.editServer.id) return
if (editing()) return
resetEdit()
})
async function handleRemove(key: ServerConnection.Key) {
try {
if (key.startsWith("wsl:")) await platform.wslServers?.removeServer(key)
tabs.removeServer(key)
server.remove(key)
if ((await platform.getDefaultServer?.()) === key) {
await setDefault(null)
}
} catch (err) {
showRequestError(language, err)
}
}
return {
defaultKey,
canDefault,
current,
sortedItems,
status: () => global.servers.health,
isFormMode,
isAddMode,
formTitle,
formBusy,
formValue: () => (isAddMode() ? store.addServer.url : store.editServer.value),
formName: () => (isAddMode() ? store.addServer.name : store.editServer.name),
formUsername: () => (isAddMode() ? store.addServer.username : store.editServer.username),
formPassword: () => (isAddMode() ? store.addServer.password : store.editServer.password),
formError: () => (isAddMode() ? store.addServer.error : store.editServer.error),
formStatus: () => (isAddMode() ? store.addServer.status : store.editServer.status),
select,
setDefault,
startAdd,
startEdit,
resetForm,
submitForm,
canRemove: server.canRemove,
handleRemove,
handleFormChange: () => (isAddMode() ? handleAddChange : handleEditChange),
handleFormNameChange: () => (isAddMode() ? handleAddNameChange : handleEditNameChange),
handleFormUsernameChange: () => (isAddMode() ? handleAddUsernameChange : handleEditUsernameChange),
handleFormPasswordChange: () => (isAddMode() ? handleAddPasswordChange : handleEditPasswordChange),
}
}
export function ServerConnectionList(props: { controller: ReturnType<typeof useServerManagementController> }) {
const language = useLanguage() const language = useLanguage()
const settings = useSettings() const settings = useSettings()
@@ -585,10 +149,10 @@ export function ServerConnectionList(props: { controller: ReturnType<typeof useS
}} }}
noInitialSelection noInitialSelection
emptyMessage={language.t("dialog.server.empty")} emptyMessage={language.t("dialog.server.empty")}
items={props.controller.sortedItems} items={props.domain.collection.items}
key={(x) => x.http.url} key={(x) => x.http.url}
onSelect={(x) => { onSelect={(x) => {
if (x && !settings.general.newLayoutDesigns()) void props.controller.select(x) if (x && !settings.general.newLayoutDesigns()) void props.domain.selection.select(x)
}} }}
divider={true} divider={true}
> >
@@ -597,15 +161,15 @@ export function ServerConnectionList(props: { controller: ReturnType<typeof useS
return ( return (
<div class="flex items-center gap-3 min-w-0 flex-1 w-full group/item"> <div class="flex items-center gap-3 min-w-0 flex-1 w-full group/item">
<div class="flex flex-col h-full items-center w-5"> <div class="flex flex-col h-full items-center w-5">
<ServerHealthIndicator health={props.controller.status()[key]} /> <ServerHealthIndicator health={props.domain.collection.health()[key]} />
</div> </div>
<ServerRow <ServerRow
conn={i} conn={i}
dimmed={props.controller.status()[key]?.healthy === false} dimmed={props.domain.collection.health()[key]?.healthy === false}
status={props.controller.status()[key]} status={props.domain.collection.health()[key]}
class="flex items-center gap-3 min-w-0 flex-1" class="flex items-center gap-3 min-w-0 flex-1"
badge={ badge={
<Show when={props.controller.defaultKey() === ServerConnection.key(i)}> <Show when={props.domain.defaults.key() === ServerConnection.key(i)}>
<span class="text-text-base bg-surface-base text-14-regular px-1.5 rounded-xs"> <span class="text-text-base bg-surface-base text-14-regular px-1.5 rounded-xs">
{language.t("dialog.server.status.default")} {language.t("dialog.server.status.default")}
</span> </span>
@@ -614,7 +178,12 @@ export function ServerConnectionList(props: { controller: ReturnType<typeof useS
showCredentials showCredentials
/> />
<div class="flex items-center justify-center gap-4 pl-4"> <div class="flex items-center justify-center gap-4 pl-4">
<Show when={props.controller.current() && ServerConnection.key(props.controller.current()!) === key}> <Show
when={
props.domain.collection.current() &&
ServerConnection.key(props.domain.collection.current()!) === key
}
>
<Icon name="check" class="h-6" /> <Icon name="check" class="h-6" />
</Show> </Show>
@@ -633,27 +202,27 @@ export function ServerConnectionList(props: { controller: ReturnType<typeof useS
<DropdownMenu.Item <DropdownMenu.Item
onSelect={() => { onSelect={() => {
if (i.type !== "http") return if (i.type !== "http") return
props.controller.startEdit(i) props.onEdit(i)
}} }}
> >
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.edit")}</DropdownMenu.ItemLabel> <DropdownMenu.ItemLabel>{language.t("dialog.server.menu.edit")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item> </DropdownMenu.Item>
<Show when={props.controller.canDefault() && props.controller.defaultKey() !== key}> <Show when={props.domain.defaults.available() && props.domain.defaults.key() !== key}>
<DropdownMenu.Item onSelect={() => props.controller.setDefault(key)}> <DropdownMenu.Item onSelect={() => props.domain.defaults.set(key)}>
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.default")}</DropdownMenu.ItemLabel> <DropdownMenu.ItemLabel>{language.t("dialog.server.menu.default")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item> </DropdownMenu.Item>
</Show> </Show>
<Show when={props.controller.canDefault() && props.controller.defaultKey() === key}> <Show when={props.domain.defaults.available() && props.domain.defaults.key() === key}>
<DropdownMenu.Item onSelect={() => props.controller.setDefault(null)}> <DropdownMenu.Item onSelect={() => props.domain.defaults.set(null)}>
<DropdownMenu.ItemLabel> <DropdownMenu.ItemLabel>
{language.t("dialog.server.menu.defaultRemove")} {language.t("dialog.server.menu.defaultRemove")}
</DropdownMenu.ItemLabel> </DropdownMenu.ItemLabel>
</DropdownMenu.Item> </DropdownMenu.Item>
</Show> </Show>
<Show when={props.controller.canRemove(key)}> <Show when={props.domain.connection.canRemove(key)}>
<DropdownMenu.Separator /> <DropdownMenu.Separator />
<DropdownMenu.Item <DropdownMenu.Item
onSelect={() => props.controller.handleRemove(ServerConnection.key(i))} onSelect={() => props.domain.connection.remove(key)}
class="text-text-on-critical-base hover:bg-surface-critical-weak" class="text-text-on-critical-base hover:bg-surface-critical-weak"
> >
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.delete")}</DropdownMenu.ItemLabel> <DropdownMenu.ItemLabel>{language.t("dialog.server.menu.delete")}</DropdownMenu.ItemLabel>
@@ -674,7 +243,7 @@ export function ServerConnectionList(props: { controller: ReturnType<typeof useS
variant="secondary" variant="secondary"
icon="plus-small" icon="plus-small"
size="large" size="large"
onClick={props.controller.startAdd} onClick={props.onAdd}
class="py-1.5 pl-1.5 pr-3 flex items-center gap-1.5" class="py-1.5 pl-1.5 pr-3 flex items-center gap-1.5"
> >
{language.t("dialog.server.add.button")} {language.t("dialog.server.add.button")}
@@ -684,38 +253,38 @@ export function ServerConnectionList(props: { controller: ReturnType<typeof useS
) )
} }
export function ServerConnectionForm(props: { controller: ReturnType<typeof useServerManagementController> }) { export function ServerConnectionForm(props: { form: ServerFormController }) {
const language = useLanguage() const language = useLanguage()
return ( return (
<div class="flex flex-1 min-h-0 flex-col gap-4"> <div class="flex flex-1 min-h-0 flex-col gap-4">
<ServerForm <ServerForm
value={props.controller.formValue()} value={props.form.state.value()}
name={props.controller.formName()} name={props.form.state.name()}
username={props.controller.formUsername()} username={props.form.state.username()}
password={props.controller.formPassword()} password={props.form.state.password()}
placeholder={language.t("dialog.server.add.placeholder")} placeholder={language.t("dialog.server.add.placeholder")}
busy={props.controller.formBusy()} busy={props.form.state.busy()}
error={props.controller.formError()} error={props.form.state.error()}
status={props.controller.formStatus()} status={props.form.state.status()}
onChange={props.controller.handleFormChange()} onChange={props.form.change.value}
onNameChange={props.controller.handleFormNameChange()} onNameChange={props.form.change.name}
onUsernameChange={props.controller.handleFormUsernameChange()} onUsernameChange={props.form.change.username}
onPasswordChange={props.controller.handleFormPasswordChange()} onPasswordChange={props.form.change.password}
onSubmit={props.controller.submitForm} onSubmit={props.form.submit}
onBack={props.controller.resetForm} onBack={props.form.reset}
/> />
<div class="shrink-0 pb-5"> <div class="shrink-0 pb-5">
<Button <Button
variant="primary" variant="primary"
size="large" size="large"
onClick={props.controller.submitForm} onClick={props.form.submit}
disabled={props.controller.formBusy()} disabled={props.form.state.busy()}
class="px-3 py-1.5" class="px-3 py-1.5"
> >
{props.controller.formBusy() {props.form.state.busy()
? language.t("dialog.server.add.checking") ? language.t("dialog.server.add.checking")
: props.controller.isAddMode() : props.form.state.adding()
? language.t("dialog.server.add.button") ? language.t("dialog.server.add.button")
: language.t("common.save")} : language.t("common.save")}
</Button> </Button>
@@ -0,0 +1,336 @@
import { useNavigate } from "@solidjs/router"
import { useMutation } from "@tanstack/solid-query"
import { createEffect, createMemo, createResource, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import { useGlobal } from "@/context/global"
import { useLanguage } from "@/context/language"
import { usePlatform } from "@/context/platform"
import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
import { useSettings } from "@/context/settings"
import { useTabs } from "@/context/tabs"
import { type ServerHealth, useCheckServerHealth } from "@/utils/server-health"
import { detectServerProtocol } from "@/utils/server-protocol"
import { showToast } from "@/utils/toast"
import { createServerHealthPreview, replaceServerConnection, type ServerFormValues } from "./server-management"
const DEFAULT_USERNAME = "opencode"
type FormMode = "list" | "add" | "edit"
function showRequestError(language: ReturnType<typeof useLanguage>, err: unknown) {
showToast({
variant: "error",
title: language.t("common.requestFailed"),
description: err instanceof Error ? err.message : String(err),
})
}
function useDefaultServer() {
const language = useLanguage()
const platform = usePlatform()
const [defaultKey, defaultKeyActions] = createResource(
async () => {
try {
return (await platform.getDefaultServer?.()) ?? null
} catch (err) {
showRequestError(language, err)
return null
}
},
{ initialValue: null },
)
const set = async (key: ServerConnection.Key | null) => {
try {
await platform.setDefaultServer?.(key)
defaultKeyActions.mutate(key)
} catch (err) {
showRequestError(language, err)
}
}
return {
key: () => defaultKey.latest,
available: createMemo(() => !!platform.getDefaultServer && !!platform.setDefaultServer),
set,
}
}
function useServerMutations() {
const server = useServer()
const tabs = useTabs()
return {
add: (connection: ServerConnection.Http) => server.add(connection),
replace: (original: ServerConnection.Http, next: ServerConnection.Http) =>
replaceServerConnection(original, next, {
active: () => server.key,
removeTabs: (key) => tabs.removeServer(key),
add: (connection) => server.add(connection),
setActive: (key) => server.setActive(key),
remove: (key) => server.remove(key),
}),
}
}
export function useServerActionsController() {
const server = useServer()
const tabs = useTabs()
const platform = usePlatform()
const language = useLanguage()
const defaults = useDefaultServer()
const remove = async (key: ServerConnection.Key) => {
try {
if (key.startsWith("wsl:")) await platform.wslServers?.removeServer(key)
tabs.removeServer(key)
server.remove(key)
if ((await platform.getDefaultServer?.()) === key) await defaults.set(null)
} catch (err) {
showRequestError(language, err)
}
}
return { defaults, connection: { canRemove: server.canRemove, remove } }
}
export type ServerActionsController = ReturnType<typeof useServerActionsController>
export function useServerCollectionController() {
const server = useServer()
const global = useGlobal()
const settings = useSettings()
const actions = useServerActionsController()
const items = createMemo(() => {
const current = server.current
const list = server.list
if (!current) return list
if (!list.includes(current)) return [current, ...list]
return [current, ...list.filter((item) => item !== current)]
})
const current = createMemo<ServerConnection.Any | undefined>(() =>
settings.general.newLayoutDesigns()
? undefined
: (items().find((item) => ServerConnection.key(item) === server.key) ?? items()[0]),
)
const sorted = createMemo(() => {
const raw = items()
const list = settings.general.newLayoutDesigns()
? raw
: raw.filter((item) => global.ensureServerCtx(item).sdk.protocolKind() !== "v2")
if (!list.length) return list
const active = current()
const order = new Map(list.map((item, index) => [item, index] as const))
const rank = (value?: ServerHealth) => {
if (value?.healthy === true) return 0
if (value?.healthy === false) return 2
return 1
}
return list.slice().sort((a, b) => {
if (a === active) return -1
if (b === active) return 1
const diff =
rank(global.servers.health[ServerConnection.key(a)]) - rank(global.servers.health[ServerConnection.key(b)])
if (diff !== 0) return diff
return (order.get(a) ?? 0) - (order.get(b) ?? 0)
})
})
return {
collection: {
items: sorted,
current,
health: () => global.servers.health,
},
...actions,
}
}
export type ServerCollectionController = ReturnType<typeof useServerCollectionController>
export function useServerDomainController(options: { onSelect?: () => void } = {}) {
const navigate = useNavigate()
const server = useServer()
const global = useGlobal()
const collection = useServerCollectionController()
const select = async (connection: ServerConnection.Any) => {
if (global.servers.health[ServerConnection.key(connection)]?.healthy === false) return
options.onSelect?.()
navigate("/")
queueMicrotask(() => server.setActive(ServerConnection.key(connection)))
}
return { ...collection, selection: { select } }
}
export type ServerDomainController = ReturnType<typeof useServerDomainController>
export function useServerFormController(options: { onSelect?: () => void; navigateOnAdd?: boolean } = {}) {
const navigate = useNavigate()
const server = useServer()
const global = useGlobal()
const platform = usePlatform()
const language = useLanguage()
const settings = useSettings()
const mutations = useServerMutations()
const checkServerHealth = useCheckServerHealth()
const healthPreview = createServerHealthPreview(checkServerHealth)
const [store, setStore] = createStore({
mode: "list" as FormMode,
originalUrl: undefined as string | undefined,
values: { url: "", name: "", username: DEFAULT_USERNAME, password: "" },
error: "",
status: undefined as boolean | undefined,
})
onCleanup(healthPreview.cancel)
const reset = () => {
healthPreview.cancel()
setStore({
mode: "list",
originalUrl: undefined,
values: { url: "", name: "", username: DEFAULT_USERNAME, password: "" },
error: "",
status: undefined,
})
}
const allServers = () => {
if (!server.current || server.list.includes(server.current)) return server.list
return [server.current, ...server.list]
}
const editing = createMemo(() =>
allServers().find((item) => item.type === "http" && item.http.url === store.originalUrl),
)
const request = useMutation(() => ({
mutationFn: async () => {
const normalized = normalizeServerUrl(store.values.url)
if (!normalized) {
reset()
return
}
const original = store.mode === "edit" ? editing() : undefined
if (store.mode === "edit" && !original) return
const name = store.values.name.trim() || undefined
const username = store.values.username || undefined
const password = store.values.password || undefined
if (
original?.type === "http" &&
normalized === original.http.url &&
name === original.displayName &&
username === original.http.username &&
password === original.http.password
) {
reset()
return
}
const connection: ServerConnection.Http = {
type: "http",
displayName: name,
http: {
url: normalized,
username: store.mode === "add" && !password ? undefined : username,
password,
},
}
const result = await checkServerHealth(connection.http)
if (!result.healthy) {
setStore("error", language.t("dialog.server.add.error"))
return
}
if (
!settings.general.newLayoutDesigns() &&
(await detectServerProtocol(connection.http, platform.fetch ?? globalThis.fetch)) === "v2"
) {
setStore("error", language.t("dialog.server.add.error"))
return
}
if (original?.type === "http") {
if (normalized === original.http.url) mutations.add(connection)
if (normalized !== original.http.url) mutations.replace(original, connection)
reset()
return
}
reset()
if (options.navigateOnAdd === false) {
mutations.add(connection)
options.onSelect?.()
return
}
mutations.add(connection)
options.onSelect?.()
navigate("/")
},
}))
const preview = () => void healthPreview.preview(store.values, (status) => setStore("status", status))
const change = (field: keyof ServerFormValues, value: string) => {
if (request.isPending) return
setStore("values", field, value)
setStore("error", "")
if (field !== "name") preview()
}
const startAdd = () => {
reset()
setStore("mode", "add")
}
const startEdit = (connection: ServerConnection.Http) => {
reset()
setStore({
mode: "edit",
originalUrl: connection.http.url,
values: {
url: connection.http.url,
name: connection.displayName ?? "",
username: connection.http.username ?? "",
password: connection.http.password ?? "",
},
error: "",
status: global.servers.health[ServerConnection.key(connection)]?.healthy,
})
}
const submit = () => {
if (store.mode === "list" || request.isPending) return
setStore("error", "")
request.mutate()
}
createEffect(() => {
if (store.mode !== "edit") return
if (editing()) return
reset()
})
return {
state: {
mode: () => store.mode,
open: () => store.mode !== "list",
adding: () => store.mode === "add",
busy: () => request.isPending,
value: () => store.values.url,
name: () => store.values.name,
username: () => store.values.username,
password: () => store.values.password,
error: () => store.error,
status: () => store.status,
},
change: {
value: (value: string) => change("url", value),
name: (value: string) => change("name", value),
username: (value: string) => change("username", value),
password: (value: string) => change("password", value),
},
start: { add: startAdd, edit: startEdit },
reset,
submit,
}
}
export type ServerFormController = ReturnType<typeof useServerFormController>
@@ -0,0 +1,99 @@
import { describe, expect, test } from "bun:test"
import { ServerConnection } from "@/context/server"
import { createServerHealthPreview, replaceServerConnection, type ServerFormValues } from "./server-management"
function deferred<T>() {
let resolve!: (value: T) => void
const promise = new Promise<T>((done) => {
resolve = done
})
return { promise, resolve }
}
const values = (url: string): ServerFormValues => ({ url, name: "", username: "opencode", password: "" })
describe("createServerHealthPreview", () => {
test("ignores an older response that resolves after the latest response", async () => {
const first = deferred<{ healthy: boolean }>()
const second = deferred<{ healthy: boolean }>()
const requests = [first, second]
const status: Array<boolean | undefined> = []
const preview = createServerHealthPreview(() => requests.shift()!.promise)
const older = preview.preview(values("old.example.com"), (value) => status.push(value))
const latest = preview.preview(values("new.example.com"), (value) => status.push(value))
second.resolve({ healthy: true })
await latest
first.resolve({ healthy: false })
await older
expect(status).toEqual([undefined, undefined, true])
})
test("an incomplete value invalidates an in-flight response", async () => {
const request = deferred<{ healthy: boolean }>()
const status: Array<boolean | undefined> = []
const preview = createServerHealthPreview(() => request.promise)
const pending = preview.preview(values("server.example.com"), (value) => status.push(value))
await preview.preview(values("server"), (value) => status.push(value))
request.resolve({ healthy: true })
await pending
expect(status).toEqual([undefined, undefined])
})
test("cancellation prevents an in-flight response from updating status", async () => {
const request = deferred<{ healthy: boolean }>()
const status: Array<boolean | undefined> = []
const preview = createServerHealthPreview(() => request.promise)
const pending = preview.preview(values("server.example.com"), (value) => status.push(value))
preview.cancel()
request.resolve({ healthy: true })
await pending
expect(status).toEqual([undefined])
})
})
describe("replaceServerConnection", () => {
const original: ServerConnection.Http = { type: "http", http: { url: "https://old.example.com" } }
const next: ServerConnection.Http = { type: "http", http: { url: "https://new.example.com" } }
test("moves active selection after adding the replacement and removes the original", () => {
const calls: string[] = []
replaceServerConnection(original, next, {
active: () => ServerConnection.key(original),
removeTabs: (key) => calls.push(`tabs:${key}`),
add: (server) => {
calls.push(`add:${ServerConnection.key(server)}`)
return server
},
setActive: (key) => calls.push(`active:${key}`),
remove: (key) => calls.push(`remove:${key}`),
})
expect(calls).toEqual([
"tabs:https://old.example.com",
"add:https://new.example.com",
"active:https://new.example.com",
"remove:https://old.example.com",
])
})
test("keeps the original when the replacement cannot be added", () => {
const removed: ServerConnection.Key[] = []
replaceServerConnection(original, next, {
active: () => ServerConnection.key(original),
removeTabs: () => {},
add: () => undefined,
setActive: () => {},
remove: (key) => removed.push(key),
})
expect(removed).toEqual([])
})
})
@@ -0,0 +1,60 @@
import { normalizeServerUrl, ServerConnection } from "@/context/server"
import type { ServerHealth } from "@/utils/server-health"
export type ServerFormValues = {
url: string
name: string
username: string
password: string
}
export function createServerHealthPreview(
check: (server: ServerConnection.HttpBase) => Promise<Pick<ServerHealth, "healthy">>,
) {
let generation = 0
const cancel = () => {
generation += 1
}
const preview = async (values: ServerFormValues, setStatus: (value: boolean | undefined) => void) => {
const current = ++generation
setStatus(undefined)
const normalized = normalizeServerUrl(values.url)
if (!normalized) return
const host = normalized.replace(/^https?:\/\//, "").split("/")[0]
if (!host) return
if (!host.includes("localhost") && !host.startsWith("127.0.0.1") && !host.includes(".") && !host.includes(":"))
return
const http: ServerConnection.HttpBase = { url: normalized }
if (values.username) http.username = values.username
if (values.password) http.password = values.password
const result = await check(http)
if (current !== generation) return
setStatus(result.healthy)
}
return { cancel, preview }
}
export function replaceServerConnection(
original: ServerConnection.Http,
next: ServerConnection.Http,
operations: {
active: () => ServerConnection.Key | undefined
removeTabs: (key: ServerConnection.Key) => void
add: (server: ServerConnection.Http) => ServerConnection.Any | undefined
setActive: (key: ServerConnection.Key) => void
remove: (key: ServerConnection.Key) => void
},
) {
const originalKey = ServerConnection.key(original)
const active = operations.active()
operations.removeTabs(originalKey)
const added = operations.add(next)
if (!added) return
const nextActive = active === originalKey ? ServerConnection.key(added) : active
if (nextActive) operations.setActive(nextActive)
operations.remove(originalKey)
}
@@ -2,13 +2,13 @@ import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2" import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
import { type Component, Show } from "solid-js" import { type Component, Show } from "solid-js"
import { useServerManagementController } from "@/components/dialog-select-server" import type { ServerActionsController } from "@/components/server/server-management-controller"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { ServerConnection } from "@/context/server" import { ServerConnection } from "@/context/server"
export const ServerRowMenu: Component<{ export const ServerRowMenu: Component<{
server: ServerConnection.Any server: ServerConnection.Any
controller: ReturnType<typeof useServerManagementController> domain: ServerActionsController
onEdit: (server: ServerConnection.Http) => void onEdit: (server: ServerConnection.Http) => void
open?: boolean open?: boolean
onOpenChange?: (open: boolean) => void onOpenChange?: (open: boolean) => void
@@ -19,13 +19,13 @@ export const ServerRowMenu: Component<{
<ServerRowMenuView <ServerRowMenuView
server={props.server} server={props.server}
labels={serverMenuLabels(language)} labels={serverMenuLabels(language)}
canDefault={props.controller.canDefault()} canDefault={props.domain.defaults.available()}
isDefault={props.controller.defaultKey() === key} isDefault={props.domain.defaults.key() === key}
canRemove={props.controller.canRemove(key)} canRemove={props.domain.connection.canRemove(key)}
onEdit={props.onEdit} onEdit={props.onEdit}
onSetDefault={() => props.controller.setDefault(key)} onSetDefault={() => props.domain.defaults.set(key)}
onRemoveDefault={() => props.controller.setDefault(null)} onRemoveDefault={() => props.domain.defaults.set(null)}
onRemove={() => props.controller.handleRemove(key)} onRemove={() => props.domain.connection.remove(key)}
open={props.open} open={props.open}
onOpenChange={props.onOpenChange} onOpenChange={props.onOpenChange}
/> />
@@ -1,16 +1,19 @@
import { Show, type Component } from "solid-js" import { Show, type Component } from "solid-js"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { ServerConnectionForm, ServerConnectionList, useServerManagementController } from "./dialog-select-server" import { useServerDomainController, useServerFormController } from "./server/server-management-controller"
import { ServerConnectionForm, ServerConnectionList } from "./dialog-select-server"
export const SettingsServers: Component = () => { export const SettingsServers: Component = () => {
const language = useLanguage() const language = useLanguage()
const controller = useServerManagementController() const domain = useServerDomainController()
const form = useServerFormController()
return ( return (
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10"> <div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
<div class="flex flex-col flex-1 min-h-0 max-w-[720px]"> <div class="flex flex-col flex-1 min-h-0 max-w-[720px]">
<Show <Show
when={controller.isFormMode()} when={form.state.open()}
fallback={ fallback={
<> <>
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-stronger-non-alpha)_calc(100%_-_24px),transparent)]"> <div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
@@ -18,13 +21,25 @@ export const SettingsServers: Component = () => {
<h2 class="text-16-medium text-text-strong">{language.t("status.popover.tab.servers")}</h2> <h2 class="text-16-medium text-text-strong">{language.t("status.popover.tab.servers")}</h2>
</div> </div>
</div> </div>
<ServerConnectionList controller={controller} /> <ServerConnectionList domain={domain} onAdd={form.start.add} onEdit={form.start.edit} />
</> </>
} }
> >
<div class="flex flex-1 min-h-0 flex-col gap-4 pt-6"> <div class="flex flex-1 min-h-0 flex-col gap-4 pt-6">
<div class="text-16-medium text-text-strong">{controller.formTitle()}</div> <div class="text-16-medium text-text-strong">
<ServerConnectionForm controller={controller} /> <div class="flex items-center gap-2 -ml-2">
<IconButton
icon="arrow-left"
variant="ghost"
onClick={form.reset}
aria-label={language.t("common.goBack")}
/>
<span>
{form.state.adding() ? language.t("dialog.server.add.title") : language.t("dialog.server.edit.title")}
</span>
</div>
</div>
<ServerConnectionForm form={form} />
</div> </div>
</Show> </Show>
</div> </div>
@@ -6,7 +6,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
import { type Component, Show, createEffect, createSignal, onCleanup, onMount } from "solid-js" import { type Component, Show, createEffect, createSignal, onCleanup, onMount } from "solid-js"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { type ServerConnection } from "@/context/server" import { type ServerConnection } from "@/context/server"
import { useServerManagementController } from "../dialog-select-server" import { useServerFormController } from "../server/server-management-controller"
import "./settings-v2.css" import "./settings-v2.css"
export const DialogServerV2: Component<{ export const DialogServerV2: Component<{
@@ -15,39 +15,39 @@ export const DialogServerV2: Component<{
}> = (props) => { }> = (props) => {
const dialog = useDialog() const dialog = useDialog()
const language = useLanguage() const language = useLanguage()
const controller = useServerManagementController({ const form = useServerFormController({
onSelect: () => dialog.close(), onSelect: () => dialog.close(),
navigateOnAdd: false, navigateOnAdd: false,
}) })
const [opened, setOpened] = createSignal(false) const [opened, setOpened] = createSignal(false)
onMount(() => { onMount(() => {
if (props.mode === "add") controller.startAdd() if (props.mode === "add") form.start.add()
if (props.mode === "edit" && props.server) controller.startEdit(props.server) if (props.mode === "edit" && props.server) form.start.edit(props.server)
setOpened(true) setOpened(true)
}) })
onCleanup(() => { onCleanup(() => {
controller.resetForm() form.reset()
}) })
createEffect(() => { createEffect(() => {
if (!opened()) return if (!opened()) return
if (controller.isFormMode()) return if (form.state.open()) return
dialog.close() dialog.close()
}) })
const keyDown = (event: KeyboardEvent) => { const keyDown = (event: KeyboardEvent) => {
if (event.key !== "Enter" || event.isComposing) return if (event.key !== "Enter" || event.isComposing) return
event.preventDefault() event.preventDefault()
controller.submitForm() form.submit()
} }
const title = () => const title = () =>
props.mode === "add" ? language.t("dialog.server.add.title") : language.t("dialog.server.edit.title") props.mode === "add" ? language.t("dialog.server.add.title") : language.t("dialog.server.edit.title")
const submitLabel = () => { const submitLabel = () => {
if (controller.formBusy()) return language.t("dialog.server.add.checking") if (form.state.busy()) return language.t("dialog.server.add.checking")
if (props.mode === "add") return language.t("dialog.server.add.button") if (props.mode === "add") return language.t("dialog.server.add.button")
return language.t("common.save") return language.t("common.save")
} }
@@ -66,16 +66,16 @@ export const DialogServerV2: Component<{
type="text" type="text"
appearance="large" appearance="large"
class="!w-full self-stretch" class="!w-full self-stretch"
value={controller.formValue()} value={form.state.value()}
placeholder={language.t("dialog.server.add.placeholder")} placeholder={language.t("dialog.server.add.placeholder")}
invalid={!!controller.formError()} invalid={!!form.state.error()}
disabled={controller.formBusy()} disabled={form.state.busy()}
autofocus autofocus
onInput={(event) => controller.handleFormChange()(event.currentTarget.value)} onInput={(event) => form.change.value(event.currentTarget.value)}
onKeyDown={keyDown} onKeyDown={keyDown}
/> />
<Show when={controller.formError()}> <Show when={form.state.error()}>
<span class="settings-v2-server-dialog-error">{controller.formError()}</span> <span class="settings-v2-server-dialog-error">{form.state.error()}</span>
</Show> </Show>
</div> </div>
<div class="flex w-full min-w-0 flex-col gap-2"> <div class="flex w-full min-w-0 flex-col gap-2">
@@ -84,10 +84,10 @@ export const DialogServerV2: Component<{
type="text" type="text"
appearance="large" appearance="large"
class="!w-full self-stretch" class="!w-full self-stretch"
value={controller.formName()} value={form.state.name()}
placeholder={language.t("dialog.server.add.namePlaceholder")} placeholder={language.t("dialog.server.add.namePlaceholder")}
disabled={controller.formBusy()} disabled={form.state.busy()}
onInput={(event) => controller.handleFormNameChange()(event.currentTarget.value)} onInput={(event) => form.change.name(event.currentTarget.value)}
onKeyDown={keyDown} onKeyDown={keyDown}
/> />
</div> </div>
@@ -98,10 +98,10 @@ export const DialogServerV2: Component<{
type="text" type="text"
appearance="large" appearance="large"
class="!w-full self-stretch" class="!w-full self-stretch"
value={controller.formUsername()} value={form.state.username()}
placeholder={language.t("dialog.server.add.usernamePlaceholder")} placeholder={language.t("dialog.server.add.usernamePlaceholder")}
disabled={controller.formBusy()} disabled={form.state.busy()}
onInput={(event) => controller.handleFormUsernameChange()(event.currentTarget.value)} onInput={(event) => form.change.username(event.currentTarget.value)}
onKeyDown={keyDown} onKeyDown={keyDown}
/> />
</div> </div>
@@ -111,10 +111,10 @@ export const DialogServerV2: Component<{
type="password" type="password"
appearance="large" appearance="large"
class="!w-full self-stretch" class="!w-full self-stretch"
value={controller.formPassword()} value={form.state.password()}
placeholder={language.t("dialog.server.add.passwordPlaceholder")} placeholder={language.t("dialog.server.add.passwordPlaceholder")}
disabled={controller.formBusy()} disabled={form.state.busy()}
onInput={(event) => controller.handleFormPasswordChange()(event.currentTarget.value)} onInput={(event) => form.change.password(event.currentTarget.value)}
onKeyDown={keyDown} onKeyDown={keyDown}
/> />
</div> </div>
@@ -122,10 +122,10 @@ export const DialogServerV2: Component<{
</div> </div>
</DialogBody> </DialogBody>
<DialogFooter> <DialogFooter>
<ButtonV2 variant="neutral" disabled={controller.formBusy()} onClick={() => dialog.close()}> <ButtonV2 variant="neutral" disabled={form.state.busy()} onClick={() => dialog.close()}>
{language.t("common.cancel")} {language.t("common.cancel")}
</ButtonV2> </ButtonV2>
<ButtonV2 variant="contrast" disabled={controller.formBusy()} onClick={controller.submitForm}> <ButtonV2 variant="contrast" disabled={form.state.busy()} onClick={form.submit}>
{submitLabel()} {submitLabel()}
</ButtonV2> </ButtonV2>
</DialogFooter> </DialogFooter>
@@ -10,7 +10,7 @@ import { ServerRowMenu } from "@/components/server/server-row-menu"
import { ServerHealthIndicator } from "@/components/server/server-row" import { ServerHealthIndicator } from "@/components/server/server-row"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { ServerConnection, serverName } from "@/context/server" import { ServerConnection, serverName } from "@/context/server"
import { useServerManagementController } from "../dialog-select-server" import { useServerCollectionController } from "../server/server-management-controller"
import { DialogServerV2 } from "./dialog-server-v2" import { DialogServerV2 } from "./dialog-server-v2"
import { SettingsListV2 } from "./parts/list" import { SettingsListV2 } from "./parts/list"
import { AddServerMenu, isWslServer, useFilteredWslServers, WslServerSettings } from "@/wsl/settings" import { AddServerMenu, isWslServer, useFilteredWslServers, WslServerSettings } from "@/wsl/settings"
@@ -19,16 +19,16 @@ import "./settings-v2.css"
export const SettingsServersV2: Component = () => { export const SettingsServersV2: Component = () => {
const dialog = useDialog() const dialog = useDialog()
const language = useLanguage() const language = useLanguage()
const controller = useServerManagementController() const domain = useServerCollectionController()
const [store, setStore] = createStore({ filter: "" }) const [store, setStore] = createStore({ filter: "" })
const wslServers = useFilteredWslServers(() => store.filter) const wslServers = useFilteredWslServers(() => store.filter)
const showSearch = createMemo( const showSearch = createMemo(
() => controller.sortedItems().filter((item) => !isWslServer(item)).length + wslServers().length > 1, () => domain.collection.items().filter((item) => !isWslServer(item)).length + wslServers().length > 1,
) )
const filtered = createMemo(() => { const filtered = createMemo(() => {
const items = controller.sortedItems().filter((item) => !isWslServer(item)) const items = domain.collection.items().filter((item) => !isWslServer(item))
const query = store.filter.trim() const query = store.filter.trim()
if (!query) return items if (!query) return items
return fuzzysort return fuzzysort
@@ -39,11 +39,11 @@ export const SettingsServersV2: Component = () => {
}) })
const openAdd = () => { const openAdd = () => {
dialog.push(() => <DialogServerV2 mode="add" />) void dialog.push(() => <DialogServerV2 mode="add" />)
} }
const openEdit = (server: ServerConnection.Http) => { const openEdit = (server: ServerConnection.Http) => {
dialog.push(() => <DialogServerV2 mode="edit" server={server} />) void dialog.push(() => <DialogServerV2 mode="edit" server={server} />)
} }
return ( return (
@@ -97,12 +97,12 @@ export const SettingsServersV2: Component = () => {
} }
> >
<SettingsListV2> <SettingsListV2>
<WslServerSettings controller={controller} servers={wslServers} /> <WslServerSettings domain={domain} servers={wslServers} />
<For each={filtered()}> <For each={filtered()}>
{(item) => { {(item) => {
const key = ServerConnection.key(item) const key = ServerConnection.key(item)
const health = () => controller.status()[key] const health = () => domain.collection.health()[key]
const isDefault = () => controller.defaultKey() === key const isDefault = () => domain.defaults.key() === key
return ( return (
<div class="settings-v2-servers-row"> <div class="settings-v2-servers-row">
<div class="settings-v2-servers-lead"> <div class="settings-v2-servers-lead">
@@ -122,10 +122,10 @@ export const SettingsServersV2: Component = () => {
</div> </div>
</div> </div>
<div class="settings-v2-servers-actions"> <div class="settings-v2-servers-actions">
<Show when={controller.canDefault() && isDefault()}> <Show when={domain.defaults.available() && isDefault()}>
<Tag>{language.t("dialog.server.status.default")}</Tag> <Tag>{language.t("dialog.server.status.default")}</Tag>
</Show> </Show>
<ServerRowMenu server={item} controller={controller} onEdit={openEdit} /> <ServerRowMenu server={item} domain={domain} onEdit={openEdit} />
</div> </div>
</div> </div>
) )
@@ -129,8 +129,8 @@ export const loadProjectsQuery = (scope: ServerScope, api: ProjectApi) =>
api.list().then((projects) => { api.list().then((projects) => {
return projects return projects
.filter((p) => !!p?.id) .filter((p) => !!p?.id)
.map(normalizeProjectInfo)
.filter((p) => !!p.worktree && !p.worktree.includes("opencode-test")) .filter((p) => !!p.worktree && !p.worktree.includes("opencode-test"))
.map(normalizeProjectInfo)
.slice() .slice()
.sort((a, b) => cmp(a.id, b.id)) .sort((a, b) => cmp(a.id, b.id))
}), }),
@@ -168,7 +168,6 @@ export function sanitizeProject(project: Project) {
export function normalizeProjectInfo(project: Project | CurrentProject): Project { export function normalizeProjectInfo(project: Project | CurrentProject): Project {
return { return {
...project, ...project,
worktree: "canonical" in project ? project.canonical : project.worktree,
vcs: project.vcs === "git" ? "git" : undefined, vcs: project.vcs === "git" ? "git" : undefined,
} }
} }
@@ -1,5 +1,5 @@
import { useDirectoryPicker } from "@/components/directory-picker" import { useDirectoryPicker } from "@/components/directory-picker"
import { useServerManagementController } from "@/components/dialog-select-server" import { useServerActionsController } from "@/components/server/server-management-controller"
import { useSettingsCommand } from "@/components/settings-dialog" import { useSettingsCommand } from "@/components/settings-dialog"
import { DialogServerV2 } from "@/components/settings-v2/dialog-server-v2" import { DialogServerV2 } from "@/components/settings-v2/dialog-server-v2"
import { type LocalProject } from "@/context/layout" import { type LocalProject } from "@/context/layout"
@@ -22,7 +22,7 @@ export function createHomeProjectsController(home: HomeController) {
const language = useLanguage() const language = useLanguage()
const notification = useNotification() const notification = useNotification()
const openSettings = useSettingsCommand() const openSettings = useSettingsCommand()
const serverManagement = useServerManagementController({ navigateOnAdd: false }) const serverManagement = useServerActionsController()
const [_state, setState, _, ready] = persisted( const [_state, setState, _, ready] = persisted(
Persist.global("home.servers", ["home.servers.v1"]), Persist.global("home.servers", ["home.servers.v1"]),
createStore({ collapsed: {} as Record<string, boolean> }), createStore({ collapsed: {} as Record<string, boolean> }),
@@ -56,12 +56,12 @@ export function createHomeProjectsController(home: HomeController) {
const key = ServerConnection.key(conn) const key = ServerConnection.key(conn)
setState("collapsed", key, !state().collapsed[key]) setState("collapsed", key, !state().collapsed[key])
}, },
canDefault: serverManagement.canDefault, canDefault: serverManagement.defaults.available,
defaultKey: serverManagement.defaultKey, defaultKey: serverManagement.defaults.key,
setDefault: (conn: ServerConnection.Any | undefined) => setDefault: (conn: ServerConnection.Any | undefined) =>
serverManagement.setDefault(conn ? ServerConnection.key(conn) : null), serverManagement.defaults.set(conn ? ServerConnection.key(conn) : null),
canRemove: (conn: ServerConnection.Any) => serverManagement.canRemove(ServerConnection.key(conn)), canRemove: (conn: ServerConnection.Any) => serverManagement.connection.canRemove(ServerConnection.key(conn)),
remove: (conn: ServerConnection.Any) => serverManagement.handleRemove(ServerConnection.key(conn)), remove: (conn: ServerConnection.Any) => serverManagement.connection.remove(ServerConnection.key(conn)),
edit: (conn: ServerConnection.Http) => dialog.show(() => <DialogServerV2 mode="edit" server={conn} />), edit: (conn: ServerConnection.Http) => dialog.show(() => <DialogServerV2 mode="edit" server={conn} />),
focus: home.selection.focusServer, focus: home.selection.focusServer,
}, },
@@ -26,6 +26,7 @@ function setup(
return new Response(undefined, { status: 204 }) return new Response(undefined, { status: 204 })
if (request.method === "POST" && request.url.endsWith("/prompt")) { if (request.method === "POST" && request.url.endsWith("/prompt")) {
return Response.json({ return Response.json({
admittedSeq: 1,
id: "msg_1", id: "msg_1",
sessionID: "ses_1", sessionID: "ses_1",
timeCreated: 1, timeCreated: 1,
+6 -10
View File
@@ -128,7 +128,7 @@ function createV1Api(input: CompatibleInput): CompatibleApi {
const located = <T>(data: T, value?: { directory?: string }) => ({ const located = <T>(data: T, value?: { directory?: string }) => ({
location: { location: {
directory: directory(value) ?? "", directory: directory(value) ?? "",
project: { id: "", directory: directory(value) ?? "", canonical: directory(value) ?? "" }, project: { id: "", directory: directory(value) ?? "" },
}, },
data, data,
}) })
@@ -229,6 +229,7 @@ function createV1Api(input: CompatibleInput): CompatibleApi {
], ],
}) })
return { return {
admittedSeq: 0,
id: value.id ?? "", id: value.id ?? "",
sessionID: value.sessionID, sessionID: value.sessionID,
timeCreated: Date.now(), timeCreated: Date.now(),
@@ -254,6 +255,7 @@ function createV1Api(input: CompatibleInput): CompatibleApi {
})), })),
}) })
return { return {
admittedSeq: 0,
id: value.id ?? "", id: value.id ?? "",
sessionID: value.sessionID, sessionID: value.sessionID,
timeCreated: Date.now(), timeCreated: Date.now(),
@@ -278,6 +280,7 @@ function createV1Api(input: CompatibleInput): CompatibleApi {
modelID: value.model.modelID, modelID: value.model.modelID,
}) })
return { return {
admittedSeq: 0,
id: value.id ?? "", id: value.id ?? "",
sessionID: value.sessionID, sessionID: value.sessionID,
timeCreated: Date.now(), timeCreated: Date.now(),
@@ -298,19 +301,12 @@ function createV1Api(input: CompatibleInput): CompatibleApi {
project: { project: {
...input.current.project, ...input.current.project,
async list() { async list() {
return ((await legacy().project.list()).data ?? []).map((project) => ({ return ((await legacy().project.list()).data ?? []) as Project[]
...project,
canonical: project.worktree,
}))
}, },
async current(value?: Parameters<ServerApi["project"]["current"]>[0]) { async current(value?: Parameters<ServerApi["project"]["current"]>[0]) {
const result = await legacy(value?.location).project.current() const result = await legacy(value?.location).project.current()
if (!result.data) throw new Error("Project not found") if (!result.data) throw new Error("Project not found")
return { return { id: result.data.id, directory: result.data.worktree } satisfies ProjectCurrent
id: result.data.id,
directory: result.data.worktree,
canonical: result.data.worktree,
} satisfies ProjectCurrent
}, },
// async update(value: Parameters<ServerApi["project"]["update"]>[0]) { // async update(value: Parameters<ServerApi["project"]["update"]>[0]) {
// const project = (await legacy().project.list()).data?.find((item) => item.id === value.projectID) // const project = (await legacy().project.list()).data?.find((item) => item.id === value.projectID)
+10 -12
View File
@@ -7,7 +7,7 @@ import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
import { useMutation } from "@tanstack/solid-query" import { useMutation } from "@tanstack/solid-query"
import fuzzysort from "fuzzysort" import fuzzysort from "fuzzysort"
import { type Accessor, For, Show, createMemo } from "solid-js" import { type Accessor, For, Show, createMemo } from "solid-js"
import type { useServerManagementController } from "@/components/dialog-select-server" import type { ServerCollectionController } from "@/components/server/server-management-controller"
import { ServerHealthIndicator } from "@/components/server/server-row" import { ServerHealthIndicator } from "@/components/server/server-row"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { usePlatform } from "@/context/platform" import { usePlatform } from "@/context/platform"
@@ -17,8 +17,6 @@ import { DialogAddWslServer } from "./dialog-add-server"
import { useWslServers } from "./context" import { useWslServers } from "./context"
import { wslOpencodeAction, wslRuntimeRetryable } from "./settings-model" import { wslOpencodeAction, wslRuntimeRetryable } from "./settings-model"
type Controller = ReturnType<typeof useServerManagementController>
export function isWslServer(server: ServerConnection.Any) { export function isWslServer(server: ServerConnection.Any) {
return server.type === "sidecar" && server.variant === "wsl" return server.type === "sidecar" && server.variant === "wsl"
} }
@@ -28,7 +26,7 @@ export function AddServerMenu(props: { onAddServer: () => void }) {
const dialog = useDialog() const dialog = useDialog()
const language = useLanguage() const language = useLanguage()
const openAddWsl = () => { const openAddWsl = () => {
dialog.push(() => <DialogAddWslServer />) void dialog.push(() => <DialogAddWslServer />)
} }
return ( return (
<Show <Show
@@ -67,7 +65,7 @@ export function useFilteredWslServers(filter: Accessor<string>) {
} }
export function WslServerSettings(props: { export function WslServerSettings(props: {
controller: Controller domain: Pick<ServerCollectionController, "collection" | "defaults" | "connection">
servers: ReturnType<typeof useFilteredWslServers> servers: ReturnType<typeof useFilteredWslServers>
}) { }) {
const platform = usePlatform() const platform = usePlatform()
@@ -86,7 +84,7 @@ export function WslServerSettings(props: {
})) }))
const remove = (key: ServerConnection.Key) => { const remove = (key: ServerConnection.Key) => {
request.mutate(() => props.controller.handleRemove(key)) request.mutate(() => props.domain.connection.remove(key))
} }
return ( return (
@@ -100,7 +98,7 @@ export function WslServerSettings(props: {
return ( return (
<div class="settings-v2-servers-row"> <div class="settings-v2-servers-row">
<div class="settings-v2-servers-lead"> <div class="settings-v2-servers-lead">
<ServerHealthIndicator health={props.controller.status()[key]} /> <ServerHealthIndicator health={props.domain.collection.health()[key]} />
<div class="settings-v2-servers-copy"> <div class="settings-v2-servers-copy">
<span class="flex min-w-0 items-center gap-1"> <span class="flex min-w-0 items-center gap-1">
<span class="settings-v2-servers-name">{item.config.distro}</span> <span class="settings-v2-servers-name">{item.config.distro}</span>
@@ -114,7 +112,7 @@ export function WslServerSettings(props: {
</div> </div>
</div> </div>
<div class="settings-v2-servers-actions"> <div class="settings-v2-servers-actions">
<Show when={props.controller.canDefault() && props.controller.defaultKey() === key}> <Show when={props.domain.defaults.available() && props.domain.defaults.key() === key}>
<Tag>{language.t("dialog.server.status.default")}</Tag> <Tag>{language.t("dialog.server.status.default")}</Tag>
</Show> </Show>
<Show when={opencodeAction()}> <Show when={opencodeAction()}>
@@ -145,13 +143,13 @@ export function WslServerSettings(props: {
{language.t("wsl.server.retryStart")} {language.t("wsl.server.retryStart")}
</MenuV2.Item> </MenuV2.Item>
</Show> </Show>
<Show when={props.controller.canDefault() && props.controller.defaultKey() !== key}> <Show when={props.domain.defaults.available() && props.domain.defaults.key() !== key}>
<MenuV2.Item onSelect={() => props.controller.setDefault(key)}> <MenuV2.Item onSelect={() => props.domain.defaults.set(key)}>
{language.t("dialog.server.menu.default")} {language.t("dialog.server.menu.default")}
</MenuV2.Item> </MenuV2.Item>
</Show> </Show>
<Show when={props.controller.canDefault() && props.controller.defaultKey() === key}> <Show when={props.domain.defaults.available() && props.domain.defaults.key() === key}>
<MenuV2.Item onSelect={() => props.controller.setDefault(null)}> <MenuV2.Item onSelect={() => props.domain.defaults.set(null)}>
{language.t("dialog.server.menu.defaultRemove")} {language.t("dialog.server.menu.defaultRemove")}
</MenuV2.Item> </MenuV2.Item>
</Show> </Show>
+1 -4
View File
@@ -244,10 +244,7 @@ export function make(input: { readonly client: OpenCodeClient; readonly connecti
return {} return {}
}, },
forkSession: async (params) => { forkSession: async (params) => {
const forked = await input.client.session.fork({ const forked = await input.client.session.fork({ sessionID: params.sessionId })
sessionID: params.sessionId,
boundary: { type: "through" },
})
const state = await attach(forked, forked.location.directory, params.mcpServers ?? []) const state = await attach(forked, forked.location.directory, params.mcpServers ?? [])
await replay(state) await replay(state)
return { sessionId: state.id, configOptions: configOptions(state) } return { sessionId: state.id, configOptions: configOptions(state) }
+4 -6
View File
@@ -105,7 +105,7 @@ async function selectSession(input: {
return { return {
session: input.fork session: input.fork
? await input.client.session ? await input.client.session
.fork({ sessionID: explicit.id, boundary: { type: "through" } }, ...requestOptions(input.signal)) .fork({ sessionID: explicit.id }, ...requestOptions(input.signal))
.catch((error) => { .catch((error) => {
throw new SessionTargetMutationError(error) throw new SessionTargetMutationError(error)
}) })
@@ -118,11 +118,9 @@ async function selectSession(input: {
if (!selected) return { session: undefined, location } if (!selected) return { session: undefined, location }
return { return {
session: input.fork session: input.fork
? await input.client.session ? await input.client.session.fork({ sessionID: selected.id }, ...requestOptions(input.signal)).catch((error) => {
.fork({ sessionID: selected.id, boundary: { type: "through" } }, ...requestOptions(input.signal)) throw new SessionTargetMutationError(error)
.catch((error) => { })
throw new SessionTargetMutationError(error)
})
: selected, : selected,
} }
} }
@@ -136,7 +136,7 @@ describe("acp service lifecycle", () => {
method: "POST", method: "POST",
path: "/api/session/ses_loaded/fork", path: "/api/session/ses_loaded/fork",
query: {}, query: {},
body: { boundary: { type: "through" } }, body: {},
}) })
}) })
+1 -1
View File
@@ -267,7 +267,7 @@ async function run(input: {
values.push(...input.turn(messageID)) values.push(...input.turn(messageID))
wake?.() wake?.()
wake = undefined wake = undefined
return ok({ id: messageID, sessionID: "ses_1", timeCreated: 1 }) as never return ok({ admittedSeq: 1, id: messageID, sessionID: "ses_1", timeCreated: 1 }) as never
}) })
await runNonInteractivePrompt({ await runNonInteractivePrompt({
client: sdk, client: sdk,
+1 -1
View File
@@ -3,7 +3,7 @@ import { OpenCode, type LocationGetOutput, type ModelRef, type SessionInfo } fro
import { resolveSessionTarget, SessionTargetMutationError } from "../src/session-target" import { resolveSessionTarget, SessionTargetMutationError } from "../src/session-target"
function location(directory: string, workspaceID?: string): LocationGetOutput { function location(directory: string, workspaceID?: string): LocationGetOutput {
return { directory, workspaceID, project: { id: "project", directory, canonical: directory } } return { directory, workspaceID, project: { id: "project", directory } }
} }
function session(id: string, directory: string, workspaceID?: string, model?: ModelRef): SessionInfo { function session(id: string, directory: string, workspaceID?: string, model?: ModelRef): SessionInfo {
+3 -5
View File
@@ -136,7 +136,7 @@ export type Endpoint5_4Input = { readonly sessionID: Session.ID }
export type Endpoint5_4Output = void export type Endpoint5_4Output = void
export type SessionRemoveOperation<E = never> = (input: Endpoint5_4Input) => Effect.Effect<Endpoint5_4Output, E> export type SessionRemoveOperation<E = never> = (input: Endpoint5_4Input) => Effect.Effect<Endpoint5_4Output, E>
export type Endpoint5_5Input = { readonly sessionID: Session.ID; readonly boundary: Session.ForkRequestBoundary } export type Endpoint5_5Input = { readonly sessionID: Session.ID; readonly messageID?: SessionMessage.ID | undefined }
export type Endpoint5_5Output = Session.Info export type Endpoint5_5Output = Session.Info
export type SessionForkOperation<E = never> = (input: Endpoint5_5Input) => Effect.Effect<Endpoint5_5Output, E> export type SessionForkOperation<E = never> = (input: Endpoint5_5Input) => Effect.Effect<Endpoint5_5Output, E>
@@ -342,10 +342,8 @@ export type Endpoint5_26Output =
readonly data: { readonly data: {
readonly sessionID: Session.ID readonly sessionID: Session.ID
readonly parentID: Session.ID readonly parentID: Session.ID
readonly boundary: Session.ForkBoundary readonly parentSeq: number
readonly instructions?: readonly from?: SessionMessage.ID | undefined
| { readonly [x: string & Brand.Brand<"Instruction.Key">]: string & Brand.Brand<"Instruction.Hash"> }
| undefined
} }
} }
| { | {
@@ -329,7 +329,7 @@ const Endpoint5_4 = (raw: RawClient["server.session"]) => (input: Endpoint5_4Inp
const Endpoint5_5 = (raw: RawClient["server.session"]) => (input: Endpoint5_5Input) => const Endpoint5_5 = (raw: RawClient["server.session"]) => (input: Endpoint5_5Input) =>
preserveEffect<Endpoint5_5Output>()( preserveEffect<Endpoint5_5Output>()(
raw["session.fork"]({ params: { sessionID: input["sessionID"] }, payload: { boundary: input["boundary"] } }).pipe( raw["session.fork"]({ params: { sessionID: input["sessionID"] }, payload: { messageID: input["messageID"] } }).pipe(
Effect.mapError(mapClientError), Effect.mapError(mapClientError),
Effect.map((value) => value.data), Effect.map((value) => value.data),
), ),
@@ -510,7 +510,7 @@ export function make(options: ClientOptions) {
{ {
method: "POST", method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/fork`, path: `/api/session/${encodeURIComponent(input.sessionID)}/fork`,
body: { boundary: input["boundary"] }, body: { messageID: input["messageID"] },
successStatus: 200, successStatus: 200,
declaredStatuses: [404, 400, 401], declaredStatuses: [404, 400, 401],
empty: false, empty: false,
+51 -51
View File
@@ -14,8 +14,6 @@ export type PermissionEffect = "allow" | "deny" | "ask"
export type PluginInfo = { id: string } export type PluginInfo = { id: string }
export type SessionForkBoundary = { type: "before"; messageID: string } | { type: "through"; messageID: string }
export type MoneyUSD = number export type MoneyUSD = number
export type TokenUsageInfo = { export type TokenUsageInfo = {
@@ -45,7 +43,13 @@ export type PromptMention = { start: number; end: number; text: string }
export type SessionPendingSyntheticData = { text: string; description?: string; metadata?: { [x: string]: JsonValue } } export type SessionPendingSyntheticData = { text: string; description?: string; metadata?: { [x: string]: JsonValue } }
export type SessionPendingCompaction = { id: string; sessionID: string; timeCreated: number; type: "compaction" } export type SessionPendingCompaction = {
admittedSeq: number
id: string
sessionID: string
timeCreated: number
type: "compaction"
}
export type SessionMessageAgentSelected = { export type SessionMessageAgentSelected = {
id: string id: string
@@ -279,7 +283,7 @@ export type ProjectCommands = { start?: string }
export type ProjectTime = { created: number; updated: number; initialized?: number } export type ProjectTime = { created: number; updated: number; initialized?: number }
export type ProjectCurrent = { id: string; directory: string; canonical: string } export type ProjectCurrent = { id: string; directory: string }
export type ProjectDirectory = { directory: string; strategy?: string } export type ProjectDirectory = { directory: string; strategy?: string }
@@ -624,7 +628,7 @@ export type SessionForked = {
type: "session.forked" type: "session.forked"
durable: { aggregateID: string; seq: number; version: 2 } durable: { aggregateID: string; seq: number; version: 2 }
location?: LocationRef location?: LocationRef
data: { sessionID: string; parentID: string; boundary: SessionForkBoundary; instructions?: { [x: string]: string } } data: { sessionID: string; parentID: string; parentSeq: number; from?: string }
} }
export type SessionInputPromoted = { export type SessionInputPromoted = {
@@ -1206,6 +1210,7 @@ export type PromptFileAttachment = {
export type PromptAgentAttachment = { name: string; mention?: PromptMention } export type PromptAgentAttachment = { name: string; mention?: PromptMention }
export type SessionPendingSynthetic = { export type SessionPendingSynthetic = {
admittedSeq: number
id: string id: string
sessionID: string sessionID: string
timeCreated: number timeCreated: number
@@ -1430,7 +1435,7 @@ export type McpResourceCatalog = { resources: Array<McpResource>; templates: Arr
export type Project = { export type Project = {
id: string id: string
canonical: string worktree: string
vcs?: ProjectVcs vcs?: ProjectVcs
name?: string name?: string
icon?: ProjectIcon icon?: ProjectIcon
@@ -1750,7 +1755,7 @@ export type PermissionRuleset = Array<PermissionRule>
export type SessionInfo = { export type SessionInfo = {
id: string id: string
parentID?: string parentID?: string
fork?: { sessionID: string; boundary: SessionForkBoundary } fork?: { sessionID: string; messageID?: string }
projectID: string projectID: string
agent?: string agent?: string
model?: ModelRef model?: ModelRef
@@ -1961,6 +1966,7 @@ export type AgentInfo = {
export type SessionsResponse = { data: Array<SessionInfo>; cursor: { previous?: string | null; next?: string | null } } export type SessionsResponse = { data: Array<SessionInfo>; cursor: { previous?: string | null; next?: string | null } }
export type SessionPendingUser = { export type SessionPendingUser = {
admittedSeq: number
id: string id: string
sessionID: string sessionID: string
timeCreated: number timeCreated: number
@@ -2515,11 +2521,7 @@ export type LocationGetInput = {
}["location"] }["location"]
} }
export type LocationGetOutput = { export type LocationGetOutput = { directory: string; workspaceID?: string; project: { id: string; directory: string } }
directory: string
workspaceID?: string
project: { id: string; directory: string; canonical: string }
}
export type AgentListInput = { export type AgentListInput = {
readonly location?: { readonly location?: {
@@ -2528,7 +2530,7 @@ export type AgentListInput = {
} }
export type AgentListOutput = { export type AgentListOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } } location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: Array<AgentInfo> data: Array<AgentInfo>
} }
@@ -2540,7 +2542,7 @@ export type AgentGetInput = {
} }
export type AgentGetOutput = { export type AgentGetOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } } location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: AgentInfo data: AgentInfo
} }
@@ -2551,7 +2553,7 @@ export type PluginListInput = {
} }
export type PluginListOutput = { export type PluginListOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } } location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: Array<PluginInfo> data: Array<PluginInfo>
} }
@@ -2700,9 +2702,7 @@ export type SessionRemoveOutput = void
export type SessionForkInput = { export type SessionForkInput = {
readonly sessionID: { readonly sessionID: string }["sessionID"] readonly sessionID: { readonly sessionID: string }["sessionID"]
readonly boundary: { readonly messageID?: { readonly messageID?: string | undefined }["messageID"]
readonly boundary: { readonly type: "before"; readonly messageID: string } | { readonly type: "through" }
}["boundary"]
} }
export type SessionForkOutput = { data: SessionInfo }["data"] export type SessionForkOutput = { data: SessionInfo }["data"]
@@ -3235,7 +3235,7 @@ export type ModelListInput = {
} }
export type ModelListOutput = { export type ModelListOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } } location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: Array<ModelInfo> data: Array<ModelInfo>
} }
@@ -3246,7 +3246,7 @@ export type ModelDefaultInput = {
} }
export type ModelDefaultOutput = { export type ModelDefaultOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } } location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: ModelInfo | null data: ModelInfo | null
} }
@@ -3273,7 +3273,7 @@ export type ProviderListInput = {
} }
export type ProviderListOutput = { export type ProviderListOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } } location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: Array<ProviderInfo> data: Array<ProviderInfo>
} }
@@ -3285,7 +3285,7 @@ export type ProviderGetInput = {
} }
export type ProviderGetOutput = { export type ProviderGetOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } } location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: ProviderInfo data: ProviderInfo
} }
@@ -3296,7 +3296,7 @@ export type IntegrationListInput = {
} }
export type IntegrationListOutput = { export type IntegrationListOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } } location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: Array<IntegrationInfo> data: Array<IntegrationInfo>
} }
@@ -3308,7 +3308,7 @@ export type IntegrationGetInput = {
} }
export type IntegrationGetOutput = { export type IntegrationGetOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } } location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: IntegrationInfo | null data: IntegrationInfo | null
} }
@@ -3355,7 +3355,7 @@ export type IntegrationOauthConnectInput = {
} }
export type IntegrationOauthConnectOutput = { export type IntegrationOauthConnectOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } } location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: { data: {
attemptID: string attemptID: string
url: string url: string
@@ -3374,7 +3374,7 @@ export type IntegrationOauthStatusInput = {
} }
export type IntegrationOauthStatusOutput = { export type IntegrationOauthStatusOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } } location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: IntegrationAttemptStatus data: IntegrationAttemptStatus
} }
@@ -3409,7 +3409,7 @@ export type IntegrationCommandConnectInput = {
} }
export type IntegrationCommandConnectOutput = { export type IntegrationCommandConnectOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } } location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: IntegrationCommandAttempt data: IntegrationCommandAttempt
} }
@@ -3422,7 +3422,7 @@ export type IntegrationCommandStatusInput = {
} }
export type IntegrationCommandStatusOutput = { export type IntegrationCommandStatusOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } } location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: IntegrationCommandAttemptStatus data: IntegrationCommandAttemptStatus
} }
@@ -3443,7 +3443,7 @@ export type McpListInput = {
} }
export type McpListOutput = { export type McpListOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } } location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: Array<McpServer> data: Array<McpServer>
} }
@@ -3532,7 +3532,7 @@ export type McpResourceCatalogInput = {
} }
export type McpResourceCatalogOutput = { export type McpResourceCatalogOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } } location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: McpResourceCatalog data: McpResourceCatalog
} }
@@ -3581,7 +3581,7 @@ export type FormRequestListInput = {
} }
export type FormRequestListOutput = { export type FormRequestListOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } } location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: Array<FormInfo> data: Array<FormInfo>
} }
@@ -4437,7 +4437,7 @@ export type PermissionRequestListInput = {
} }
export type PermissionRequestListOutput = { export type PermissionRequestListOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } } location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: Array<PermissionRequest> data: Array<PermissionRequest>
} }
@@ -4559,7 +4559,7 @@ export type FileListInput = {
} }
export type FileListOutput = { export type FileListOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } } location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: Array<FileSystemEntry> data: Array<FileSystemEntry>
} }
@@ -4591,7 +4591,7 @@ export type FileFindInput = {
} }
export type FileFindOutput = { export type FileFindOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } } location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: Array<FileSystemEntry> data: Array<FileSystemEntry>
} }
@@ -4602,7 +4602,7 @@ export type CommandListInput = {
} }
export type CommandListOutput = { export type CommandListOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } } location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: Array<CommandInfo> data: Array<CommandInfo>
} }
@@ -4613,7 +4613,7 @@ export type SkillListInput = {
} }
export type SkillListOutput = { export type SkillListOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } } location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: Array<SkillInfo> data: Array<SkillInfo>
} }
@@ -4626,7 +4626,7 @@ export type PtyListInput = {
} }
export type PtyListOutput = { export type PtyListOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } } location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: Array<Pty> data: Array<Pty>
} }
@@ -4672,7 +4672,7 @@ export type PtyCreateInput = {
} }
export type PtyCreateOutput = { export type PtyCreateOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } } location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: Pty data: Pty
} }
@@ -4684,7 +4684,7 @@ export type PtyGetInput = {
} }
export type PtyGetOutput = { export type PtyGetOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } } location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: Pty data: Pty
} }
@@ -4701,7 +4701,7 @@ export type PtyUpdateInput = {
} }
export type PtyUpdateOutput = { export type PtyUpdateOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } } location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: Pty data: Pty
} }
@@ -4721,7 +4721,7 @@ export type ShellListInput = {
} }
export type ShellListOutput = { export type ShellListOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } } location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: Array<ShellInfo1> data: Array<ShellInfo1>
} }
@@ -4756,7 +4756,7 @@ export type ShellCreateInput = {
} }
export type ShellCreateOutput = { export type ShellCreateOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } } location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: ShellInfo1 data: ShellInfo1
} }
@@ -4768,7 +4768,7 @@ export type ShellGetInput = {
} }
export type ShellGetOutput = { export type ShellGetOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } } location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: ShellInfo1 data: ShellInfo1
} }
@@ -4781,7 +4781,7 @@ export type ShellTimeoutInput = {
} }
export type ShellTimeoutOutput = { export type ShellTimeoutOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } } location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: ShellInfo1 data: ShellInfo1
} }
@@ -4805,7 +4805,7 @@ export type ShellOutputInput = {
} }
export type ShellOutputOutput = { export type ShellOutputOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } } location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: { output: string; cursor: number; size: number; truncated: boolean } data: { output: string; cursor: number; size: number; truncated: boolean }
} }
@@ -4825,7 +4825,7 @@ export type QuestionRequestListInput = {
} }
export type QuestionRequestListOutput = { export type QuestionRequestListOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } } location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: Array<QuestionRequest> data: Array<QuestionRequest>
} }
@@ -4855,7 +4855,7 @@ export type ReferenceListInput = {
} }
export type ReferenceListOutput = { export type ReferenceListOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } } location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: Array<ReferenceInfo> data: Array<ReferenceInfo>
} }
@@ -4898,7 +4898,7 @@ export type VcsStatusInput = {
} }
export type VcsStatusOutput = { export type VcsStatusOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } } location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: Array<VcsFileStatus> data: Array<VcsFileStatus>
} }
@@ -4921,7 +4921,7 @@ export type VcsDiffInput = {
} }
export type VcsDiffOutput = { export type VcsDiffOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } } location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: Array<FileDiffInfo> data: Array<FileDiffInfo>
} }
@@ -4942,7 +4942,7 @@ export type WebsearchProvidersInput = {
} }
export type WebsearchProvidersOutput = { export type WebsearchProvidersOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } } location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: Array<WebSearchProvider> data: Array<WebSearchProvider>
} }
@@ -4955,6 +4955,6 @@ export type WebsearchQueryInput = {
} }
export type WebsearchQueryOutput = { export type WebsearchQueryOutput = {
location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } } location: { directory: string; workspaceID?: string; project: { id: string; directory: string } }
data: { providerID: string; results: Array<WebSearchResult> } data: { providerID: string; results: Array<WebSearchResult> }
} }
+2
View File
@@ -268,6 +268,7 @@ const session = {
const admission = { const admission = {
data: { data: {
admittedSeq: 0,
id: "msg_test", id: "msg_test",
sessionID: "ses_test", sessionID: "ses_test",
type: "user", type: "user",
@@ -280,6 +281,7 @@ const admission = {
const compactionAdmission = { const compactionAdmission = {
data: { data: {
type: "compaction", type: "compaction",
admittedSeq: 1,
id: "msg_compaction", id: "msg_compaction",
sessionID: "ses_test", sessionID: "ses_test",
timeCreated: 1_717_171_717_000, timeCreated: 1_717_171_717_000,
+4
View File
@@ -304,6 +304,7 @@ test("session.pending.list uses the public HTTP contract", async () => {
const requests: Array<{ method: string; url: string }> = [] const requests: Array<{ method: string; url: string }> = []
const pending = [ const pending = [
{ {
admittedSeq: 3,
id: "msg_pending", id: "msg_pending",
sessionID: "ses_test", sessionID: "ses_test",
timeCreated: 1_717_171_717_000, timeCreated: 1_717_171_717_000,
@@ -546,6 +547,7 @@ const session = {
const admission = { const admission = {
data: { data: {
admittedSeq: 0,
id: "msg_test", id: "msg_test",
sessionID: "ses_test", sessionID: "ses_test",
type: "user", type: "user",
@@ -557,6 +559,7 @@ const admission = {
const syntheticAdmission = { const syntheticAdmission = {
data: { data: {
admittedSeq: 1,
id: "msg_synthetic", id: "msg_synthetic",
sessionID: "ses_test", sessionID: "ses_test",
type: "synthetic", type: "synthetic",
@@ -569,6 +572,7 @@ const syntheticAdmission = {
const compactionAdmission = { const compactionAdmission = {
data: { data: {
type: "compaction", type: "compaction",
admittedSeq: 1,
id: "msg_compaction", id: "msg_compaction",
sessionID: "ses_test", sessionID: "ses_test",
timeCreated: 1_717_171_717_000, timeCreated: 1_717_171_717_000,
+14 -12
View File
@@ -264,7 +264,8 @@ export const dict = {
"go.cta.text": "اشترك في Go", "go.cta.text": "اشترك في Go",
"go.cta.price": "$10/شهر", "go.cta.price": "$10/شهر",
"go.cta.promo": "$5 للشهر الأول", "go.cta.promo": "$5 للشهر الأول",
"go.pricing.body": "يبدأ Go من $5 للشهر الأول، ثم $10/شهر.", "go.pricing.body":
"استخدمه مع أي وكيل. $5 للشهر الأول، ثم $10/شهر. قم بزيادة الرصيد إذا لزم الأمر. الإلغاء في أي وقت.",
"go.graph.free": "مجاني", "go.graph.free": "مجاني",
"go.graph.freePill": "Big Pickle ونماذج مجانية", "go.graph.freePill": "Big Pickle ونماذج مجانية",
"go.graph.go": "Go", "go.graph.go": "Go",
@@ -303,15 +304,15 @@ export const dict = {
"go.problem.item4": "go.problem.item4":
"يتضمن Grok 4.5 وGLM-5.2 وGLM-5.1 وKimi K3 وKimi K2.7 Code وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.7 Max وQwen3.7 Plus وQwen3.6 Plus وMiniMax M2.7 وMiniMax M3 وDeepSeek V4 Pro وDeepSeek V4 Flash وHy3", "يتضمن Grok 4.5 وGLM-5.2 وGLM-5.1 وKimi K3 وKimi K2.7 Code وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.7 Max وQwen3.7 Plus وQwen3.6 Plus وMiniMax M2.7 وMiniMax M3 وDeepSeek V4 Pro وDeepSeek V4 Flash وHy3",
"go.how.title": "كيف يعمل Go", "go.how.title": "كيف يعمل Go",
"go.how.body": "يبدأ Go من $5 للشهر الأول، ثم $10/شهر.", "go.how.body": "يبدأ Go من $5 للشهر الأول، ثم $10/شهر. يمكنك استخدامه مع OpenCode أو أي وكيل.",
"go.how.step1.title": "اشترك في Go", "go.how.step1.title": "أنشئ حسابًا",
"go.how.step1.beforeLink": "في", "go.how.step1.beforeLink": "اتبع",
"go.how.step1.link": "OpenCode Console", "go.how.step1.link": "تعليمات الإعداد",
"go.how.step2.title": "ربط OpenCode", "go.how.step2.title": "اشترك في Go",
"go.how.step2.link": "opencode2 console login", "go.how.step2.link": "$5 للشهر الأول",
"go.how.step2.afterLink": "ووافق على الجهاز في متصفحك", "go.how.step2.afterLink": "ثم $10/شهر مع حدود سخية",
"go.how.step3.title": "ابدأ البرمجة", "go.how.step3.title": "ابدأ البرمجة",
"go.how.step3.body": "مع مزود opencode", "go.how.step3.body": "مع وصول موثوق لنماذج مفتوحة المصدر",
"go.privacy.title": "خصوصيتك مهمة بالنسبة لنا", "go.privacy.title": "خصوصيتك مهمة بالنسبة لنا",
"go.privacy.body": "go.privacy.body":
"تم تصميم الخطة بشكل أساسي للمستخدمين الدوليين، مع استضافة النماذج في الولايات المتحدة والاتحاد الأوروبي وسنغافورة للحصول على وصول عالمي مستقر.", "تم تصميم الخطة بشكل أساسي للمستخدمين الدوليين، مع استضافة النماذج في الولايات المتحدة والاتحاد الأوروبي وسنغافورة للحصول على وصول عالمي مستقر.",
@@ -343,8 +344,8 @@ export const dict = {
"go.faq.a6": "إذا كنت بحاجة إلى مزيد من الاستخدام، يمكنك شحن رصيد في حسابك.", "go.faq.a6": "إذا كنت بحاجة إلى مزيد من الاستخدام، يمكنك شحن رصيد في حسابك.",
"go.faq.q7": "هل يمكنني الإلغاء؟", "go.faq.q7": "هل يمكنني الإلغاء؟",
"go.faq.a7": "نعم، يمكنك الإلغاء في أي وقت.", "go.faq.a7": "نعم، يمكنك الإلغاء في أي وقت.",
"go.faq.q8": "ما الوصول المؤجل؟", "go.faq.q8": "هل يمكنني استخدام Go مع وكلاء برمجة آخرين؟",
"go.faq.a8": "دعم الوكلاء الخارجيين وحسابات الخدمة مؤجل.", "go.faq.a8": "نعم، يمكنك استخدام Go مع أي وكيل. اتبع تعليمات الإعداد في وكيل البرمجة المفضل لديك.",
"go.faq.q9": "ما الفرق بين النماذج المجانية وGo؟", "go.faq.q9": "ما الفرق بين النماذج المجانية وGo؟",
"go.faq.a9": "go.faq.a9":
@@ -649,7 +650,8 @@ export const dict = {
"workspace.lite.subscription.monthlyUsage": "الاستخدام الشهري", "workspace.lite.subscription.monthlyUsage": "الاستخدام الشهري",
"workspace.lite.subscription.resetsIn": "إعادة تعيين في", "workspace.lite.subscription.resetsIn": "إعادة تعيين في",
"workspace.lite.subscription.useBalance": "استخدم رصيدك المتوفر بعد الوصول إلى حدود الاستخدام", "workspace.lite.subscription.useBalance": "استخدم رصيدك المتوفر بعد الوصول إلى حدود الاستخدام",
"workspace.lite.subscription.selectProvider": "اختر مزود opencode لاستخدام نماذج Go.", "workspace.lite.subscription.selectProvider":
'اختر "OpenCode Go" كمزود في إعدادات opencode الخاصة بك لاستخدام نماذج Go.',
"workspace.lite.providers.title": "المزودون", "workspace.lite.providers.title": "المزودون",
"workspace.lite.providers.description": "تحكم في المزودين المستخدمين للتوجيه.", "workspace.lite.providers.description": "تحكم في المزودين المستخدمين للتوجيه.",
"workspace.lite.providers.useChina": "تفعيل النماذج المستضافة في الصين", "workspace.lite.providers.useChina": "تفعيل النماذج المستضافة في الصين",
+16 -12
View File
@@ -268,7 +268,8 @@ export const dict = {
"go.cta.text": "Assinar o Go", "go.cta.text": "Assinar o Go",
"go.cta.price": "$10/mês", "go.cta.price": "$10/mês",
"go.cta.promo": "$5 no primeiro mês", "go.cta.promo": "$5 no primeiro mês",
"go.pricing.body": "O Go começa em $5 no primeiro mês, depois $10/mês.", "go.pricing.body":
"Use com qualquer agente. $5 no primeiro mês, depois $10/mês. Recarregue o crédito se necessário. Cancele a qualquer momento.",
"go.graph.free": "Grátis", "go.graph.free": "Grátis",
"go.graph.freePill": "Big Pickle e modelos gratuitos", "go.graph.freePill": "Big Pickle e modelos gratuitos",
"go.graph.go": "Go", "go.graph.go": "Go",
@@ -308,15 +309,16 @@ export const dict = {
"go.problem.item4": "go.problem.item4":
"Inclui Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash e Hy3", "Inclui Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash e Hy3",
"go.how.title": "Como o Go funciona", "go.how.title": "Como o Go funciona",
"go.how.body": "O Go começa em $5 no primeiro mês, depois $10/mês.", "go.how.body":
"go.how.step1.title": "Assinar o Go", "O Go começa em $5 no primeiro mês, depois $10/mês. Você pode usá-lo com o OpenCode ou qualquer agente.",
"go.how.step1.beforeLink": "no", "go.how.step1.title": "Crie uma conta",
"go.how.step1.link": "OpenCode Console", "go.how.step1.beforeLink": "siga as",
"go.how.step2.title": "Conectar o OpenCode", "go.how.step1.link": "instruções de configuração",
"go.how.step2.link": "opencode2 console login", "go.how.step2.title": "Assinar o Go",
"go.how.step2.afterLink": "e aprove o dispositivo no navegador", "go.how.step2.link": "$5 no primeiro mês",
"go.how.step2.afterLink": "depois $10/mês com limites generosos",
"go.how.step3.title": "Comece a codificar", "go.how.step3.title": "Comece a codificar",
"go.how.step3.body": "com o provedor opencode", "go.how.step3.body": "com acesso confiável a modelos de código aberto",
"go.privacy.title": "Sua privacidade é importante para nós", "go.privacy.title": "Sua privacidade é importante para nós",
"go.privacy.body": "go.privacy.body":
"O plano é projetado principalmente para usuários internacionais, com modelos hospedados nos EUA, UE e Singapura para acesso global estável.", "O plano é projetado principalmente para usuários internacionais, com modelos hospedados nos EUA, UE e Singapura para acesso global estável.",
@@ -349,8 +351,9 @@ export const dict = {
"go.faq.a6": "Se você precisar de mais uso, pode recarregar crédito em sua conta.", "go.faq.a6": "Se você precisar de mais uso, pode recarregar crédito em sua conta.",
"go.faq.q7": "Posso cancelar?", "go.faq.q7": "Posso cancelar?",
"go.faq.a7": "Sim, você pode cancelar a qualquer momento.", "go.faq.a7": "Sim, você pode cancelar a qualquer momento.",
"go.faq.q8": "Qual acesso foi adiado?", "go.faq.q8": "Posso usar o Go com outros agentes de codificação?",
"go.faq.a8": "O suporte a agentes externos e contas de serviço foi adiado.", "go.faq.a8":
"Sim, você pode usar o Go com qualquer agente. Siga as instruções de configuração no seu agente de codificação preferido.",
"go.faq.q9": "Qual a diferença entre os modelos gratuitos e o Go?", "go.faq.q9": "Qual a diferença entre os modelos gratuitos e o Go?",
"go.faq.a9": "go.faq.a9":
@@ -657,7 +660,8 @@ export const dict = {
"workspace.lite.subscription.monthlyUsage": "Uso Mensal", "workspace.lite.subscription.monthlyUsage": "Uso Mensal",
"workspace.lite.subscription.resetsIn": "Reinicia em", "workspace.lite.subscription.resetsIn": "Reinicia em",
"workspace.lite.subscription.useBalance": "Use seu saldo disponível após atingir os limites de uso", "workspace.lite.subscription.useBalance": "Use seu saldo disponível após atingir os limites de uso",
"workspace.lite.subscription.selectProvider": "Selecione o provedor opencode para usar os modelos Go.", "workspace.lite.subscription.selectProvider":
'Selecione "OpenCode Go" como provedor na sua configuração do opencode para usar os modelos Go.',
"workspace.lite.providers.title": "Provedores", "workspace.lite.providers.title": "Provedores",
"workspace.lite.providers.description": "Controle quais provedores são usados para roteamento.", "workspace.lite.providers.description": "Controle quais provedores são usados para roteamento.",
"workspace.lite.providers.useChina": "Ativar modelos hospedados na China", "workspace.lite.providers.useChina": "Ativar modelos hospedados na China",
+15 -12
View File
@@ -266,7 +266,8 @@ export const dict = {
"go.cta.text": "Abonner på Go", "go.cta.text": "Abonner på Go",
"go.cta.price": "$10/måned", "go.cta.price": "$10/måned",
"go.cta.promo": "$5 første måned", "go.cta.promo": "$5 første måned",
"go.pricing.body": "Go starter ved $5 for den første måned, derefter $10/måned.", "go.pricing.body":
"Brug med enhver agent. $5 første måned, derefter $10/måned. Tank op med kredit efter behov. Afmeld når som helst.",
"go.graph.free": "Gratis", "go.graph.free": "Gratis",
"go.graph.freePill": "Big Pickle og gratis modeller", "go.graph.freePill": "Big Pickle og gratis modeller",
"go.graph.go": "Go", "go.graph.go": "Go",
@@ -305,15 +306,16 @@ export const dict = {
"go.problem.item4": "go.problem.item4":
"Inkluderer Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash og Hy3", "Inkluderer Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash og Hy3",
"go.how.title": "Hvordan Go virker", "go.how.title": "Hvordan Go virker",
"go.how.body": "Go starter ved $5 for den første måned, derefter $10/måned.", "go.how.body":
"go.how.step1.title": "Abonner på Go", "Go starter ved $5 for den første måned, derefter $10/måned. Du kan bruge det med OpenCode eller enhver agent.",
"go.how.step1.beforeLink": "i", "go.how.step1.title": "Opret en konto",
"go.how.step1.link": "OpenCode Console", "go.how.step1.beforeLink": "følg",
"go.how.step2.title": "Forbind OpenCode", "go.how.step1.link": "opsætningsinstruktionerne",
"go.how.step2.link": "opencode2 console login", "go.how.step2.title": "Abonner på Go",
"go.how.step2.afterLink": "og godkend enheden i din browser", "go.how.step2.link": "$5 første måned",
"go.how.step2.afterLink": "derefter $10/måned med generøse grænser",
"go.how.step3.title": "Start kodning", "go.how.step3.title": "Start kodning",
"go.how.step3.body": "med opencode-udbyderen", "go.how.step3.body": "med pålidelig adgang til open source-modeller",
"go.privacy.title": "Dit privatliv er vigtigt for os", "go.privacy.title": "Dit privatliv er vigtigt for os",
"go.privacy.body": "go.privacy.body":
"Planen er primært designet til internationale brugere, med modeller hostet i USA, EU og Singapore for stabil global adgang.", "Planen er primært designet til internationale brugere, med modeller hostet i USA, EU og Singapore for stabil global adgang.",
@@ -346,8 +348,8 @@ export const dict = {
"go.faq.a6": "Hvis du har brug for mere forbrug, kan du tanke kredit op på din konto.", "go.faq.a6": "Hvis du har brug for mere forbrug, kan du tanke kredit op på din konto.",
"go.faq.q7": "Kan jeg annullere?", "go.faq.q7": "Kan jeg annullere?",
"go.faq.a7": "Ja, du kan annullere til enhver tid.", "go.faq.a7": "Ja, du kan annullere til enhver tid.",
"go.faq.q8": "Hvilken adgang er udskudt?", "go.faq.q8": "Kan jeg bruge Go med andre kodningsagenter?",
"go.faq.a8": "Understøttelse af eksterne agenter og tjenestekonti er udskudt.", "go.faq.a8": "Ja, du kan bruge Go med enhver agent. Følg opsætningsinstruktionerne i din foretrukne kodningsagent.",
"go.faq.q9": "Hvad er forskellen på gratis modeller og Go?", "go.faq.q9": "Hvad er forskellen på gratis modeller og Go?",
"go.faq.a9": "go.faq.a9":
@@ -654,7 +656,8 @@ export const dict = {
"workspace.lite.subscription.monthlyUsage": "Månedligt forbrug", "workspace.lite.subscription.monthlyUsage": "Månedligt forbrug",
"workspace.lite.subscription.resetsIn": "Nulstiller i", "workspace.lite.subscription.resetsIn": "Nulstiller i",
"workspace.lite.subscription.useBalance": "Brug din tilgængelige saldo, når du har nået forbrugsgrænserne", "workspace.lite.subscription.useBalance": "Brug din tilgængelige saldo, når du har nået forbrugsgrænserne",
"workspace.lite.subscription.selectProvider": "Vælg opencode-udbyderen for at bruge Go-modeller.", "workspace.lite.subscription.selectProvider":
'Vælg "OpenCode Go" som udbyder i din opencode-konfiguration for at bruge Go-modeller.',
"workspace.lite.providers.title": "Udbydere", "workspace.lite.providers.title": "Udbydere",
"workspace.lite.providers.description": "Styr, hvilke udbydere der bruges til routing.", "workspace.lite.providers.description": "Styr, hvilke udbydere der bruges til routing.",
"workspace.lite.providers.useChina": "Aktivér modeller hostet i Kina", "workspace.lite.providers.useChina": "Aktivér modeller hostet i Kina",
+16 -12
View File
@@ -268,7 +268,8 @@ export const dict = {
"go.cta.text": "Go abonnieren", "go.cta.text": "Go abonnieren",
"go.cta.price": "$10/Monat", "go.cta.price": "$10/Monat",
"go.cta.promo": "$5 im ersten Monat", "go.cta.promo": "$5 im ersten Monat",
"go.pricing.body": "Go beginnt bei $5 für den ersten Monat, danach $10/Monat.", "go.pricing.body":
"Mit jedem Agenten nutzbar. $5 im ersten Monat, danach $10/Monat. Guthaben bei Bedarf aufladen. Jederzeit kündbar.",
"go.graph.free": "Kostenlos", "go.graph.free": "Kostenlos",
"go.graph.freePill": "Big Pickle und kostenlose Modelle", "go.graph.freePill": "Big Pickle und kostenlose Modelle",
"go.graph.go": "Go", "go.graph.go": "Go",
@@ -307,15 +308,16 @@ export const dict = {
"go.problem.item4": "go.problem.item4":
"Beinhaltet Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash und Hy3", "Beinhaltet Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash und Hy3",
"go.how.title": "Wie Go funktioniert", "go.how.title": "Wie Go funktioniert",
"go.how.body": "Go beginnt bei $5 für den ersten Monat, danach $10/Monat.", "go.how.body":
"go.how.step1.title": "Go abonnieren", "Go beginnt bei $5 für den ersten Monat, danach $10/Monat. Du kannst es mit OpenCode oder jedem Agenten nutzen.",
"go.how.step1.beforeLink": "in", "go.how.step1.title": "Konto erstellen",
"go.how.step1.link": "OpenCode Console", "go.how.step1.beforeLink": "folge den",
"go.how.step2.title": "OpenCode verbinden", "go.how.step1.link": "Einrichtungsanweisungen",
"go.how.step2.link": "opencode2 console login", "go.how.step2.title": "Go abonnieren",
"go.how.step2.afterLink": "und autorisiere das Gerät in deinem Browser", "go.how.step2.link": "$5 im ersten Monat",
"go.how.step2.afterLink": "danach $10/Monat mit großzügigen Limits",
"go.how.step3.title": "Loslegen mit Coding", "go.how.step3.title": "Loslegen mit Coding",
"go.how.step3.body": "mit dem opencode-Anbieter", "go.how.step3.body": "mit zuverlässigem Zugang zu Open-Source-Modellen",
"go.privacy.title": "Deine Privatsphäre ist uns wichtig", "go.privacy.title": "Deine Privatsphäre ist uns wichtig",
"go.privacy.body": "go.privacy.body":
"Der Plan ist primär für internationale Nutzer konzipiert, mit Modellen gehostet in den USA, der EU und Singapur für stabilen globalen Zugang.", "Der Plan ist primär für internationale Nutzer konzipiert, mit Modellen gehostet in den USA, der EU und Singapur für stabilen globalen Zugang.",
@@ -348,8 +350,9 @@ export const dict = {
"go.faq.a6": "Wenn du mehr Nutzung benötigst, kannst du Guthaben in deinem Konto aufladen.", "go.faq.a6": "Wenn du mehr Nutzung benötigst, kannst du Guthaben in deinem Konto aufladen.",
"go.faq.q7": "Kann ich kündigen?", "go.faq.q7": "Kann ich kündigen?",
"go.faq.a7": "Ja, du kannst jederzeit kündigen.", "go.faq.a7": "Ja, du kannst jederzeit kündigen.",
"go.faq.q8": "Welcher Zugriff ist zurückgestellt?", "go.faq.q8": "Kann ich Go mit anderen Coding-Agenten nutzen?",
"go.faq.a8": "Unterstützung für externe Agenten und Dienstkonten ist zurückgestellt.", "go.faq.a8":
"Ja, du kannst Go mit jedem Agenten nutzen. Folge den Einrichtungsanweisungen in deinem bevorzugten Coding-Agenten.",
"go.faq.q9": "Was ist der Unterschied zwischen kostenlosen Modellen und Go?", "go.faq.q9": "Was ist der Unterschied zwischen kostenlosen Modellen und Go?",
"go.faq.a9": "go.faq.a9":
@@ -656,7 +659,8 @@ export const dict = {
"workspace.lite.subscription.monthlyUsage": "Monatliche Nutzung", "workspace.lite.subscription.monthlyUsage": "Monatliche Nutzung",
"workspace.lite.subscription.resetsIn": "Setzt zurück in", "workspace.lite.subscription.resetsIn": "Setzt zurück in",
"workspace.lite.subscription.useBalance": "Nutze dein verfügbares Guthaben, nachdem die Nutzungslimits erreicht sind", "workspace.lite.subscription.useBalance": "Nutze dein verfügbares Guthaben, nachdem die Nutzungslimits erreicht sind",
"workspace.lite.subscription.selectProvider": "Wähle den opencode-Anbieter, um Go-Modelle zu verwenden.", "workspace.lite.subscription.selectProvider":
'Wähle "OpenCode Go" als Anbieter in deiner opencode-Konfiguration, um Go-Modelle zu verwenden.',
"workspace.lite.providers.title": "Anbieter", "workspace.lite.providers.title": "Anbieter",
"workspace.lite.providers.description": "Steuere, welche Anbieter für das Routing verwendet werden.", "workspace.lite.providers.description": "Steuere, welche Anbieter für das Routing verwendet werden.",
"workspace.lite.providers.useChina": "In China gehostete Modelle aktivieren", "workspace.lite.providers.useChina": "In China gehostete Modelle aktivieren",
+13 -14
View File
@@ -265,8 +265,7 @@ export const dict = {
"go.cta.text": "Subscribe to Go", "go.cta.text": "Subscribe to Go",
"go.cta.price": "$10/month", "go.cta.price": "$10/month",
"go.cta.promo": "$5 first month", "go.cta.promo": "$5 first month",
"go.pricing.body": "go.pricing.body": "Use with any agent. $5 first month, then $10/month. Top up credit if needed. Cancel any time.",
"For a named OpenCode subscriber. $5 first month, then $10/month. Service accounts are not eligible. Cancel any time.",
"go.graph.free": "Free", "go.graph.free": "Free",
"go.graph.freePill": "Big Pickle and free models", "go.graph.freePill": "Big Pickle and free models",
"go.graph.go": "Go", "go.graph.go": "Go",
@@ -305,15 +304,15 @@ export const dict = {
"go.problem.item4": "go.problem.item4":
"Includes Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash, and Hy3", "Includes Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash, and Hy3",
"go.how.title": "How Go works", "go.how.title": "How Go works",
"go.how.body": "Go is available to a named subscriber using OpenCode. No API key needs to be copied.", "go.how.body": "Go starts at $5 for your first month, then $10/month. You can use it with OpenCode or any agent.",
"go.how.step1.title": "Subscribe to Go", "go.how.step1.title": "Create an account",
"go.how.step1.beforeLink": "in", "go.how.step1.beforeLink": "follow the",
"go.how.step1.link": "OpenCode Console", "go.how.step1.link": "setup instructions",
"go.how.step2.title": "Connect OpenCode", "go.how.step2.title": "Subscribe to Go",
"go.how.step2.link": "run opencode2 console login", "go.how.step2.link": "$5 first month",
"go.how.step2.afterLink": "and authorize the device in your browser", "go.how.step2.afterLink": "then $10/month with generous limits",
"go.how.step3.title": "Start coding", "go.how.step3.title": "Start coding",
"go.how.step3.body": "with the opencode provider", "go.how.step3.body": "with reliable access to open-source models",
"go.privacy.title": "Your privacy is important to us", "go.privacy.title": "Your privacy is important to us",
"go.privacy.body": "go.privacy.body":
"The plan is designed primarily for international users, with models hosted in the US, EU, and Singapore for stable global access.", "The plan is designed primarily for international users, with models hosted in the US, EU, and Singapore for stable global access.",
@@ -347,9 +346,8 @@ export const dict = {
"go.faq.a6": "If you need more usage, you can top up credit in your account.", "go.faq.a6": "If you need more usage, you can top up credit in your account.",
"go.faq.q7": "Can I cancel?", "go.faq.q7": "Can I cancel?",
"go.faq.a7": "Yes, you can cancel any time.", "go.faq.a7": "Yes, you can cancel any time.",
"go.faq.q8": "Who can use Go?", "go.faq.q8": "Can I use Go with other coding agents?",
"go.faq.a8": "go.faq.a8": "Yes, you can use Go with any agent. Follow the setup instructions in your preferred coding agent.",
"Go is available to the named subscriber through OpenCode. Other coding agents and service accounts are not eligible.",
"go.faq.q9": "What is the difference between free models and Go?", "go.faq.q9": "What is the difference between free models and Go?",
"go.faq.a9": "go.faq.a9":
@@ -656,7 +654,8 @@ export const dict = {
"workspace.lite.subscription.monthlyUsage": "Monthly Usage", "workspace.lite.subscription.monthlyUsage": "Monthly Usage",
"workspace.lite.subscription.resetsIn": "Resets in", "workspace.lite.subscription.resetsIn": "Resets in",
"workspace.lite.subscription.useBalance": "Use your available balance after reaching the usage limits", "workspace.lite.subscription.useBalance": "Use your available balance after reaching the usage limits",
"workspace.lite.subscription.selectProvider": 'Select the "opencode" provider to use Go models.', "workspace.lite.subscription.selectProvider":
'Select "OpenCode Go" as the provider in your opencode configuration to use Go models.',
"workspace.lite.providers.title": "Providers", "workspace.lite.providers.title": "Providers",
"workspace.lite.providers.description": "Control which providers are used for routing.", "workspace.lite.providers.description": "Control which providers are used for routing.",
"workspace.lite.providers.useChina": "Enable models hosted in China", "workspace.lite.providers.useChina": "Enable models hosted in China",
+15 -12
View File
@@ -269,7 +269,8 @@ export const dict = {
"go.cta.text": "Suscribirse a Go", "go.cta.text": "Suscribirse a Go",
"go.cta.price": "10 $/mes", "go.cta.price": "10 $/mes",
"go.cta.promo": "$5 el primer mes", "go.cta.promo": "$5 el primer mes",
"go.pricing.body": "Go comienza en $5 el primer mes, luego 10 $/mes.", "go.pricing.body":
"Úsalo con cualquier agente. $5 el primer mes, luego 10 $/mes. Recarga crédito si es necesario. Cancela en cualquier momento.",
"go.graph.free": "Gratis", "go.graph.free": "Gratis",
"go.graph.freePill": "Big Pickle y modelos gratuitos", "go.graph.freePill": "Big Pickle y modelos gratuitos",
"go.graph.go": "Go", "go.graph.go": "Go",
@@ -309,15 +310,15 @@ export const dict = {
"go.problem.item4": "go.problem.item4":
"Incluye Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash y Hy3", "Incluye Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash y Hy3",
"go.how.title": "Cómo funciona Go", "go.how.title": "Cómo funciona Go",
"go.how.body": "Go comienza en $5 el primer mes, luego 10 $/mes.", "go.how.body": "Go comienza en $5 el primer mes, luego 10 $/mes. Puedes usarlo con OpenCode o cualquier agente.",
"go.how.step1.title": "Suscribirse a Go", "go.how.step1.title": "Crear una cuenta",
"go.how.step1.beforeLink": "en", "go.how.step1.beforeLink": "sigue las",
"go.how.step1.link": "OpenCode Console", "go.how.step1.link": "instrucciones de configuración",
"go.how.step2.title": "Conectar OpenCode", "go.how.step2.title": "Suscribirse a Go",
"go.how.step2.link": "opencode2 console login", "go.how.step2.link": "$5 el primer mes",
"go.how.step2.afterLink": "y autoriza el dispositivo en tu navegador", "go.how.step2.afterLink": "luego 10 $/mes con límites generosos",
"go.how.step3.title": "Empezar a programar", "go.how.step3.title": "Empezar a programar",
"go.how.step3.body": "con el proveedor opencode", "go.how.step3.body": "con acceso fiable a modelos de código abierto",
"go.privacy.title": "Tu privacidad es importante para nosotros", "go.privacy.title": "Tu privacidad es importante para nosotros",
"go.privacy.body": "go.privacy.body":
"El plan está diseñado principalmente para usuarios internacionales, con modelos alojados en EE. UU., UE y Singapur para un acceso global estable.", "El plan está diseñado principalmente para usuarios internacionales, con modelos alojados en EE. UU., UE y Singapur para un acceso global estable.",
@@ -350,8 +351,9 @@ export const dict = {
"go.faq.a6": "Si necesitas más uso, puedes recargar crédito en tu cuenta.", "go.faq.a6": "Si necesitas más uso, puedes recargar crédito en tu cuenta.",
"go.faq.q7": "¿Puedo cancelar?", "go.faq.q7": "¿Puedo cancelar?",
"go.faq.a7": "Sí, puedes cancelar en cualquier momento.", "go.faq.a7": "Sí, puedes cancelar en cualquier momento.",
"go.faq.q8": "¿Qué acceso está aplazado?", "go.faq.q8": "¿Puedo usar Go con otros agentes de programación?",
"go.faq.a8": "La compatibilidad con agentes externos y cuentas de servicio está aplazada.", "go.faq.a8":
"Sí, puedes usar Go con cualquier agente. Sigue las instrucciones de configuración en tu agente de programación preferido.",
"go.faq.q9": "¿Cuál es la diferencia entre los modelos gratuitos y Go?", "go.faq.q9": "¿Cuál es la diferencia entre los modelos gratuitos y Go?",
"go.faq.a9": "go.faq.a9":
@@ -658,7 +660,8 @@ export const dict = {
"workspace.lite.subscription.monthlyUsage": "Uso Mensual", "workspace.lite.subscription.monthlyUsage": "Uso Mensual",
"workspace.lite.subscription.resetsIn": "Se reinicia en", "workspace.lite.subscription.resetsIn": "Se reinicia en",
"workspace.lite.subscription.useBalance": "Usa tu saldo disponible después de alcanzar los límites de uso", "workspace.lite.subscription.useBalance": "Usa tu saldo disponible después de alcanzar los límites de uso",
"workspace.lite.subscription.selectProvider": "Selecciona el proveedor opencode para usar los modelos de Go.", "workspace.lite.subscription.selectProvider":
'Selecciona "OpenCode Go" como proveedor en tu configuración de opencode para usar los modelos Go.',
"workspace.lite.providers.title": "Proveedores", "workspace.lite.providers.title": "Proveedores",
"workspace.lite.providers.description": "Controla qué proveedores se usan para el enrutamiento.", "workspace.lite.providers.description": "Controla qué proveedores se usan para el enrutamiento.",
"workspace.lite.providers.useChina": "Activar modelos alojados en China", "workspace.lite.providers.useChina": "Activar modelos alojados en China",
+16 -12
View File
@@ -270,7 +270,8 @@ export const dict = {
"go.cta.text": "S'abonner à Go", "go.cta.text": "S'abonner à Go",
"go.cta.price": "10 $/mois", "go.cta.price": "10 $/mois",
"go.cta.promo": "$5 le premier mois", "go.cta.promo": "$5 le premier mois",
"go.pricing.body": "Go commence à $5 pour le premier mois, puis 10 $/mois.", "go.pricing.body":
"Utilisez-le avec n'importe quel agent. $5 le premier mois, puis 10 $/mois. Rechargez du crédit si nécessaire. Annulez à tout moment.",
"go.graph.free": "Gratuit", "go.graph.free": "Gratuit",
"go.graph.freePill": "Big Pickle et modèles gratuits", "go.graph.freePill": "Big Pickle et modèles gratuits",
"go.graph.go": "Go", "go.graph.go": "Go",
@@ -309,15 +310,16 @@ export const dict = {
"go.problem.item4": "go.problem.item4":
"Inclut Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash et Hy3", "Inclut Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash et Hy3",
"go.how.title": "Comment fonctionne Go", "go.how.title": "Comment fonctionne Go",
"go.how.body": "Go commence à $5 pour le premier mois, puis 10 $/mois.", "go.how.body":
"go.how.step1.title": "Abonnez-vous à Go", "Go commence à $5 pour le premier mois, puis 10 $/mois. Vous pouvez l'utiliser avec OpenCode ou n'importe quel agent.",
"go.how.step1.beforeLink": "dans", "go.how.step1.title": "Créez un compte",
"go.how.step1.link": "OpenCode Console", "go.how.step1.beforeLink": "suivez les",
"go.how.step2.title": "Connecter OpenCode", "go.how.step1.link": "instructions de configuration",
"go.how.step2.link": "opencode2 console login", "go.how.step2.title": "Abonnez-vous à Go",
"go.how.step2.afterLink": "et autorisez lappareil dans votre navigateur", "go.how.step2.link": "$5 le premier mois",
"go.how.step2.afterLink": "puis 10 $/mois avec des limites généreuses",
"go.how.step3.title": "Commencez à coder", "go.how.step3.title": "Commencez à coder",
"go.how.step3.body": "avec le fournisseur opencode", "go.how.step3.body": "avec un accès fiable aux modèles open source",
"go.privacy.title": "Votre vie privée est importante pour nous", "go.privacy.title": "Votre vie privée est importante pour nous",
"go.privacy.body": "go.privacy.body":
"Le plan est conçu principalement pour les utilisateurs internationaux, avec des modèles hébergés aux États-Unis, dans l'UE et à Singapour pour un accès mondial stable.", "Le plan est conçu principalement pour les utilisateurs internationaux, avec des modèles hébergés aux États-Unis, dans l'UE et à Singapour pour un accès mondial stable.",
@@ -350,8 +352,9 @@ export const dict = {
"go.faq.a6": "Si vous avez besoin de plus d'utilisation, vous pouvez recharger du crédit dans votre compte.", "go.faq.a6": "Si vous avez besoin de plus d'utilisation, vous pouvez recharger du crédit dans votre compte.",
"go.faq.q7": "Puis-je annuler ?", "go.faq.q7": "Puis-je annuler ?",
"go.faq.a7": "Oui, vous pouvez annuler à tout moment.", "go.faq.a7": "Oui, vous pouvez annuler à tout moment.",
"go.faq.q8": "Quel accès est reporté ?", "go.faq.q8": "Puis-je utiliser Go avec d'autres agents de code ?",
"go.faq.a8": "La prise en charge des agents externes et des comptes de service est reportée.", "go.faq.a8":
"Oui, vous pouvez utiliser Go avec n'importe quel agent. Suivez les instructions de configuration dans votre agent de code préféré.",
"go.faq.q9": "Quelle est la différence entre les modèles gratuits et Go ?", "go.faq.q9": "Quelle est la différence entre les modèles gratuits et Go ?",
"go.faq.a9": "go.faq.a9":
"Les modèles gratuits incluent Big Pickle ainsi que des modèles promotionnels disponibles à ce moment-là, avec un quota de 200 requêtes/jour. Go inclut Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash et Hy3 avec des quotas de requêtes plus élevés appliqués sur des fenêtres glissantes (5 heures, hebdomadaire et mensuelle), à peu près équivalent à 12 $ par 5 heures, 30 $ par semaine et 60 $ par mois (le nombre réel de requêtes varie selon le modèle et l'utilisation).", "Les modèles gratuits incluent Big Pickle ainsi que des modèles promotionnels disponibles à ce moment-là, avec un quota de 200 requêtes/jour. Go inclut Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash et Hy3 avec des quotas de requêtes plus élevés appliqués sur des fenêtres glissantes (5 heures, hebdomadaire et mensuelle), à peu près équivalent à 12 $ par 5 heures, 30 $ par semaine et 60 $ par mois (le nombre réel de requêtes varie selon le modèle et l'utilisation).",
@@ -663,7 +666,8 @@ export const dict = {
"workspace.lite.subscription.resetsIn": "Réinitialisation dans", "workspace.lite.subscription.resetsIn": "Réinitialisation dans",
"workspace.lite.subscription.useBalance": "workspace.lite.subscription.useBalance":
"Utilisez votre solde disponible après avoir atteint les limites d'utilisation", "Utilisez votre solde disponible après avoir atteint les limites d'utilisation",
"workspace.lite.subscription.selectProvider": "Sélectionnez le fournisseur opencode pour utiliser les modèles Go.", "workspace.lite.subscription.selectProvider":
'Sélectionnez "OpenCode Go" comme fournisseur dans votre configuration opencode pour utiliser les modèles Go.',
"workspace.lite.providers.title": "Fournisseurs", "workspace.lite.providers.title": "Fournisseurs",
"workspace.lite.providers.description": "Contrôlez les fournisseurs utilisés pour le routage.", "workspace.lite.providers.description": "Contrôlez les fournisseurs utilisés pour le routage.",
"workspace.lite.providers.useChina": "Activer les modèles hébergés en Chine", "workspace.lite.providers.useChina": "Activer les modèles hébergés en Chine",
+15 -12
View File
@@ -266,7 +266,8 @@ export const dict = {
"go.cta.text": "Abbonati a Go", "go.cta.text": "Abbonati a Go",
"go.cta.price": "$10/mese", "go.cta.price": "$10/mese",
"go.cta.promo": "$5 il primo mese", "go.cta.promo": "$5 il primo mese",
"go.pricing.body": "Go inizia a $5 per il primo mese, poi $10/mese.", "go.pricing.body":
"Usalo con qualsiasi agente. $5 il primo mese, poi $10/mese. Ricarica il credito se necessario. Annulla in qualsiasi momento.",
"go.graph.free": "Gratis", "go.graph.free": "Gratis",
"go.graph.freePill": "Big Pickle e modelli gratuiti", "go.graph.freePill": "Big Pickle e modelli gratuiti",
"go.graph.go": "Go", "go.graph.go": "Go",
@@ -305,15 +306,15 @@ export const dict = {
"go.problem.item4": "go.problem.item4":
"Include Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash e Hy3", "Include Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash e Hy3",
"go.how.title": "Come funziona Go", "go.how.title": "Come funziona Go",
"go.how.body": "Go inizia a $5 per il primo mese, poi $10/mese.", "go.how.body": "Go inizia a $5 per il primo mese, poi $10/mese. Puoi usarlo con OpenCode o qualsiasi agente.",
"go.how.step1.title": "Abbonati a Go", "go.how.step1.title": "Crea un account",
"go.how.step1.beforeLink": "in", "go.how.step1.beforeLink": "segui le",
"go.how.step1.link": "OpenCode Console", "go.how.step1.link": "istruzioni di configurazione",
"go.how.step2.title": "Connetti OpenCode", "go.how.step2.title": "Abbonati a Go",
"go.how.step2.link": "opencode2 console login", "go.how.step2.link": "$5 il primo mese",
"go.how.step2.afterLink": "e autorizza il dispositivo nel browser", "go.how.step2.afterLink": "poi $10/mese con limiti generosi",
"go.how.step3.title": "Inizia a programmare", "go.how.step3.title": "Inizia a programmare",
"go.how.step3.body": "con il provider opencode", "go.how.step3.body": "con accesso affidabile ai modelli open source",
"go.privacy.title": "La tua privacy è importante per noi", "go.privacy.title": "La tua privacy è importante per noi",
"go.privacy.body": "go.privacy.body":
"Il piano è progettato principalmente per gli utenti internazionali, con modelli ospitati negli Stati Uniti, UE e Singapore per un accesso globale stabile.", "Il piano è progettato principalmente per gli utenti internazionali, con modelli ospitati negli Stati Uniti, UE e Singapore per un accesso globale stabile.",
@@ -346,8 +347,9 @@ export const dict = {
"go.faq.a6": "Se hai bisogno di più utilizzo, puoi ricaricare il credito nel tuo account.", "go.faq.a6": "Se hai bisogno di più utilizzo, puoi ricaricare il credito nel tuo account.",
"go.faq.q7": "Posso annullare?", "go.faq.q7": "Posso annullare?",
"go.faq.a7": "Sì, puoi annullare in qualsiasi momento.", "go.faq.a7": "Sì, puoi annullare in qualsiasi momento.",
"go.faq.q8": "Quale accesso è rinviato?", "go.faq.q8": "Posso usare Go con altri agenti di coding?",
"go.faq.a8": "Il supporto per agenti esterni e account di servizio è rinviato.", "go.faq.a8":
"Sì, puoi usare Go con qualsiasi agente. Segui le istruzioni di configurazione nel tuo agente di coding preferito.",
"go.faq.q9": "Qual è la differenza tra i modelli gratuiti e Go?", "go.faq.q9": "Qual è la differenza tra i modelli gratuiti e Go?",
"go.faq.a9": "go.faq.a9":
@@ -656,7 +658,8 @@ export const dict = {
"workspace.lite.subscription.monthlyUsage": "Utilizzo Mensile", "workspace.lite.subscription.monthlyUsage": "Utilizzo Mensile",
"workspace.lite.subscription.resetsIn": "Si resetta tra", "workspace.lite.subscription.resetsIn": "Si resetta tra",
"workspace.lite.subscription.useBalance": "Usa il tuo saldo disponibile dopo aver raggiunto i limiti di utilizzo", "workspace.lite.subscription.useBalance": "Usa il tuo saldo disponibile dopo aver raggiunto i limiti di utilizzo",
"workspace.lite.subscription.selectProvider": "Seleziona il provider opencode per usare i modelli Go.", "workspace.lite.subscription.selectProvider":
'Seleziona "OpenCode Go" come provider nella tua configurazione opencode per utilizzare i modelli Go.',
"workspace.lite.providers.title": "Provider", "workspace.lite.providers.title": "Provider",
"workspace.lite.providers.description": "Controlla quali provider vengono usati per il routing.", "workspace.lite.providers.description": "Controlla quali provider vengono usati per il routing.",
"workspace.lite.providers.useChina": "Abilita modelli ospitati in Cina", "workspace.lite.providers.useChina": "Abilita modelli ospitati in Cina",
+15 -12
View File
@@ -265,7 +265,8 @@ export const dict = {
"go.cta.text": "Goを購読する", "go.cta.text": "Goを購読する",
"go.cta.price": "$10/月", "go.cta.price": "$10/月",
"go.cta.promo": "初月 $5", "go.cta.promo": "初月 $5",
"go.pricing.body": "Goは最初の月$5、その後$10/月で始まります。", "go.pricing.body":
"どのエージェントでも使えます。最初の月$5、その後$10/月。必要に応じてクレジットを追加。いつでもキャンセルできます。",
"go.graph.free": "無料", "go.graph.free": "無料",
"go.graph.freePill": "Big Pickleと無料モデル", "go.graph.freePill": "Big Pickleと無料モデル",
"go.graph.go": "Go", "go.graph.go": "Go",
@@ -305,15 +306,15 @@ export const dict = {
"go.problem.item4": "go.problem.item4":
"Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flash、Hy3を含む", "Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flash、Hy3を含む",
"go.how.title": "Goの仕組み", "go.how.title": "Goの仕組み",
"go.how.body": "Goは最初の月$5、その後$10/月で始まります。", "go.how.body": "Goは最初の月$5、その後$10/月で始まります。OpenCodeまたは任意のエージェントで使えます。",
"go.how.step1.title": "Goを購読する", "go.how.step1.title": "アカウントを作成",
"go.how.step1.beforeLink": "", "go.how.step1.beforeLink": "",
"go.how.step1.link": "OpenCode Console", "go.how.step1.link": "セットアップ手順はこちら",
"go.how.step2.title": "OpenCodeを接続", "go.how.step2.title": "Goを購読する",
"go.how.step2.link": "opencode2 console login", "go.how.step2.link": "最初の月$5",
"go.how.step2.afterLink": "ブラウザでデバイスを承認します", "go.how.step2.afterLink": "その後$10/月、ゆとりある上限付き",
"go.how.step3.title": "コーディングを開始", "go.how.step3.title": "コーディングを開始",
"go.how.step3.body": "opencodeプロバイダーで", "go.how.step3.body": "オープンソースモデルへの安定したアクセスで",
"go.privacy.title": "あなたのプライバシーは私たちにとって重要です", "go.privacy.title": "あなたのプライバシーは私たちにとって重要です",
"go.privacy.body": "go.privacy.body":
"このプランは主に海外ユーザー向けに設計されており、米国、EU、シンガポールでホストされたモデルにより安定したグローバルアクセスを提供します。", "このプランは主に海外ユーザー向けに設計されており、米国、EU、シンガポールでホストされたモデルにより安定したグローバルアクセスを提供します。",
@@ -346,8 +347,9 @@ export const dict = {
"go.faq.a6": "利用枠を追加したい場合は、アカウントでクレジットをチャージできます。", "go.faq.a6": "利用枠を追加したい場合は、アカウントでクレジットをチャージできます。",
"go.faq.q7": "キャンセルできますか?", "go.faq.q7": "キャンセルできますか?",
"go.faq.a7": "はい、いつでもキャンセル可能です。", "go.faq.a7": "はい、いつでもキャンセル可能です。",
"go.faq.q8": "どのアクセスが延期されていますか?", "go.faq.q8": "他のコーディングエージェントでGoを使えますか?",
"go.faq.a8": "外部エージェントとサービスアカウントのサポートは延期されています。", "go.faq.a8":
"はい、Goは任意のエージェントで使用できます。お使いのコーディングエージェントのセットアップ手順に従ってください。",
"go.faq.q9": "無料モデルとGoの違いは何ですか?", "go.faq.q9": "無料モデルとGoの違いは何ですか?",
"go.faq.a9": "go.faq.a9":
@@ -656,7 +658,8 @@ export const dict = {
"workspace.lite.subscription.monthlyUsage": "月間利用量", "workspace.lite.subscription.monthlyUsage": "月間利用量",
"workspace.lite.subscription.resetsIn": "リセットまで", "workspace.lite.subscription.resetsIn": "リセットまで",
"workspace.lite.subscription.useBalance": "利用限度額に達したら利用可能な残高を使用する", "workspace.lite.subscription.useBalance": "利用限度額に達したら利用可能な残高を使用する",
"workspace.lite.subscription.selectProvider": "Goモデルを使用するにはopencodeプロバイダーを選択してください。", "workspace.lite.subscription.selectProvider":
"Go モデルを使用するには、opencode の設定で「OpenCode Go」をプロバイダーとして選択してください。",
"workspace.lite.providers.title": "プロバイダー", "workspace.lite.providers.title": "プロバイダー",
"workspace.lite.providers.description": "ルーティングに使用するプロバイダーを管理します。", "workspace.lite.providers.description": "ルーティングに使用するプロバイダーを管理します。",
"workspace.lite.providers.useChina": "中国でホストされているモデルを有効にする", "workspace.lite.providers.useChina": "中国でホストされているモデルを有効にする",
+14 -12
View File
@@ -262,7 +262,8 @@ export const dict = {
"go.cta.text": "Go 구독하기", "go.cta.text": "Go 구독하기",
"go.cta.price": "$10/월", "go.cta.price": "$10/월",
"go.cta.promo": "첫 달 $5", "go.cta.promo": "첫 달 $5",
"go.pricing.body": "Go는 첫 달 $5, 이후 $10/월로 시작합니다.", "go.pricing.body":
"어떤 에이전트와도 사용할 수 있습니다. 첫 달 $5, 이후 $10/월. 필요하면 크레딧을 충전하세요. 언제든지 취소할 수 있습니다.",
"go.graph.free": "무료", "go.graph.free": "무료",
"go.graph.freePill": "Big Pickle 및 무료 모델", "go.graph.freePill": "Big Pickle 및 무료 모델",
"go.graph.go": "Go", "go.graph.go": "Go",
@@ -302,15 +303,15 @@ export const dict = {
"go.problem.item4": "go.problem.item4":
"Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash, Hy3 포함", "Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash, Hy3 포함",
"go.how.title": "Go 작동 방식", "go.how.title": "Go 작동 방식",
"go.how.body": "Go는 첫 달 $5, 이후 $10/월로 시작합니다.", "go.how.body": "Go는 첫 달 $5, 이후 $10/월로 시작합니다. OpenCode 또는 어떤 에이전트와도 함께 사용할 수 있습니다.",
"go.how.step1.title": "Go 구독", "go.how.step1.title": "계정 생성",
"go.how.step1.beforeLink": "에서", "go.how.step1.beforeLink": "",
"go.how.step1.link": "OpenCode Console", "go.how.step1.link": "설정 지침을 따르세요",
"go.how.step2.title": "OpenCode 연결", "go.how.step2.title": "Go 구독",
"go.how.step2.link": "opencode2 console login", "go.how.step2.link": "첫 달 $5",
"go.how.step2.afterLink": "브라우저에서 기기를 승인하세요", "go.how.step2.afterLink": "이후 $10/월, 넉넉한 한도 포함",
"go.how.step3.title": "코딩 시작", "go.how.step3.title": "코딩 시작",
"go.how.step3.body": "opencode 공급자로", "go.how.step3.body": "오픈 소스 모델에 대한 안정적인 액세스와 함께",
"go.privacy.title": "귀하의 프라이버시는 우리에게 중요합니다", "go.privacy.title": "귀하의 프라이버시는 우리에게 중요합니다",
"go.privacy.body": "go.privacy.body":
"이 플랜은 주로 글로벌 사용자를 위해 설계되었으며, 안정적인 글로벌 액세스를 위해 미국, EU, 싱가포르에 모델이 호스팅되어 있습니다.", "이 플랜은 주로 글로벌 사용자를 위해 설계되었으며, 안정적인 글로벌 액세스를 위해 미국, EU, 싱가포르에 모델이 호스팅되어 있습니다.",
@@ -342,8 +343,8 @@ export const dict = {
"go.faq.a6": "사용량이 더 필요한 경우 계정에서 크레딧을 충전할 수 있습니다.", "go.faq.a6": "사용량이 더 필요한 경우 계정에서 크레딧을 충전할 수 있습니다.",
"go.faq.q7": "취소할 수 있나요?", "go.faq.q7": "취소할 수 있나요?",
"go.faq.a7": "네, 언제든지 취소할 수 있습니다.", "go.faq.a7": "네, 언제든지 취소할 수 있습니다.",
"go.faq.q8": "어떤 액세스가 연기되었나요?", "go.faq.q8": "다른 코딩 에이전트와 Go를 사용할 수 있나요?",
"go.faq.a8": "외부 에이전트와 서비스 계정 지원은 연기되었습니다.", "go.faq.a8": "네, Go는 어떤 에이전트와도 사용할 수 있습니다. 선호하는 코딩 에이전트의 설정 지침을 따르세요.",
"go.faq.q9": "무료 모델과 Go의 차이점은 무엇인가요?", "go.faq.q9": "무료 모델과 Go의 차이점은 무엇인가요?",
"go.faq.a9": "go.faq.a9":
@@ -649,7 +650,8 @@ export const dict = {
"workspace.lite.subscription.monthlyUsage": "월간 사용량", "workspace.lite.subscription.monthlyUsage": "월간 사용량",
"workspace.lite.subscription.resetsIn": "초기화까지 남은 시간:", "workspace.lite.subscription.resetsIn": "초기화까지 남은 시간:",
"workspace.lite.subscription.useBalance": "사용 한도 도달 후에는 보유 잔액 사용", "workspace.lite.subscription.useBalance": "사용 한도 도달 후에는 보유 잔액 사용",
"workspace.lite.subscription.selectProvider": "Go 모델을 사용하려면 opencode 공급자를 선택하세요.", "workspace.lite.subscription.selectProvider":
'Go 모델을 사용하려면 opencode 설정에서 "OpenCode Go"를 공급자로 선택하세요.',
"workspace.lite.providers.title": "공급자", "workspace.lite.providers.title": "공급자",
"workspace.lite.providers.description": "라우팅에 사용할 공급자를 제어합니다.", "workspace.lite.providers.description": "라우팅에 사용할 공급자를 제어합니다.",
"workspace.lite.providers.useChina": "중국에서 호스팅되는 모델 활성화", "workspace.lite.providers.useChina": "중국에서 호스팅되는 모델 활성화",
+16 -12
View File
@@ -266,7 +266,8 @@ export const dict = {
"go.cta.text": "Abonner på Go", "go.cta.text": "Abonner på Go",
"go.cta.price": "$10/måned", "go.cta.price": "$10/måned",
"go.cta.promo": "$5 første måned", "go.cta.promo": "$5 første måned",
"go.pricing.body": "Go starter på $5 for den første måneden, deretter $10/måned.", "go.pricing.body":
"Bruk med hvilken som helst agent. $5 første måned, deretter $10/måned. Fyll på kreditt ved behov. Avslutt når som helst.",
"go.graph.free": "Gratis", "go.graph.free": "Gratis",
"go.graph.freePill": "Big Pickle og gratis modeller", "go.graph.freePill": "Big Pickle og gratis modeller",
"go.graph.go": "Go", "go.graph.go": "Go",
@@ -305,15 +306,16 @@ export const dict = {
"go.problem.item4": "go.problem.item4":
"Inkluderer Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash og Hy3", "Inkluderer Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash og Hy3",
"go.how.title": "Hvordan Go fungerer", "go.how.title": "Hvordan Go fungerer",
"go.how.body": "Go starter på $5 for den første måneden, deretter $10/måned.", "go.how.body":
"go.how.step1.title": "Abonner på Go", "Go starter på $5 for den første måneden, deretter $10/måned. Du kan bruke det med OpenCode eller hvilken som helst agent.",
"go.how.step1.beforeLink": "i", "go.how.step1.title": "Opprett en konto",
"go.how.step1.link": "OpenCode Console", "go.how.step1.beforeLink": "følg",
"go.how.step2.title": "Koble til OpenCode", "go.how.step1.link": "oppsettsinstruksjonene",
"go.how.step2.link": "opencode2 console login", "go.how.step2.title": "Abonner på Go",
"go.how.step2.afterLink": "og godkjenn enheten i nettleseren", "go.how.step2.link": "$5 første måned",
"go.how.step2.afterLink": "deretter $10/måned med sjenerøse grenser",
"go.how.step3.title": "Begynn å kode", "go.how.step3.title": "Begynn å kode",
"go.how.step3.body": "med opencode-leverandøren", "go.how.step3.body": "med pålitelig tilgang til åpen kildekode-modeller",
"go.privacy.title": "Personvernet ditt er viktig for oss", "go.privacy.title": "Personvernet ditt er viktig for oss",
"go.privacy.body": "go.privacy.body":
"Planen er primært designet for internasjonale brukere, med modeller driftet i USA, EU og Singapore for stabil global tilgang.", "Planen er primært designet for internasjonale brukere, med modeller driftet i USA, EU og Singapore for stabil global tilgang.",
@@ -346,8 +348,9 @@ export const dict = {
"go.faq.a6": "Hvis du trenger mer bruk, kan du fylle på kreditt i kontoen din.", "go.faq.a6": "Hvis du trenger mer bruk, kan du fylle på kreditt i kontoen din.",
"go.faq.q7": "Kan jeg avslutte?", "go.faq.q7": "Kan jeg avslutte?",
"go.faq.a7": "Ja, du kan avslutte når som helst.", "go.faq.a7": "Ja, du kan avslutte når som helst.",
"go.faq.q8": "Hvilken tilgang er utsatt?", "go.faq.q8": "Kan jeg bruke Go med andre kodeagenter?",
"go.faq.a8": "Støtte for eksterne agenter og tjenestekontoer er utsatt.", "go.faq.a8":
"Ja, du kan bruke Go med hvilken som helst agent. Følg oppsettinstruksjonene i din foretrukne kodeagent.",
"go.faq.q9": "Hva er forskjellen mellom gratis modeller og Go?", "go.faq.q9": "Hva er forskjellen mellom gratis modeller og Go?",
"go.faq.a9": "go.faq.a9":
@@ -654,7 +657,8 @@ export const dict = {
"workspace.lite.subscription.monthlyUsage": "Månedlig bruk", "workspace.lite.subscription.monthlyUsage": "Månedlig bruk",
"workspace.lite.subscription.resetsIn": "Nullstilles om", "workspace.lite.subscription.resetsIn": "Nullstilles om",
"workspace.lite.subscription.useBalance": "Bruk din tilgjengelige saldo etter å ha nådd bruksgrensene", "workspace.lite.subscription.useBalance": "Bruk din tilgjengelige saldo etter å ha nådd bruksgrensene",
"workspace.lite.subscription.selectProvider": "Velg opencode-leverandøren for å bruke Go-modeller.", "workspace.lite.subscription.selectProvider":
'Velg "OpenCode Go" som leverandør i opencode-konfigurasjonen din for å bruke Go-modeller.',
"workspace.lite.providers.title": "Leverandører", "workspace.lite.providers.title": "Leverandører",
"workspace.lite.providers.description": "Kontroller hvilke leverandører som brukes til ruting.", "workspace.lite.providers.description": "Kontroller hvilke leverandører som brukes til ruting.",
"workspace.lite.providers.useChina": "Aktiver modeller hostet i Kina", "workspace.lite.providers.useChina": "Aktiver modeller hostet i Kina",
+16 -12
View File
@@ -267,7 +267,8 @@ export const dict = {
"go.cta.text": "Zasubskrybuj Go", "go.cta.text": "Zasubskrybuj Go",
"go.cta.price": "$10/miesiąc", "go.cta.price": "$10/miesiąc",
"go.cta.promo": "$5 pierwszy miesiąc", "go.cta.promo": "$5 pierwszy miesiąc",
"go.pricing.body": "Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc.", "go.pricing.body":
"Używaj z dowolnym agentem. $5 za pierwszy miesiąc, potem $10/miesiąc. Doładuj konto w razie potrzeby. Anuluj w dowolnym momencie.",
"go.graph.free": "Darmowe", "go.graph.free": "Darmowe",
"go.graph.freePill": "Big Pickle i darmowe modele", "go.graph.freePill": "Big Pickle i darmowe modele",
"go.graph.go": "Go", "go.graph.go": "Go",
@@ -306,15 +307,16 @@ export const dict = {
"go.problem.item4": "go.problem.item4":
"Zawiera Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash i Hy3", "Zawiera Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash i Hy3",
"go.how.title": "Jak działa Go", "go.how.title": "Jak działa Go",
"go.how.body": "Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc.", "go.how.body":
"go.how.step1.title": "Zasubskrybuj Go", "Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc. Możesz go używać z OpenCode lub dowolnym agentem.",
"go.how.step1.beforeLink": "w", "go.how.step1.title": "Załóż konto",
"go.how.step1.link": "OpenCode Console", "go.how.step1.beforeLink": "postępuj zgodnie z",
"go.how.step2.title": "Połącz OpenCode", "go.how.step1.link": "instrukcją konfiguracji",
"go.how.step2.link": "opencode2 console login", "go.how.step2.title": "Zasubskrybuj Go",
"go.how.step2.afterLink": "i zatwierdź urządzenie w przeglądarce", "go.how.step2.link": "$5 za pierwszy miesiąc",
"go.how.step2.afterLink": "potem $10/miesiąc z hojnymi limitami",
"go.how.step3.title": "Zacznij kodować", "go.how.step3.title": "Zacznij kodować",
"go.how.step3.body": "z dostawcą opencode", "go.how.step3.body": "z niezawodnym dostępem do modeli open source",
"go.privacy.title": "Twoja prywatność jest dla nas ważna", "go.privacy.title": "Twoja prywatność jest dla nas ważna",
"go.privacy.body": "go.privacy.body":
"Plan został zaprojektowany głównie dla użytkowników międzynarodowych, z modelami hostowanymi w USA, UE i Singapurze, aby zapewnić stabilny globalny dostęp.", "Plan został zaprojektowany głównie dla użytkowników międzynarodowych, z modelami hostowanymi w USA, UE i Singapurze, aby zapewnić stabilny globalny dostęp.",
@@ -347,8 +349,9 @@ export const dict = {
"go.faq.a6": "Jeśli potrzebujesz większego użycia, możesz doładować środki na swoim koncie.", "go.faq.a6": "Jeśli potrzebujesz większego użycia, możesz doładować środki na swoim koncie.",
"go.faq.q7": "Czy mogę anulować?", "go.faq.q7": "Czy mogę anulować?",
"go.faq.a7": "Tak, możesz anulować w dowolnym momencie.", "go.faq.a7": "Tak, możesz anulować w dowolnym momencie.",
"go.faq.q8": "Jaki dostęp jest odroczony?", "go.faq.q8": "Czy mogę używać Go z innymi agentami kodującymi?",
"go.faq.a8": "Obsługa zewnętrznych agentów i kont usług jest odroczona.", "go.faq.a8":
"Tak, możesz używać Go z dowolnym agentem. Postępuj zgodnie z instrukcjami konfiguracji w swoim preferowanym agencie.",
"go.faq.q9": "Jaka jest różnica między darmowymi modelami a Go?", "go.faq.q9": "Jaka jest różnica między darmowymi modelami a Go?",
"go.faq.a9": "go.faq.a9":
@@ -655,7 +658,8 @@ export const dict = {
"workspace.lite.subscription.monthlyUsage": "Użycie miesięczne", "workspace.lite.subscription.monthlyUsage": "Użycie miesięczne",
"workspace.lite.subscription.resetsIn": "Resetuje się za", "workspace.lite.subscription.resetsIn": "Resetuje się za",
"workspace.lite.subscription.useBalance": "Użyj dostępnego salda po osiągnięciu limitów użycia", "workspace.lite.subscription.useBalance": "Użyj dostępnego salda po osiągnięciu limitów użycia",
"workspace.lite.subscription.selectProvider": "Wybierz dostawcę opencode, aby używać modeli Go.", "workspace.lite.subscription.selectProvider":
'Wybierz "OpenCode Go" jako dostawcę w konfiguracji opencode, aby używać modeli Go.',
"workspace.lite.providers.title": "Dostawcy", "workspace.lite.providers.title": "Dostawcy",
"workspace.lite.providers.description": "Kontroluj, którzy dostawcy są używani do routingu.", "workspace.lite.providers.description": "Kontroluj, którzy dostawcy są używani do routingu.",
"workspace.lite.providers.useChina": "Włącz modele hostowane w Chinach", "workspace.lite.providers.useChina": "Włącz modele hostowane w Chinach",
+16 -12
View File
@@ -270,7 +270,8 @@ export const dict = {
"go.cta.text": "Подписаться на Go", "go.cta.text": "Подписаться на Go",
"go.cta.price": "$10/месяц", "go.cta.price": "$10/месяц",
"go.cta.promo": "$5 первый месяц", "go.cta.promo": "$5 первый месяц",
"go.pricing.body": "Go начинается с $5 за первый месяц, затем $10/месяц.", "go.pricing.body":
"Используйте с любым агентом. $5 за первый месяц, затем $10/месяц. Пополняйте баланс при необходимости. Отменить можно в любое время.",
"go.graph.free": "Бесплатно", "go.graph.free": "Бесплатно",
"go.graph.freePill": "Big Pickle и бесплатные модели", "go.graph.freePill": "Big Pickle и бесплатные модели",
"go.graph.go": "Go", "go.graph.go": "Go",
@@ -310,15 +311,16 @@ export const dict = {
"go.problem.item4": "go.problem.item4":
"Включает Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash и Hy3", "Включает Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash и Hy3",
"go.how.title": "Как работает Go", "go.how.title": "Как работает Go",
"go.how.body": "Go начинается с $5 за первый месяц, затем $10/месяц.", "go.how.body":
"go.how.step1.title": "Подпишитесь на Go", "Go начинается с $5 за первый месяц, затем $10/месяц. Вы можете использовать его с OpenCode или любым агентом.",
"go.how.step1.beforeLink": ", "go.how.step1.title": "Создайте аккаунт",
"go.how.step1.link": "OpenCode Console", "go.how.step1.beforeLink": "следуйте",
"go.how.step2.title": "Подключить OpenCode", "go.how.step1.link": "инструкциям по настройке",
"go.how.step2.link": "opencode2 console login", "go.how.step2.title": "Подпишитесь на Go",
"go.how.step2.afterLink": "и подтвердите устройство в браузере", "go.how.step2.link": "$5 за первый месяц",
"go.how.step2.afterLink": "затем $10/месяц с щедрыми лимитами",
"go.how.step3.title": "Начните кодить", "go.how.step3.title": "Начните кодить",
"go.how.step3.body": "с провайдером opencode", "go.how.step3.body": "с надежным доступом к open-source моделям",
"go.privacy.title": "Ваша приватность важна для нас", "go.privacy.title": "Ваша приватность важна для нас",
"go.privacy.body": "go.privacy.body":
"План разработан в первую очередь для международных пользователей, с моделями, размещенными в США, ЕС и Сингапуре для стабильного глобального доступа.", "План разработан в первую очередь для международных пользователей, с моделями, размещенными в США, ЕС и Сингапуре для стабильного глобального доступа.",
@@ -351,8 +353,9 @@ export const dict = {
"go.faq.a6": "Если вам нужно больше использования, вы можете пополнить баланс в своем аккаунте.", "go.faq.a6": "Если вам нужно больше использования, вы можете пополнить баланс в своем аккаунте.",
"go.faq.q7": "Могу ли я отменить подписку?", "go.faq.q7": "Могу ли я отменить подписку?",
"go.faq.a7": "Да, вы можете отменить подписку в любое время.", "go.faq.a7": "Да, вы можете отменить подписку в любое время.",
"go.faq.q8": "Какая поддержка отложена?", "go.faq.q8": "Могу ли я использовать Go с другими кодинг-агентами?",
"go.faq.a8": "Поддержка внешних агентов и сервисных аккаунтов отложена.", "go.faq.a8":
"Да, вы можете использовать Go с любым агентом. Следуйте инструкциям по настройке в вашем предпочитаемом агенте.",
"go.faq.q9": "В чем разница между бесплатными моделями и Go?", "go.faq.q9": "В чем разница между бесплатными моделями и Go?",
"go.faq.a9": "go.faq.a9":
@@ -661,7 +664,8 @@ export const dict = {
"workspace.lite.subscription.monthlyUsage": "Ежемесячное использование", "workspace.lite.subscription.monthlyUsage": "Ежемесячное использование",
"workspace.lite.subscription.resetsIn": "Сброс через", "workspace.lite.subscription.resetsIn": "Сброс через",
"workspace.lite.subscription.useBalance": "Использовать доступный баланс после достижения лимитов", "workspace.lite.subscription.useBalance": "Использовать доступный баланс после достижения лимитов",
"workspace.lite.subscription.selectProvider": "Выберите провайдер opencode для использования моделей Go.", "workspace.lite.subscription.selectProvider":
'Выберите "OpenCode Go" в качестве провайдера в настройках opencode для использования моделей Go.',
"workspace.lite.providers.title": "Провайдеры", "workspace.lite.providers.title": "Провайдеры",
"workspace.lite.providers.description": "Управляйте провайдерами, используемыми для маршрутизации.", "workspace.lite.providers.description": "Управляйте провайдерами, используемыми для маршрутизации.",
"workspace.lite.providers.useChina": "Включить модели, размещенные в Китае", "workspace.lite.providers.useChina": "Включить модели, размещенные в Китае",
+13 -12
View File
@@ -265,7 +265,7 @@ export const dict = {
"go.cta.text": "สมัครสมาชิก Go", "go.cta.text": "สมัครสมาชิก Go",
"go.cta.price": "$10/เดือน", "go.cta.price": "$10/เดือน",
"go.cta.promo": "$5 เดือนแรก", "go.cta.promo": "$5 เดือนแรก",
"go.pricing.body": "Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน", "go.pricing.body": "ใช้กับเอเจนต์ใดก็ได้ $5 ในเดือนแรก จากนั้น $10/เดือน เติมเครดิตหากจำเป็น ยกเลิกได้ตลอดเวลา",
"go.graph.free": "ฟรี", "go.graph.free": "ฟรี",
"go.graph.freePill": "Big Pickle และโมเดลฟรี", "go.graph.freePill": "Big Pickle และโมเดลฟรี",
"go.graph.go": "Go", "go.graph.go": "Go",
@@ -304,15 +304,15 @@ export const dict = {
"go.problem.item4": "go.problem.item4":
"รวมถึง Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash และ Hy3", "รวมถึง Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash และ Hy3",
"go.how.title": "Go ทำงานอย่างไร", "go.how.title": "Go ทำงานอย่างไร",
"go.how.body": "Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน", "go.how.body": "Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน คุณสามารถใช้กับ OpenCode หรือเอเจนต์ใดก็ได้",
"go.how.step1.title": "สมัครสมาชิก Go", "go.how.step1.title": "สร้างบัญชี",
"go.how.step1.beforeLink": "ใน", "go.how.step1.beforeLink": "ทำตาม",
"go.how.step1.link": "OpenCode Console", "go.how.step1.link": "คำแนะนำการตั้งค่า",
"go.how.step2.title": "เชื่อมต่อ OpenCode", "go.how.step2.title": "สมัครสมาชิก Go",
"go.how.step2.link": "opencode2 console login", "go.how.step2.link": "$5 เดือนแรก",
"go.how.step2.afterLink": "และอนุมัติอุปกรณ์ในเบราว์เซอร์", "go.how.step2.afterLink": "จากนั้น $10/เดือน พร้อมขีดจำกัดที่เอื้อเฟื้อ",
"go.how.step3.title": "เริ่มเขียนโค้ด", "go.how.step3.title": "เริ่มเขียนโค้ด",
"go.how.step3.body": "ด้วยผู้ให้บริการ opencode", "go.how.step3.body": "ด้วยการเข้าถึงโมเดลโอเพนซอร์สที่เชื่อถือได้",
"go.privacy.title": "ความเป็นส่วนตัวของคุณสำคัญสำหรับเรา", "go.privacy.title": "ความเป็นส่วนตัวของคุณสำคัญสำหรับเรา",
"go.privacy.body": "go.privacy.body":
"แผนนี้ออกแบบมาเพื่อผู้ใช้งานระหว่างประเทศเป็นหลัก โดยมีโมเดลโฮสต์ในสหรัฐอเมริกา สหภาพยุโรป และสิงคโปร์ เพื่อการเข้าถึงทั่วโลกที่เสถียร", "แผนนี้ออกแบบมาเพื่อผู้ใช้งานระหว่างประเทศเป็นหลัก โดยมีโมเดลโฮสต์ในสหรัฐอเมริกา สหภาพยุโรป และสิงคโปร์ เพื่อการเข้าถึงทั่วโลกที่เสถียร",
@@ -345,8 +345,8 @@ export const dict = {
"go.faq.a6": "หากคุณต้องการใช้งานเพิ่ม คุณสามารถเติมเครดิตในบัญชีของคุณได้", "go.faq.a6": "หากคุณต้องการใช้งานเพิ่ม คุณสามารถเติมเครดิตในบัญชีของคุณได้",
"go.faq.q7": "ฉันสามารถยกเลิกได้หรือไม่?", "go.faq.q7": "ฉันสามารถยกเลิกได้หรือไม่?",
"go.faq.a7": "ได้ คุณสามารถยกเลิกได้ตลอดเวลา", "go.faq.a7": "ได้ คุณสามารถยกเลิกได้ตลอดเวลา",
"go.faq.q8": "การเข้าถึงใดถูกเลื่อนออกไป?", "go.faq.q8": "ฉันสามารถใช้ Go กับเอเจนต์เขียนโค้ดอื่นได้หรือไม่?",
"go.faq.a8": "การรองรับเอเจนต์ภายนอกและบัญชีบริการถูกเลื่อนออกไป", "go.faq.a8": "ได้ คุณสามารถใช้ Go กับเอเจนต์ใดก็ได้ ทำตามคำแนะนำการตั้งค่าในเอเจนต์เขียนโค้ดที่คุณต้องการ",
"go.faq.q9": "ความแตกต่างระหว่างโมเดลฟรีและ Go คืออะไร?", "go.faq.q9": "ความแตกต่างระหว่างโมเดลฟรีและ Go คืออะไร?",
"go.faq.a9": "go.faq.a9":
@@ -653,7 +653,8 @@ export const dict = {
"workspace.lite.subscription.monthlyUsage": "การใช้งานรายเดือน", "workspace.lite.subscription.monthlyUsage": "การใช้งานรายเดือน",
"workspace.lite.subscription.resetsIn": "รีเซ็ตใน", "workspace.lite.subscription.resetsIn": "รีเซ็ตใน",
"workspace.lite.subscription.useBalance": "ใช้ยอดคงเหลือของคุณหลังจากถึงขีดจำกัดการใช้งาน", "workspace.lite.subscription.useBalance": "ใช้ยอดคงเหลือของคุณหลังจากถึงขีดจำกัดการใช้งาน",
"workspace.lite.subscription.selectProvider": "เลือกผู้ให้บริการ opencode เพื่อใช้โมเดล Go", "workspace.lite.subscription.selectProvider":
'เลือก "OpenCode Go" เป็นผู้ให้บริการในการตั้งค่า opencode ของคุณเพื่อใช้โมเดล Go',
"workspace.lite.providers.title": "ผู้ให้บริการ", "workspace.lite.providers.title": "ผู้ให้บริการ",
"workspace.lite.providers.description": "ควบคุมผู้ให้บริการที่ใช้สำหรับการกำหนดเส้นทาง", "workspace.lite.providers.description": "ควบคุมผู้ให้บริการที่ใช้สำหรับการกำหนดเส้นทาง",
"workspace.lite.providers.useChina": "เปิดใช้โมเดลที่โฮสต์ในจีน", "workspace.lite.providers.useChina": "เปิดใช้โมเดลที่โฮสต์ในจีน",
+16 -12
View File
@@ -268,7 +268,8 @@ export const dict = {
"go.cta.text": "Go'ya abone ol", "go.cta.text": "Go'ya abone ol",
"go.cta.price": "Ayda 10$", "go.cta.price": "Ayda 10$",
"go.cta.promo": "İlk ay $5", "go.cta.promo": "İlk ay $5",
"go.pricing.body": "Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar.", "go.pricing.body":
"Herhangi bir ajanla kullanın. İlk ay $5, sonrasında ayda 10$. Gerekirse kredi yükleyin. İstediğiniz zaman iptal edin.",
"go.graph.free": "Ücretsiz", "go.graph.free": "Ücretsiz",
"go.graph.freePill": "Big Pickle ve ücretsiz modeller", "go.graph.freePill": "Big Pickle ve ücretsiz modeller",
"go.graph.go": "Go", "go.graph.go": "Go",
@@ -308,15 +309,16 @@ export const dict = {
"go.problem.item4": "go.problem.item4":
"Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash ve Hy3 içerir", "Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash ve Hy3 içerir",
"go.how.title": "Go nasıl çalışır?", "go.how.title": "Go nasıl çalışır?",
"go.how.body": "Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar.", "go.how.body":
"go.how.step1.title": "Go'ya abone olun", "Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar. OpenCode veya herhangi bir ajanla kullanabilirsiniz.",
"go.how.step1.beforeLink": "içinde", "go.how.step1.title": "Bir hesap oluşturun",
"go.how.step1.link": "OpenCode Console", "go.how.step1.beforeLink": "takip edin",
"go.how.step2.title": "OpenCodeu bağlayın", "go.how.step1.link": "kurulum talimatları",
"go.how.step2.link": "opencode2 console login", "go.how.step2.title": "Go'ya abone olun",
"go.how.step2.afterLink": "ve cihazı tarayıcınızda onaylayın", "go.how.step2.link": "İlk ay $5",
"go.how.step2.afterLink": "sonrasında cömert limitlerle ayda 10$",
"go.how.step3.title": "Kodlamaya başlayın", "go.how.step3.title": "Kodlamaya başlayın",
"go.how.step3.body": "opencode sağlayıcısıyla", "go.how.step3.body": "açık kaynaklı modellere güvenilir erişimle",
"go.privacy.title": "Gizliliğiniz bizim için önemlidir", "go.privacy.title": "Gizliliğiniz bizim için önemlidir",
"go.privacy.body": "go.privacy.body":
"Bu plan öncelikle uluslararası kullanıcılar için tasarlanmış olup, istikrarlı küresel erişim için modeller ABD, AB ve Singapur'da barındırılmaktadır.", "Bu plan öncelikle uluslararası kullanıcılar için tasarlanmış olup, istikrarlı küresel erişim için modeller ABD, AB ve Singapur'da barındırılmaktadır.",
@@ -349,8 +351,9 @@ export const dict = {
"go.faq.a6": "Daha fazla kullanıma ihtiyacınız varsa, hesabınıza kredi yükleyebilirsiniz.", "go.faq.a6": "Daha fazla kullanıma ihtiyacınız varsa, hesabınıza kredi yükleyebilirsiniz.",
"go.faq.q7": "İptal edebilir miyim?", "go.faq.q7": "İptal edebilir miyim?",
"go.faq.a7": "Evet, istediğiniz zaman iptal edebilirsiniz.", "go.faq.a7": "Evet, istediğiniz zaman iptal edebilirsiniz.",
"go.faq.q8": "Hangi erişim ertelendi?", "go.faq.q8": "Go'yu diğer kodlama ajanlarıyla kullanabilir miyim?",
"go.faq.a8": "Harici ajan ve hizmet hesabı desteği ertelendi.", "go.faq.a8":
"Evet, Go'yu herhangi bir ajanla kullanabilirsiniz. Tercih ettiğiniz kodlama ajanındaki kurulum talimatlarını izleyin.",
"go.faq.q9": "Ücretsiz modeller ve Go arasındaki fark nedir?", "go.faq.q9": "Ücretsiz modeller ve Go arasındaki fark nedir?",
"go.faq.a9": "go.faq.a9":
@@ -657,7 +660,8 @@ export const dict = {
"workspace.lite.subscription.monthlyUsage": "Aylık Kullanım", "workspace.lite.subscription.monthlyUsage": "Aylık Kullanım",
"workspace.lite.subscription.resetsIn": "Sıfırlama süresi", "workspace.lite.subscription.resetsIn": "Sıfırlama süresi",
"workspace.lite.subscription.useBalance": "Kullanım limitlerine ulaştıktan sonra mevcut bakiyenizi kullanın", "workspace.lite.subscription.useBalance": "Kullanım limitlerine ulaştıktan sonra mevcut bakiyenizi kullanın",
"workspace.lite.subscription.selectProvider": "Go modellerini kullanmak için opencode sağlayıcısını seçin.", "workspace.lite.subscription.selectProvider":
'Go modellerini kullanmak için opencode yapılandırmanızda "OpenCode Go"\'yu sağlayıcı olarak seçin.',
"workspace.lite.providers.title": "Sağlayıcılar", "workspace.lite.providers.title": "Sağlayıcılar",
"workspace.lite.providers.description": "Yönlendirme için hangi sağlayıcıların kullanılacağını kontrol edin.", "workspace.lite.providers.description": "Yönlendirme için hangi sağlayıcıların kullanılacağını kontrol edin.",
"workspace.lite.providers.useChina": "Çin'de barındırılan modelleri etkinleştir", "workspace.lite.providers.useChina": "Çin'de barındırılan modelleri etkinleştir",

Some files were not shown because too many files have changed in this diff Show More