Compare commits

..
Author SHA1 Message Date
Aiden Cline 8997d8662f fix(schema): keep legacy blobs on replay, import, and V1 migration
Freeze session.message.content.updated at the stored shape and translate
on replay, run persisted() on CLI session import, write native from the
V1 migration, regenerate the OpenAPI docs, and mirror text.ended state in
the client fold.
2026-09-22 00:37:59 -05:00
Aiden Cline e4dd41b033 test(core): use native for text and reasoning provider blobs 2026-09-21 23:39:41 -05:00
Aiden Cline 347bf1d749 Merge remote-tracking branch 'origin/v2' into message-native 2026-09-21 23:37:54 -05:00
Aiden Cline 07d48e1ffb feat(ai): add experimental evaluation API (#50506) 2026-09-21 23:30:38 -05:00
Aiden Cline e7c4bffd38 refactor(schema): drop native migration and fix fixtures 2026-09-21 23:27:12 -05:00
Aiden Cline 9fdcb8da41 fix(codemode): coerce built-in arguments as in JS instead of requiring numbers and strings (#50492) 2026-09-21 23:04:04 -05:00
Aiden Cline ba61ac6730 fix(ai): preserve Vertex function call ids (#50504) 2026-09-21 23:03:24 -05:00
Aiden Cline 94b9133910 fix(core): share child prompt cache affinity (#50495) 2026-09-21 22:44:43 -05:00
Aiden Cline 6f8c5ae0aa fix(codemode): bind generator parameters at the call, not the first next() (#50489) 2026-09-21 22:42:10 -05:00
Aiden Cline 60673aaef3 fix(core): run session HTTP hooks on the AI SDK route (#50487) 2026-09-21 22:41:06 -05:00
Aiden Cline 651529d64e fix(codemode): coerce any value to a property key (#50479) 2026-09-21 21:35:20 -05:00
opencode-agent[bot] 532f25d0d4 feat(tui): restore sidebar onboarding (#50475) 2026-09-21 22:17:22 -04:00
opencode-agent[bot]andjlongster 643c4c3500 fix: remove first-month Go pricing (#50473)
Co-authored-by: jlongster <17031+jlongster@users.noreply.github.com>
2026-09-21 21:55:12 -04:00
opencode-agent[bot]andBrendonovich 9d531435b4 fix(desktop): stop signing macOS DMGs (#50469)
Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>
2026-09-22 01:35:21 +00:00
opencode-agent[bot] 5b9dc35eec feat(tui): add automatic tabs mode (#50456) 2026-09-22 01:00:26 +00:00
Aiden Cline 1814dd9799 test: stabilize Windows CI without longer timeouts (#50454) 2026-09-21 18:30:30 -05:00
Aiden Cline 1e1cd042ea feat(codemode): name the closest tool in unknown-tool errors (#50455) 2026-09-21 18:14:31 -05:00
Aiden Cline 02566f6219 fix(codemode): live Map/Set forEach, generator prototypes, repeated function declarations, delete on non-references (#50450) 2026-09-21 18:09:45 -05:00
opencode-agent[bot]andrekram1-node f488aa3f79 fix(tui): persist MCP sidebar state (#50447)
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2026-09-21 17:28:23 -05:00
Aiden Cline 85962e49b7 refactor(schema): rename message provider blobs to native 2026-09-20 21:41:03 -05:00
199 changed files with 2420 additions and 944 deletions
+3 -2
View File
@@ -112,11 +112,12 @@ jobs:
- name: Run unit tests
timeout-minutes: 20
run: |
# The runners have four vCPUs, and each Bun test process performs its own concurrent work.
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
GITHUB_ACTIONS=false bun turbo test
GITHUB_ACTIONS=false bun turbo test --concurrency=3
exit 0
fi
GITHUB_ACTIONS=false bun turbo test --affected
GITHUB_ACTIONS=false bun turbo test --affected --concurrency=3
env:
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: ${{ runner.os == 'Windows' && 'true' || 'false' }}
TURBO_SCM_BASE: ${{ github.event_name == 'pull_request' && format('{0}^1', github.sha) || github.event.before }}
+41
View File
@@ -29,6 +29,47 @@ await Effect.runPromise(program.pipe(Effect.provide(llmLayer)))
Run `LLMClient.stream(request)` instead of `generate` when you want incremental `LLMEvent`s. The event stream is provider-neutral — same shape across OpenAI Chat, OpenAI Responses, Anthropic Messages, Gemini, Bedrock Converse, and any OpenAI-compatible deployment.
## Experimental evaluation
Evaluation models compare shared state with typed choice, score, and boolean questions. The API is
isolated under an experimental entrypoint and provider namespace while the contract evolves:
```ts
import { Effect } from "effect"
import { Evaluation, EvaluationClient } from "@opencode/ai/experimental"
import { TypeSafeAI } from "@opencode/ai/providers"
const model = TypeSafeAI.configure().experimental.evaluation("jev-latest")
const program = Evaluation.evaluate({
model,
state: "I was charged twice. Please refund the duplicate payment.",
questions: {
department: {
type: "choice",
instructions: "Which team should handle this?",
criteria: { billing: "Payments and refunds", technical: "Bugs and outages" },
},
urgency: {
type: "score",
instructions: "How urgent is this?",
criteria: ["Can wait", "Needs prompt attention", "Blocking revenue"],
},
refund: { type: "boolean", instructions: "Is the customer asking for a refund?" },
},
})
const response = await Effect.runPromise(program.pipe(Effect.provide(EvaluationClient.fetchLayer)))
console.log(response.answers.department.choice)
console.log(response.answers.refund.probability)
```
`TypeSafeAI` reads `TYPESAFE_API_KEY`. `OpenCodeZen` exposes the same selector and reads
`OPENCODE_API_KEY`. The common API uses `boolean`; System One routes lower it to native `noul`.
Choice and score confidence plus score legends remain available in provider metadata, and the
provider's rounded probabilities are returned unchanged.
## Alibaba Cloud Model Studio
`Alibaba` provides standard Model Studio inference. Configure a region explicitly, then select
+29
View File
@@ -0,0 +1,29 @@
export { EvaluationClient } from "./experimental/evaluation-client.js"
export {
BooleanAnswer,
BooleanQuestion,
ChoiceAnswer,
ChoiceQuestion,
Evaluation,
EvaluationAnswer,
EvaluationInput,
EvaluationModel,
EvaluationModelSchema,
EvaluationQuestion,
EvaluationRequest,
EvaluationResponse,
EvaluationRounding,
ScoreAnswer,
ScoreQuestion,
} from "./experimental/evaluation.js"
export type {
AnswerFor,
AnswersFor,
EvaluationModelOptions,
EvaluationOptions,
EvaluationQuestions,
EvaluationRequestFor,
EvaluationRequestInput,
EvaluationResponseFor,
EvaluationRoute,
} from "./experimental/evaluation.js"
@@ -0,0 +1,54 @@
import { Context, Effect, Layer } from "effect"
import { RequestExecutor } from "../route/executor.js"
import { mergeHttpOptions, type AIError } from "../schema/index.js"
import { sanitizeSurrogates } from "../utils/sanitize.js"
import {
type EvaluationOptions,
type EvaluationQuestions,
type EvaluationRequestFor,
type EvaluationResponseFor,
} from "./evaluation.js"
export type Execute = RequestExecutor.Interface["execute"]
export interface Interface {
readonly evaluate: <Options extends EvaluationOptions, const Questions extends EvaluationQuestions>(
request: EvaluationRequestFor<Options, Questions>,
) => Effect.Effect<EvaluationResponseFor<Questions>, AIError>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/AI/Experimental/EvaluationClient") {}
export const evaluate = <Options extends EvaluationOptions, const Questions extends EvaluationQuestions>(
request: EvaluationRequestFor<Options, Questions>,
): Effect.Effect<EvaluationResponseFor<Questions>, AIError, Service> =>
Effect.flatMap(Service, (client) => client.evaluate(request))
export const layer: Layer.Layer<Service, never, RequestExecutor.Service> = Layer.effect(
Service,
Effect.gen(function* () {
const executor = yield* RequestExecutor.Service
return Service.of({
evaluate: (request) =>
request.model.route.evaluate(
{
...sanitizeSurrogates({
...request,
model: undefined,
http: mergeHttpOptions(request.model.http, request.http),
}),
model: request.model,
},
executor.execute,
),
})
}),
)
export const fetchLayer = layer.pipe(Layer.provide(RequestExecutor.fetchLayer))
export const EvaluationClient = {
Service,
layer,
fetchLayer,
evaluate,
} as const
+245
View File
@@ -0,0 +1,245 @@
import { Effect, Schema } from "effect"
import {
AIError,
HttpOptions,
InvalidRequestError,
ModelID,
ProviderID,
ProviderMetadata,
Usage,
} from "../schema/index.js"
import { EvaluationClient, Service, type Execute } from "./evaluation-client.js"
export const EvaluationInput = Schema.Union([Schema.String, Schema.JsonObject, Schema.Array(Schema.Json)])
export type EvaluationInput = Schema.Schema.Type<typeof EvaluationInput>
const EvaluationCriterion = Schema.NullOr(EvaluationInput)
const ChoiceCriteria = Schema.Record(Schema.String, EvaluationCriterion).pipe(
Schema.refine((x): x is typeof x => Object.keys(x).length > 0, {
message: "Choice criteria must be a nonempty option map",
}),
)
export const ChoiceQuestion = Schema.Struct({
type: Schema.Literal("choice"),
instructions: EvaluationInput,
criteria: ChoiceCriteria,
})
export type ChoiceQuestion = Schema.Schema.Type<typeof ChoiceQuestion>
export const ScoreQuestion = Schema.Struct({
type: Schema.Literal("score"),
instructions: EvaluationInput,
criteria: Schema.Array(EvaluationCriterion).check(Schema.isMinLength(2)),
})
export type ScoreQuestion = Schema.Schema.Type<typeof ScoreQuestion>
export const BooleanQuestion = Schema.Struct({
type: Schema.Literal("boolean"),
instructions: EvaluationInput,
criteria: Schema.optional(
Schema.Struct({
true: Schema.optional(EvaluationCriterion),
false: Schema.optional(EvaluationCriterion),
}),
),
})
export type BooleanQuestion = Schema.Schema.Type<typeof BooleanQuestion>
export const EvaluationQuestion = Schema.Union([ChoiceQuestion, ScoreQuestion, BooleanQuestion]).pipe(
Schema.toTaggedUnion("type"),
)
export type EvaluationQuestion = Schema.Schema.Type<typeof EvaluationQuestion>
export type EvaluationQuestions = Readonly<Record<string, EvaluationQuestion>>
const EvaluationQuestions = Schema.Record(Schema.String, EvaluationQuestion).pipe(
Schema.refine((x): x is typeof x => Object.keys(x).length > 0, {
message: "Evaluation questions must be a nonempty map",
}),
)
const Probability = Schema.Number.check(Schema.isBetween({ minimum: 0, maximum: 1 }))
export const ChoiceAnswer = Schema.Struct({
type: Schema.Literal("choice"),
choice: Schema.String,
probabilities: Schema.optional(Schema.Record(Schema.String, Probability)),
})
export type ChoiceAnswer = Schema.Schema.Type<typeof ChoiceAnswer>
export const ScoreAnswer = Schema.Struct({
type: Schema.Literal("score"),
score: Schema.Number,
probabilities: Schema.optional(Schema.Record(Schema.String, Probability)),
})
export type ScoreAnswer = Schema.Schema.Type<typeof ScoreAnswer>
export const BooleanAnswer = Schema.Struct({
type: Schema.Literal("boolean"),
probability: Probability,
})
export type BooleanAnswer = Schema.Schema.Type<typeof BooleanAnswer>
export const EvaluationAnswer = Schema.Union([ChoiceAnswer, ScoreAnswer, BooleanAnswer]).pipe(
Schema.toTaggedUnion("type"),
)
export type EvaluationAnswer = Schema.Schema.Type<typeof EvaluationAnswer>
export type AnswerFor<Question extends EvaluationQuestion> = Question extends {
readonly type: "choice"
readonly criteria: infer Criteria
}
? {
readonly type: "choice"
readonly choice: Extract<keyof Criteria, string>
readonly probabilities?: Readonly<Record<Extract<keyof Criteria, string>, number>>
}
: Question extends { readonly type: "score" }
? ScoreAnswer
: BooleanAnswer
export type AnswersFor<Questions extends EvaluationQuestions> = {
readonly [ID in keyof Questions]: AnswerFor<Questions[ID]>
}
export type EvaluationOptions = Record<string, unknown>
export interface EvaluationRoute<Options extends EvaluationOptions = EvaluationOptions> {
readonly id: string
readonly evaluate: <const Questions extends EvaluationQuestions>(
request: EvaluationRequestFor<Options, Questions>,
execute: Execute,
) => Effect.Effect<EvaluationResponseFor<Questions>, AIError>
}
export class EvaluationModel<Options extends EvaluationOptions = EvaluationOptions> {
declare protected readonly _Options: (options: Options) => Options
readonly id: ModelID
readonly provider: ProviderID
readonly route: EvaluationRoute<Options>
readonly http?: HttpOptions
constructor(input: EvaluationModel.Input<Options>) {
this.id = input.id
this.provider = input.provider
this.route = input.route
this.http = input.http
}
static make<Options extends EvaluationOptions = EvaluationOptions>(input: EvaluationModel.MakeInput<Options>) {
return new EvaluationModel<Options>({
id: ModelID.make(input.id),
provider: ProviderID.make(input.provider),
route: input.route,
http: input.http,
})
}
}
export namespace EvaluationModel {
export interface Input<Options extends EvaluationOptions = EvaluationOptions> {
readonly id: ModelID
readonly provider: ProviderID
readonly route: EvaluationRoute<Options>
readonly http?: HttpOptions
}
export interface MakeInput<Options extends EvaluationOptions = EvaluationOptions>
extends Omit<Input<Options>, "id" | "provider"> {
readonly id: string | ModelID
readonly provider: string | ProviderID
}
}
export const EvaluationModelSchema = Schema.declare(
(value): value is EvaluationModel => value instanceof EvaluationModel,
{
expected: "Evaluation.Model",
},
)
export class EvaluationRequest extends Schema.Class<EvaluationRequest>("Evaluation.Request")({
model: EvaluationModelSchema,
state: EvaluationInput,
questions: EvaluationQuestions,
options: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
http: Schema.optional(HttpOptions),
}) {
declare protected readonly _EvaluationRequest: void
}
export type EvaluationModelOptions<Model> = Model extends EvaluationModel<infer Options> ? Options : never
export type EvaluationRequestFor<
Options extends EvaluationOptions = EvaluationOptions,
Questions extends EvaluationQuestions = EvaluationQuestions,
> = Omit<EvaluationRequest, "model" | "questions" | "options"> & {
readonly model: EvaluationModel<Options>
readonly questions: Questions
readonly options?: Options
}
export type EvaluationRequestInput<
Model extends object = EvaluationModel,
Questions extends EvaluationQuestions = EvaluationQuestions,
> = Omit<ConstructorParameters<typeof EvaluationRequest>[0], "model" | "questions" | "options" | "http"> & {
readonly model: Model
readonly questions: Questions
readonly options?: NoInfer<EvaluationModelOptions<Model>>
readonly http?: HttpOptions.Input
} & (Model extends EvaluationModel<EvaluationModelOptions<Model>> ? unknown : never)
export class EvaluationRounding extends Schema.Class<EvaluationRounding>("Evaluation.Rounding")({
probabilityDecimals: Schema.optional(Schema.Int),
scoreDecimals: Schema.optional(Schema.Int),
}) {}
export class EvaluationResponse extends Schema.Class<EvaluationResponse>("Evaluation.Response")({
model: ModelID,
answers: Schema.Record(Schema.String, EvaluationAnswer),
usage: Schema.optional(Usage),
rounding: Schema.optional(EvaluationRounding),
providerMetadata: Schema.optional(ProviderMetadata),
}) {}
export type EvaluationResponseFor<Questions extends EvaluationQuestions> = Omit<EvaluationResponse, "answers"> & {
readonly answers: AnswersFor<Questions>
}
export function request<const Model extends object, const Questions extends EvaluationQuestions>(
input: EvaluationRequestInput<Model, Questions>,
): EvaluationRequestFor<EvaluationModelOptions<Model>, Questions>
export function request(input: EvaluationRequest): EvaluationRequest
export function request(input: EvaluationRequest | EvaluationRequestInput) {
if (input instanceof EvaluationRequest) return input
return new EvaluationRequest({
...input,
model: input.model as unknown as EvaluationModel,
http: input.http === undefined ? undefined : HttpOptions.make(input.http),
})
}
export function evaluate<const Model extends object, const Questions extends EvaluationQuestions>(
input: EvaluationRequestInput<Model, Questions>,
): Effect.Effect<EvaluationResponseFor<Questions>, AIError, Service>
export function evaluate(input: EvaluationRequest): Effect.Effect<EvaluationResponse, AIError, Service>
export function evaluate(input: EvaluationRequest | EvaluationRequestInput) {
return Effect.try({
try: () => (input instanceof EvaluationRequest ? input : request(input)),
catch: (cause) =>
new AIError({
reason: new InvalidRequestError({
message: cause instanceof Error ? cause.message : String(cause),
cause,
}),
}),
}).pipe(
Effect.flatMap((request) =>
EvaluationClient.evaluate(request as EvaluationRequestFor<EvaluationOptions, EvaluationQuestions>),
),
)
}
export const Evaluation = {
request,
evaluate,
} as const
+222
View File
@@ -0,0 +1,222 @@
import { Effect, Schema } from "effect"
import { Headers, HttpClientRequest } from "effect/unstable/http"
import {
BooleanAnswer,
ChoiceAnswer,
ChoiceQuestion,
EvaluationInput,
EvaluationModel,
EvaluationResponse,
EvaluationRounding,
ScoreAnswer,
ScoreQuestion,
type EvaluationResponseFor,
type EvaluationRoute,
} from "./evaluation.js"
import { Auth, type Definition as AuthDefinition } from "../route/auth.js"
import {
AIError,
HttpContext,
HttpOptions,
InvalidProviderOutputError,
InvalidRequestError,
ModelID,
Usage,
mergeJsonRecords,
} from "../schema/index.js"
const Noul = Schema.Struct({
type: Schema.Literal("noul"),
instructions: EvaluationInput,
criteria: Schema.optional(
Schema.Struct({
true: Schema.optional(Schema.NullOr(EvaluationInput)),
false: Schema.optional(Schema.NullOr(EvaluationInput)),
}),
),
})
const Question = Schema.Union([
ChoiceQuestion.pipe(
Schema.refine((x): x is typeof x => Object.keys(x.criteria).length <= 255, {
message: "System One Choice questions support at most 255 options",
}),
),
ScoreQuestion.pipe(
Schema.refine((x): x is typeof x => x.criteria.length <= 10, {
message: "System One Score questions support at most 10 levels",
}),
),
Noul,
])
const Request = Schema.StructWithRest(
Schema.Struct({
model: Schema.String,
state: EvaluationInput,
questions: Schema.Record(Schema.String, Question),
}),
[Schema.Record(Schema.String, Schema.Any)],
)
const Probability = Schema.Number.check(Schema.isBetween({ minimum: 0, maximum: 1 }))
const NoulAnswer = Schema.Struct({ type: Schema.Literal("noul"), noul: Probability })
const Choice = Schema.Struct({
type: Schema.Literal("choice"),
choice: Schema.String,
probabilities: Schema.Record(Schema.String, Probability),
confidence: Schema.optional(Probability),
})
const Score = Schema.Struct({
type: Schema.Literal("score"),
score: Schema.Number,
probabilities: Schema.Record(Schema.String, Probability),
legend: Schema.optional(Schema.Record(Schema.String, Schema.Json)),
confidence: Schema.optional(Probability),
})
const NativeUsage = Schema.Struct({
input_tokens: Schema.optional(Schema.Number),
output_tokens: Schema.optional(Schema.Number),
})
const encode = Schema.encodeUnknownEffect(Schema.fromJsonString(Request))
const exact = (x: Readonly<Record<string, unknown>>, keys: ReadonlyArray<string>) =>
Object.keys(x).length === keys.length && keys.every((key) => Object.hasOwn(x, key))
export interface ModelInput {
readonly id: string | ModelID
readonly provider: string
readonly providerMetadataKey: string
readonly auth: AuthDefinition
readonly baseURL: string
readonly headers?: Record<string, string>
readonly http?: HttpOptions
}
export const model = (cfg: ModelInput) => {
const route: EvaluationRoute = {
id: "system-one",
evaluate: (req, send) =>
Effect.gen(function* () {
const url = new URL(`${cfg.baseURL.replace(/\/$/, "")}/systemone`)
Object.entries(req.http?.query ?? {}).forEach(([key, value]) => url.searchParams.set(key, value))
const body = yield* encode({
...mergeJsonRecords(req.options, req.http?.body),
model: req.model.id,
state: req.state,
questions: Object.fromEntries(
Object.entries(req.questions).map(([id, x]) => [id, x.type === "boolean" ? { ...x, type: "noul" } : x]),
),
}).pipe(
Effect.mapError(
(cause) => new AIError({ reason: new InvalidRequestError({ message: cause.message, cause }) }),
),
)
const headers = yield* Auth.toEffect(cfg.auth)({
request: req,
method: "POST",
url: url.toString(),
body,
headers: Headers.fromInput({ ...cfg.headers, ...req.http?.headers }),
})
const res = yield* send(
HttpClientRequest.post(url).pipe(
HttpClientRequest.setHeaders(headers),
HttpClientRequest.bodyText(body, "application/json"),
),
)
const http = new HttpContext({ url: res.request.url, status: res.status, headers: res.headers })
const fail = (message: string, cause: unknown, body?: string) =>
new AIError({ reason: new InvalidProviderOutputError({ route: route.id, message, body, http, cause }) })
const text = yield* res.text.pipe(
Effect.mapError((cause) => fail("Failed to read the System One response", cause)),
)
const entries = Object.entries(req.questions)
const output = Schema.Struct({
model: Schema.String,
answers: Schema.Struct(
Object.fromEntries(
entries.map(([id, question]) => {
if (question.type === "boolean") return [id, NoulAnswer]
if (question.type === "choice") {
const keys = Object.keys(question.criteria)
return [
id,
Choice.pipe(
Schema.refine(
(x): x is typeof x =>
Object.hasOwn(question.criteria, x.choice) && exact(x.probabilities, keys),
{ message: `Question "${id}" returned an invalid choice answer` },
),
),
]
}
const keys = question.criteria.map((_, index) => String(index))
return [
id,
Score.pipe(
Schema.refine(
(x): x is typeof x =>
x.score >= 0 && x.score <= question.criteria.length - 1 && exact(x.probabilities, keys),
{ message: `Question "${id}" returned an invalid score answer` },
),
),
]
}),
) as Record<string, Schema.Codec<unknown>>,
),
usage: Schema.optional(NativeUsage),
})
const data = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(output))(text).pipe(
Effect.mapError((cause) => fail("System One returned an invalid response", cause, text)),
)
const confidence: Record<string, number> = {}
const legend: Record<string, Record<string, Schema.Json>> = {}
const answers = Object.fromEntries(
entries.map(([id, question]) => {
const answer = data.answers[id]
if (question.type === "boolean")
return [
id,
{ type: "boolean", probability: (answer as typeof NoulAnswer.Type).noul } satisfies BooleanAnswer,
]
if (question.type === "choice") {
const value = answer as typeof Choice.Type
if (value.confidence !== undefined) confidence[id] = value.confidence
return [
id,
{ type: "choice", choice: value.choice, probabilities: value.probabilities } satisfies ChoiceAnswer,
]
}
const value = answer as typeof Score.Type
if (value.confidence !== undefined) confidence[id] = value.confidence
if (value.legend !== undefined) legend[id] = value.legend
return [id, { type: "score", score: value.score, probabilities: value.probabilities } satisfies ScoreAnswer]
}),
)
const meta = {
...(Object.keys(confidence).length === 0 ? {} : { confidence }),
...(Object.keys(legend).length === 0 ? {} : { legend }),
}
return new EvaluationResponse({
model: ModelID.make(data.model),
answers,
usage: data.usage
? new Usage({
inputTokens: data.usage.input_tokens,
outputTokens: data.usage.output_tokens,
totalTokens:
data.usage.input_tokens === undefined && data.usage.output_tokens === undefined
? undefined
: (data.usage.input_tokens ?? 0) + (data.usage.output_tokens ?? 0),
providerMetadata: { [cfg.providerMetadataKey]: data.usage },
})
: undefined,
rounding: new EvaluationRounding({ probabilityDecimals: 2, scoreDecimals: 2 }),
providerMetadata: Object.keys(meta).length === 0 ? undefined : { [cfg.providerMetadataKey]: meta },
}) as EvaluationResponseFor<typeof req.questions>
}),
}
return EvaluationModel.make({ id: cfg.id, provider: cfg.provider, route, http: cfg.http })
}
export const SystemOne = { model } as const
+1 -11
View File
@@ -38,23 +38,13 @@ export type Settings = ProviderPackage.Settings &
const fromRequest = Effect.fn("GoogleVertex.fromRequest")(function* (request: LLMRequest) {
const { serviceTier: _, ...body } = yield* Gemini.protocol.body.from(request)
// Vertex's native REST schema rejects `id` on FunctionCall/FunctionResponse parts with HTTP 400,
// unlike AI Studio, so history minted there cannot be lowered verbatim.
const contents = body.contents.map((content) => ({
...content,
parts: (content.parts ?? []).map((part) => {
if ("functionCall" in part) return { ...part, functionCall: { ...part.functionCall, id: undefined } }
if ("functionResponse" in part) return { ...part, functionResponse: { ...part.functionResponse, id: undefined } }
return part
}),
}))
const value = request.providerOptions?.labels
const labels = ProviderShared.isRecord(value)
? Object.fromEntries(
Object.entries(value).filter((entry): entry is [string, string] => typeof entry[1] === "string"),
)
: undefined
return { ...body, contents, labels }
return { ...body, labels }
})
const protocol = {
+2
View File
@@ -24,8 +24,10 @@ export * as Moonshot from "./moonshot.js"
export * as OpenAI from "./openai.js"
export * as OpenAICompatible from "./openai-compatible.js"
export * as OpenAICompatibleResponses from "./openai-compatible-responses.js"
export * as OpenCodeZen from "./opencode-zen.js"
export * as OpenRouter from "./openrouter.js"
export * as TogetherAI from "./togetherai.js"
export * as TypeSafeAI from "./typesafe-ai.js"
export * as XAI from "./xai.js"
export * as ZAI from "./zai.js"
export * as ZAICodingPlan from "./zai-coding-plan.js"
+31
View File
@@ -0,0 +1,31 @@
import { HttpOptions, ProviderID, type ModelID } from "../schema/index.js"
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options.js"
import { SystemOne } from "../experimental/system-one.js"
export const id = ProviderID.make("opencode")
const baseURL = "https://opencode.ai/zen/v1"
export type Options = ProviderAuthOption<"optional"> & {
readonly baseURL?: string
readonly headers?: Record<string, string>
readonly http?: HttpOptions.Input
}
export const configure = (input: Options = {}) => {
const evaluation = (modelID: string | ModelID) =>
SystemOne.model({
id: modelID,
provider: id,
providerMetadataKey: "opencode",
auth: AuthOptions.bearer(input, "OPENCODE_API_KEY"),
baseURL: input.baseURL ?? baseURL,
headers: input.headers,
http: input.http === undefined ? undefined : HttpOptions.make(input.http),
})
return { id, experimental: { evaluation }, configure }
}
export const provider = configure()
export const experimental = provider.experimental
export * as OpenCodeZen from "./opencode-zen.js"
+31
View File
@@ -0,0 +1,31 @@
import { HttpOptions, ProviderID, type ModelID } from "../schema/index.js"
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options.js"
import { SystemOne } from "../experimental/system-one.js"
export const id = ProviderID.make("typesafe-ai")
const baseURL = "https://api.typesafe.ai/v1"
export type Options = ProviderAuthOption<"optional"> & {
readonly baseURL?: string
readonly headers?: Record<string, string>
readonly http?: HttpOptions.Input
}
export const configure = (input: Options = {}) => {
const evaluation = (modelID: string | ModelID) =>
SystemOne.model({
id: modelID,
provider: id,
providerMetadataKey: "typesafe",
auth: AuthOptions.bearer(input, "TYPESAFE_API_KEY"),
baseURL: input.baseURL ?? baseURL,
headers: input.headers,
http: input.http === undefined ? undefined : HttpOptions.make(input.http),
})
return { id, experimental: { evaluation }, configure }
}
export const provider = configure()
export const experimental = provider.experimental
export * as TypeSafeAI from "./typesafe-ai.js"
+173
View File
@@ -0,0 +1,173 @@
import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import { HttpClientRequest } from "effect/unstable/http"
import { Evaluation, EvaluationClient } from "../src/experimental.js"
import { OpenCodeZen, TypeSafeAI } from "../src/providers.js"
import { it } from "./lib/effect.js"
import { dynamicResponse } from "./lib/http.js"
describe("experimental Evaluation", () => {
it.effect("evaluates typed questions through System One", () =>
Effect.gen(function* () {
const response = yield* Evaluation.evaluate({
model: TypeSafeAI.configure({
apiKey: "test",
baseURL: "https://typesafe.test/v1/",
headers: { "x-default": "yes" },
http: { body: { deployment: "test" }, query: { api: "v1" } },
}).experimental.evaluation("jev-latest"),
state: { ticket: "Please refund the duplicate charge." },
questions: {
department: {
type: "choice",
instructions: "Which team should handle this?",
criteria: { billing: "Payments and refunds", technical: "Bugs and outages" },
},
urgency: {
type: "score",
instructions: "How urgent is this?",
criteria: ["Can wait", "Needs attention", "Blocking"],
},
refund: { type: "boolean", instructions: "Is the customer asking for a refund?" },
},
options: { trace: { enabled: true } },
http: { body: { request_metadata: "value" }, headers: { "x-request": "yes" }, query: { trace: "1" } },
})
expect(response.model).toBe("jev-1.13.0")
expect(response.answers.department).toEqual({
type: "choice",
choice: "billing",
probabilities: { billing: 0.9, technical: 0.1 },
})
expect(response.answers.urgency).toEqual({
type: "score",
score: 1.2,
probabilities: { "0": 0, "1": 0.8, "2": 0.2 },
})
expect(response.answers.refund).toEqual({ type: "boolean", probability: 0.97 })
expect(response.usage?.totalTokens).toBe(36)
expect(response.providerMetadata).toEqual({
typesafe: {
confidence: { department: 0.8, urgency: 0.6 },
legend: { urgency: { "0": "Can wait", "1": "Needs attention", "2": "Blocking" } },
},
})
}).pipe(
Effect.provide(
EvaluationClient.layer.pipe(
Layer.provide(
dynamicResponse((input) =>
Effect.gen(function* () {
const request = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
expect(request.url).toBe("https://typesafe.test/v1/systemone?api=v1&trace=1")
expect(request.headers.get("authorization")).toBe("Bearer test")
expect(request.headers.get("x-default")).toBe("yes")
expect(request.headers.get("x-request")).toBe("yes")
expect(JSON.parse(input.text)).toEqual({
deployment: "test",
request_metadata: "value",
trace: { enabled: true },
model: "jev-latest",
state: { ticket: "Please refund the duplicate charge." },
questions: {
department: {
type: "choice",
instructions: "Which team should handle this?",
criteria: { billing: "Payments and refunds", technical: "Bugs and outages" },
},
urgency: {
type: "score",
instructions: "How urgent is this?",
criteria: ["Can wait", "Needs attention", "Blocking"],
},
refund: { type: "noul", instructions: "Is the customer asking for a refund?" },
},
})
return input.respond(
JSON.stringify({
model: "jev-1.13.0",
answers: {
department: {
type: "choice",
choice: "billing",
probabilities: { billing: 0.9, technical: 0.1 },
confidence: 0.8,
},
urgency: {
type: "score",
score: 1.2,
probabilities: { "0": 0, "1": 0.8, "2": 0.2 },
legend: { "0": "Can wait", "1": "Needs attention", "2": "Blocking" },
confidence: 0.6,
},
refund: { type: "noul", noul: 0.97 },
},
usage: { input_tokens: 30, output_tokens: 6 },
}),
{ headers: { "content-type": "application/json" } },
)
}),
),
),
),
),
),
)
it.effect("configures the OpenCode Zen System One endpoint", () =>
Evaluation.evaluate({
model: OpenCodeZen.configure({ apiKey: "zen-key", baseURL: "https://zen.test/v1" }).experimental.evaluation(
"jev-1.13",
),
state: "hello",
questions: { greeting: { type: "boolean", instructions: "Is this a greeting?" } },
}).pipe(
Effect.tap((response) =>
Effect.sync(() => {
expect(response.answers.greeting.probability).toBe(0.99)
expect(response.usage?.providerMetadata).toEqual({
opencode: { input_tokens: 10, output_tokens: 2 },
})
}),
),
Effect.provide(
EvaluationClient.layer.pipe(
Layer.provide(
dynamicResponse((input) => {
expect(input.request.url).toBe("https://zen.test/v1/systemone")
expect(input.request.headers.authorization).toBe("Bearer zen-key")
return Effect.succeed(
input.respond(
JSON.stringify({
model: "jev-1.13.0",
answers: { greeting: { type: "noul", noul: 0.99 } },
usage: { input_tokens: 10, output_tokens: 2 },
}),
{ headers: { "content-type": "application/json" } },
),
)
}),
),
),
),
),
)
it.effect("rejects malformed questions before network I/O", () =>
Effect.gen(function* () {
const error = yield* Evaluation.evaluate({
model: TypeSafeAI.experimental.evaluation("jev-latest"),
state: "hello",
questions: { score: { type: "score", instructions: "How much?", criteria: ["only"] } },
}).pipe(Effect.flip)
expect(error.reason._tag).toBe("InvalidRequest")
}).pipe(
Effect.provide(
EvaluationClient.layer.pipe(
Layer.provide(dynamicResponse(() => Effect.die("invalid evaluation reached the network"))),
),
),
),
)
})
+60
View File
@@ -0,0 +1,60 @@
import { Effect } from "effect"
import { Evaluation, EvaluationClient, EvaluationModel, type EvaluationRoute } from "../src/experimental.js"
import type { Service } from "../src/experimental/evaluation-client.js"
import { OpenCodeZen, TypeSafeAI } from "../src/providers.js"
type Requirements<T> = T extends Effect.Effect<infer _A, infer _E, infer R> ? R : never
type Success<T> = T extends Effect.Effect<infer A, infer _E, infer _R> ? A : never
type Equal<A, B> = [A, B] extends [B, A] ? true : false
type Assert<T extends true> = T
const model = TypeSafeAI.configure({ apiKey: "test" }).experimental.evaluation("jev-latest")
const request = Evaluation.request({
model,
state: { ticket: "refund" },
questions: {
topic: {
type: "choice",
instructions: "Which team?",
criteria: { billing: null, support: { includes: ["help"] } },
},
severity: { type: "score", instructions: "How severe?", criteria: ["Low", "High"] },
refund: { type: "boolean", instructions: "Refund?" },
},
})
const result = EvaluationClient.evaluate(request)
type Result = Success<typeof result>
type Choice = Assert<Equal<Result["answers"]["topic"]["choice"], "billing" | "support">>
type ClientRequirements = Assert<Equal<Requirements<typeof result>, Service>>
void (true satisfies Choice)
void (true satisfies ClientRequirements)
Effect.gen(function* () {
const response = yield* Evaluation.evaluate({
model: OpenCodeZen.experimental.evaluation("jev-1.13"),
state: ["hello"],
questions: { greeting: { type: "boolean", instructions: "Greeting?" } },
})
response.answers.greeting.probability satisfies number
// @ts-expect-error Boolean answers do not contain a selected choice.
response.answers.greeting.choice
// @ts-expect-error Unknown question IDs are not exposed.
response.answers.missing
})
declare const route: EvaluationRoute<{ readonly temperature?: number }>
const custom = EvaluationModel.make({ id: "custom", provider: "custom", route })
Evaluation.evaluate({
model: custom,
state: "hello",
questions: { ok: { type: "boolean", instructions: "OK?" } },
options: { temperature: 0.5 },
})
// @ts-expect-error Selected evaluation models retain their request option types.
Evaluation.evaluate({
model: custom,
state: "hello",
questions: { ok: { type: "boolean", instructions: "OK?" } },
options: { temperature: "high" },
})
+8
View File
@@ -8,9 +8,11 @@ import {
CloudflareWorkersAI,
DeepSeek,
Fireworks,
OpenCodeZen,
OpenAI,
OpenAICompatible,
OpenRouter,
TypeSafeAI,
XAI,
} from "@opencode/ai/providers"
import {
@@ -23,6 +25,7 @@ import {
} from "@opencode/ai/protocols"
import * as AnthropicMessages from "@opencode/ai/protocols/anthropic-messages"
import { TestLLM } from "@opencode/ai/testing"
import { Evaluation, EvaluationClient } from "@opencode/ai/experimental"
describe("public exports", () => {
test("root exposes app-facing runtime APIs", () => {
@@ -37,6 +40,9 @@ describe("public exports", () => {
expect(TestLLM.layer).toBeFunction()
expect(TestLLM.testLayer).toBeFunction()
expect(TestLLM.Test.of).toBeFunction()
expect(Evaluation.evaluate).toBeFunction()
expect(EvaluationClient.layer).toBeDefined()
expect(EvaluationClient.fetchLayer).toBeDefined()
})
test("route barrel exposes route-authoring APIs", () => {
@@ -66,6 +72,8 @@ describe("public exports", () => {
expect(CloudflareWorkersAI.configure).toBeFunction()
expect(CloudflareWorkersAI.configure({ accountId: "fixture", apiKey: "fixture" }).model).toBeFunction()
expect(OpenRouter.model).toBeFunction()
expect(TypeSafeAI.experimental.evaluation).toBeFunction()
expect(OpenCodeZen.experimental.evaluation).toBeFunction()
expect(XAI.model).toBeFunction()
expect(XAI.provider.responses).toBe(XAI.responses)
expect(XAI.provider.chat).toBe(XAI.chat)
@@ -1,9 +1,13 @@
{
"version": 1,
"metadata": {
"tags": ["prefix:google-vertex", "provider:google-vertex", "protocol:gemini"],
"tags": [
"prefix:google-vertex",
"provider:google-vertex",
"protocol:gemini"
],
"name": "google-vertex/calls-a-tool",
"recordedAt": "2026-08-23T17:21:51.036Z"
"recordedAt": "2026-09-22T03:46:29.725Z"
},
"interactions": [
{
@@ -21,7 +25,7 @@
"headers": {
"content-type": "text/event-stream"
},
"body": "data: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"functionCall\": {\"name\": \"lookup_weather\",\"args\": {\"city\": \"Paris\"},\"id\": \"call_425130\"},\"thoughtSignature\": \"AY89a1+1fXnLgYhHMuN3Ak6LBhT6PcrYOW7iPav4LfsacvG/Z6l1yJ+AsU7vWhFj/JyPIbsJJQ+GjohM9sCIZ6nqUOIg3reo/7osmrCvFrVHedTHQcwiPzoz2Kp3gb+uWjFAXxk1EX4IRAKcu0ox1W/Z9PpuZvHkTerGO2a82e02N6MAF1YhhtbXFvSdqLRih2Os68rdOk5/Bcld7ol8qUgeyIZ3CtI3OJ5jwRcD8LjvK33A7ZFzH5Bxp/peUmXvqnu5iNhnGBxZaJy/vupCtxRZxjaS+ojG0/UhyrnRiKIpbzQ0FBkxePPn8GCX/LOe2y3GUc98co8lN8OOuCd9ZmEdx5AjHmQkPO9fAV9SxG6Bda6SDWVL8o/Uz3WSQYoUEfAdoajEWIBvcisoeCJjb7zgmRRZ9VQSPl3RXj5LFRvX8jn0YKV1CahYbc24jA==\"}]}}],\"usageMetadata\": {\"trafficType\": \"ON_DEMAND\"},\"modelVersion\": \"gemini-3.5-flash\",\"createTime\": \"2026-08-23T17:21:50.308576Z\",\"responseId\": \"LiyLauDqErCErb8Pj8aWkAs\"}\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \"\"}]},\"finishReason\": \"STOP\"}],\"usageMetadata\": {\"promptTokenCount\": 39,\"candidatesTokenCount\": 16,\"totalTokenCount\": 102,\"trafficType\": \"ON_DEMAND\",\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 39}],\"candidatesTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 16}],\"thoughtsTokenCount\": 47},\"modelVersion\": \"gemini-3.5-flash\",\"createTime\": \"2026-08-23T17:21:50.308576Z\",\"responseId\": \"LiyLauDqErCErb8Pj8aWkAs\"}\r\n\r\n"
"body": "data: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"functionCall\": {\"name\": \"lookup_weather\",\"args\": {\"city\": \"Paris\"},\"id\": \"call_28936\"},\"thoughtSignature\": \"AY89a1/MVeBxtwu9l/96clFobOrrd7Q5MTf5o8/A22fc++1EsFYVeCFx9WCGmJ+D3yW3FWlRXKtJECYfprSoxixCSOYBNiV9g8/IkejbAxF26k1vxTsHkoqts0O2s8GqqBSRAavJfKQ/taTRbPuIq+b+RKZ8SDJFoWDnFjXpdS5S164uJTjmgs4ALd8oB1oQRycMsTbAY51TmkcNUnhARpjKQfqiizz4KfSRFjx7xDnza0v4jj73m2GV86eLDhVT7trIT+Dl5DsJt6RBLOE2Amse54mcpIbsYaANlY1YDU6FKuUBqnuwpl0OfhMXwWuYMogv1tH/0rTXsqNwmHyt/b9ZZXqYCdFXpMGt8udmoZ0d\"}]}}],\"usageMetadata\": {\"trafficType\": \"ON_DEMAND\"},\"modelVersion\": \"gemini-3.5-flash\",\"createTime\": \"2026-09-22T03:46:28.953809Z\",\"responseId\": \"FPqxatGbOsSorb8PtY2z8Q4\"}\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \"\"}]},\"finishReason\": \"STOP\"}],\"usageMetadata\": {\"promptTokenCount\": 39,\"candidatesTokenCount\": 16,\"totalTokenCount\": 93,\"trafficType\": \"ON_DEMAND\",\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 39}],\"candidatesTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 16}],\"thoughtsTokenCount\": 38},\"modelVersion\": \"gemini-3.5-flash\",\"createTime\": \"2026-09-22T03:46:28.953809Z\",\"responseId\": \"FPqxatGbOsSorb8PtY2z8Q4\"}\r\n\r\n"
}
}
]
@@ -1,9 +1,13 @@
{
"version": 1,
"metadata": {
"tags": ["prefix:google-vertex", "provider:google-vertex", "protocol:gemini"],
"tags": [
"prefix:google-vertex",
"provider:google-vertex",
"protocol:gemini"
],
"name": "google-vertex/continues-after-a-tool-result",
"recordedAt": "2026-08-23T17:21:51.853Z"
"recordedAt": "2026-09-22T03:46:30.691Z"
},
"interactions": [
{
@@ -14,14 +18,14 @@
"headers": {
"content-type": "application/json"
},
"body": "{\"contents\":[{\"role\":\"user\",\"parts\":[{\"text\":\"What is the weather in Paris?\"}]},{\"role\":\"model\",\"parts\":[{\"functionCall\":{\"name\":\"lookup_weather\",\"args\":{\"city\":\"Paris\"}},\"thoughtSignature\":\"skip_thought_signature_validator\"}]},{\"role\":\"user\",\"parts\":[{\"functionResponse\":{\"name\":\"lookup_weather\",\"response\":{\"name\":\"lookup_weather\",\"content\":\"18C, light rain\"}}}]}],\"tools\":[{\"functionDeclarations\":[{\"name\":\"lookup_weather\",\"description\":\"Look up the current weather for a city\",\"parameters\":{\"required\":[\"city\"],\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}}}}]}]}"
"body": "{\"contents\":[{\"role\":\"user\",\"parts\":[{\"text\":\"What is the weather in Paris?\"}]},{\"role\":\"model\",\"parts\":[{\"functionCall\":{\"id\":\"call_paris_1\",\"name\":\"lookup_weather\",\"args\":{\"city\":\"Paris\"}},\"thoughtSignature\":\"skip_thought_signature_validator\"}]},{\"role\":\"user\",\"parts\":[{\"functionResponse\":{\"id\":\"call_paris_1\",\"name\":\"lookup_weather\",\"response\":{\"name\":\"lookup_weather\",\"content\":\"18C, light rain\"}}}]}],\"tools\":[{\"functionDeclarations\":[{\"name\":\"lookup_weather\",\"description\":\"Look up the current weather for a city\",\"parameters\":{\"required\":[\"city\"],\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}}}}]}]}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream"
},
"body": "data: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \"The weather in Paris is currently 18°C with light rain.\"}]}}],\"usageMetadata\": {\"trafficType\": \"ON_DEMAND\"},\"modelVersion\": \"gemini-3.5-flash\",\"createTime\": \"2026-08-23T17:21:51.220919Z\",\"responseId\": \"LyyLave9DbWnrb8P1IjLmQQ\"}\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \"\",\"thoughtSignature\": \"AY89a197c+fpHJftPtcufnqMAyoRQKVEQK+KeG+RVHVx2wKil3L4jP4YWvfVbcuOFr2jio4Kre/hCrDANAoMFSvaZrdaPeo1b5bXQSmJKMH03yM5M6q6ME6JiBvXym143U4exIde4UbOh2tMeyXMvB3aWxcavIHd78g5G5QPLreo6A3LO5871cYYVeRwteY+/zbEdqfaAq1hlk6WYpWkNljYpjMyKwr15YC8rFLh3HYayS9tTN++GGrk/reZn6C3OEPlzPou/pXRATzcEAGVl/TW\"}]},\"finishReason\": \"STOP\"}],\"usageMetadata\": {\"promptTokenCount\": 59,\"candidatesTokenCount\": 15,\"totalTokenCount\": 98,\"trafficType\": \"ON_DEMAND\",\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 59}],\"candidatesTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 15}],\"thoughtsTokenCount\": 24},\"modelVersion\": \"gemini-3.5-flash\",\"createTime\": \"2026-08-23T17:21:51.220919Z\",\"responseId\": \"LyyLave9DbWnrb8P1IjLmQQ\"}\r\n\r\n"
"body": "data: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \"The weather\"}]}}],\"usageMetadata\": {\"trafficType\": \"ON_DEMAND\"},\"modelVersion\": \"gemini-3.5-flash\",\"createTime\": \"2026-09-22T03:46:29.830958Z\",\"responseId\": \"Ffqxau7bMoCOrb8P_Iu5sA0\"}\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \" in Paris is currently 18°C with light rain.\"}]}}],\"usageMetadata\": {\"trafficType\": \"ON_DEMAND\"},\"modelVersion\": \"gemini-3.5-flash\",\"createTime\": \"2026-09-22T03:46:29.830958Z\",\"responseId\": \"Ffqxau7bMoCOrb8P_Iu5sA0\"}\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \"\",\"thoughtSignature\": \"AY89a18aVI8Uxr2VaOI822u0DyuQdy4B00uhLnjqYb5Qb6Mkscccm018knLtYThB5UX8dRv1VFsORSQ0Qo6Gx9RCng2AK9EPce7p\"}]},\"finishReason\": \"STOP\"}],\"usageMetadata\": {\"promptTokenCount\": 59,\"candidatesTokenCount\": 15,\"totalTokenCount\": 74,\"trafficType\": \"ON_DEMAND\",\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 59}],\"candidatesTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 15}]},\"modelVersion\": \"gemini-3.5-flash\",\"createTime\": \"2026-09-22T03:46:29.830958Z\",\"responseId\": \"Ffqxau7bMoCOrb8P_Iu5sA0\"}\r\n\r\n"
}
}
]
@@ -1,9 +1,13 @@
{
"version": 1,
"metadata": {
"tags": ["prefix:google-vertex", "provider:google-vertex", "protocol:gemini"],
"tags": [
"prefix:google-vertex",
"provider:google-vertex",
"protocol:gemini"
],
"name": "google-vertex/streams-text",
"recordedAt": "2026-08-23T17:21:50.112Z"
"recordedAt": "2026-09-22T03:46:28.840Z"
},
"interactions": [
{
@@ -21,7 +25,7 @@
"headers": {
"content-type": "text/event-stream"
},
"body": "data: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \"Hello\"}]}}],\"usageMetadata\": {\"trafficType\": \"ON_DEMAND\"},\"modelVersion\": \"gemini-3.5-flash\",\"createTime\": \"2026-08-23T17:21:48.528714Z\",\"responseId\": \"LCyLasqiIO6crb8P1sDboQc\"}\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \"\",\"thoughtSignature\": \"AY89a1+BGsRqlGpfT0psLB4jeTkT5rDV2HFOlrRuF7aVxDOjqNVUku6t4azeSnxpd+msHWuwXj4RS+7gmVlzVs+JNi8uj+iZWTBCi71vSh9kdK9ed/sHv9J7uL9ZWSOcgbhX/hxdXaUp5yVbQzHFXPjR9A/IkEkHV8VKarDZVFE1T1uASia74lkmyBeZZz+DQmRsLwbUHzFUKlF3qnk/SliLo21ZgASd7itlALQ0PBLJZwgeI3g7tDscDSE18hnB11Fky8q7MLd3HY16zbDvHBEMb18pmmPelPI01KdrCIwMSou/01/u5jiSUCc3pFksZawUj3tAHocHSC3ZKAQQQuUXGe5tm61C2E40/NANBeePc1S4HYE6Yo/vtX6tE02LDky5IQWX09H6+DZ7fpopP5nCUfcKPHa3hVjYquWYYMtZgXO4ZpxfVd3lt1VUDuJNN3BMMCZapjBoJZFPXPJ5t/yg9Rnd791+msGH77b4wztz1vtsPrT9oV9g6SDo9ZUH6BaOcbK7fw8FaXcGw+55malEwQy6zpRLGecooBu70p6RwhaAUyKIMX49y+F2hkNxQxDeBUNckJnu6n4w+KLyjP+bR0gqPJbGjVfteHm+QujqjJdBBT/m1u9kPo1nIbzdEs/PIADBdbuV7TkD/HoRFKpLnNmM2no8ioTtFEjKBDz4ippGi15r8pGgA6wIb/1HAvOGh+PVERdGcbelVTgfONwBqjQ7B1wmEizCfyYuMIskfwjxDGayfKlpDxrnNeogtEct9u5/DjEKlURlg9MtmW1B9P8BXYJ+7SCiRJWwW6bzB+5C+MLCnETl/mljDizoJMHK8DKIhI4oxBsrWXEuoHFwEwGIeOZq0BofH2Jz/l6+KIboV/zd581Kk0zPg/rlI6acfjUEtXtbF+t0+jzoJN7006x4i2tqXeJZ+4e5yisSArEsfJ0YzNWoJtBHG9V9/euDcEP3+jsr98efaQaQbLMPvT/Hb7CYQ7ChhGfcGxQ=\"}]},\"finishReason\": \"STOP\"}],\"usageMetadata\": {\"promptTokenCount\": 7,\"candidatesTokenCount\": 1,\"totalTokenCount\": 150,\"trafficType\": \"ON_DEMAND\",\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 7}],\"candidatesTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 1}],\"thoughtsTokenCount\": 142},\"modelVersion\": \"gemini-3.5-flash\",\"createTime\": \"2026-08-23T17:21:48.528714Z\",\"responseId\": \"LCyLasqiIO6crb8P1sDboQc\"}\r\n\r\n"
"body": "data: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \"Hello\"}]}}],\"usageMetadata\": {\"trafficType\": \"ON_DEMAND\"},\"modelVersion\": \"gemini-3.5-flash\",\"createTime\": \"2026-09-22T03:46:26.748531Z\",\"responseId\": \"EvqxavPXLbqerb8P1eSViQs\"}\r\n\r\ndata: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \"\",\"thoughtSignature\": \"AY89a18msz0aRAZc4U/Zl7rEXjdTJsE0EWee1+hD2dT7h711q6uIRem7KWCLlji7JVSu2hDV1L8uFh8SGAO5jU4MBSZ+mq661EVXf9DZBF1CV+fetSpCUggurbYHWGGGvLRxjI96W0wR93EMoV/1pLHa4fgGVcFupoFsz/3n9J3wCkT4pRvHjn+hC7V11aSMI07t/OUeo1ssAziob9apuB+HUm2/EO17ph9TZvg3rydE5J0nmJ+DnYGLl5FfQJHgnnsjR5qaKs5wVNIKNA811lv/K5Iee/O6gMNXI3GAcGUvPKHMD5oQSs5B343blFUiqw47H1WZ9KWBoLXhVcnss6hLPmpDchInVhr3ObAHQhZIaBDBnhQR6nuc5BtgoFnwWw3w5G1yNNFt3qT3c75Rd/8NamdtveyskDbrHxQ5GkUo7ICuE/3SQG0yqWhVDjakYgVqjLG3BTKRg6I/dT5+VCeQbdxTZ+ScWkJfXYDEnlZlTjMLz2FS+B+9Zwez/1Dy/pm9H51jvuXI942kv31DfDVcTTwvKmb3SqIB9D4aomb16gDAywC7I4L/+ZwsqmcQoUvVtHGEe5NL3r6fgWtWzKht+60ZgBalmzdBfhZHd/bDXvTAX7SVpHraXjfpcdGrtv1LhU6Kr3gol94TR5Xh9dWKNhwR0s6at7MNCpj3eG9ogrWkmYSiqNeVT1ovscWvH70tDCtCNwY5sIUJVUSWdHc2OdzFxNIPkzh9OuHIn0o9Dbmup6obFoQOgi587HA/Sqvre9pDVqVMzn7w57naDmg+lsR9wS67BxbV4tVrvvfeVQAQQRft5SmSu7WCBnaWuM2/o890nE5ynFG+M2FqApJLgdfXozOguo/hROrD3WxLUyT8+FDsoCY8ky6YYTaF3OlOHpuD5Zmw4UGSdDbcEzhc6cRqBAYfMpxzN58VwNRrjG/MGTY3jrvOLJDk3sF7URa7Tn5YuRLp/2jL5YKWF2QftWiW9jl1YeORYQwNdLBuRK9L4/WOxNRWhYZToAJfwT4byh5dKzY8OnjeFcJy8RVvuRpoYeQY1P63ie/K1D3TgJEwKQXylLdPwtZRGM/P5sm2dcf5F+lRfZGufmv+Q++unrqWr6uv3IlgVGKPa8zTh+ipSaik7taKd+jertOSYkgjrrUfjC0/oyg5giuNqXh+ebIxQC7TMX8bManc5N3aomFuYXmIJ4iT/euQctq8N3gPPWmUNmrnXztdEZgpQdV2ahj+yRr1yxCriOfRo3Az3oPMPEaXxrqDc8RoWt/lOgzzp7KfBGoOVzYdvaDGr8MhsZ8Pd7HD0vZW8TNSl8R7cEK3G5EotNA+s6lF/n/RH5ewmg3cQK8bnnZ4oNjqYo3pGtw+H2rbefMMzAk4mybGe9uDqSdhWJgBzRGsMXRW+0atn9tuJTu6vPNn9asNvwXS0lcvpQskbe5sK3yhzyFzqwrJxs7Ji6yiu6kOtsQ5Girzk1835A2Zun6ZegGtQH8DbgIPdSBMkh6kypndBu4ns2gg97g9yB8XFGbv3c60sxaXPRuycl9IjBL81Yy6n6PCOBx7Pqm9xrzPn7QzgtetKxCmNUMYJvF+myiQFrMdL1w+1DD7FoZX9VNBDbNWgcfhgnL8tlHSmTb36KnScJOe8D+3n3+aDfkWpiHjrLrGJsHlaek/Ji2Cgwa0dIC6EhQitNttskIKVMa+yD6D1T6Dvz/eolnutjYwbGb3y9mtpQzmVwf2Zp2dzxYRWZk=\"}]},\"finishReason\": \"STOP\"}],\"usageMetadata\": {\"promptTokenCount\": 7,\"candidatesTokenCount\": 1,\"totalTokenCount\": 285,\"trafficType\": \"ON_DEMAND\",\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 7}],\"candidatesTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 1}],\"thoughtsTokenCount\": 277},\"modelVersion\": \"gemini-3.5-flash\",\"createTime\": \"2026-09-22T03:46:26.748531Z\",\"responseId\": \"EvqxavPXLbqerb8P1eSViQs\"}\r\n\r\n"
}
}
]
@@ -0,0 +1,33 @@
{
"version": 1,
"metadata": {
"model": "jev-1.13-free",
"tags": [
"prefix:opencode-zen-evaluation",
"provider:opencode",
"protocol:system-one"
],
"name": "opencode-zen-evaluation/evaluates-choice-score-and-boolean-questions",
"recordedAt": "2026-09-22T03:44:57.511Z"
},
"interactions": [
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://opencode.ai/zen/v1/systemone",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"jev-1.13-free\",\"state\":\"I was charged twice for the same invoice. Please refund the duplicate payment today.\",\"questions\":{\"department\":{\"type\":\"choice\",\"instructions\":\"Which team should handle this support request?\",\"criteria\":{\"billing\":\"Payments, invoices, refunds, or failed charges\",\"technical\":\"Bugs, outages, or integrations\",\"sales\":\"Pricing, upgrades, or new accounts\"}},\"urgency\":{\"type\":\"score\",\"instructions\":\"How urgent is this support request?\",\"criteria\":[\"Can wait for normal support\",\"Needs prompt attention\",\"Actively blocking revenue\"]},\"refund\":{\"type\":\"noul\",\"instructions\":\"Is the customer asking for a refund?\"}}}"
},
"response": {
"status": 200,
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"jev-1.13-free\",\"answers\":{\"department\":{\"type\":\"choice\",\"choice\":\"billing\",\"confidence\":1,\"probabilities\":{\"technical\":0,\"sales\":0,\"billing\":1}},\"urgency\":{\"type\":\"score\",\"score\":1.02,\"confidence\":0.95,\"legend\":{\"0\":\"Can wait for normal support\",\"1\":\"Needs prompt attention\",\"2\":\"Actively blocking revenue\"},\"probabilities\":{\"0\":0,\"1\":0.97,\"2\":0.03}},\"refund\":{\"type\":\"noul\",\"noul\":0.99}},\"usage\":{\"input_tokens\":422,\"output_tokens\":69}}"
}
}
]
}
@@ -0,0 +1,33 @@
{
"version": 1,
"metadata": {
"model": "jev-latest",
"tags": [
"prefix:typesafe-evaluation",
"provider:typesafe-ai",
"protocol:system-one"
],
"name": "typesafe-evaluation/evaluates-choice-score-and-boolean-questions",
"recordedAt": "2026-09-22T03:44:28.626Z"
},
"interactions": [
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://api.typesafe.ai/v1/systemone",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"jev-latest\",\"state\":\"I was charged twice for the same invoice. Please refund the duplicate payment today.\",\"questions\":{\"department\":{\"type\":\"choice\",\"instructions\":\"Which team should handle this support request?\",\"criteria\":{\"billing\":\"Payments, invoices, refunds, or failed charges\",\"technical\":\"Bugs, outages, or integrations\",\"sales\":\"Pricing, upgrades, or new accounts\"}},\"urgency\":{\"type\":\"score\",\"instructions\":\"How urgent is this support request?\",\"criteria\":[\"Can wait for normal support\",\"Needs prompt attention\",\"Actively blocking revenue\"]},\"refund\":{\"type\":\"noul\",\"instructions\":\"Is the customer asking for a refund?\"}}}"
},
"response": {
"status": 200,
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"jev-1.13.0\",\"answers\":{\"department\":{\"type\":\"choice\",\"choice\":\"billing\",\"confidence\":1.0,\"probabilities\":{\"billing\":1.0,\"sales\":0.0,\"technical\":0.0}},\"urgency\":{\"type\":\"score\",\"score\":1.01,\"confidence\":0.97,\"legend\":{\"0\":\"Can wait for normal support\",\"1\":\"Needs prompt attention\",\"2\":\"Actively blocking revenue\"},\"probabilities\":{\"0\":0.0,\"1\":0.98,\"2\":0.02}},\"refund\":{\"type\":\"noul\",\"noul\":0.99}},\"usage\":{\"input_tokens\":422,\"output_tokens\":69}}"
}
}
]
}
@@ -0,0 +1,79 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { Evaluation } from "../../src/experimental.js"
import { OpenCodeZen, TypeSafeAI } from "../../src/providers.js"
import { recordedTests } from "../recorded-test.js"
const questions = {
department: {
type: "choice",
instructions: "Which team should handle this support request?",
criteria: {
billing: "Payments, invoices, refunds, or failed charges",
technical: "Bugs, outages, or integrations",
sales: "Pricing, upgrades, or new accounts",
},
},
urgency: {
type: "score",
instructions: "How urgent is this support request?",
criteria: ["Can wait for normal support", "Needs prompt attention", "Actively blocking revenue"],
},
refund: { type: "boolean", instructions: "Is the customer asking for a refund?" },
} as const
const state = "I was charged twice for the same invoice. Please refund the duplicate payment today."
const typesafe = recordedTests({
prefix: "typesafe-evaluation",
provider: "typesafe-ai",
protocol: "system-one",
requires: ["TYPESAFE_API_KEY"],
metadata: { model: "jev-latest" },
})
const zen = recordedTests({
prefix: "opencode-zen-evaluation",
provider: "opencode",
protocol: "system-one",
requires: ["OPENCODE_API_KEY"],
metadata: { model: "jev-1.13-free" },
})
describe("experimental Evaluation recorded", () => {
typesafe.effect("evaluates choice score and boolean questions", () =>
assertEvaluation(
TypeSafeAI.configure({ apiKey: process.env.TYPESAFE_API_KEY ?? "fixture" }).experimental.evaluation("jev-latest"),
"typesafe",
),
)
zen.effect("evaluates choice score and boolean questions", () =>
assertEvaluation(
OpenCodeZen.configure({ apiKey: process.env.OPENCODE_API_KEY ?? "fixture" }).experimental.evaluation(
"jev-1.13-free",
),
"opencode",
),
)
})
const assertEvaluation = (
model: ReturnType<typeof TypeSafeAI.experimental.evaluation>,
metadataKey: "typesafe" | "opencode",
) =>
Effect.gen(function* () {
const response = yield* Evaluation.evaluate({ model, state, questions })
expect(response.model).toStartWith("jev-")
expect(response.answers.department.type).toBe("choice")
expect(response.answers.department.choice).toBe("billing")
expect(response.answers.department.probabilities?.billing).toBeGreaterThan(0.5)
expect(response.answers.urgency.type).toBe("score")
expect(response.answers.urgency.score).toBeGreaterThanOrEqual(0)
expect(response.answers.urgency.score).toBeLessThanOrEqual(2)
expect(response.answers.refund.type).toBe("boolean")
expect(response.answers.refund.probability).toBeGreaterThan(0.5)
expect(response.usage?.inputTokens).toBeGreaterThan(0)
expect(response.usage?.outputTokens).toBeGreaterThan(0)
expect(response.providerMetadata?.[metadataKey]?.confidence).toBeDefined()
})
@@ -114,7 +114,7 @@ describe("Google Vertex providers", () => {
}),
)
it.effect("strips function call ids Vertex does not accept from lowered bodies", () =>
it.effect("preserves function call ids in lowered Vertex bodies", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
@@ -142,15 +142,14 @@ describe("Google Vertex providers", () => {
}),
)
expect(JSON.stringify(prepared.body.contents)).not.toContain('"id"')
expect(prepared.body.contents).toMatchObject([
{ role: "model", parts: [{ functionCall: { id: undefined, name: "lookup", args: { query: "weather" } } }] },
{ role: "model", parts: [{ functionCall: { id: "call_1", name: "lookup", args: { query: "weather" } } }] },
{
role: "user",
parts: [
{
functionResponse: {
id: undefined,
id: "call_1",
name: "lookup",
response: { name: "lookup", content: "sunny" },
},
@@ -235,12 +234,23 @@ describe("Google Vertex providers", () => {
parts: [
{ text: "Thinking.", thought: true, thoughtSignature: "reasoning_sig" },
{ text: "Checking.", thoughtSignature: "text_sig" },
{ functionCall: { name: "lookup", args: { query: "weather" } }, thoughtSignature: "tool_sig" },
{
functionCall: { id: "provider_call_1", name: "lookup", args: { query: "weather" } },
thoughtSignature: "tool_sig",
},
],
},
{
role: "user",
parts: [{ functionResponse: { name: "lookup", response: { name: "lookup", content: "sunny" } } }],
parts: [
{
functionResponse: {
id: "provider_call_1",
name: "lookup",
response: { name: "lookup", content: "sunny" },
},
},
],
},
])
}),
+9 -1
View File
@@ -6,6 +6,8 @@ import * as path from "node:path"
import { fileURLToPath } from "node:url"
import { LLMClient, RequestExecutor } from "../src/route.js"
import { ImageClient } from "../src/image-client.js"
import { EvaluationClient } from "../src/experimental/evaluation-client.js"
import type { Service as EvaluationClientService } from "../src/experimental/evaluation-client.js"
import type { Service as ImageClientService } from "../src/image-client.js"
import type { Service as LLMClientService } from "../src/route/client.js"
import type { Service as RequestExecutorService } from "../src/route/executor.js"
@@ -18,7 +20,12 @@ import {
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const FIXTURES_DIR = path.resolve(__dirname, "fixtures", "recordings")
type RecordedEnv = RequestExecutorService | LLMClientService | ImageClientService | Socket.WebSocketConstructor
type RecordedEnv =
| RequestExecutorService
| LLMClientService
| ImageClientService
| EvaluationClientService
| Socket.WebSocketConstructor
type RecordedTestsOptions = RecordedGroupOptions & {
readonly options?: HttpRecorder.RecorderOptions
@@ -92,6 +99,7 @@ export const recordedTests = (options: RecordedTestsOptions) =>
requestExecutor,
LLMClient.layer.pipe(Layer.provide(requestExecutor)),
ImageClient.layer.pipe(Layer.provide(requestExecutor)),
EvaluationClient.layer.pipe(Layer.provide(requestExecutor)),
webSocket,
)
},
@@ -703,7 +703,7 @@ function messageContent(
return {
type: "reasoning",
text: part.text,
state: jsonRecord(part.metadata),
native: jsonRecord(part.metadata),
time: part.time
? { created: part.time.start, ...(part.time.end === undefined ? {} : { completed: part.time.end }) }
: undefined,
+13 -3
View File
@@ -55,7 +55,7 @@ try {
if ((await pluginIDs(info.url, headers)).includes("smoke")) throw new Error("Smoke plugin existed before creation")
const plugin = path.join(root, ".opencode", "plugins", "smoke.ts")
await fs.writeFile(plugin, pluginSource())
await waitForPlugin(info.url, headers)
await waitForPlugin(info.url, headers, plugin)
const unauthorizedInfo = await fetch(new URL("/api/info", info.url), {
signal: AbortSignal.timeout(5_000),
@@ -139,7 +139,13 @@ async function waitForReady(url: string, headers: HeadersInit) {
}
function exitsWithin(process: Bun.Subprocess, milliseconds: number) {
return Promise.race([process.exited.then(() => true), Bun.sleep(milliseconds).then(() => false)])
return new Promise<boolean>((resolve) => {
const timeout = setTimeout(() => resolve(false), milliseconds)
process.exited.then(() => {
clearTimeout(timeout)
resolve(true)
})
})
}
function pluginSource() {
@@ -159,11 +165,15 @@ async function pluginIDs(url: string, headers: HeadersInit) {
)
}
async function waitForPlugin(url: string, headers: HeadersInit) {
async function waitForPlugin(url: string, headers: HeadersInit, plugin: string) {
const deadline = Date.now() + 10_000
let attempt = 0
while (Date.now() < deadline) {
if ((await pluginIDs(url, headers)).includes("smoke")) return
await Bun.sleep(25)
// Native watchers may coalesce a single creation edge. Keep changing valid source so
// the smoke proves that a later native event is delivered.
if (++attempt % 10 === 0) await fs.writeFile(plugin, `${pluginSource()}// watcher retry ${attempt}\n`)
}
throw new Error("Compiled service did not discover the created plugin")
}
@@ -1,8 +1,9 @@
import { OpenCode } from "@opencode/client"
import { Service } from "@opencode/client/effect/service"
import { Session } from "@opencode/schema/session"
import { SessionMessage } from "@opencode/schema/session-message"
import { SessionTransfer } from "@opencode/schema/session-transfer"
import { Effect, Option, Schema } from "effect"
import { Effect, Option, Predicate, Schema } from "effect"
import { EOL } from "node:os"
import path from "node:path"
import { Commands } from "../../commands"
@@ -23,7 +24,13 @@ export default Runtime.handler(
catch: (cause) =>
new Error(`Failed to read session data: ${cause instanceof Error ? cause.message : String(cause)}`),
})
const data = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(SessionTransfer.Data))(text)
const raw = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown))(text)
// Exports written before provider blobs were renamed to `native` still carry the old keys.
const data = yield* Schema.decodeUnknownEffect(SessionTransfer.Data)(
Predicate.isObject(raw) && Array.isArray(raw.messages)
? { ...raw, messages: raw.messages.map(SessionMessage.persisted) }
: raw,
)
const encoded = Schema.encodeSync(SessionTransfer.Data)(data)
const server = yield* ServerConnection.resolve({
server: Option.getOrUndefined(input.server),
+2 -2
View File
@@ -7,7 +7,7 @@ import { produce, type Draft } from "immer"
import { applyEdits, modify, parse, type ParseError } from "jsonc-parser"
import path from "path"
import { ConfigMigration } from "./migrate"
import { Info, SchemaURL } from "./schema"
import { Info, normalizeLegacyTabs, SchemaURL } from "./schema"
export * from "./schema"
@@ -119,7 +119,7 @@ function merge(...values: readonly (Info | undefined)[]) {
return Option.getOrElse(
decode(
values.reduce<Record<string, unknown>>(
(result, value) => mergeRecords(result, value ?? {}),
(result, value) => mergeRecords(result, normalizeLegacyTabs(value) ?? {}),
{},
),
),
+8
View File
@@ -8,3 +8,11 @@ export const Info = Schema.Struct({
...Config.Info.fields,
})
export type Info = Schema.Schema.Type<typeof Info>
export function normalizeLegacyTabs(info: Info | undefined) {
if (info?.tabs?.enabled === undefined) return info
const tabs = { ...info.tabs }
tabs.mode ??= tabs.enabled ? "on" : "off"
delete tabs.enabled
return { ...info, tabs }
}
+1 -1
View File
@@ -564,7 +564,7 @@ export async function runNonInteractivePrompt(input: Input) {
messageID: message.id,
type: "reasoning",
text,
metadata: item.state,
metadata: item.native,
time: { start: message.time.created, end: timestamp },
}
renderedReasoning.set(key, item.text)
+22 -2
View File
@@ -78,7 +78,7 @@ test("merges inline CLI config content over the global config", async () => {
await Bun.write(
file,
JSON.stringify({
tabs: { enabled: true, scope: "global" },
tabs: { mode: "on", scope: "global" },
keybinds: { "app.exit": "ctrl+q" },
plugins: ["global"],
animations: true,
@@ -105,7 +105,7 @@ test("merges inline CLI config content over the global config", async () => {
}),
)
expect(result.loaded.tabs).toEqual({ enabled: false, scope: "global" })
expect(result.loaded.tabs).toEqual({ mode: "off", scope: "global" })
expect(result.loaded.keybinds).toEqual({ "app.exit": "ctrl+q", "help.show": false })
expect(result.loaded.plugins).toEqual(["inline"])
expect(result.updated).toMatchObject({ animations: false, mouse: false })
@@ -116,6 +116,26 @@ test("merges inline CLI config content over the global config", async () => {
}
})
test("reads the legacy tabs toggle without rewriting it", async () => {
await using directory = await tmpdir()
const file = path.join(directory.path, "cli.json")
await Bun.write(file, JSON.stringify({ tabs: { enabled: false } }))
const config = await run(
directory.path,
Effect.gen(function* () {
const service = yield* Config.Service
expect((yield* service.get()).tabs).toEqual({ mode: "off" })
return yield* service.update((draft) => {
draft.animations = false
})
}),
)
expect(config.tabs).toEqual({ mode: "off" })
expect(await Bun.file(file).json()).toEqual({ tabs: { enabled: false }, animations: false })
})
test("migrates tui and kv config into cli.json", async () => {
await using directory = await tmpdir()
await Bun.write(
+23 -27
View File
@@ -509,12 +509,12 @@ export type PromptAgentAttachment = { name: string; mention?: PromptMention }
export type PromptSkillAttachment = { id: string; name: string; text?: string; mention?: PromptMention }
export type SessionMessageAssistantText = { type: "text"; text: string; state?: SessionMessageProviderState }
export type SessionMessageAssistantText = { type: "text"; text: string; native?: SessionMessageProviderState }
export type SessionMessageAssistantReasoning = {
type: "reasoning"
text: string
state?: SessionMessageProviderState
native?: SessionMessageProviderState
time?: { created: number; completed?: number }
}
@@ -1354,15 +1354,6 @@ export type SessionToolCalled = {
}
}
export type SessionMessageAssistantText1 = { type: "text"; text: string; state?: SessionMessageProviderState1 }
export type SessionMessageAssistantReasoning1 = {
type: "reasoning"
text: string
state?: SessionMessageProviderState1
time?: { created: number; completed?: number }
}
export type ToolContent1 = ToolTextContent | ToolFileContent1
export type FormNumberField = {
@@ -1762,7 +1753,7 @@ export type SessionMessageCompactionCompleted = {
status: "completed"
reason: "auto" | "manual"
model?: ModelRef
providerState?: SessionMessageProviderState
native?: SessionMessageProviderState
summary: string
recent: string
providerContext?: SessionProviderContext
@@ -2228,7 +2219,7 @@ export type SessionMessageAssistant = {
snapshot?: { start?: string; end?: string; files?: Array<string> }
finish?: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown"
rawFinish?: string
providerState?: SessionMessageProviderState
native?: SessionMessageProviderState
cost?: MoneyUSD
tokens?: TokenUsageInfo
error?: SessionStructuredError
@@ -2236,8 +2227,13 @@ export type SessionMessageAssistant = {
}
export type SessionMessageAssistantContentEncoded =
| SessionMessageAssistantText1
| SessionMessageAssistantReasoning1
| { type: "text"; text: string; state?: SessionMessageProviderState1 }
| {
type: "reasoning"
text: string
time?: { created: number; completed?: number }
state?: SessionMessageProviderState1
}
| SessionMessageAssistantTool1
export type FormInfo = { id: string; sessionID: string; title: string; metadata?: FormMetadata; fields: FormFields }
@@ -3102,11 +3098,11 @@ export type SessionImportInput = {
readonly agent: string
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
readonly content: ReadonlyArray<
| { readonly type: "text"; readonly text: string; readonly state?: { readonly [x: string]: JsonValue } }
| { readonly type: "text"; readonly text: string; readonly native?: { readonly [x: string]: JsonValue } }
| {
readonly type: "reasoning"
readonly text: string
readonly state?: { readonly [x: string]: JsonValue }
readonly native?: { readonly [x: string]: JsonValue }
readonly time?: { readonly created: number; readonly completed?: number }
}
| {
@@ -3180,7 +3176,7 @@ export type SessionImportInput = {
readonly snapshot?: { readonly start?: string; readonly end?: string; readonly files?: ReadonlyArray<string> }
readonly finish?: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown"
readonly rawFinish?: string
readonly providerState?: { readonly [x: string]: JsonValue }
readonly native?: { readonly [x: string]: JsonValue }
readonly cost?: number
readonly tokens?: {
readonly input: number
@@ -3214,7 +3210,7 @@ export type SessionImportInput = {
readonly status: "completed"
readonly reason: "auto" | "manual"
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string }
readonly providerState?: { readonly [x: string]: JsonValue }
readonly native?: { readonly [x: string]: JsonValue }
readonly summary: string
readonly recent: string
readonly providerContext?: {
@@ -3419,11 +3415,11 @@ export type SessionImportInput = {
readonly agent: string
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
readonly content: ReadonlyArray<
| { readonly type: "text"; readonly text: string; readonly state?: { readonly [x: string]: JsonValue } }
| { readonly type: "text"; readonly text: string; readonly native?: { readonly [x: string]: JsonValue } }
| {
readonly type: "reasoning"
readonly text: string
readonly state?: { readonly [x: string]: JsonValue }
readonly native?: { readonly [x: string]: JsonValue }
readonly time?: { readonly created: number; readonly completed?: number }
}
| {
@@ -3497,7 +3493,7 @@ export type SessionImportInput = {
readonly snapshot?: { readonly start?: string; readonly end?: string; readonly files?: ReadonlyArray<string> }
readonly finish?: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown"
readonly rawFinish?: string
readonly providerState?: { readonly [x: string]: JsonValue }
readonly native?: { readonly [x: string]: JsonValue }
readonly cost?: number
readonly tokens?: {
readonly input: number
@@ -3531,7 +3527,7 @@ export type SessionImportInput = {
readonly status: "completed"
readonly reason: "auto" | "manual"
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string }
readonly providerState?: { readonly [x: string]: JsonValue }
readonly native?: { readonly [x: string]: JsonValue }
readonly summary: string
readonly recent: string
readonly providerContext?: {
@@ -3736,11 +3732,11 @@ export type SessionImportInput = {
readonly agent: string
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
readonly content: ReadonlyArray<
| { readonly type: "text"; readonly text: string; readonly state?: { readonly [x: string]: JsonValue } }
| { readonly type: "text"; readonly text: string; readonly native?: { readonly [x: string]: JsonValue } }
| {
readonly type: "reasoning"
readonly text: string
readonly state?: { readonly [x: string]: JsonValue }
readonly native?: { readonly [x: string]: JsonValue }
readonly time?: { readonly created: number; readonly completed?: number }
}
| {
@@ -3814,7 +3810,7 @@ export type SessionImportInput = {
readonly snapshot?: { readonly start?: string; readonly end?: string; readonly files?: ReadonlyArray<string> }
readonly finish?: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown"
readonly rawFinish?: string
readonly providerState?: { readonly [x: string]: JsonValue }
readonly native?: { readonly [x: string]: JsonValue }
readonly cost?: number
readonly tokens?: {
readonly input: number
@@ -3848,7 +3844,7 @@ export type SessionImportInput = {
readonly status: "completed"
readonly reason: "auto" | "manual"
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string }
readonly providerState?: { readonly [x: string]: JsonValue }
readonly native?: { readonly [x: string]: JsonValue }
readonly summary: string
readonly recent: string
readonly providerContext?: {
+8 -7
View File
@@ -846,7 +846,7 @@ export function createData(config: CreateDataInput) {
existing.error = undefined
existing.finish = undefined
existing.rawFinish = undefined
existing.providerState = undefined
existing.native = undefined
existing.time.created = event.data.started
existing.time.streamed = undefined
existing.time.completed = undefined
@@ -880,7 +880,7 @@ export function createData(config: CreateDataInput) {
assistant.time.completed = event.created
assistant.finish = event.data.finish
assistant.rawFinish = event.data.rawFinish
assistant.providerState = event.data.providerState
assistant.native = event.data.providerState
assistant.cost = event.data.cost
assistant.tokens = event.data.tokens
if (event.data.snapshot) assistant.snapshot = { ...assistant.snapshot, end: event.data.snapshot }
@@ -892,7 +892,7 @@ export function createData(config: CreateDataInput) {
assistant.time.completed = event.created
assistant.finish = event.data.finish ?? "error"
assistant.rawFinish = event.data.rawFinish
assistant.providerState = event.data.providerState
assistant.native = event.data.providerState
assistant.error = event.data.error
assistant.retry = undefined
if (event.data.cost !== undefined && event.data.tokens !== undefined) {
@@ -914,6 +914,7 @@ export function createData(config: CreateDataInput) {
case "session.text.ended":
message.editText(event.data.sessionID, event.data.assistantMessageID, (text) => {
text.text = event.data.text
text.native = event.data.state
})
return
case "session.tool.input.started":
@@ -984,7 +985,7 @@ export function createData(config: CreateDataInput) {
assistant.content.push({
type: "reasoning",
text: "",
state: event.data.state,
native: event.data.state,
time: { created: event.created },
})
})
@@ -998,7 +999,7 @@ export function createData(config: CreateDataInput) {
message.editReasoning(event.data.sessionID, event.data.assistantMessageID, (reasoning) => {
reasoning.text = event.data.text
reasoning.time = { created: reasoning.time?.created ?? event.created, completed: event.created }
if (event.data.state !== undefined) reasoning.state = event.data.state
if (event.data.state !== undefined) reasoning.native = event.data.state
})
return
case "session.retry.scheduled":
@@ -1105,7 +1106,7 @@ export function createData(config: CreateDataInput) {
status: "completed",
reason: event.data.reason,
model: event.data.model,
providerState: event.data.providerState,
native: event.data.providerState,
providerContext: event.data.providerContext,
summary: event.data.text,
recent: event.data.recent,
@@ -1120,7 +1121,7 @@ export function createData(config: CreateDataInput) {
status: "completed",
reason: event.data.reason,
model: event.data.model,
providerState: event.data.providerState,
native: event.data.providerState,
providerContext: event.data.providerContext,
summary: event.data.text,
recent: event.data.recent,
@@ -135,7 +135,7 @@ test.each(["started", "cancelled", "failed"])(
status: "completed",
summary: "Summary",
model,
providerState,
native: providerState,
providerContext,
cost: 0.01,
tokens,
+24 -15
View File
@@ -124,8 +124,8 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [x] `Boolean`, `Number`, `String`, `parseInt`, `parseFloat`, `isFinite`, `isNaN`, and URI helpers as callbacks.
- [x] Built-in method references as callbacks, such as `values.map(Math.abs)`, `records.map(JSON.stringify)`,
`items.forEach(console.log)`, and `Promise.resolve(-1).then(Math.abs)`. Extra callback arguments a built-in
does not consume are ignored, like JS; consumed arguments stay strictly validated (`Math.floor` still rejects a
string). A detached method loses its receiver, as in JS: `values.filter("abc".includes)` is a `TypeError`
does not consume are ignored, like JS, and consumed arguments coerce, like JS (`"3.7".replace(/\d\.\d/,
Math.floor)` is `"3"`). A detached method loses its receiver, as in JS: `values.filter("abc".includes)` is a `TypeError`
because `includes` is called without a string `this`.
- [x] Constructors work as callbacks with JS call semantics: `Error` types construct (`messages.map(Error)`),
and new-requiring constructors (`Map`, `Set`, `URL`, `URLSearchParams`, `Headers`, `Promise`) throw a `TypeError`,
@@ -145,10 +145,13 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
assignments, object literal keys, and destructuring or parameter defaults.
- [x] Built-in functions are objects too, with `name` and `length` (`Math.max.length === 2`,
`Array.prototype.push.name === "push"`).
- [ ] A named function expression's name is not bound inside its own body.
- [ ] Redeclaring a function in the same scope is rejected; in JavaScript the last declaration wins.
- [ ] Generator and async generator functions evaluate parameter defaults and destructuring at the first `next()`
rather than at the call, so their errors are not thrown synchronously.
- [x] A named function expression's name is bound read-only inside its own body; assigning to it throws a
`TypeError`, as in strict mode.
- [x] Redeclaring a function in the same scope, or alongside a `var`, is allowed: the last declaration wins.
- [x] Generator functions have their own `prototype` (inheriting the shared generator prototype), so
`g() instanceof g` holds. Plain functions have none, since they cannot construct.
- [x] Generator and async generator functions bind parameters (defaults, destructuring) at the call and defer only the
body to the first `next()`, so a bad argument throws synchronously from the call site, as in JS.
- [x] Synchronous and async generator declarations/expressions, `yield`, and `yield*`, including lazy bodies,
`next(value)`, `return(value)`, `throw(value)`, exhaustion, promise adoption, async request ordering,
`try`/`catch`/`finally`, and sync/async iterator symbols. Async `yield*` awaits values while adapting a sync
@@ -196,13 +199,14 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [x] Plain, arithmetic, bitwise, and logical assignment operators.
- [x] Property deletion on plain data objects and arrays, including computed and optional forms; deleting an array index
creates a hole without changing its length. Deleting a non-configurable property (`length`) or
assigning a read-only one (`Math.PI`, `fn.name`) throws a `TypeError`, as in strict mode.
assigning a read-only one (`Math.PI`, `fn.name`) throws a `TypeError`, as in strict mode. `delete` of a
non-reference (`delete 0`, `delete f()`) evaluates the operand and is `true`; `delete x` on a variable throws.
- [ ] Operators, `switch` discriminants, template interpolation, and coercion helpers such as `String` and `isNaN`
applied to functions and namespaces; JavaScript coerces them, the interpreter rejects non-data operands.
- [ ] ToPrimitive on object operands: operators, `Error(message)`, `Date` arguments, and `parseInt` radix should call
`valueOf`/`toString` in spec order and surface their throws.
- [ ] Property keys follow ToPropertyKey: `x[null]`, `x[true]`, and objects (via `toString`) become string keys; only
strings and numbers are accepted.
- [x] Property keys follow ToPropertyKey: `x[null]`, `x[true]`, and objects (via their built-in string form) become
string keys.
## Promises and tools
@@ -239,7 +243,7 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
accepted, including `.then`/`.catch` handlers and collection callbacks, and vanish at the data boundary like
any function.
- [x] `Promise.withResolvers()`: the same promise and resolver callables as the constructor, as a `{ promise, resolve,
reject }` object.
reject }` object.
- [x] `Promise.try(fn, ...args)`: calls `fn` synchronously; a throw rejects, a return fulfils, and a returned promise or
thenable is adopted.
- [x] Recursive assimilation of objects with an own callable `then` field across `Promise.resolve`, combinators,
@@ -262,7 +266,9 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
not construct.
- [x] `Object()` and `new Object()` return `{}` for nullish arguments and pass objects through unchanged;
primitive wrapper objects (`Object(1)`) are rejected explicitly.
- [x] Computed property names and object spread.
- [x] Computed property names and object spread. Any value works as a key (ToPropertyKey): strings, numbers, and the
two confined symbols as themselves, everything else as its string form (`o[null]` is `o["null"]`, `o[{}]` is
`o["[object Object]"]`), in reads, writes, literals, `in`, and destructuring.
- [x] `Object.keys`, `Object.values`, `Object.entries`, `Object.hasOwn`, `Object.assign`, and `Object.fromEntries`, with
synchronous iterator support for `fromEntries`. Sources follow ToObject: strings enumerate by index, other
primitives and wrappers contribute nothing, and `null`/`undefined` throw. `Object.assign` accepts array
@@ -313,10 +319,12 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [x] Assigning `length` to truncate or extend an array; invalid lengths throw `RangeError`.
- [x] Non-index own properties on arrays (`arr.foo = 1`, `arr.constructor = null`). They are excluded from the JSON
form, like `JSON.stringify`.
- [ ] Argument coercion for `indexOf`, `lastIndexOf`, `includes`, `fill`, `flat`, `copyWithin`, and the `join`
separator: JavaScript applies ToIntegerOrInfinity/ToString (including `valueOf`, strings, and `undefined`), the
interpreter requires numbers and strings. `indexOf()` and `lastIndexOf()` with no argument already search for
`undefined`; `includes()` still requires a value.
- [x] Numeric arguments coerce as in JS (ToIntegerOrInfinity): `indexOf(x, "1")`, `slice("1", "3")`, `at(null)`,
`flat(1.9)`, `with(1.5, v)`, `Math.max("3", "2")`, `parseInt("11", "2")`, `(1.5).toFixed("2")`,
`String.fromCharCode("65")`, and the Uint8Array equivalents. `join(sep)` and `JSON.parse(text)` apply ToString
(`join(null)` is `"1null2"`, `JSON.parse(123)` is `123`). `Array.from({ length: "2" })` applies ToLength; a
promise source still throws with an `await` hint rather than JS's silent `[]`. A program object's own
`valueOf`/`toString` is not consulted yet (see ToPrimitive above).
## Strings
@@ -437,6 +445,7 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [x] Static `Map.groupBy` over finite collections and custom synchronous iterators/generators, preserving key identity.
- [x] `new Map()` from synchronous iterables of entries.
- [x] Map `get`, `set`, `has`, `delete`, `clear`, `size`, `forEach`, `getOrInsert`, and `getOrInsertComputed`.
`forEach` is live: entries deleted during the walk are skipped and entries added are visited, as in JS.
- [x] `new Set()` from synchronous iterables.
- [x] Set `add`, `has`, `delete`, `clear`, `size`, and `forEach`.
- [x] Live `keys`, `values`, `entries`, and `[Symbol.iterator]` iterators for Map and Set; a Set-like operand's `keys()`
@@ -74,6 +74,7 @@ import {
IteratorObj,
type Cursor,
has,
hidden,
hasPrototype,
keys,
Native,
@@ -454,8 +455,9 @@ class Frame<R> {
node: FunctionDeclaration | FunctionExpression | ArrowFunctionExpression,
name = node.type === "ArrowFunctionExpression" ? "" : (node.id?.name ?? ""),
): Fn {
return new Fn(
this.ctx.builtins.Function,
const builtins = this.ctx.builtins
const fn = new Fn(
builtins.Function,
name,
node.params,
node.body,
@@ -463,6 +465,14 @@ class Frame<R> {
node.async,
node.generator,
)
// Each generator function gets its own prototype, so `g() instanceof g` holds as in JS.
if (node.generator)
define(fn, "prototype", new Obj(node.async ? builtins.AsyncGenerator : builtins.Generator), hidden)
// The body of a named function expression sees its own name, read-only.
if (node.type === "FunctionExpression" && node.id) {
fn.capturedScopes.push(new Map([[node.id.name, { mutable: false, value: fn, initialized: true }]]))
}
return fn
}
// NamedEvaluation: an anonymous function definition takes the name of what it is assigned to.
@@ -473,10 +483,11 @@ class Frame<R> {
return this.evaluateExpression(node)
}
// Repeated `function` declarations and `var` clashes are legal: the last declaration wins.
private hoistFunctions(statements: ReadonlyArray<Statement | ModuleDeclaration>): void {
for (const node of statements) {
if (node.type !== "FunctionDeclaration") continue
this.scopes.declare(node.id.name, this.createFunction(node), true, node)
this.scopes.current().set(node.id.name, { mutable: true, value: this.createFunction(node), initialized: true })
}
}
@@ -1233,7 +1244,7 @@ class Frame<R> {
}
const keyNode = property.key
if (property.computed) {
return Effect.map(this.evaluateExpression(keyNode), (value) => this.toPropertyKey(value, keyNode))
return Effect.map(this.evaluateExpression(keyNode), (value) => this.toPropertyKey(value))
}
if (keyNode.type === "Identifier") return Effect.succeed(keyNode.name)
if (keyNode.type === "Literal") return Effect.succeed(String(keyNode.value))
@@ -1648,14 +1659,14 @@ class Frame<R> {
const depth = Math.max(self.depth, site.depth) + 1
if (depth > MAX_CALL_DEPTH) throw rangeError("Maximum call stack size exceeded", node)
const invocation = new Frame(this.ctx, new ScopeStack([...fn.capturedScopes, new Map()]), depth)
const run = Effect.gen(function* () {
// Seed all parameters first so defaults cannot fall through to same-named outer bindings.
const paramScope = invocation.scopes.current()
for (const parameter of fn.parameters) {
for (const name of collectPatternNames(parameter)) {
paramScope.set(name, { mutable: true, value: undefined, initialized: false })
}
// Seed all parameters first so defaults cannot fall through to same-named outer bindings.
const paramScope = invocation.scopes.current()
for (const parameter of fn.parameters) {
for (const name of collectPatternNames(parameter)) {
paramScope.set(name, { mutable: true, value: undefined, initialized: false })
}
}
const bind = Effect.gen(function* () {
for (const [index, parameter] of fn.parameters.entries()) {
if (parameter.type === "RestElement") {
yield* invocation.declarePattern(
@@ -1669,7 +1680,8 @@ class Frame<R> {
}
yield* invocation.declarePattern(parameter, args[index], true, parameter, true)
}
})
const body = Effect.gen(function* () {
if (fn.body.type === "BlockStatement") {
invocation.scopes.push()
invocation.hoistVars(fn.body.body, paramScope)
@@ -1679,7 +1691,9 @@ class Frame<R> {
return yield* invocation.evaluateExpression(fn.body)
})
if (fn.generator) return Effect.succeed(this.createGenerator(invocation, run, fn.async))
// Generators bind parameters at the call and defer only the body to the first `next()`, as in JS.
if (fn.generator) return Effect.map(bind, () => this.createGenerator(invocation, body, fn))
const run = Effect.andThen(bind, body)
if (!fn.async) return run
return this.ctx.pending.createWithSelf((self) =>
Effect.flatMap(run, (value) => resolvePromiseValue(invocation.ctx, value, self)),
@@ -1687,11 +1701,8 @@ class Frame<R> {
})
}
private createGenerator(
invocation: Frame<R>,
run: Effect.Effect<Value, unknown, R>,
asynchronous: boolean,
): GeneratorObj {
private createGenerator(invocation: Frame<R>, run: Effect.Effect<Value, unknown, R>, fn: Fn): GeneratorObj {
const asynchronous = fn.async
const state: GeneratorState = { started: false, completed: false, draining: false, pending: [], pendingIndex: 0 }
invocation.generatorState = state
invocation.generatorAsync = asynchronous
@@ -1759,12 +1770,8 @@ class Frame<R> {
}
return Deferred.await(request.response)
}
const generator = new GeneratorObj(
asynchronous ? builtins.AsyncGenerator : builtins.Generator,
asynchronous,
request,
)
return generator
const proto = get(fn, "prototype")
return new GeneratorObj(proto instanceof Obj ? proto : builtins.Generator, asynchronous, request)
}
private completeGeneratorRequests(state: GeneratorState, asynchronous: boolean): Effect.Effect<void, never, R> {
@@ -1939,11 +1946,11 @@ class Frame<R> {
let key: PropertyKey
if (property.computed) {
key = self.toPropertyKey(yield* self.evaluateExpression(keyNode), keyNode)
key = self.toPropertyKey(yield* self.evaluateExpression(keyNode))
} else if (keyNode.type === "Identifier") {
key = keyNode.name
} else if (keyNode.type === "Literal") {
key = self.toPropertyKey(literal(keyNode), keyNode)
key = self.toPropertyKey(literal(keyNode))
} else {
throw typeError("Unsupported object property key shape.", keyNode)
}
@@ -2044,10 +2051,10 @@ class Frame<R> {
if ((objectValue === null || objectValue === undefined) && node.optional) return OptionalShortCircuit
const key = node.computed
? self.toPropertyKey(yield* self.evaluateExpression(propertyNode), propertyNode)
? self.toPropertyKey(yield* self.evaluateExpression(propertyNode))
: propertyNode.type === "Identifier"
? propertyNode.name
: self.toPropertyKey(yield* self.evaluateExpression(propertyNode), propertyNode)
: self.toPropertyKey(yield* self.evaluateExpression(propertyNode))
if (objectValue instanceof ToolReference) {
if (typeof key !== "string") {
@@ -2109,9 +2116,9 @@ class Frame<R> {
private evaluateDeleteExpression(argument: Expression): Effect.Effect<boolean, unknown, R> {
const target = argument.type === "ChainExpression" ? argument.expression : argument
if (target.type !== "MemberExpression") {
throw typeError("Only data fields may be deleted.", argument)
}
if (target.type === "Identifier") throw typeError("Only data fields may be deleted.", argument)
// `delete <non-reference>` evaluates the operand and is true, as in JS.
if (target.type !== "MemberExpression") return Effect.map(this.evaluateExpression(target), () => true)
return Effect.map(this.getMemberReference(target), (reference) => {
if (reference === OptionalShortCircuit) return true
if (reference instanceof ToolReference || "value" in reference || reference.receiver !== reference.target) {
@@ -2164,12 +2171,9 @@ class Frame<R> {
throw typeError(`Cannot assign to read only property '${String(key)}'.`, node)
}
private toPropertyKey(value: Value, node: AstNode): PropertyKey {
if (typeof value === "string" || typeof value === "number") {
return value
}
if (value === AsyncIteratorSymbol || value === IteratorSymbol) return value
throw typeError("Property key must be a string or number, or Symbol.asyncIterator/Symbol.iterator.", node)
// ToPropertyKey: anything else becomes its string form, so `counts[row.category]` works when the field is null.
private toPropertyKey(value: Value): PropertyKey {
if (typeof value === "string" || typeof value === "number" || typeof value === "symbol") return value
return coerceToString(value)
}
}
+7 -1
View File
@@ -159,7 +159,7 @@ export class Fn extends Callable {
name: string,
readonly parameters: ReadonlyArray<Pattern>,
readonly body: BlockStatement | Expression,
readonly capturedScopes: ReadonlyArray<Map<string, Binding>>,
readonly capturedScopes: Array<Map<string, Binding>>,
readonly async: boolean,
readonly generator: boolean,
) {
@@ -422,6 +422,12 @@ export const coerceToNumber = (value: Value): number => {
return value instanceof ToolReference ? Number.NaN : Number(value)
}
/** ToIntegerOrInfinity: NaN is 0, fractions truncate. */
export const coerceToInteger = (value: Value): number => {
const number = coerceToNumber(value)
return Number.isNaN(number) ? 0 : Math.trunc(number)
}
/** Values that cannot cross the data boundary: opaque machinery and host-backed wrappers. */
export const isRuntimeReference = (value: Value): boolean =>
value instanceof Opaque || value instanceof Wrapper || value instanceof ToolReference
+24 -53
View File
@@ -10,6 +10,8 @@ import {
GeneratorObj,
hostIterator,
Obj,
PromiseObj,
coerceToInteger,
coerceToNumber,
coerceToString,
type Value,
@@ -20,11 +22,11 @@ import type { Interpreter } from "../interpreter/interpreter.js"
import { compareText } from "../tool-runtime.js"
const arrayLikeSource = (source: Value): { readonly length: number; readonly source: Obj } => {
if (source instanceof Obj && typeof get(source, "length") === "number") {
const length = get(source, "length") as number
const normalized = Number.isNaN(length) || length <= 0 ? 0 : Math.trunc(length)
checkArrayLength(normalized)
return { length: normalized, source }
// JS would treat a promise as an empty array-like; that would hide a missing `await`.
if (source instanceof Obj && !(source instanceof PromiseObj)) {
const length = Math.max(0, coerceToInteger(get(source, "length")))
checkArrayLength(length)
return { length, source }
}
throw invalidData(`Array.from expects an iterable or array-like value, received ${describeValue(source)}.`)
}
@@ -122,13 +124,7 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
])
const self = (thisValue: Value, name: string) => receiver(Arr, thisValue, `Array.prototype.${name}`)
const optNumber = (name: string, value: Value, label: string): number | undefined => {
if (value === undefined) return undefined
if (typeof value !== "number") {
throw typeError(`Array.${name} expects ${label} to be a number.`)
}
return value
}
const optNumber = (value: Value): number | undefined => (value === undefined ? undefined : coerceToInteger(value))
// Callback methods fix the iteration length while reading existing elements live.
const iterate = (
name: string,
@@ -153,13 +149,9 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
"join",
1,
(thisValue, args) => {
const target = self(thisValue, "join").items
if (args.length > 1 || (args.length === 1 && typeof args[0] !== "string")) {
throw typeError("Array.join expects zero arguments or one string separator.")
}
const joined = target
.map((item) => coerceToString(item ?? ""))
.join(args.length === 0 ? "," : (args[0] as string))
const joined = self(thisValue, "join")
.items.map((item) => coerceToString(item ?? ""))
.join(args[0] === undefined ? "," : coerceToString(args[0]))
checkStringLength(joined.length)
return joined
},
@@ -176,40 +168,23 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
"includes",
1,
(thisValue, args) => {
const target = self(thisValue, "includes").items
if (args.length === 0 || args.length > 2) {
throw typeError("Array.includes expects a value and optional start index.")
}
return target.includes(args[0], optNumber("includes", args[1], "start index"))
return self(thisValue, "includes").items.includes(args[0], optNumber(args[1]))
},
],
[
"indexOf",
1,
(thisValue, args) =>
self(thisValue, "indexOf").items.indexOf(args[0], optNumber("indexOf", args[1], "start index")),
],
["indexOf", 1, (thisValue, args) => self(thisValue, "indexOf").items.indexOf(args[0], optNumber(args[1]))],
[
"lastIndexOf",
1,
(thisValue, args) => {
const target = self(thisValue, "lastIndexOf").items
return args[1] === undefined
? target.lastIndexOf(args[0])
: target.lastIndexOf(args[0], optNumber("lastIndexOf", args[1], "start index"))
return args[1] === undefined ? target.lastIndexOf(args[0]) : target.lastIndexOf(args[0], optNumber(args[1]))
},
],
["at", 1, (thisValue, args) => self(thisValue, "at").items.at(optNumber("at", args[0], "index") ?? 0)],
["at", 1, (thisValue, args) => self(thisValue, "at").items.at(optNumber(args[0]) ?? 0)],
[
"slice",
2,
(thisValue, args) =>
wrap(
self(thisValue, "slice").items.slice(
optNumber("slice", args[0], "start"),
optNumber("slice", args[1], "end"),
),
),
(thisValue, args) => wrap(self(thisValue, "slice").items.slice(optNumber(args[0]), optNumber(args[1]))),
],
[
"concat",
@@ -228,7 +203,7 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
(thisValue, args) => {
const flatten = (items: Array<Value>, depth: number): Array<Value> =>
items.flatMap((item) => (item instanceof Arr && depth > 0 ? flatten(item.items, depth - 1) : [item]))
const flattened = flatten(self(thisValue, "flat").items, optNumber("flat", args[0], "depth") ?? 1)
const flattened = flatten(self(thisValue, "flat").items, optNumber(args[0]) ?? 1)
checkArrayLength(flattened.length)
return wrap(flattened)
},
@@ -274,7 +249,7 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
2,
(thisValue, args) => {
const target = self(thisValue, "with").items
const index = optNumber("with", args[0], "index") ?? 0
const index = optNumber(args[0]) ?? 0
const resolved = index < 0 ? target.length + index : index
if (resolved < 0 || resolved >= target.length) throw rangeError("Array.with index is out of range.")
const copied = [...target]
@@ -309,9 +284,9 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
(thisValue, args) => {
const target = self(thisValue, "splice")
if (args.length === 0) return wrap(target.items.splice(0, 0))
const start = optNumber("splice", args[0], "start") ?? 0
const start = optNumber(args[0]) ?? 0
if (args.length === 1) return wrap(target.items.splice(start))
const deleteCount = optNumber("splice", args[1], "delete count") ?? 0
const deleteCount = optNumber(args[1]) ?? 0
const inserted = args.slice(2)
for (const item of inserted) rejectCircularInsertion(target, item, "Array.splice result")
return wrap(target.items.splice(start, deleteCount, ...inserted))
@@ -323,9 +298,9 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
(thisValue, args) => {
const copied = [...self(thisValue, "toSpliced").items]
if (args.length === 0) return wrap(copied)
const start = optNumber("toSpliced", args[0], "start") ?? 0
const start = optNumber(args[0]) ?? 0
if (args.length === 1) copied.splice(start)
else copied.splice(start, optNumber("toSpliced", args[1], "delete count") ?? 0, ...args.slice(2))
else copied.splice(start, optNumber(args[1]) ?? 0, ...args.slice(2))
return wrap(copied)
},
],
@@ -335,7 +310,7 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
(thisValue, args) => {
const target = self(thisValue, "fill")
rejectCircularInsertion(target, args[0], "Array.fill result")
target.items.fill(args[0], optNumber("fill", args[1], "start"), optNumber("fill", args[2], "end"))
target.items.fill(args[0], optNumber(args[1]), optNumber(args[2]))
return target
},
],
@@ -344,11 +319,7 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
2,
(thisValue, args) => {
const target = self(thisValue, "copyWithin")
target.items.copyWithin(
optNumber("copyWithin", args[0], "target index") ?? 0,
optNumber("copyWithin", args[1], "start") ?? 0,
optNumber("copyWithin", args[2], "end"),
)
target.items.copyWithin(optNumber(args[0]) ?? 0, optNumber(args[1]) ?? 0, optNumber(args[2]))
return target
},
],
+11 -38
View File
@@ -11,6 +11,7 @@ import {
Bytes,
hostIterator,
Obj,
coerceToInteger,
coerceToNumber,
coerceToString,
type Value,
@@ -75,44 +76,28 @@ export const uint8ArrayGlobal = <R>(ctx: Interpreter<R>) => {
])
const self = (thisValue: Value, name: string) => receiver(Bytes, thisValue, `Uint8Array.prototype.${name}`)
const optNumber = (name: string, value: Value, label: string): number | undefined => {
if (value === undefined) return undefined
if (typeof value !== "number") throw typeError(`Uint8Array.${name} expects ${label} to be a number.`)
return value
}
const optNumber = (value: Value): number | undefined => (value === undefined ? undefined : coerceToInteger(value))
const wrapAll = (items: Array<Value>) => new Arr(builtins.Array, items)
defineAccessor(proto, "length", (thisValue) => self(thisValue, "length").bytes.length)
methods(builtins, proto, [
["at", 1, (thisValue, args) => self(thisValue, "at").bytes.at(optNumber("at", args[0], "index") ?? 0)],
["at", 1, (thisValue, args) => self(thisValue, "at").bytes.at(optNumber(args[0]) ?? 0)],
[
"slice",
2,
(thisValue, args) =>
wrap(
self(thisValue, "slice").bytes.slice(
optNumber("slice", args[0], "start"),
optNumber("slice", args[1], "end"),
),
),
(thisValue, args) => wrap(self(thisValue, "slice").bytes.slice(optNumber(args[0]), optNumber(args[1]))),
],
// A view on the same bytes, as in JS: writes through one are visible through the other.
[
"subarray",
2,
(thisValue, args) =>
wrap(
self(thisValue, "subarray").bytes.subarray(
optNumber("subarray", args[0], "start"),
optNumber("subarray", args[1], "end"),
),
),
(thisValue, args) => wrap(self(thisValue, "subarray").bytes.subarray(optNumber(args[0]), optNumber(args[1]))),
],
[
"set",
1,
(thisValue, args) => {
const target = self(thisValue, "set")
const offset = optNumber("set", args[1], "offset") ?? 0
const offset = optNumber(args[1]) ?? 0
return Effect.map(collectBytes(ctx, args[0], "Uint8Array.set"), (source) => {
if (!Number.isInteger(offset) || offset < 0 || source.length + offset > target.bytes.length) {
throw rangeError("Uint8Array.set: the source does not fit at that offset.")
@@ -127,11 +112,7 @@ export const uint8ArrayGlobal = <R>(ctx: Interpreter<R>) => {
1,
(thisValue, args) => {
const target = self(thisValue, "fill")
target.bytes.fill(
coerceToNumber(args[0]),
optNumber("fill", args[1], "start"),
optNumber("fill", args[2], "end"),
)
target.bytes.fill(coerceToNumber(args[0]), optNumber(args[1]), optNumber(args[2]))
return target
},
],
@@ -147,8 +128,7 @@ export const uint8ArrayGlobal = <R>(ctx: Interpreter<R>) => {
[
"indexOf",
1,
(thisValue, args) =>
self(thisValue, "indexOf").bytes.indexOf(coerceToNumber(args[0]), optNumber("indexOf", args[1], "start index")),
(thisValue, args) => self(thisValue, "indexOf").bytes.indexOf(coerceToNumber(args[0]), optNumber(args[1])),
],
[
"lastIndexOf",
@@ -157,26 +137,19 @@ export const uint8ArrayGlobal = <R>(ctx: Interpreter<R>) => {
const target = self(thisValue, "lastIndexOf").bytes
return args[1] === undefined
? target.lastIndexOf(coerceToNumber(args[0]))
: target.lastIndexOf(coerceToNumber(args[0]), optNumber("lastIndexOf", args[1], "start index"))
: target.lastIndexOf(coerceToNumber(args[0]), optNumber(args[1]))
},
],
[
"includes",
1,
(thisValue, args) =>
self(thisValue, "includes").bytes.includes(
coerceToNumber(args[0]),
optNumber("includes", args[1], "start index"),
),
(thisValue, args) => self(thisValue, "includes").bytes.includes(coerceToNumber(args[0]), optNumber(args[1])),
],
[
"join",
1,
(thisValue, args) => {
if (args.length > 1 || (args.length === 1 && typeof args[0] !== "string")) {
throw typeError("Uint8Array.join expects zero arguments or one string separator.")
}
const joined = self(thisValue, "join").bytes.join(args.length === 0 ? "," : (args[0] as string))
const joined = self(thisValue, "join").bytes.join(args[0] === undefined ? "," : coerceToString(args[0]))
checkStringLength(joined.length)
return joined
},
+2 -2
View File
@@ -188,7 +188,7 @@ export const mapGlobal = <R>(ctx: Interpreter<R>) => {
const target = self(thisValue, "forEach")
const apply = applyCollectionCallback(ctx, args[0], "Map.forEach")
return Effect.gen(function* () {
for (const [key, item] of Array.from(target.map.entries())) yield* apply([item, key, target])
for (const [key, item] of target.map.entries()) yield* apply([item, key, target])
return undefined
})
},
@@ -386,7 +386,7 @@ export const setGlobal = <R>(ctx: Interpreter<R>) => {
const target = self(thisValue, "forEach")
const apply = applyCollectionCallback(ctx, args[0], "Set.forEach")
return Effect.gen(function* () {
for (const item of Array.from(target.set.values())) yield* apply([item, item, target])
for (const item of target.set.values()) yield* apply([item, item, target])
return undefined
})
},
+3 -4
View File
@@ -3,10 +3,10 @@ import { methods } from "../interpreter/native.js"
import { applyCollectionCallback } from "../interpreter/callback.js"
import type { Interpreter } from "../interpreter/interpreter.js"
import { checkStringLength } from "../interpreter/limits.js"
import { syntaxError, typeError } from "../interpreter/model.js"
import { syntaxError } from "../interpreter/model.js"
import { typeofValue } from "../interpreter/references.js"
import { fromJson, toJson } from "../data.js"
import { get, keys, Arr, Obj, record, remove, set, type Value } from "../interpreter/objects.js"
import { get, keys, Arr, Obj, coerceToString, record, remove, set, type Value } from "../interpreter/objects.js"
export const jsonGlobal = <R>(ctx: Interpreter<R>) => {
const json = new Obj(ctx.builtins.Object)
@@ -18,8 +18,7 @@ export const jsonGlobal = <R>(ctx: Interpreter<R>) => {
}
const parse = <R>(ctx: Interpreter<R>, args: Array<Value>): Effect.Effect<Value, unknown, R> => {
const text = args[0]
if (typeof text !== "string") throw typeError("JSON.parse expects a string.")
const text = coerceToString(args[0])
const parsed = (() => {
try {
+4 -17
View File
@@ -1,7 +1,7 @@
import { Effect } from "effect"
import { constants, type Method, methods } from "../interpreter/native.js"
import { typeError } from "../interpreter/model.js"
import { Obj, type Value } from "../interpreter/objects.js"
import { Obj, coerceToNumber } from "../interpreter/objects.js"
import { preserveConsumerError } from "../interpreter/callback.js"
import type { Interpreter } from "../interpreter/interpreter.js"
@@ -14,31 +14,18 @@ declare global {
// Validate only the arguments a method consumes; like JS, extras are ignored
// (so built-ins work as callbacks receiving (element, index, array)).
const number = (name: string, args: Array<Value>, index: number): number => {
if (index >= args.length) return Number.NaN
const arg = args[index]
if (typeof arg !== "number") throw typeError(`Math.${name} expects number arguments.`)
return arg
}
const unary = (name: string, op: (a: number) => number): Method => [name, 1, (_, args) => op(number(name, args, 0))]
const unary = (name: string, op: (a: number) => number): Method => [name, 1, (_, args) => op(coerceToNumber(args[0]))]
const binary = (name: string, op: (a: number, b: number) => number): Method => [
name,
2,
(_, args) => op(number(name, args, 0), number(name, args, 1)),
(_, args) => op(coerceToNumber(args[0]), coerceToNumber(args[1])),
]
const variadic = (name: string, op: (...values: Array<number>) => number): Method => [
name,
2,
(_, args) =>
op(
...args.map((arg) => {
if (typeof arg !== "number") throw typeError(`Math.${name} expects number arguments.`)
return arg
}),
),
(_, args) => op(...args.map(coerceToNumber)),
]
export const mathGlobal = <R>(ctx: Interpreter<R>) => {
+7 -19
View File
@@ -1,5 +1,5 @@
import { constructor, constants, methods } from "../interpreter/native.js"
import { coerceToString, type Value } from "../interpreter/objects.js"
import { coerceToNumber, coerceToString, type Value } from "../interpreter/objects.js"
import { rangeError, typeError } from "../interpreter/model.js"
import type { Interpreter } from "../interpreter/interpreter.js"
import { coercion } from "./value.js"
@@ -30,11 +30,7 @@ export const numberGlobal = <R>(ctx: Interpreter<R>) => {
"parseInt",
2,
(_, args) => {
const radix = args[1]
if (radix !== undefined && typeof radix !== "number") {
throw typeError("Number.parseInt expects a numeric radix.")
}
return parseInt(coerceToString(args[0]), radix)
return parseInt(coerceToString(args[0]), coerceToNumber(args[1]))
},
],
["parseFloat", 1, (_, args) => parseFloat(coerceToString(args[0]))],
@@ -44,25 +40,17 @@ export const numberGlobal = <R>(ctx: Interpreter<R>) => {
if (typeof thisValue === "number") return thisValue
throw typeError(`Number.prototype.${name} requires that 'this' be a Number.`)
}
const optNum = (name: string, arg: Value): number | undefined => {
if (arg === undefined) return undefined
if (typeof arg !== "number") throw typeError(`Number.${name} expects a number argument.`)
return arg
}
const optNum = (arg: Value): number | undefined => (arg === undefined ? undefined : coerceToNumber(arg))
methods(builtins, builtins.Number, [
["toFixed", 1, (thisValue, args) => self(thisValue, "toFixed").toFixed(optNum("toFixed", args[0]))],
["toFixed", 1, (thisValue, args) => self(thisValue, "toFixed").toFixed(optNum(args[0]))],
["toLocaleString", 0, (thisValue) => self(thisValue, "toLocaleString").toLocaleString("en-US")],
[
"toExponential",
1,
(thisValue, args) => self(thisValue, "toExponential").toExponential(optNum("toExponential", args[0])),
],
["toExponential", 1, (thisValue, args) => self(thisValue, "toExponential").toExponential(optNum(args[0]))],
[
"toPrecision",
1,
(thisValue, args) => {
const value = self(thisValue, "toPrecision")
const digits = optNum("toPrecision", args[0])
const digits = optNum(args[0])
return digits === undefined ? value.toString() : value.toPrecision(digits)
},
],
@@ -71,7 +59,7 @@ export const numberGlobal = <R>(ctx: Interpreter<R>) => {
1,
(thisValue, args) => {
const value = self(thisValue, "toString")
const radix = optNum("toString", args[0])
const radix = optNum(args[0])
if (radix !== undefined && (radix < 2 || radix > 36)) {
throw rangeError("Number.toString radix must be between 2 and 36.")
}
+1 -9
View File
@@ -93,15 +93,7 @@ export const stringGlobal = <R>(ctx: Interpreter<R>) => {
const codeUnits = (name: string, op: (...codes: Array<number>) => string): Method => [
name,
1,
(_, args) =>
op(
...args.map((arg) => {
if (typeof arg !== "number") {
throw typeError(`String.${name} expects number arguments.`)
}
return arg
}),
),
(_, args) => op(...args.map(coerceToNumber)),
]
methods(builtins, string, [
codeUnits("fromCharCode", String.fromCharCode),
+1 -6
View File
@@ -1,5 +1,4 @@
import { fn } from "../interpreter/native.js"
import { typeError } from "../interpreter/model.js"
import { coerceToNumber, coerceToString, type Native, type Value } from "../interpreter/objects.js"
import type { Interpreter } from "../interpreter/interpreter.js"
@@ -20,11 +19,7 @@ const coerce = <R>(ctx: Interpreter<R>, name: Coercion, args: Array<Value>): Val
if (name === "isFinite") return Number.isFinite(coerceToNumber(raw))
if (name === "isNaN") return Number.isNaN(coerceToNumber(raw))
if (name === "parseInt") {
const radix = args[1]
if (radix !== undefined && typeof radix !== "number") {
throw typeError("parseInt expects a numeric radix.")
}
return parseInt(coerceToString(raw), radix)
return parseInt(coerceToString(raw), coerceToNumber(args[1]))
}
if (name === "parseFloat") return parseFloat(coerceToString(raw))
return coerceToString(raw)
+42 -31
View File
@@ -211,6 +211,32 @@ const termForms = (term: string): Array<string> => {
return forms
}
const rank = (entries: ReadonlyArray<SearchEntry>, query: string): Array<SearchEntry> => {
const terms = tokenize(query).map(termForms)
return entries
.map((entry) => {
const path = entry.description.path.toLowerCase()
const description = entry.description.description.toLowerCase()
const score = terms.reduce(
(total, forms) =>
total +
(forms.some((form) => path === form || path.endsWith(`.${form}`)) ? 20 : 0) +
(forms.some((form) => entry.pathWords.includes(form)) ? 12 : 0) +
(forms.some((form) => path.includes(form)) ? 8 : 0) +
(forms.some((form) => description.includes(form)) ? 4 : 0) +
(forms.some((form) => entry.searchText.includes(form)) ? 2 : 0),
0,
)
return { entry, score }
})
.filter(({ score }) => terms.length === 0 || score > 0)
.sort(
(left, right) =>
right.score - left.score || compareText(left.entry.description.path, right.entry.description.path),
)
.map(({ entry }) => entry)
}
const makeSearchTool = (searchIndex: ReadonlyArray<SearchEntry>): Tool => ({
_tag: "CodeModeTool",
description: "Search available tools",
@@ -238,32 +264,7 @@ const makeSearchTool = (searchIndex: ReadonlyArray<SearchEntry>): Tool => ({
: scoped.find(
(entry) => entry.description.path === pathQuery || toolExpression(entry.description.path) === trimmed,
)
const terms = tokenize(query).map(termForms)
const ranked =
exact !== undefined
? [exact]
: scoped
.map((entry) => {
const path = entry.description.path.toLowerCase()
const description = entry.description.description.toLowerCase()
const score = terms.reduce(
(total, forms) =>
total +
(forms.some((form) => path === form || path.endsWith(`.${form}`)) ? 20 : 0) +
(forms.some((form) => entry.pathWords.includes(form)) ? 12 : 0) +
(forms.some((form) => path.includes(form)) ? 8 : 0) +
(forms.some((form) => description.includes(form)) ? 4 : 0) +
(forms.some((form) => entry.searchText.includes(form)) ? 2 : 0),
0,
)
return { entry, score }
})
.filter(({ score }) => terms.length === 0 || score > 0)
.sort(
(left, right) =>
right.score - left.score || compareText(left.entry.description.path, right.entry.description.path),
)
.map(({ entry }) => entry)
const ranked = exact !== undefined ? [exact] : rank(scoped, query)
const items = ranked.slice(offset, offset + (request.limit ?? defaultSearchLimit)).map(({ description }) => ({
...description,
path: toolExpression(description.path),
@@ -329,13 +330,23 @@ const namespaceKeys = <R>(root: ToolNode<R>, path: ReadonlyArray<string>): Reado
return Array.from(node.children.keys())
}
const resolve = <R>(root: ToolNode<R>, path: ReadonlyArray<string>): Tool<R> => {
const resolve = <R>(root: ToolNode<R>, path: ReadonlyArray<string>, index: ReadonlyArray<SearchEntry>): Tool<R> => {
const segments = canonicalSegments(path)
const node = lookup(root, segments)
if (node === undefined) {
throw new ToolRuntimeError("UnknownTool", `Unknown tool '${segments.join(".")}'.`, [
"The tool may have been removed or renamed. Use search to find available tools.",
])
const name = segments.join(".")
const ns = segments.length > 1 && root.children.has(segments[0]) ? segments[0] : undefined
const closest = rank(
ns ? index.filter((entry) => entry.description.path.startsWith(`${ns}.`)) : index,
ns ? segments.slice(1).join(" ") : name,
)[0]
throw new ToolRuntimeError(
"UnknownTool",
closest
? `Unknown tool '${name}'. Did you mean ${toolExpression(closest.description.path)}?`
: `Unknown tool '${name}'.`,
["Use search to find available tools."],
)
}
if (node.tool === undefined) {
throw new ToolRuntimeError("UnknownTool", `Tool '${segments.join(".")}' is not callable.`)
@@ -440,7 +451,7 @@ export const make = <R>(
// Models often write `tools.search(...)` for the bare `search(...)`; honor it unless a tool owns that path.
if (segments.length === 1 && segments[0] === "search" && lookup(root, segments) === undefined)
return executeTool("search", makeSearchTool(prepared.searchIndex), args)
return executeTool(segments.join("."), resolve(root, path), args)
return executeTool(segments.join("."), resolve(root, path, prepared.searchIndex), args)
}),
}
}
+2 -4
View File
@@ -271,10 +271,8 @@ describe("still-rejected callables get the wrap hint", () => {
test("built-in references work as replacers", async () => {
// Like real JS: JSON.stringify(match, offset, string) quotes the match.
expect(await value(`return "abc".replace(/b/, JSON.stringify)`)).toBe('a"b"c')
// Math methods stay strict about consumed arguments: a match string is not coerced.
expect((await error(`return "3.7".replace(/\\d\\.\\d/, Math.floor)`)).message).toContain(
"Math.floor expects number arguments",
)
// Math methods coerce the match string, as in JS.
expect(await value(`return "3.7".replace(/\\d\\.\\d/, Math.floor)`)).toBe("3")
})
test("non-callables still get the plain callback error", async () => {
@@ -23,6 +23,27 @@ const value = async (code: string) => {
}
describe("confined generators", () => {
test("parameters are bound at the call; only the body waits for next()", async () => {
expect(
await value(`
const log = []
function* g(a = log.push("param")) { log.push("body"); yield a }
const it = g()
log.push("created")
it.next()
function* bad({ x } = null) { yield x }
async function* asyncBad({ x } = null) { yield x }
let failures = []
try { bad() } catch (error) { failures.push(error.constructor.name) }
try { asyncBad() } catch (error) { failures.push(error.constructor.name) }
return [log, failures]
`),
).toEqual([
["param", "created", "body"],
["TypeError", "TypeError"],
])
})
// test/built-ins/GeneratorPrototype/next/return-yield-expr.js
test("is lazy and preserves next(value), nested suspension, return, and exhaustion", async () => {
expect(
+3 -3
View File
@@ -37,7 +37,7 @@ describe("one built-in cannot build an unbounded value", () => {
"RangeError: Invalid string length",
)
}
}, 30_000)
})
// split and matchAll share the same result check but must build ten million items first to reach it,
// which is too slow for CI.
@@ -54,7 +54,7 @@ describe("one built-in cannot build an unbounded value", () => {
"RangeError: Invalid array length",
)
}
}, 30_000)
})
test("promises: too many pending at once, while settled ones do not count", async () => {
const n = MAX_PENDING_PROMISES
@@ -67,5 +67,5 @@ describe("one built-in cannot build an unbounded value", () => {
expect(await failure(`await Promise.all(Array(${n + 1}).fill(0).map(() => new Promise(() => {})))`)).toContain(
"Too many pending promises",
)
}, 30_000)
})
})
+54 -3
View File
@@ -218,6 +218,19 @@ describe("property deletion", () => {
).toEqual([true, true, { keep: 1 }])
})
test("a non-reference operand is evaluated and the result is true; a variable cannot be deleted", async () => {
expect(
await value(`
let called = false
const results = [delete 0, delete null, delete { x: 1 }, delete void 0, delete (() => { called = true })()]
let variable = 1
let failure
try { delete variable } catch (error) { failure = error.constructor.name }
return [results, called, failure]
`),
).toEqual([[true, true, true, true, true], true, "TypeError"])
})
test("evaluates computed object and key expressions once", async () => {
expect(
await value(`
@@ -771,9 +784,17 @@ describe("destructuring assignment", () => {
).toEqual({ declared: "a", declarationRest: { 1: "b" }, assigned: "c", assignmentRest: { 1: "d" } })
})
test("rejects computed keys that are not confined property keys", async () => {
const err = await error(`const key = {}; const { [key]: value } = {}`)
expect(err.message).toContain("Property key must be a string or number")
test("computed keys of any type become their string form, as in JS", async () => {
expect(
await value(`
const counts = {}
for (const category of ["a", null, undefined, "a", true, 1.5]) counts[category] = (counts[category] ?? 0) + 1
const key = {}
const { [key]: value } = { "[object Object]": 7 }
const o = { null: 1, "1,2": 2 }
return [counts, value, o[null], o[[1, 2]], undefined in o]
`),
).toEqual([{ a: 2, null: 1, undefined: 1, true: 1, "1.5": 1 }, 7, 1, 2, false])
})
})
@@ -818,6 +839,36 @@ describe("coercion parity: global isFinite and isNaN", () => {
})
})
describe("coercion parity: built-in arguments coerce as in JS", () => {
test("numeric arguments apply ToIntegerOrInfinity", async () => {
expect(
await value(`
return [
[1, 2, 3].indexOf(2, "1"), [1, 2, 3].lastIndexOf(3, "5"), [1, 2, 3].includes(1, "1"),
[1, 2, 3, 4].slice("1", "3"), [1, 2, 3].at(null), [1, 2, 3].at(1.7),
[1, [2, [3]]].flat(1.9), [1, 2, 3].with(1.5, 9), [1, 2, 3, 4].splice("1", "2"),
Math.max("3", "2"), Math.floor(null), Math.hypot("3", "4"),
parseInt("11", "2"), Number.parseInt("ff", "16"), (1.5).toFixed("2"), (255).toString("16"),
String.fromCharCode("65", 66.9), new Uint8Array([1, 2, 3]).indexOf(2, "1"),
]
`),
).toEqual([1, 2, false, [2, 3], 1, 2, [1, 2, [3]], [1, 9, 3], [2, 3], 3, 0, 5, 3, 255, "1.50", "ff", "AB", 1])
})
test("join separators, JSON.parse text, and Array.from length coerce", async () => {
expect(
await value(`
return [
[1, 2].join(null), [1, 2].join(0), [1, 2].join(undefined), new Uint8Array([1, 2]).join(null),
JSON.parse(123), JSON.parse(true),
Array.from({ length: "2" }), Array.from({ length: 2.5 }), Array.from({ length: -1 }), Array.from({}),
]
`),
).toEqual(["1null2", "102", "1,2", "1null2", 123, true, [null, null], [null, null], [], []])
expect((await error(`return JSON.parse(undefined)`)).message).toContain("JSON")
})
})
describe("coercion parity: arrays coerce to numbers through their string form", () => {
test("arrays with objects become NaN instead of crashing on host ToPrimitive", async () => {
expect(await value(`let x = [{}]; x++; return Number.isNaN(x)`)).toBe(true)
+13
View File
@@ -947,6 +947,19 @@ describe("Map", () => {
})
describe("Set", () => {
test("forEach is live on Map and Set: deleted entries are skipped and added ones visited", async () => {
expect(
await value(`
const m = new Map([[1, "a"], [2, "b"]])
const s = new Set([1, 2])
const seen = []
m.forEach((v, k) => { seen.push(k); if (k === 1) { m.delete(2); m.set(3, "c") } })
s.forEach((v) => { seen.push(v); if (v === 1) { s.delete(2); s.add(3) } })
return seen
`),
).toEqual([1, 3, 1, 3])
})
test("add/has/delete/size with chaining", async () => {
expect(
await value(`
-140
View File
@@ -1,22 +1,17 @@
built-ins/Array/prototype/at/index-argument-tointeger.js # Array.at expects index to be a number.
built-ins/Array/prototype/at/index-non-numeric-argument-tointeger.js # Array.at expects index to be a number.
built-ins/Array/prototype/concat/S15.4.4.4_A2_T1.js # Array.prototype.concat called on incompatible receiver a data object.
built-ins/Array/prototype/concat/S15.4.4.4_A2_T2.js # Array.prototype.concat called on incompatible receiver a data object.
built-ins/Array/prototype/concat/S15.4.4.4_A3_T1.js # arr.hasOwnProperty("1") must return true Expected SameValue(«false», «true») to be true
built-ins/Array/prototype/concat/S15.4.4.4_A3_T2.js # b.hasOwnProperty("2") must return true Expected SameValue(«false», «true») to be true
built-ins/Array/prototype/concat/S15.4.4.4_A3_T3.js # b.hasOwnProperty("2") must return true Expected SameValue(«false», «true») to be true
built-ins/Array/prototype/concat/create-ctor-non-object.js # a.concat() throws a TypeError exception Expected a TypeError to be thrown but no exception was throw
built-ins/Array/prototype/copyWithin/coerced-values-end.js # Array.copyWithin expects end to be a number.
built-ins/Array/prototype/copyWithin/coerced-values-start-change-target.js # Array.copyWithin expects start to be a number.
built-ins/Array/prototype/copyWithin/coerced-values-start.js # Array.copyWithin expects start to be a number.
built-ins/Array/prototype/copyWithin/coerced-values-target.js # Array.copyWithin expects target index to be a number.
built-ins/Array/prototype/copyWithin/return-abrupt-from-end.js # Expected a Test262Error but got a TypeError
built-ins/Array/prototype/copyWithin/return-abrupt-from-start.js # Expected a Test262Error but got a TypeError
built-ins/Array/prototype/copyWithin/return-abrupt-from-target.js # Expected a Test262Error but got a TypeError
built-ins/Array/prototype/every/15.4.4.16-7-6.js # res Expected SameValue(«true», «false») to be true
built-ins/Array/prototype/every/15.4.4.16-7-c-i-8.js # [, , , ].every(callbackfn) Expected SameValue(«true», «false») to be true
built-ins/Array/prototype/every/15.4.4.16-8-10.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/every/15.4.4.16-8-13.js # Property key must be a string or number, or Symbol.asyncIterator/Symbol.iterator.
built-ins/Array/prototype/every/15.4.4.16-8-2.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/every/15.4.4.16-8-3.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/every/15.4.4.16-8-4.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
@@ -24,11 +19,9 @@ built-ins/Array/prototype/every/15.4.4.16-8-5.js # … cannot be constructed: u
built-ins/Array/prototype/every/15.4.4.16-8-6.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/every/15.4.4.16-8-7.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/every/15.4.4.16-8-8.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/fill/coerced-indexes.js # Array.fill expects start to be a number.
built-ins/Array/prototype/fill/return-abrupt-from-end.js # Expected a Test262Error but got a TypeError
built-ins/Array/prototype/fill/return-abrupt-from-start.js # Expected a Test262Error but got a TypeError
built-ins/Array/prototype/filter/15.4.4.20-10-3.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/filter/15.4.4.20-10-4.js # Property key must be a string or number, or Symbol.asyncIterator/Symbol.iterator.
built-ins/Array/prototype/filter/15.4.4.20-6-2.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/filter/15.4.4.20-6-3.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/filter/15.4.4.20-6-4.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
@@ -39,12 +32,10 @@ built-ins/Array/prototype/filter/15.4.4.20-6-8.js # … cannot be constructed:
built-ins/Array/prototype/filter/15.4.4.20-9-6.js # resArr.length Expected SameValue(«3», «4») to be true
built-ins/Array/prototype/filter/15.4.4.20-9-c-i-8.js # newArr.length Expected SameValue(«0», «1») to be true
built-ins/Array/prototype/filter/create-ctor-non-object.js # null value Expected a TypeError to be thrown but no exception was thrown at all
built-ins/Array/prototype/flat/non-numeric-depth-should-not-throw.js # Array.flat expects depth to be a number.
built-ins/Array/prototype/flat/non-object-ctor-throws.js # null value Expected a TypeError to be thrown but no exception was thrown at all
built-ins/Array/prototype/forEach/15.4.4.18-7-5.js # callCnt Expected SameValue(«4», «5») to be true
built-ins/Array/prototype/forEach/15.4.4.18-7-c-i-8.js # testResult !== true
built-ins/Array/prototype/forEach/15.4.4.18-8-10.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/forEach/15.4.4.18-8-12.js # Property key must be a string or number, or Symbol.asyncIterator/Symbol.iterator.
built-ins/Array/prototype/forEach/15.4.4.18-8-2.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/forEach/15.4.4.18-8-3.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/forEach/15.4.4.18-8-4.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
@@ -53,61 +44,33 @@ built-ins/Array/prototype/forEach/15.4.4.18-8-6.js # … cannot be constructed:
built-ins/Array/prototype/forEach/15.4.4.18-8-7.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/forEach/15.4.4.18-8-8.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/forEach/15.4.4.18-8-9.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/includes/length-zero-returns-false.js # Array.includes expects a value and optional start index.
built-ins/Array/prototype/includes/no-arg.js # Array.includes expects a value and optional start index.
built-ins/Array/prototype/includes/return-abrupt-tointeger-fromindex.js # Expected a Test262Error but got a TypeError
built-ins/Array/prototype/includes/tointeger-fromindex.js # Array.includes expects start index to be a number.
built-ins/Array/prototype/indexOf/15.4.4.14-5-1.js # Array.indexOf expects start index to be a number.
built-ins/Array/prototype/indexOf/15.4.4.14-5-15.js # Array.indexOf expects start index to be a number.
built-ins/Array/prototype/indexOf/15.4.4.14-5-16.js # Array.indexOf expects start index to be a number.
built-ins/Array/prototype/indexOf/15.4.4.14-5-17.js # Array.indexOf expects start index to be a number.
built-ins/Array/prototype/indexOf/15.4.4.14-5-18.js # Array.indexOf expects start index to be a number.
built-ins/Array/prototype/indexOf/15.4.4.14-5-19.js # Array.indexOf expects start index to be a number.
built-ins/Array/prototype/indexOf/15.4.4.14-5-20.js # Array.indexOf expects start index to be a number.
built-ins/Array/prototype/indexOf/15.4.4.14-5-21.js # Array.indexOf expects start index to be a number.
built-ins/Array/prototype/indexOf/15.4.4.14-5-22.js # Array.indexOf expects start index to be a number.
built-ins/Array/prototype/indexOf/15.4.4.14-5-23.js # Array.indexOf expects start index to be a number.
built-ins/Array/prototype/indexOf/15.4.4.14-5-24.js # toStringAccessed
built-ins/Array/prototype/indexOf/15.4.4.14-5-25.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/indexOf/15.4.4.14-5-3.js # Array.indexOf expects start index to be a number.
built-ins/Array/prototype/indexOf/15.4.4.14-5-5.js # Array.indexOf expects start index to be a number.
built-ins/Array/prototype/indexOf/15.4.4.14-9-7.js # Array assignment result contains a circular value.
built-ins/Array/prototype/indexOf/15.4.4.14-9-a-3.js # Array.indexOf expects start index to be a number.
built-ins/Array/prototype/indexOf/15.4.4.14-9-a-6.js # Array.indexOf expects start index to be a number.
built-ins/Array/prototype/indexOf/15.4.4.14-9-b-i-7.js # [, , , ].indexOf(true) Expected SameValue(«-1», «0») to be true
built-ins/Array/prototype/indexOf/length-zero-returns-minus-one.js # Array.indexOf expects start index to be a number.
built-ins/Array/prototype/join/S15.4.4.5_A1.2_T2.js # Array.join expects zero arguments or one string separator.
built-ins/Array/prototype/join/S15.4.4.5_A2_T1.js # Array.prototype.join called on incompatible receiver a data object.
built-ins/Array/prototype/join/S15.4.4.5_A2_T4.js # Array.prototype.join called on incompatible receiver a data object.
built-ins/Array/prototype/join/S15.4.4.5_A3.1_T1.js # Array.join expects zero arguments or one string separator.
built-ins/Array/prototype/join/S15.4.4.5_A3.1_T2.js # Array.join expects zero arguments or one string separator.
built-ins/Array/prototype/join/S15.4.4.5_A3.2_T2.js # x.join() must return "*" Expected SameValue(«"[object Object]"», «"*"») to be true
built-ins/Array/prototype/join/S15.4.4.5_A4_T3.js # Array.prototype.join called on incompatible receiver a data object.
built-ins/Array/prototype/join/S15.4.4.5_A5_T1.js # #1: Array.prototype[1] = 1; x = [0]; x.length = 2; x.join() === "0,1". Actual: 0,
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-1.js # Array.lastIndexOf expects start index to be a number.
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-15.js # Array.lastIndexOf expects start index to be a number.
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-16.js # Array.lastIndexOf expects start index to be a number.
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-17.js # Array.lastIndexOf expects start index to be a number.
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-18.js # Array.lastIndexOf expects start index to be a number.
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-19.js # Array.lastIndexOf expects start index to be a number.
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-20.js # Array.lastIndexOf expects start index to be a number.
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-21.js # Array.lastIndexOf expects start index to be a number.
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-22.js # Array.lastIndexOf expects start index to be a number.
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-23.js # Array.lastIndexOf expects start index to be a number.
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-24.js # toStringAccessed
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-25.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-3.js # Array.lastIndexOf expects start index to be a number.
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-4.js # a.lastIndexOf(2,undefined) Expected SameValue(«1», «-1») to be true
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-5.js # Array.lastIndexOf expects start index to be a number.
built-ins/Array/prototype/lastIndexOf/15.4.4.15-8-7.js # Array assignment result contains a circular value.
built-ins/Array/prototype/lastIndexOf/15.4.4.15-8-a-3.js # Array.lastIndexOf expects start index to be a number.
built-ins/Array/prototype/lastIndexOf/15.4.4.15-8-a-6.js # Array.lastIndexOf expects start index to be a number.
built-ins/Array/prototype/lastIndexOf/15.4.4.15-8-b-i-7.js # [, , , ].lastIndexOf(true) Expected SameValue(«-1», «0») to be true
built-ins/Array/prototype/lastIndexOf/length-zero-returns-minus-one.js # Array.lastIndexOf expects start index to be a number.
built-ins/Array/prototype/map/15.4.4.19-8-6.js # resArr[4] Expected SameValue(«undefined», «1») to be true
built-ins/Array/prototype/map/15.4.4.19-8-c-i-8.js # newArr[1] Expected SameValue(«13», «true») to be true
built-ins/Array/prototype/map/15.4.4.19-9-3.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/map/15.4.4.19-9-4.js # Property key must be a string or number, or Symbol.asyncIterator/Symbol.iterator.
built-ins/Array/prototype/map/create-ctor-non-object.js # null value Expected a TypeError to be thrown but no exception was thrown at all
built-ins/Array/prototype/pop/S15.4.4.6_A2_T1.js # Array.prototype.pop called on incompatible receiver a data object.
built-ins/Array/prototype/pop/S15.4.4.6_A2_T4.js # Array.prototype.pop called on incompatible receiver a data object.
@@ -127,7 +90,6 @@ built-ins/Array/prototype/reduce/15.4.4.21-10-3.js # … cannot be constructed:
built-ins/Array/prototype/reduce/15.4.4.21-10-4.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/reduce/15.4.4.21-10-6.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/reduce/15.4.4.21-10-7.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/reduce/15.4.4.21-10-8.js # Property key must be a string or number, or Symbol.asyncIterator/Symbol.iterator.
built-ins/Array/prototype/reduce/15.4.4.21-5-2.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/reduce/15.4.4.21-5-3.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/reduce/15.4.4.21-5-4.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
@@ -151,7 +113,6 @@ built-ins/Array/prototype/reduceRight/15.4.4.22-10-3.js # … cannot be constru
built-ins/Array/prototype/reduceRight/15.4.4.22-10-4.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/reduceRight/15.4.4.22-10-6.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/reduceRight/15.4.4.22-10-7.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/reduceRight/15.4.4.22-10-8.js # Property key must be a string or number, or Symbol.asyncIterator/Symbol.iterator.
built-ins/Array/prototype/reduceRight/15.4.4.22-5-2.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/reduceRight/15.4.4.22-5-3.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/reduceRight/15.4.4.22-5-4.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
@@ -181,7 +142,6 @@ built-ins/Array/prototype/shift/S15.4.4.9_A2_T5.js # Array.prototype.shift call
built-ins/Array/prototype/shift/S15.4.4.9_A3_T3.js # Array.prototype.shift called on incompatible receiver a data object.
built-ins/Array/prototype/shift/S15.4.4.9_A4_T1.js # #2: Array.prototype[1] = 1; x = [0]; x.length = 2; x.shift(); x[0] === 1. Actual: undefined
built-ins/Array/prototype/shift/S15.4.4.9_A4_T2.js # Array.prototype.shift called on incompatible receiver a data object.
built-ins/Array/prototype/slice/S15.4.4.10_A2.1_T5.js # Array.slice expects start to be a number.
built-ins/Array/prototype/slice/S15.4.4.10_A2.2_T5.js # Array.slice expects end to be a number.
built-ins/Array/prototype/slice/S15.4.4.10_A2_T1.js # Array.prototype.slice called on incompatible receiver a data object.
built-ins/Array/prototype/slice/S15.4.4.10_A2_T2.js # Array.prototype.slice called on incompatible receiver a data object.
@@ -197,7 +157,6 @@ built-ins/Array/prototype/slice/create-ctor-non-object.js # null value Expected
built-ins/Array/prototype/some/15.4.4.17-7-6.js # res Expected SameValue(«false», «true») to be true
built-ins/Array/prototype/some/15.4.4.17-7-c-i-8.js # [, ].some(callbackfn) !== true
built-ins/Array/prototype/some/15.4.4.17-8-10.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/some/15.4.4.17-8-13.js # Property key must be a string or number, or Symbol.asyncIterator/Symbol.iterator.
built-ins/Array/prototype/some/15.4.4.17-8-2.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/some/15.4.4.17-8-3.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
built-ins/Array/prototype/some/15.4.4.17-8-4.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
@@ -213,7 +172,6 @@ built-ins/Array/prototype/sort/S15.4.4.11_A4_T3.js # Array.prototype.sort calle
built-ins/Array/prototype/sort/S15.4.4.11_A6_T2.js # Array.prototype.sort called on incompatible receiver a data object.
built-ins/Array/prototype/sort/bug_596_1.js # #1: [object, object].sort(); counter < 2. Actual: 0
built-ins/Array/prototype/sort/precise-prototype-element.js # Expected SameValue(«undefined», «4») to be true
built-ins/Array/prototype/splice/S15.4.4.12_A2.1_T5.js # Array.splice expects start to be a number.
built-ins/Array/prototype/splice/S15.4.4.12_A2.2_T5.js # Array.splice expects delete count to be a number.
built-ins/Array/prototype/splice/S15.4.4.12_A2_T1.js # Array.prototype.splice called on incompatible receiver a data object.
built-ins/Array/prototype/splice/S15.4.4.12_A2_T2.js # Array.prototype.splice called on incompatible receiver a data object.
@@ -241,53 +199,8 @@ built-ins/Iterator/prototype/drop/limit-tonumber-throws.js # Expected a Test262
built-ins/Iterator/prototype/drop/limit-tonumber.js # Iterator.prototype.drop expects a non-negative count, received NaN.
built-ins/Iterator/prototype/take/limit-tonumber-throws.js # Expected a Test262Error but got a RangeError
built-ins/Iterator/prototype/take/limit-tonumber.js # Iterator.prototype.take expects a non-negative count, received NaN.
language/statements/async-generator/dflt-params-abrupt.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dflt-params-ref-later.js # Expected a ReferenceError to be thrown but no exception was thrown at all
language/statements/async-generator/dflt-params-ref-self.js # Expected a ReferenceError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/ary-init-iter-get-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/ary-ptrn-elem-ary-val-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/ary-ptrn-elem-id-init-throws.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/ary-ptrn-elem-id-init-unresolvable.js # Expected a ReferenceError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/ary-ptrn-elem-id-iter-step-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/ary-ptrn-elem-obj-val-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/ary-ptrn-elem-obj-val-undef.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/ary-ptrn-elision-step-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/ary-ptrn-rest-id-elision-next-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/ary-ptrn-rest-id-iter-step-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-ary-init-iter-get-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-ary-ptrn-elem-ary-val-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-ary-ptrn-elem-id-init-throws.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-ary-ptrn-elem-id-init-unresolvable.js # Expected a ReferenceError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-ary-ptrn-elem-id-iter-step-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-ary-ptrn-elem-obj-val-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-ary-ptrn-elem-obj-val-undef.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-ary-ptrn-elision-step-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-ary-ptrn-rest-id-elision-next-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-ary-ptrn-rest-id-iter-step-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-obj-init-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-obj-init-undefined.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-obj-ptrn-id-init-throws.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-obj-ptrn-id-init-unresolvable.js # Expected a ReferenceError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-obj-ptrn-list-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-obj-ptrn-prop-ary-value-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-obj-ptrn-prop-eval-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-obj-ptrn-prop-id-init-throws.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-obj-ptrn-prop-id-init-unresolvable.js # Expected a ReferenceError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-obj-ptrn-prop-obj-value-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/dflt-obj-ptrn-prop-obj-value-undef.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/obj-init-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/obj-init-undefined.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/obj-ptrn-id-init-throws.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/obj-ptrn-id-init-unresolvable.js # Expected a ReferenceError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/obj-ptrn-list-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/obj-ptrn-prop-ary-value-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/obj-ptrn-prop-eval-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/obj-ptrn-prop-id-init-throws.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/obj-ptrn-prop-id-init-unresolvable.js # Expected a ReferenceError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/obj-ptrn-prop-obj-value-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/obj-ptrn-prop-obj-value-undef.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/async-generator/return-undefined-implicit-and-explicit.js # Actual ["tick 1", "tick 2", "g1 ret", "g2 ret", "g3 ret", "g4 ret"] and expected ["tick 1", "g1 ret"
language/statements/const/dstr/ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/for-await-of/async-func-decl-dstr-obj-empty-bool.js # TypeError: Object destructuring requires a data object or array value, received a boolean.
@@ -336,63 +249,10 @@ language/statements/function/S13.2_A1_T1.js # #1: __func.prototype !== undefine
language/statements/function/S13.2_A1_T2.js # #1: __func.prototype !== undefined
language/statements/function/S13.2_A4_T1.js # Unknown identifier '…'.
language/statements/function/S13.2_A4_T2.js # #1: typeof __gunc.prototype === '…'. Actual: typeof __gunc.prototype ===undefined
language/statements/function/S13_A3_T1.js # Unknown identifier '…'.
language/statements/function/S13_A6_T1.js # Identifier '…' has already been declared.
language/statements/function/S14_A2.js # Unknown identifier '…'.
language/statements/function/S14_A5_T1.js # Identifier '…' has already been declared.
language/statements/function/S14_A5_T2.js # Identifier '…' has already been declared.
language/statements/function/dstr/ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/function/dstr/dflt-ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/dflt-params-abrupt.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/generators/dflt-params-ref-later.js # Expected a ReferenceError to be thrown but no exception was thrown at all
language/statements/generators/dflt-params-ref-self.js # Expected a ReferenceError to be thrown but no exception was thrown at all
language/statements/generators/dstr/ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/dstr/ary-init-iter-get-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/generators/dstr/ary-ptrn-elem-ary-val-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/dstr/ary-ptrn-elem-id-init-throws.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/generators/dstr/ary-ptrn-elem-id-init-unresolvable.js # Expected a ReferenceError to be thrown but no exception was thrown at all
language/statements/generators/dstr/ary-ptrn-elem-id-iter-step-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/generators/dstr/ary-ptrn-elem-obj-val-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/dstr/ary-ptrn-elem-obj-val-undef.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/dstr/ary-ptrn-elision-step-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/generators/dstr/ary-ptrn-rest-id-elision-next-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/generators/dstr/ary-ptrn-rest-id-iter-step-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-ary-init-iter-get-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-ary-ptrn-elem-ary-val-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-ary-ptrn-elem-id-init-throws.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-ary-ptrn-elem-id-init-unresolvable.js # Expected a ReferenceError to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-ary-ptrn-elem-id-iter-step-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-ary-ptrn-elem-obj-val-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-ary-ptrn-elem-obj-val-undef.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-ary-ptrn-elision-step-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-ary-ptrn-rest-id-elision-next-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-ary-ptrn-rest-id-iter-step-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-obj-init-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-obj-init-undefined.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-obj-ptrn-id-init-throws.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-obj-ptrn-id-init-unresolvable.js # Expected a ReferenceError to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-obj-ptrn-list-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-obj-ptrn-prop-ary-value-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-obj-ptrn-prop-eval-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-obj-ptrn-prop-id-init-throws.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-obj-ptrn-prop-id-init-unresolvable.js # Expected a ReferenceError to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-obj-ptrn-prop-obj-value-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-obj-ptrn-prop-obj-value-undef.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/dstr/obj-init-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/dstr/obj-init-undefined.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/dstr/obj-ptrn-id-init-throws.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/generators/dstr/obj-ptrn-id-init-unresolvable.js # Expected a ReferenceError to be thrown but no exception was thrown at all
language/statements/generators/dstr/obj-ptrn-list-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/generators/dstr/obj-ptrn-prop-ary-value-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/dstr/obj-ptrn-prop-eval-err.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/generators/dstr/obj-ptrn-prop-id-init-throws.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/generators/dstr/obj-ptrn-prop-id-init-unresolvable.js # Expected a ReferenceError to be thrown but no exception was thrown at all
language/statements/generators/dstr/obj-ptrn-prop-obj-value-null.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/dstr/obj-ptrn-prop-obj-value-undef.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/has-instance.js # The right-hand side of '…' has no '…' object.
language/statements/generators/prototype-typeof.js # Expected SameValue(«"undefined"», «"object"») to be true
language/statements/generators/prototype-uniqueness.js # Expected true but got false
language/statements/labeled/value-await-non-module.js # Failed to parse TypeScript: Expression expected.
language/statements/let/dstr/ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/return/S12.9_A1_T1.js # expected SyntaxError but the program ran
+6 -3
View File
@@ -106,9 +106,12 @@ describe("callable namespaces", () => {
const diagnostic = await failure(runtime, `return await tools.issues.missing({})`)
expect(diagnostic.kind).toBe("UnknownTool")
expect(diagnostic.message).toContain("Unknown tool 'issues.missing'")
expect(diagnostic.suggestions).toEqual([
"The tool may have been removed or renamed. Use search to find available tools.",
])
expect(diagnostic.suggestions).toEqual(["Use search to find available tools."])
})
test("an unknown tool names the closest match", async () => {
const diagnostic = await failure(runtime, `return await tools.issues["get-list"]({})`)
expect(diagnostic.message).toBe("Unknown tool 'issues.get-list'. Did you mean tools.issues.list?")
})
test("a namespace without its own tool stays non-callable", async () => {
@@ -234,6 +234,44 @@ describe("var semantics beyond Test262", () => {
})
})
describe("function declarations and expressions", () => {
test("the last of repeated function declarations wins, and a var may share the name", async () => {
expect(
await value(`
function f() { return 1 }
const first = f()
function f() { return 2 }
var g = 1
function g() { return 3 }
return [first, f(), typeof g]
`),
).toEqual([2, 2, "number"])
})
test("a named function expression sees its own name inside its body, read-only", async () => {
expect(
await value(`
const fact = function inner(n) { return n <= 1 ? 1 : n * inner(n - 1) }
const reassign = function inner() { inner = 5 }
let failure
try { reassign() } catch (error) { failure = error.constructor.name }
return [fact(4), typeof inner, failure]
`),
).toEqual([24, "undefined", "TypeError"])
})
test("generator functions have their own prototype", async () => {
expect(
await value(`
function* g() {}
async function* ag() {}
function f() {}
return [g() instanceof g, ag() instanceof ag, g.prototype === ag.prototype, typeof g.prototype, typeof f.prototype]
`),
).toEqual([true, true, false, "object", "undefined"])
})
})
describe("switch case function hoisting", () => {
test("function declarations are visible across all cases before their statement runs", async () => {
expect(await value(`switch (1) { case 1: return foo(); function foo() { return "hoisted" } }`)).toBe("hoisted")
+11 -12
View File
@@ -254,7 +254,7 @@ export const dict = {
"go.title": "OpenCode Go | نماذج برمجة منخفضة التكلفة للجميع",
"go.banner.text": "يحصل DeepSeek V4 Flash على حدود استخدام مضاعفة لفترة محدودة",
"go.meta.description":
"يبدأ Go بسعر $5 للشهر الأول، ثم $10/شهر، مع حدود استخدام سخية ووصول موثوق إلى نماذج البرمجة الرائدة.",
"يبلغ سعر Go $10/شهر، مع حدود استخدام سخية ووصول موثوق إلى نماذج البرمجة الرائدة.",
"go.hero.title": "نماذج برمجة منخفضة التكلفة للجميع",
"go.hero.body":
"يجلب Go البرمجة الوكيلة للمبرمجين حول العالم. يوفر حدودًا سخية ووصولًا موثوقًا إلى أقوى النماذج مفتوحة المصدر، حتى تتمكن من البناء باستخدام وكلاء أقوياء دون القلق بشأن التكلفة أو التوفر.",
@@ -263,9 +263,8 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "اشترك في Go",
"go.cta.price": "$10/شهر",
"go.cta.promo": "$5 للشهر الأول",
"go.pricing.body":
"استخدمه مع أي وكيل. $5 للشهر الأول، ثم $10/شهر. قم بزيادة الرصيد إذا لزم الأمر. الإلغاء في أي وقت.",
"استخدمه مع أي وكيل. $10/شهر. قم بزيادة الرصيد إذا لزم الأمر. الإلغاء في أي وقت.",
"go.graph.free": "مجاني",
"go.graph.freePill": "Big Pickle ونماذج مجانية",
"go.graph.go": "Go",
@@ -296,20 +295,20 @@ export const dict = {
"go.testimonials.frank.quote": "أتمنى لو كنت لا أزال في Nvidia.",
"go.problem.title": "ما المشكلة التي يحلها Go؟",
"go.problem.body":
"نحن نركز على تقديم تجربة OpenCode لأكبر عدد ممكن من الناس. OpenCode Go هو اشتراك منخفض التكلفة: $5 للشهر الأول، ثم $10/شهر. يوفر حدودا سخية ووصولا موثوقا إلى نماذج المصدر المفتوح الأكثر قدرة.",
"نحن نركز على تقديم تجربة OpenCode لأكبر عدد ممكن من الناس. OpenCode Go هو اشتراك منخفض التكلفة بسعر $10/شهر. يوفر حدودا سخية ووصولا موثوقا إلى نماذج المصدر المفتوح الأكثر قدرة.",
"go.problem.subtitle": " ",
"go.problem.item1": "أسعار اشتراك منخفضة التكلفة",
"go.problem.item2": "حدود سخية ووصول موثوق",
"go.problem.item3": "مصمم لأكبر عدد ممكن من المبرمجين",
"go.problem.item4": "مجموعة منسقة من النماذج المختبرة للبرمجة الوكيلة",
"go.how.title": "كيف يعمل Go",
"go.how.body": "يبدأ Go من $5 للشهر الأول، ثم $10/شهر. يمكنك استخدامه مع OpenCode أو أي وكيل.",
"go.how.body": "يبلغ سعر Go $10/شهر. يمكنك استخدامه مع OpenCode أو أي وكيل.",
"go.how.step1.title": "أنشئ حسابًا",
"go.how.step1.beforeLink": "اتبع",
"go.how.step1.link": "تعليمات الإعداد",
"go.how.step2.title": "اشترك في Go",
"go.how.step2.link": "$5 للشهر الأول",
"go.how.step2.afterLink": "ثم $10/شهر مع حدود سخية",
"go.how.step2.link": "$10/شهر",
"go.how.step2.afterLink": "مع حدود سخية",
"go.how.step3.title": "ابدأ البرمجة",
"go.how.step3.body": "مع وصول موثوق لنماذج مفتوحة المصدر",
"go.privacy.title": "خصوصيتك مهمة بالنسبة لنا",
@@ -325,11 +324,11 @@ export const dict = {
"go.faq.a2": "يتضمن Go النماذج المدرجة أدناه، مع حدود سخية وإتاحة موثوقة.",
"go.faq.q3": "هل Go هو نفسه Zen؟",
"go.faq.a3":
"لا. يعتمد Zen على الدفع حسب الاستخدام، بينما يبدأ Go بسعر $5 للشهر الأول، ثم $10/شهر، مع حدود سخية ووصول موثوق إلى مجموعة منسقة من النماذج.",
"لا. يعتمد Zen على الدفع حسب الاستخدام، بينما يبلغ سعر Go $10/شهر، مع حدود سخية ووصول موثوق إلى مجموعة منسقة من النماذج.",
"go.faq.q4": "كم تكلفة Go؟",
"go.faq.a4.p1.beforePricing": "تكلفة Go",
"go.faq.a4.p1.pricingLink": "$5 للشهر الأول",
"go.faq.a4.p1.afterPricing": "ثم $10/شهر مع حدود سخية.",
"go.faq.a4.p1.pricingLink": "$10/شهر",
"go.faq.a4.p1.afterPricing": "مع حدود سخية.",
"go.faq.a4.p2.beforeAccount": "يمكنك إدارة اشتراكك في",
"go.faq.a4.p2.accountLink": "حسابك",
"go.faq.a4.p3": "ألغِ في أي وقت.",
@@ -671,8 +670,8 @@ export const dict = {
"workspace.lite.other.message":
"عضو آخر في مساحة العمل هذه مشترك بالفعل في OpenCode Go. يمكن لعضو واحد فقط لكل مساحة عمل الاشتراك.",
"workspace.lite.promo.description":
"يبدأ OpenCode Go بسعر {{price}}، ثم $10/شهر، ويوفر وصولا موثوقا لنماذج البرمجة المفتوحة الشهيرة مع حدود استخدام سخية.",
"workspace.lite.promo.price": "$5 للشهر الأول",
"يبلغ سعر OpenCode Go {{price}}، ويوفر وصولا موثوقا لنماذج البرمجة المفتوحة الشهيرة مع حدود استخدام سخية.",
"workspace.lite.promo.price": "$10/شهر",
"workspace.lite.promo.modelsTitle": "ما يتضمنه",
"workspace.lite.promo.footer":
"صُممت الخطة بشكل أساسي للمستخدمين الدوليين، وتوفر وصولًا عالميًا مستقرًا. قد تتغير الأسعار وحدود الاستخدام بينما نتعلم من الاستخدام المبكر والملاحظات.",
+11 -12
View File
@@ -258,7 +258,7 @@ export const dict = {
"go.title": "OpenCode Go | Modelos de codificação de baixo custo para todos",
"go.banner.text": "DeepSeek V4 Flash tem limites de uso 2x maiores por tempo limitado",
"go.meta.description":
"O Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos de uso e acesso confiável aos principais modelos de codificação.",
"O Go custa $10/mês, com limites generosos de uso e acesso confiável aos principais modelos de codificação.",
"go.hero.title": "Modelos de codificação de baixo custo para todos",
"go.hero.body":
"O Go traz a codificação com agentes para programadores em todo o mundo. Oferecendo limites generosos e acesso confiável aos modelos de código aberto mais capazes, para que você possa construir com agentes poderosos sem se preocupar com custos ou disponibilidade.",
@@ -267,9 +267,8 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "Assinar o Go",
"go.cta.price": "$10/mês",
"go.cta.promo": "$5 no primeiro 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.",
"Use com qualquer agente. $10/mês. Recarregue o crédito se necessário. Cancele a qualquer momento.",
"go.graph.free": "Grátis",
"go.graph.freePill": "Big Pickle e modelos gratuitos",
"go.graph.go": "Go",
@@ -301,7 +300,7 @@ export const dict = {
"go.testimonials.frank.quote": "Eu queria ainda estar na Nvidia.",
"go.problem.title": "Que problema o Go resolve?",
"go.problem.body":
"Estamos focados em levar a experiência do OpenCode para o maior número de pessoas possível. OpenCode Go é uma assinatura de baixo custo: $5 no primeiro mês, depois $10/mês. Oferece limites generosos e acesso confiável aos modelos open source mais capazes.",
"Estamos focados em levar a experiência do OpenCode para o maior número de pessoas possível. OpenCode Go é uma assinatura de baixo custo de $10/mês. Oferece limites generosos e acesso confiável aos modelos open source mais capazes.",
"go.problem.subtitle": " ",
"go.problem.item1": "Preço de assinatura de baixo custo",
"go.problem.item2": "Limites generosos e acesso confiável",
@@ -309,13 +308,13 @@ export const dict = {
"go.problem.item4": "Uma seleção de modelos testados para codificação com agentes",
"go.how.title": "Como o Go funciona",
"go.how.body":
"O Go começa em $5 no primeiro mês, depois $10/mês. Você pode usá-lo com o OpenCode ou qualquer agente.",
"O Go custa $10/mês. Você pode usá-lo com o OpenCode ou qualquer agente.",
"go.how.step1.title": "Crie uma conta",
"go.how.step1.beforeLink": "siga as",
"go.how.step1.link": "instruções de configuração",
"go.how.step2.title": "Assinar o Go",
"go.how.step2.link": "$5 no primeiro mês",
"go.how.step2.afterLink": "depois $10/mês com limites generosos",
"go.how.step2.link": "$10/mês",
"go.how.step2.afterLink": "com limites generosos",
"go.how.step3.title": "Comece a codificar",
"go.how.step3.body": "com acesso confiável a modelos de código aberto",
"go.privacy.title": "Sua privacidade é importante para nós",
@@ -332,11 +331,11 @@ export const dict = {
"go.faq.a2": "O Go inclui os modelos listados abaixo, com limites generosos e acesso confiável.",
"go.faq.q3": "O Go é o mesmo que o Zen?",
"go.faq.a3":
"Não. Zen é pay-as-you-go, enquanto o Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos e acesso confiável a uma seleção de modelos.",
"Não. Zen é pay-as-you-go, enquanto o Go custa $10/mês, com limites generosos e acesso confiável a uma seleção de modelos.",
"go.faq.q4": "Quanto custa o Go?",
"go.faq.a4.p1.beforePricing": "O Go custa",
"go.faq.a4.p1.pricingLink": "$5 no primeiro mês",
"go.faq.a4.p1.afterPricing": "depois $10/mês com limites generosos.",
"go.faq.a4.p1.pricingLink": "$10/mês",
"go.faq.a4.p1.afterPricing": "com limites generosos.",
"go.faq.a4.p2.beforeAccount": "Você pode gerenciar sua assinatura em sua",
"go.faq.a4.p2.accountLink": "conta",
"go.faq.a4.p3": "Cancele a qualquer momento.",
@@ -682,8 +681,8 @@ export const dict = {
"workspace.lite.other.message":
"Outro membro neste workspace já assina o OpenCode Go. Apenas um membro por workspace pode assinar.",
"workspace.lite.promo.description":
"O OpenCode Go começa em {{price}}, depois $10/mês, e oferece acesso confiável a modelos de codificação abertos populares com limites de uso generosos.",
"workspace.lite.promo.price": "$5 no primeiro mês",
"O OpenCode Go custa {{price}} e oferece acesso confiável a modelos de codificação abertos populares com limites de uso generosos.",
"workspace.lite.promo.price": "$10/mês",
"workspace.lite.promo.modelsTitle": "O que está incluído",
"workspace.lite.promo.footer":
"O plano foi desenvolvido principalmente para usuários internacionais e oferece acesso global estável. Os preços e limites de uso podem mudar à medida que aprendemos com o uso inicial e o feedback recebido.",
+11 -12
View File
@@ -256,7 +256,7 @@ export const dict = {
"go.title": "OpenCode Go | Kodningsmodeller til lav pris for alle",
"go.banner.text": "DeepSeek V4 Flash får fordoblet brugsgrænse i en begrænset periode",
"go.meta.description":
"Go starter ved $5 for den første måned, derefter $10/måned, med generøse brugsgrænser og pålidelig adgang til førende kodningsmodeller.",
"Go koster $10/måned, med generøse brugsgrænser og pålidelig adgang til førende kodningsmodeller.",
"go.hero.title": "Kodningsmodeller til lav pris for alle",
"go.hero.body":
"Go bringer agentisk kodning til programmører over hele verden. Med generøse grænser og pålidelig adgang til de mest kapable open source-modeller, så du kan bygge med kraftfulde agenter uden at bekymre dig om omkostninger eller tilgængelighed.",
@@ -265,9 +265,8 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "Abonner på Go",
"go.cta.price": "$10/måned",
"go.cta.promo": "$5 første 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.",
"Brug med enhver agent. $10/måned. Tank op med kredit efter behov. Afmeld når som helst.",
"go.graph.free": "Gratis",
"go.graph.freePill": "Big Pickle og gratis modeller",
"go.graph.go": "Go",
@@ -298,7 +297,7 @@ export const dict = {
"go.testimonials.frank.quote": "Jeg ville ønske, jeg stadig var hos Nvidia.",
"go.problem.title": "Hvilket problem løser Go?",
"go.problem.body":
"Vi fokuserer på at bringe OpenCode-oplevelsen ud til så mange som muligt. OpenCode Go er et lavprisabonnement: $5 for den første måned, derefter $10/måned. Det giver generøse grænser og pålidelig adgang til de mest kapable open source-modeller.",
"Vi fokuserer på at bringe OpenCode-oplevelsen ud til så mange som muligt. OpenCode Go er et lavprisabonnement til $10/måned. Det giver generøse grænser og pålidelig adgang til de mest kapable open source-modeller.",
"go.problem.subtitle": " ",
"go.problem.item1": "Lavpris abonnementspriser",
"go.problem.item2": "Generøse grænser og pålidelig adgang",
@@ -306,13 +305,13 @@ export const dict = {
"go.problem.item4": "Et kurateret modeludvalg testet til agentisk kodning",
"go.how.title": "Hvordan Go virker",
"go.how.body":
"Go starter ved $5 for den første måned, derefter $10/måned. Du kan bruge det med OpenCode eller enhver agent.",
"Go koster $10/måned. Du kan bruge det med OpenCode eller enhver agent.",
"go.how.step1.title": "Opret en konto",
"go.how.step1.beforeLink": "følg",
"go.how.step1.link": "opsætningsinstruktionerne",
"go.how.step2.title": "Abonner på Go",
"go.how.step2.link": "$5 første måned",
"go.how.step2.afterLink": "derefter $10/måned med generøse grænser",
"go.how.step2.link": "$10/måned",
"go.how.step2.afterLink": "med generøse grænser",
"go.how.step3.title": "Start kodning",
"go.how.step3.body": "med pålidelig adgang til open source-modeller",
"go.privacy.title": "Dit privatliv er vigtigt for os",
@@ -329,11 +328,11 @@ export const dict = {
"go.faq.a2": "Go inkluderer modellerne nedenfor med generøse grænser og pålidelig adgang.",
"go.faq.q3": "Er Go det samme som Zen?",
"go.faq.a3":
"Nej. Zen er pay-as-you-go, mens Go starter ved $5 for den første måned, derefter $10/måned, med generøse grænser og pålidelig adgang til et kurateret modeludvalg.",
"Nej. Zen er pay-as-you-go, mens Go koster $10/måned, med generøse grænser og pålidelig adgang til et kurateret modeludvalg.",
"go.faq.q4": "Hvad koster Go?",
"go.faq.a4.p1.beforePricing": "Go koster",
"go.faq.a4.p1.pricingLink": "$5 første måned",
"go.faq.a4.p1.afterPricing": "derefter $10/måned med generøse grænser.",
"go.faq.a4.p1.pricingLink": "$10/måned",
"go.faq.a4.p1.afterPricing": "med generøse grænser.",
"go.faq.a4.p2.beforeAccount": "Du kan administrere dit abonnement i din",
"go.faq.a4.p2.accountLink": "konto",
"go.faq.a4.p3": "Annuller til enhver tid.",
@@ -679,8 +678,8 @@ export const dict = {
"workspace.lite.other.message":
"Et andet medlem i dette workspace abonnerer allerede på OpenCode Go. Kun ét medlem pr. workspace kan abonnere.",
"workspace.lite.promo.description":
"OpenCode Go starter ved {{price}}, derefter $10/måned, og giver pålidelig adgang til populære åbne kodningsmodeller med generøse brugsgrænser.",
"workspace.lite.promo.price": "$5 for den første måned",
"OpenCode Go koster {{price}} og giver pålidelig adgang til populære åbne kodningsmodeller med generøse brugsgrænser.",
"workspace.lite.promo.price": "$10/måned",
"workspace.lite.promo.modelsTitle": "Hvad er inkluderet",
"workspace.lite.promo.footer":
"Planen er primært udviklet til internationale brugere og giver stabil adgang i hele verden. Priser og forbrugsgrænser kan ændre sig, efterhånden som vi lærer af de første brugserfaringer og tilbagemeldinger.",
+11 -12
View File
@@ -258,7 +258,7 @@ export const dict = {
"go.title": "OpenCode Go | Kostengünstige Coding-Modelle für alle",
"go.banner.text": "DeepSeek V4 Flash erhält für begrenzte Zeit 2x Nutzungslimits",
"go.meta.description":
"Go beginnt bei $5 für deinen ersten Monat, danach $10/Monat, mit großzügigen Nutzungslimits und zuverlässigem Zugang zu führenden Coding-Modellen.",
"Go kostet $10/Monat, mit großzügigen Nutzungslimits und zuverlässigem Zugang zu führenden Coding-Modellen.",
"go.hero.title": "Kostengünstige Coding-Modelle für alle",
"go.hero.body":
"Go bringt Agentic Coding zu Programmierern auf der ganzen Welt. Mit großzügigen Limits und zuverlässigem Zugang zu den leistungsfähigsten Open-Source-Modellen, damit du mit leistungsstarken Agenten entwickeln kannst, ohne dir Gedanken über Kosten oder Verfügbarkeit zu machen.",
@@ -267,9 +267,8 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "Go abonnieren",
"go.cta.price": "$10/Monat",
"go.cta.promo": "$5 im ersten Monat",
"go.pricing.body":
"Mit jedem Agenten nutzbar. $5 im ersten Monat, danach $10/Monat. Guthaben bei Bedarf aufladen. Jederzeit kündbar.",
"Mit jedem Agenten nutzbar. $10/Monat. Guthaben bei Bedarf aufladen. Jederzeit kündbar.",
"go.graph.free": "Kostenlos",
"go.graph.freePill": "Big Pickle und kostenlose Modelle",
"go.graph.go": "Go",
@@ -300,7 +299,7 @@ export const dict = {
"go.testimonials.frank.quote": "Ich wünschte, ich wäre noch bei Nvidia.",
"go.problem.title": "Welches Problem löst Go?",
"go.problem.body":
"Wir konzentrieren uns darauf, die OpenCode-Erfahrung so vielen Menschen wie möglich zugänglich zu machen. OpenCode Go ist ein kostengünstiges Abonnement: $5 im ersten Monat, danach $10/Monat. Es bietet großzügige Limits und zuverlässigen Zugang zu den leistungsfähigsten Open-Source-Modellen.",
"Wir konzentrieren uns darauf, die OpenCode-Erfahrung so vielen Menschen wie möglich zugänglich zu machen. OpenCode Go ist ein kostengünstiges Abonnement für $10/Monat. Es bietet großzügige Limits und zuverlässigen Zugang zu den leistungsfähigsten Open-Source-Modellen.",
"go.problem.subtitle": " ",
"go.problem.item1": "Kostengünstiges Abonnement",
"go.problem.item2": "Großzügige Limits und zuverlässiger Zugang",
@@ -308,13 +307,13 @@ export const dict = {
"go.problem.item4": "Eine kuratierte, für Agentic Coding getestete Modellauswahl",
"go.how.title": "Wie Go funktioniert",
"go.how.body":
"Go beginnt bei $5 für den ersten Monat, danach $10/Monat. Du kannst es mit OpenCode oder jedem Agenten nutzen.",
"Go kostet $10/Monat. Du kannst es mit OpenCode oder jedem Agenten nutzen.",
"go.how.step1.title": "Konto erstellen",
"go.how.step1.beforeLink": "folge den",
"go.how.step1.link": "Einrichtungsanweisungen",
"go.how.step2.title": "Go abonnieren",
"go.how.step2.link": "$5 im ersten Monat",
"go.how.step2.afterLink": "danach $10/Monat mit großzügigen Limits",
"go.how.step2.link": "$10/Monat",
"go.how.step2.afterLink": "mit großzügigen Limits",
"go.how.step3.title": "Loslegen mit Coding",
"go.how.step3.body": "mit zuverlässigem Zugang zu Open-Source-Modellen",
"go.privacy.title": "Deine Privatsphäre ist uns wichtig",
@@ -331,11 +330,11 @@ export const dict = {
"go.faq.a2": "Go umfasst die unten aufgeführten Modelle mit großzügigen Limits und zuverlässigem Zugriff.",
"go.faq.q3": "Ist Go dasselbe wie Zen?",
"go.faq.a3":
"Nein. Zen ist Pay-as-you-go, während Go bei $5 für deinen ersten Monat beginnt, danach $10/Monat, mit großzügigen Limits und zuverlässigem Zugang zu einer kuratierten Modellauswahl.",
"Nein. Zen ist Pay-as-you-go, während Go $10/Monat kostet, mit großzügigen Limits und zuverlässigem Zugang zu einer kuratierten Modellauswahl.",
"go.faq.q4": "Wie viel kostet Go?",
"go.faq.a4.p1.beforePricing": "Go kostet",
"go.faq.a4.p1.pricingLink": "$5 im ersten Monat",
"go.faq.a4.p1.afterPricing": "danach $10/Monat mit großzügigen Limits.",
"go.faq.a4.p1.pricingLink": "$10/Monat",
"go.faq.a4.p1.afterPricing": "mit großzügigen Limits.",
"go.faq.a4.p2.beforeAccount": "Du kannst dein Abonnement in deinem",
"go.faq.a4.p2.accountLink": "Konto verwalten",
"go.faq.a4.p3": "Jederzeit kündbar.",
@@ -681,8 +680,8 @@ export const dict = {
"workspace.lite.other.message":
"Ein anderes Mitglied in diesem Workspace hat OpenCode Go bereits abonniert. Nur ein Mitglied pro Workspace kann abonnieren.",
"workspace.lite.promo.description":
"OpenCode Go startet bei {{price}}, danach $10/Monat, und bietet zuverlässigen Zugang zu beliebten offenen Coding-Modellen mit großzügigen Nutzungslimits.",
"workspace.lite.promo.price": "$5 im ersten Monat",
"OpenCode Go kostet {{price}} und bietet zuverlässigen Zugang zu beliebten offenen Coding-Modellen mit großzügigen Nutzungslimits.",
"workspace.lite.promo.price": "$10/Monat",
"workspace.lite.promo.modelsTitle": "Was enthalten ist",
"workspace.lite.promo.footer":
"Der Plan richtet sich in erster Linie an internationale Nutzer und bietet stabilen weltweiten Zugriff. Preise und Nutzungslimits können sich ändern, wenn wir Erkenntnisse aus der ersten Nutzung und dem Feedback gewinnen.",
+11 -12
View File
@@ -255,7 +255,7 @@ export const dict = {
"go.title": "OpenCode Go | Low cost coding models for everyone",
"go.banner.text": "DeepSeek V4 Flash gets 2× usage limits for a limited time",
"go.meta.description":
"Go starts at $5 for your first month, then $10/month, with generous usage limits and reliable access to leading coding models.",
"Go costs $10/month, with generous usage limits and reliable access to leading coding models.",
"go.hero.title": "Low cost coding models for everyone",
"go.hero.body":
"Go brings agentic coding to programmers around the world. Offering generous limits and reliable access to the most capable open-source models, so you can build with powerful agents without worrying about cost or availability.",
@@ -264,8 +264,7 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "Subscribe to Go",
"go.cta.price": "$10/month",
"go.cta.promo": "$5 first month",
"go.pricing.body": "Use with any agent. $5 first month, then $10/month. Top up credit if needed. Cancel any time.",
"go.pricing.body": "Use with any agent. $10/month. Top up credit if needed. Cancel any time.",
"go.graph.free": "Free",
"go.graph.freePill": "Big Pickle and free models",
"go.graph.go": "Go",
@@ -297,20 +296,20 @@ export const dict = {
"go.testimonials.frank.quote": "I wish I was still at Nvidia.",
"go.problem.title": "What problem is Go solving?",
"go.problem.body":
"We're focused on bringing the OpenCode experience to as many people as possible. OpenCode Go is a low cost subscription: $5 for your first month, then $10/month. It provides generous limits and reliable access to the most capable open source models.",
"We're focused on bringing the OpenCode experience to as many people as possible. OpenCode Go is a low cost $10/month subscription. It provides generous limits and reliable access to the most capable open source models.",
"go.problem.subtitle": " ",
"go.problem.item1": "Low cost subscription pricing",
"go.problem.item2": "Generous limits and reliable access",
"go.problem.item3": "Built for as many programmers as possible",
"go.problem.item4": "A curated model lineup tested for agentic coding",
"go.how.title": "How Go works",
"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.body": "Go costs $10/month. You can use it with OpenCode or any agent.",
"go.how.step1.title": "Create an account",
"go.how.step1.beforeLink": "follow the",
"go.how.step1.link": "setup instructions",
"go.how.step2.title": "Subscribe to Go",
"go.how.step2.link": "$5 first month",
"go.how.step2.afterLink": "then $10/month with generous limits",
"go.how.step2.link": "$10/month",
"go.how.step2.afterLink": "with generous limits",
"go.how.step3.title": "Start coding",
"go.how.step3.body": "with reliable access to open-source models",
"go.privacy.title": "Your privacy is important to us",
@@ -327,11 +326,11 @@ export const dict = {
"go.faq.a2": "Go includes the models listed below, with generous limits and reliable access.",
"go.faq.q3": "Is Go the same as Zen?",
"go.faq.a3":
"No. Zen is pay-as-you-go, while Go starts at $5 for your first month, then $10/month, with generous limits and reliable access to a curated model lineup.",
"No. Zen is pay-as-you-go, while Go costs $10/month, with generous limits and reliable access to a curated model lineup.",
"go.faq.q4": "How much does Go cost?",
"go.faq.a4.p1.beforePricing": "Go costs",
"go.faq.a4.p1.pricingLink": "$5 first month",
"go.faq.a4.p1.afterPricing": "then $10/month with generous limits.",
"go.faq.a4.p1.pricingLink": "$10/month",
"go.faq.a4.p1.afterPricing": "with generous limits.",
"go.faq.a4.p2.beforeAccount": "You can manage your subscription in your",
"go.faq.a4.p2.accountLink": "account",
"go.faq.a4.p3": "Cancel any time.",
@@ -677,8 +676,8 @@ export const dict = {
"workspace.lite.other.message":
"Another member in this workspace is already subscribed to OpenCode Go. Only one member per workspace can subscribe.",
"workspace.lite.promo.description":
"OpenCode Go starts at {{price}}, then $10/month, and provides reliable access to popular open coding models with generous usage limits.",
"workspace.lite.promo.price": "$5 for your first month",
"OpenCode Go costs {{price}} and provides reliable access to popular open coding models with generous usage limits.",
"workspace.lite.promo.price": "$10/month",
"workspace.lite.promo.modelsTitle": "What's Included",
"workspace.lite.promo.footer":
"The plan is designed primarily for international users and provides stable global access. Pricing and usage limits may change as we learn from early usage and feedback.",
+11 -12
View File
@@ -259,7 +259,7 @@ export const dict = {
"go.title": "OpenCode Go | Modelos de programación de bajo coste para todos",
"go.banner.text": "DeepSeek V4 Flash tiene límites de uso 2x mayores por tiempo limitado",
"go.meta.description":
"Go comienza en $5 el primer mes, luego 10 $/mes, con límites de uso generosos y acceso fiable a modelos de programación líderes.",
"Go cuesta 10 $/mes, con límites de uso generosos y acceso fiable a modelos de programación líderes.",
"go.hero.title": "Modelos de programación de bajo coste para todos",
"go.hero.body":
"Go lleva la programación agéntica a programadores de todo el mundo. Ofrece límites generosos y acceso fiable a los modelos de código abierto más capaces, para que puedas crear con agentes potentes sin preocuparte por el coste o la disponibilidad.",
@@ -268,9 +268,8 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "Suscribirse a Go",
"go.cta.price": "10 $/mes",
"go.cta.promo": "$5 el primer 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.",
"Úsalo con cualquier agente. 10 $/mes. Recarga crédito si es necesario. Cancela en cualquier momento.",
"go.graph.free": "Gratis",
"go.graph.freePill": "Big Pickle y modelos gratuitos",
"go.graph.go": "Go",
@@ -302,20 +301,20 @@ export const dict = {
"go.testimonials.frank.quote": "Ojalá siguiera en Nvidia.",
"go.problem.title": "¿Qué problema resuelve Go?",
"go.problem.body":
"Nos enfocamos en llevar la experiencia de OpenCode a tantas personas como sea posible. OpenCode Go es una suscripción de bajo coste: $5 el primer mes, luego 10 $/mes. Proporciona límites generosos y acceso fiable a los modelos de código abierto más capaces.",
"Nos enfocamos en llevar la experiencia de OpenCode a tantas personas como sea posible. OpenCode Go es una suscripción de bajo coste de 10 $/mes. Proporciona límites generosos y acceso fiable a los modelos de código abierto más capaces.",
"go.problem.subtitle": " ",
"go.problem.item1": "Precios de suscripción de bajo coste",
"go.problem.item2": "Límites generosos y acceso fiable",
"go.problem.item3": "Creado para tantos programadores como sea posible",
"go.problem.item4": "Una selección de modelos probados para programación agéntica",
"go.how.title": "Cómo funciona Go",
"go.how.body": "Go comienza en $5 el primer mes, luego 10 $/mes. Puedes usarlo con OpenCode o cualquier agente.",
"go.how.body": "Go cuesta 10 $/mes. Puedes usarlo con OpenCode o cualquier agente.",
"go.how.step1.title": "Crear una cuenta",
"go.how.step1.beforeLink": "sigue las",
"go.how.step1.link": "instrucciones de configuración",
"go.how.step2.title": "Suscribirse a Go",
"go.how.step2.link": "$5 el primer mes",
"go.how.step2.afterLink": "luego 10 $/mes con límites generosos",
"go.how.step2.link": "10 $/mes",
"go.how.step2.afterLink": "con límites generosos",
"go.how.step3.title": "Empezar a programar",
"go.how.step3.body": "con acceso fiable a modelos de código abierto",
"go.privacy.title": "Tu privacidad es importante para nosotros",
@@ -332,11 +331,11 @@ export const dict = {
"go.faq.a2": "Go incluye los modelos que se indican abajo, con límites generosos y acceso confiable.",
"go.faq.q3": "¿Es Go lo mismo que Zen?",
"go.faq.a3":
"No. Zen es de pago por uso, mientras que Go comienza en $5 el primer mes, luego 10 $/mes, con límites generosos y acceso fiable a una selección de modelos.",
"No. Zen es de pago por uso, mientras que Go cuesta 10 $/mes, con límites generosos y acceso fiable a una selección de modelos.",
"go.faq.q4": "¿Cuánto cuesta Go?",
"go.faq.a4.p1.beforePricing": "Go cuesta",
"go.faq.a4.p1.pricingLink": "$5 el primer mes",
"go.faq.a4.p1.afterPricing": "luego 10 $/mes con límites generosos.",
"go.faq.a4.p1.pricingLink": "10 $/mes",
"go.faq.a4.p1.afterPricing": "con límites generosos.",
"go.faq.a4.p2.beforeAccount": "Puedes gestionar tu suscripción en tu",
"go.faq.a4.p2.accountLink": "cuenta",
"go.faq.a4.p3": "Cancela en cualquier momento.",
@@ -682,8 +681,8 @@ export const dict = {
"workspace.lite.other.message":
"Otro miembro de este espacio de trabajo ya está suscrito a OpenCode Go. Solo un miembro por espacio de trabajo puede suscribirse.",
"workspace.lite.promo.description":
"OpenCode Go comienza en {{price}}, luego $10/mes, y ofrece acceso confiable a modelos de codificación abiertos populares con límites de uso generosos.",
"workspace.lite.promo.price": "$5 el primer mes",
"OpenCode Go cuesta {{price}} y ofrece acceso confiable a modelos de codificación abiertos populares con límites de uso generosos.",
"workspace.lite.promo.price": "$10/mes",
"workspace.lite.promo.modelsTitle": "Qué incluye",
"workspace.lite.promo.footer":
"El plan está diseñado principalmente para usuarios internacionales y ofrece un acceso global estable. Los precios y los límites de uso pueden cambiar a medida que aprendemos del uso inicial y de los comentarios recibidos.",
+11 -12
View File
@@ -260,7 +260,7 @@ export const dict = {
"go.title": "OpenCode Go | Modèles de code à faible coût pour tous",
"go.banner.text": "DeepSeek V4 Flash bénéficie de limites dutilisation 2x supérieures pour une durée limitée",
"go.meta.description":
"Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites d'utilisation généreuses et un accès fiable aux principaux modèles de codage.",
"Go coûte 10 $/mois, avec des limites d'utilisation généreuses et un accès fiable aux principaux modèles de codage.",
"go.hero.title": "Modèles de code à faible coût pour tous",
"go.hero.body":
"Go apporte le codage agentique aux programmeurs du monde entier. Offrant des limites généreuses et un accès fiable aux modèles open source les plus capables, pour que vous puissiez construire avec des agents puissants sans vous soucier du coût ou de la disponibilité.",
@@ -269,9 +269,8 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "S'abonner à Go",
"go.cta.price": "10 $/mois",
"go.cta.promo": "$5 le premier 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.",
"Utilisez-le avec n'importe quel agent. 10 $/mois. Rechargez du crédit si nécessaire. Annulez à tout moment.",
"go.graph.free": "Gratuit",
"go.graph.freePill": "Big Pickle et modèles gratuits",
"go.graph.go": "Go",
@@ -302,7 +301,7 @@ export const dict = {
"go.testimonials.frank.quote": "J'aimerais être encore chez Nvidia.",
"go.problem.title": "Quel problème Go résout-il ?",
"go.problem.body":
"Nous nous efforçons d'apporter l'expérience OpenCode au plus grand nombre. OpenCode Go est un abonnement à faible coût : $5 pour le premier mois, puis 10 $/mois. Il offre des limites généreuses et un accès fiable aux modèles open source les plus performants.",
"Nous nous efforçons d'apporter l'expérience OpenCode au plus grand nombre. OpenCode Go est un abonnement à faible coût de 10 $/mois. Il offre des limites généreuses et un accès fiable aux modèles open source les plus performants.",
"go.problem.subtitle": " ",
"go.problem.item1": "Prix d'abonnement bas",
"go.problem.item2": "Limites généreuses et accès fiable",
@@ -310,13 +309,13 @@ export const dict = {
"go.problem.item4": "Une sélection de modèles testés pour le codage agentique",
"go.how.title": "Comment fonctionne Go",
"go.how.body":
"Go commence à $5 pour le premier mois, puis 10 $/mois. Vous pouvez l'utiliser avec OpenCode ou n'importe quel agent.",
"Go coûte 10 $/mois. Vous pouvez l'utiliser avec OpenCode ou n'importe quel agent.",
"go.how.step1.title": "Créez un compte",
"go.how.step1.beforeLink": "suivez les",
"go.how.step1.link": "instructions de configuration",
"go.how.step2.title": "Abonnez-vous à Go",
"go.how.step2.link": "$5 le premier mois",
"go.how.step2.afterLink": "puis 10 $/mois avec des limites généreuses",
"go.how.step2.link": "10 $/mois",
"go.how.step2.afterLink": "avec des limites généreuses",
"go.how.step3.title": "Commencez à coder",
"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",
@@ -333,11 +332,11 @@ export const dict = {
"go.faq.a2": "Go inclut les modèles ci-dessous, avec des limites généreuses et un accès fiable.",
"go.faq.q3": "Est-ce que Go est la même chose que Zen ?",
"go.faq.a3":
"Non. Zen est un paiement à l'utilisation, tandis que Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites généreuses et un accès fiable à une sélection de modèles.",
"Non. Zen est un paiement à l'utilisation, tandis que Go coûte 10 $/mois, avec des limites généreuses et un accès fiable à une sélection de modèles.",
"go.faq.q4": "Combien coûte Go ?",
"go.faq.a4.p1.beforePricing": "Go coûte",
"go.faq.a4.p1.pricingLink": "$5 le premier mois",
"go.faq.a4.p1.afterPricing": "puis 10 $/mois avec des limites généreuses.",
"go.faq.a4.p1.pricingLink": "10 $/mois",
"go.faq.a4.p1.afterPricing": "avec des limites généreuses.",
"go.faq.a4.p2.beforeAccount": "Vous pouvez gérer votre abonnement dans votre",
"go.faq.a4.p2.accountLink": "compte",
"go.faq.a4.p3": "Annulez à tout moment.",
@@ -689,8 +688,8 @@ export const dict = {
"workspace.lite.other.message":
"Un autre membre de cet espace de travail est déjà abonné à OpenCode Go. Un seul membre par espace de travail peut s'abonner.",
"workspace.lite.promo.description":
"OpenCode Go commence à {{price}}, puis 10 $/mois, et offre un accès fiable aux modèles de code ouverts populaires avec des limites d'utilisation généreuses.",
"workspace.lite.promo.price": "$5 le premier mois",
"OpenCode Go coûte {{price}} et offre un accès fiable aux modèles de code ouverts populaires avec des limites d'utilisation généreuses.",
"workspace.lite.promo.price": "10 $/mois",
"workspace.lite.promo.modelsTitle": "Ce qui est inclus",
"workspace.lite.promo.footer":
"Ce forfait est principalement conçu pour les utilisateurs internationaux et offre un accès mondial stable. Les tarifs et les limites d'utilisation peuvent évoluer à mesure que nous tirons les enseignements des premières utilisations et des retours reçus.",
+11 -12
View File
@@ -256,7 +256,7 @@ export const dict = {
"go.title": "OpenCode Go | Modelli di coding a basso costo per tutti",
"go.banner.text": "DeepSeek V4 Flash offre limiti di utilizzo 2x superiori per un periodo limitato",
"go.meta.description":
"Go inizia a $5 per il primo mese, poi $10/mese, con limiti di utilizzo generosi e un accesso affidabile ai principali modelli di coding.",
"Go costa $10/mese, con limiti di utilizzo generosi e un accesso affidabile ai principali modelli di coding.",
"go.hero.title": "Modelli di coding a basso costo per tutti",
"go.hero.body":
"Go porta il coding agentico ai programmatori di tutto il mondo. Offrendo limiti generosi e un accesso affidabile ai modelli open source più capaci, in modo da poter costruire con agenti potenti senza preoccuparsi dei costi o della disponibilità.",
@@ -265,9 +265,8 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "Abbonati a Go",
"go.cta.price": "$10/mese",
"go.cta.promo": "$5 il primo mese",
"go.pricing.body":
"Usalo con qualsiasi agente. $5 il primo mese, poi $10/mese. Ricarica il credito se necessario. Annulla in qualsiasi momento.",
"Usalo con qualsiasi agente. $10/mese. Ricarica il credito se necessario. Annulla in qualsiasi momento.",
"go.graph.free": "Gratis",
"go.graph.freePill": "Big Pickle e modelli gratuiti",
"go.graph.go": "Go",
@@ -298,20 +297,20 @@ export const dict = {
"go.testimonials.frank.quote": "Vorrei essere ancora a Nvidia.",
"go.problem.title": "Quale problema risolve Go?",
"go.problem.body":
"Ci concentriamo nel portare l'esperienza OpenCode a quante più persone possibile. OpenCode Go è un abbonamento a basso costo: $5 il primo mese, poi $10/mese. Offre limiti generosi e accesso affidabile ai modelli open source più capaci.",
"Ci concentriamo nel portare l'esperienza OpenCode a quante più persone possibile. OpenCode Go è un abbonamento a basso costo da $10/mese. Offre limiti generosi e accesso affidabile ai modelli open source più capaci.",
"go.problem.subtitle": " ",
"go.problem.item1": "Prezzo di abbonamento a basso costo",
"go.problem.item2": "Limiti generosi e accesso affidabile",
"go.problem.item3": "Costruito per il maggior numero possibile di programmatori",
"go.problem.item4": "Una selezione curata di modelli testati per il coding agentico",
"go.how.title": "Come funziona Go",
"go.how.body": "Go inizia a $5 per il primo mese, poi $10/mese. Puoi usarlo con OpenCode o qualsiasi agente.",
"go.how.body": "Go costa $10/mese. Puoi usarlo con OpenCode o qualsiasi agente.",
"go.how.step1.title": "Crea un account",
"go.how.step1.beforeLink": "segui le",
"go.how.step1.link": "istruzioni di configurazione",
"go.how.step2.title": "Abbonati a Go",
"go.how.step2.link": "$5 il primo mese",
"go.how.step2.afterLink": "poi $10/mese con limiti generosi",
"go.how.step2.link": "$10/mese",
"go.how.step2.afterLink": "con limiti generosi",
"go.how.step3.title": "Inizia a programmare",
"go.how.step3.body": "con accesso affidabile ai modelli open source",
"go.privacy.title": "La tua privacy è importante per noi",
@@ -328,11 +327,11 @@ export const dict = {
"go.faq.a2": "Go include i modelli elencati di seguito, con limiti generosi e accesso affidabile.",
"go.faq.q3": "Go è lo stesso di Zen?",
"go.faq.a3":
"No. Zen è a consumo, mentre Go inizia a $5 per il primo mese, poi $10/mese, con limiti generosi e un accesso affidabile a una selezione curata di modelli.",
"No. Zen è a consumo, mentre Go costa $10/mese, con limiti generosi e un accesso affidabile a una selezione curata di modelli.",
"go.faq.q4": "Quanto costa Go?",
"go.faq.a4.p1.beforePricing": "Go costa",
"go.faq.a4.p1.pricingLink": "$5 il primo mese",
"go.faq.a4.p1.afterPricing": "poi $10/mese con limiti generosi.",
"go.faq.a4.p1.pricingLink": "$10/mese",
"go.faq.a4.p1.afterPricing": "con limiti generosi.",
"go.faq.a4.p2.beforeAccount": "Puoi gestire il tuo abbonamento nel tuo",
"go.faq.a4.p2.accountLink": "account",
"go.faq.a4.p3": "Annulla in qualsiasi momento.",
@@ -680,8 +679,8 @@ export const dict = {
"workspace.lite.other.message":
"Un altro membro in questo workspace è già abbonato a OpenCode Go. Solo un membro per workspace può abbonarsi.",
"workspace.lite.promo.description":
"OpenCode Go parte da {{price}}, poi $10/mese, e offre un accesso affidabile a popolari modelli di coding aperti con generosi limiti di utilizzo.",
"workspace.lite.promo.price": "$5 il primo mese",
"OpenCode Go costa {{price}} e offre un accesso affidabile a popolari modelli di coding aperti con generosi limiti di utilizzo.",
"workspace.lite.promo.price": "$10/mese",
"workspace.lite.promo.modelsTitle": "Cosa è incluso",
"workspace.lite.promo.footer":
"Il piano è pensato principalmente per gli utenti internazionali e offre un accesso globale stabile. I prezzi e i limiti di utilizzo potrebbero cambiare in base a quanto apprenderemo dall'utilizzo iniziale e dai feedback.",
+11 -12
View File
@@ -255,7 +255,7 @@ export const dict = {
"go.title": "OpenCode Go | すべての人のための低価格なコーディングモデル",
"go.banner.text": "DeepSeek V4 Flashの利用上限が期間限定で2倍に",
"go.meta.description":
"Goは最初の月$5、その後$10/月で、主要なコーディングモデルへのゆとりある利用上限と安定したアクセスを提供します。",
"Goは月額$10で、主要なコーディングモデルへのゆとりある利用上限と安定したアクセスを提供します。",
"go.hero.title": "すべての人のための低価格なコーディングモデル",
"go.hero.body":
"Goは、世界中のプログラマーにエージェント型コーディングをもたらします。最も高性能なオープンソースモデルへの十分な制限と安定したアクセスを提供し、コストや可用性を気にすることなく強力なエージェントで構築できます。",
@@ -264,9 +264,8 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "Goを購読する",
"go.cta.price": "$10/月",
"go.cta.promo": "初月 $5",
"go.pricing.body":
"どのエージェントでも使えます。最初の月$5、その後$10/月。必要に応じてクレジットを追加。いつでもキャンセルできます。",
"どのエージェントでも使えます。月額$10。必要に応じてクレジットを追加。いつでもキャンセルできます。",
"go.graph.free": "無料",
"go.graph.freePill": "Big Pickleと無料モデル",
"go.graph.go": "Go",
@@ -298,20 +297,20 @@ export const dict = {
"go.testimonials.frank.quote": "まだNvidiaにいられたらよかったのに。",
"go.problem.title": "Goはどのような問題を解決していますか?",
"go.problem.body":
"私たちはOpenCodeの体験をできるだけ多くの人に届けることに注力しています。OpenCode Goは低価格サブスクリプションで、最初の月は$5、その後は$10/月です。ゆとりある上限と、最も高性能なオープンソースモデルへの信頼できるアクセスを提供します。",
"私たちはOpenCodeの体験をできるだけ多くの人に届けることに注力しています。OpenCode Goは月額$10の低価格サブスクリプションです。ゆとりある上限と、最も高性能なオープンソースモデルへの信頼できるアクセスを提供します。",
"go.problem.subtitle": " ",
"go.problem.item1": "低価格なサブスクリプション料金",
"go.problem.item2": "十分な制限と安定したアクセス",
"go.problem.item3": "できるだけ多くのプログラマーのために構築",
"go.problem.item4": "エージェント型コーディング向けにテストされた厳選モデルラインナップ",
"go.how.title": "Goの仕組み",
"go.how.body": "Goは最初の月$5、その後$10/月で始まります。OpenCodeまたは任意のエージェントで使えます。",
"go.how.body": "Goは月額$10です。OpenCodeまたは任意のエージェントで使えます。",
"go.how.step1.title": "アカウントを作成",
"go.how.step1.beforeLink": "",
"go.how.step1.link": "セットアップ手順はこちら",
"go.how.step2.title": "Goを購読する",
"go.how.step2.link": "最初の月$5",
"go.how.step2.afterLink": "その後$10/月、ゆとりある上限付き",
"go.how.step2.link": "月額$10",
"go.how.step2.afterLink": "ゆとりある上限付き",
"go.how.step3.title": "コーディングを開始",
"go.how.step3.body": "オープンソースモデルへの安定したアクセスで",
"go.privacy.title": "あなたのプライバシーは私たちにとって重要です",
@@ -328,11 +327,11 @@ export const dict = {
"go.faq.a2": "Go には、十分な利用上限と安定したアクセスを備えた、以下のモデルが含まれます。",
"go.faq.q3": "GoはZenと同じですか?",
"go.faq.a3":
"いいえ。Zenは従量課金制ですが、Goは最初の月$5、その後$10/月で、厳選されたモデルラインナップへのゆとりある上限と安定したアクセスを提供します。",
"いいえ。Zenは従量課金制ですが、Goは月額$10で、厳選されたモデルラインナップへのゆとりある上限と安定したアクセスを提供します。",
"go.faq.q4": "Goの料金は?",
"go.faq.a4.p1.beforePricing": "Goは",
"go.faq.a4.p1.pricingLink": "最初の月$5",
"go.faq.a4.p1.afterPricing": "その後$10/月、ゆとりある上限付き。",
"go.faq.a4.p1.pricingLink": "月額$10",
"go.faq.a4.p1.afterPricing": "ゆとりある上限付き。",
"go.faq.a4.p2.beforeAccount": "管理画面:",
"go.faq.a4.p2.accountLink": "アカウント",
"go.faq.a4.p3": "いつでもキャンセル可能です。",
@@ -678,8 +677,8 @@ export const dict = {
"workspace.lite.other.message":
"このワークスペースの別のメンバーが既に OpenCode Go を購読しています。ワークスペースにつき1人のメンバーのみが購読できます。",
"workspace.lite.promo.description":
"OpenCode Goは{{price}}で始まり、その後は$10/月で、人気の高いオープンコーディングモデルへの安定したアクセスと余裕のある利用枠を提供します。",
"workspace.lite.promo.price": "初月$5",
"OpenCode Goは{{price}}で、人気の高いオープンコーディングモデルへの安定したアクセスと余裕のある利用枠を提供します。",
"workspace.lite.promo.price": "$10/月",
"workspace.lite.promo.modelsTitle": "含まれるもの",
"workspace.lite.promo.footer":
"このプランは主に海外のユーザー向けに設計されており、世界中から安定してご利用いただけます。料金と利用上限は、初期の利用状況やフィードバックを踏まえて変更される場合があります。",
+11 -12
View File
@@ -252,7 +252,7 @@ export const dict = {
"go.title": "OpenCode Go | 모두를 위한 저비용 코딩 모델",
"go.banner.text": "DeepSeek V4 Flash 사용 한도가 한시적으로 2배 확대됩니다",
"go.meta.description":
"Go는 첫 달 $5, 이후 $10/월로 시작하며, 넉넉한 사용 한도와 주요 코딩 모델에 대한 안정적인 액세스를 제공합니다.",
"Go는 월 $10이며, 넉넉한 사용 한도와 주요 코딩 모델에 대한 안정적인 액세스를 제공합니다.",
"go.hero.title": "모두를 위한 저비용 코딩 모델",
"go.hero.body":
"Go는 전 세계 프로그래머들에게 에이전트 코딩을 제공합니다. 가장 유능한 오픈 소스 모델에 대한 넉넉한 한도와 안정적인 액세스를 제공하므로, 비용이나 가용성 걱정 없이 강력한 에이전트로 빌드할 수 있습니다.",
@@ -261,9 +261,8 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "Go 구독하기",
"go.cta.price": "$10/월",
"go.cta.promo": "첫 달 $5",
"go.pricing.body":
"어떤 에이전트와도 사용할 수 있습니다. 첫 달 $5, 이후 $10/월. 필요하면 크레딧을 충전하세요. 언제든지 취소할 수 있습니다.",
"어떤 에이전트와도 사용할 수 있습니다. $10. 필요하면 크레딧을 충전하세요. 언제든지 취소할 수 있습니다.",
"go.graph.free": "무료",
"go.graph.freePill": "Big Pickle 및 무료 모델",
"go.graph.go": "Go",
@@ -295,20 +294,20 @@ export const dict = {
"go.testimonials.frank.quote": "아직 Nvidia에 있었으면 좋았을 텐데요.",
"go.problem.title": "Go는 어떤 문제를 해결하나요?",
"go.problem.body":
"우리는 가능한 많은 사람들에게 OpenCode 경험을 제공하는 데 집중하고 있습니다. OpenCode Go는 저렴한 구독 서비스로, 첫 달 $5, 이후 $10/월입니다. 넉넉한 한도와 가장 뛰어난 오픈 소스 모델에 대한 안정적인 액세스를 제공합니다.",
"우리는 가능한 많은 사람들에게 OpenCode 경험을 제공하는 데 집중하고 있습니다. OpenCode Go는 월 $10의 저렴한 구독 서비스입니다. 넉넉한 한도와 가장 뛰어난 오픈 소스 모델에 대한 안정적인 액세스를 제공합니다.",
"go.problem.subtitle": " ",
"go.problem.item1": "저렴한 구독 가격",
"go.problem.item2": "넉넉한 한도와 안정적인 액세스",
"go.problem.item3": "가능한 한 많은 프로그래머를 위해 제작됨",
"go.problem.item4": "에이전트 코딩용으로 테스트된 엄선된 모델 라인업",
"go.how.title": "Go 작동 방식",
"go.how.body": "Go는 첫 달 $5, 이후 $10/월로 시작합니다. OpenCode 또는 어떤 에이전트와도 함께 사용할 수 있습니다.",
"go.how.body": "Go는 월 $10입니다. OpenCode 또는 어떤 에이전트와도 함께 사용할 수 있습니다.",
"go.how.step1.title": "계정 생성",
"go.how.step1.beforeLink": "",
"go.how.step1.link": "설정 지침을 따르세요",
"go.how.step2.title": "Go 구독",
"go.how.step2.link": "첫 달 $5",
"go.how.step2.afterLink": "이후 $10/월, 넉넉한 한도 포함",
"go.how.step2.link": "월 $10",
"go.how.step2.afterLink": "넉넉한 한도 포함",
"go.how.step3.title": "코딩 시작",
"go.how.step3.body": "오픈 소스 모델에 대한 안정적인 액세스와 함께",
"go.privacy.title": "귀하의 프라이버시는 우리에게 중요합니다",
@@ -324,11 +323,11 @@ export const dict = {
"go.faq.a2": "Go에는 넉넉한 한도와 안정적인 액세스를 제공하는 아래 모델이 포함됩니다.",
"go.faq.q3": "Go는 Zen과 같은가요?",
"go.faq.a3":
"아니요. Zen은 종량제인 반면, Go는 첫 달 $5, 이후 $10/월로 시작하며, 엄선된 모델 라인업에 대한 넉넉한 한도와 안정적인 액세스를 제공합니다.",
"아니요. Zen은 종량제인 반면, Go는 월 $10이며, 엄선된 모델 라인업에 대한 넉넉한 한도와 안정적인 액세스를 제공합니다.",
"go.faq.q4": "Go 비용은 얼마인가요?",
"go.faq.a4.p1.beforePricing": "Go 비용은",
"go.faq.a4.p1.pricingLink": "첫 달 $5",
"go.faq.a4.p1.afterPricing": "이후 $10/월, 넉넉한 한도 포함.",
"go.faq.a4.p1.pricingLink": "월 $10",
"go.faq.a4.p1.afterPricing": "넉넉한 한도 포함.",
"go.faq.a4.p2.beforeAccount": "구독 관리는 다음에서 가능합니다:",
"go.faq.a4.p2.accountLink": "계정",
"go.faq.a4.p3": "언제든지 취소할 수 있습니다.",
@@ -670,8 +669,8 @@ export const dict = {
"workspace.lite.other.message":
"이 워크스페이스의 다른 멤버가 이미 OpenCode Go를 구독 중입니다. 워크스페이스당 한 명의 멤버만 구독할 수 있습니다.",
"workspace.lite.promo.description":
"OpenCode Go는 {{price}}부터 시작하며, 이후 $10/월로 넉넉한 사용량 한도와 함께 인기 있는 오픈 코딩 모델에 대한 안정적인 액세스를 제공합니다.",
"workspace.lite.promo.price": "첫 달 $5",
"OpenCode Go는 {{price}}로 넉넉한 사용량 한도와 함께 인기 있는 오픈 코딩 모델에 대한 안정적인 액세스를 제공합니다.",
"workspace.lite.promo.price": "$10/월",
"workspace.lite.promo.modelsTitle": "포함 내역",
"workspace.lite.promo.footer":
"이 플랜은 주로 해외 사용자를 위해 설계되었으며, 전 세계에서 안정적으로 이용할 수 있습니다. 초기 이용 현황과 피드백을 반영하는 과정에서 가격과 사용 한도가 변경될 수 있습니다.",
+11 -12
View File
@@ -256,7 +256,7 @@ export const dict = {
"go.title": "OpenCode Go | Rimelige kodemodeller for alle",
"go.banner.text": "DeepSeek V4 Flash får 2x bruksgrense i en begrenset periode",
"go.meta.description":
"Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse bruksgrenser og pålitelig tilgang til ledende kodemodeller.",
"Go koster $10/måned, med sjenerøse bruksgrenser og pålitelig tilgang til ledende kodemodeller.",
"go.hero.title": "Rimelige kodemodeller for alle",
"go.hero.body":
"Go bringer agent-koding til programmerere over hele verden. Med rause grenser og pålitelig tilgang til de mest kapable åpen kildekode-modellene, kan du bygge med kraftige agenter uten å bekymre deg for kostnader eller tilgjengelighet.",
@@ -265,9 +265,8 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "Abonner på Go",
"go.cta.price": "$10/måned",
"go.cta.promo": "$5 første 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.",
"Bruk med hvilken som helst agent. $10/måned. Fyll på kreditt ved behov. Avslutt når som helst.",
"go.graph.free": "Gratis",
"go.graph.freePill": "Big Pickle og gratis modeller",
"go.graph.go": "Go",
@@ -298,7 +297,7 @@ export const dict = {
"go.testimonials.frank.quote": "Jeg skulle ønske jeg fortsatt var hos Nvidia.",
"go.problem.title": "Hvilket problem løser Go?",
"go.problem.body":
"Vi fokuserer på å bringe OpenCode-opplevelsen til så mange som mulig. OpenCode Go er et rimelig abonnement: $5 for den første måneden, deretter $10/måned. Det gir sjenerøse grenser og pålitelig tilgang til de mest kapable åpen kildekode-modellene.",
"Vi fokuserer på å bringe OpenCode-opplevelsen til så mange som mulig. OpenCode Go er et rimelig abonnement til $10/måned. Det gir sjenerøse grenser og pålitelig tilgang til de mest kapable åpen kildekode-modellene.",
"go.problem.subtitle": " ",
"go.problem.item1": "Rimelig abonnementspris",
"go.problem.item2": "Rause grenser og pålitelig tilgang",
@@ -306,13 +305,13 @@ export const dict = {
"go.problem.item4": "Et kuratert modellutvalg testet for agent-koding",
"go.how.title": "Hvordan Go fungerer",
"go.how.body":
"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 koster $10/måned. Du kan bruke det med OpenCode eller hvilken som helst agent.",
"go.how.step1.title": "Opprett en konto",
"go.how.step1.beforeLink": "følg",
"go.how.step1.link": "oppsettsinstruksjonene",
"go.how.step2.title": "Abonner på Go",
"go.how.step2.link": "$5 første måned",
"go.how.step2.afterLink": "deretter $10/måned med sjenerøse grenser",
"go.how.step2.link": "$10/måned",
"go.how.step2.afterLink": "med sjenerøse grenser",
"go.how.step3.title": "Begynn å kode",
"go.how.step3.body": "med pålitelig tilgang til åpen kildekode-modeller",
"go.privacy.title": "Personvernet ditt er viktig for oss",
@@ -329,11 +328,11 @@ export const dict = {
"go.faq.a2": "Go inkluderer modellene nedenfor, med høye grenser og pålitelig tilgang.",
"go.faq.q3": "Er Go det samme som Zen?",
"go.faq.a3":
"Nei. Zen er betaling etter bruk, mens Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse grenser og pålitelig tilgang til et kuratert modellutvalg.",
"Nei. Zen er betaling etter bruk, mens Go koster $10/måned, med sjenerøse grenser og pålitelig tilgang til et kuratert modellutvalg.",
"go.faq.q4": "Hva koster Go?",
"go.faq.a4.p1.beforePricing": "Go koster",
"go.faq.a4.p1.pricingLink": "$5 første måned",
"go.faq.a4.p1.afterPricing": "deretter $10/måned med sjenerøse grenser.",
"go.faq.a4.p1.pricingLink": "$10/måned",
"go.faq.a4.p1.afterPricing": "med sjenerøse grenser.",
"go.faq.a4.p2.beforeAccount": "Du kan administrere abonnementet ditt i din",
"go.faq.a4.p2.accountLink": "konto",
"go.faq.a4.p3": "Avslutt når som helst.",
@@ -680,8 +679,8 @@ export const dict = {
"workspace.lite.other.message":
"Et annet medlem i dette arbeidsområdet abonnerer allerede på OpenCode Go. Kun ett medlem per arbeidsområde kan abonnere.",
"workspace.lite.promo.description":
"OpenCode Go starter på {{price}}, deretter $10/måned, og gir pålitelig tilgang til populære åpne kodingsmodeller med sjenerøse bruksgrenser.",
"workspace.lite.promo.price": "$5 for den første måneden",
"OpenCode Go koster {{price}} og gir pålitelig tilgang til populære åpne kodingsmodeller med sjenerøse bruksgrenser.",
"workspace.lite.promo.price": "$10/måned",
"workspace.lite.promo.modelsTitle": "Hva som er inkludert",
"workspace.lite.promo.footer":
"Planen er primært utviklet for internasjonale brukere og gir stabil global tilgang. Priser og bruksgrenser kan endres etter hvert som vi lærer av tidlig bruk og tilbakemeldinger.",
+11 -12
View File
@@ -257,7 +257,7 @@ export const dict = {
"go.title": "OpenCode Go | Niskokosztowe modele do kodowania dla każdego",
"go.banner.text": "DeepSeek V4 Flash oferuje 2x wyższe limity użycia przez ograniczony czas",
"go.meta.description":
"Go kosztuje $5 za pierwszy miesiąc, a następnie $10/miesiąc, oferując hojne limity użycia i niezawodny dostęp do wiodących modeli do kodowania.",
"Go kosztuje $10/miesiąc, oferując hojne limity użycia i niezawodny dostęp do wiodących modeli do kodowania.",
"go.hero.title": "Niskokosztowe modele do kodowania dla każdego",
"go.hero.body":
"Go udostępnia programowanie z agentami programistom na całym świecie. Oferuje hojne limity i niezawodny dostęp do najzdolniejszych modeli open source, dzięki czemu możesz budować za pomocą potężnych agentów, nie martwiąc się o koszty czy dostępność.",
@@ -266,9 +266,8 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "Zasubskrybuj Go",
"go.cta.price": "$10/miesiąc",
"go.cta.promo": "$5 pierwszy 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.",
"Używaj z dowolnym agentem. $10/miesiąc. Doładuj konto w razie potrzeby. Anuluj w dowolnym momencie.",
"go.graph.free": "Darmowe",
"go.graph.freePill": "Big Pickle i darmowe modele",
"go.graph.go": "Go",
@@ -299,7 +298,7 @@ export const dict = {
"go.testimonials.frank.quote": "Chciałbym wciąż być w Nvidia.",
"go.problem.title": "Jaki problem rozwiązuje Go?",
"go.problem.body":
"Skupiamy się na udostępnieniu doświadczenia OpenCode jak największej liczbie osób. OpenCode Go to tania subskrypcja: $5 za pierwszy miesiąc, potem $10/miesiąc. Zapewnia hojne limity i niezawodny dostęp do najbardziej wydajnych modeli open source.",
"Skupiamy się na udostępnieniu doświadczenia OpenCode jak największej liczbie osób. OpenCode Go to tania subskrypcja za $10/miesiąc. Zapewnia hojne limity i niezawodny dostęp do najbardziej wydajnych modeli open source.",
"go.problem.subtitle": " ",
"go.problem.item1": "Niskokosztowa cena subskrypcji",
"go.problem.item2": "Hojne limity i niezawodny dostęp",
@@ -307,13 +306,13 @@ export const dict = {
"go.problem.item4": "Starannie dobrany zestaw modeli przetestowanych pod kątem kodowania z agentami",
"go.how.title": "Jak działa Go",
"go.how.body":
"Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc. Możesz go używać z OpenCode lub dowolnym agentem.",
"Go kosztuje $10/miesiąc. Możesz go używać z OpenCode lub dowolnym agentem.",
"go.how.step1.title": "Załóż konto",
"go.how.step1.beforeLink": "postępuj zgodnie z",
"go.how.step1.link": "instrukcją konfiguracji",
"go.how.step2.title": "Zasubskrybuj Go",
"go.how.step2.link": "$5 za pierwszy miesiąc",
"go.how.step2.afterLink": "potem $10/miesiąc z hojnymi limitami",
"go.how.step2.link": "$10/miesiąc",
"go.how.step2.afterLink": "z hojnymi limitami",
"go.how.step3.title": "Zacznij kodować",
"go.how.step3.body": "z niezawodnym dostępem do modeli open source",
"go.privacy.title": "Twoja prywatność jest dla nas ważna",
@@ -330,11 +329,11 @@ export const dict = {
"go.faq.a2": "Go obejmuje poniższe modele z wysokimi limitami i niezawodnym dostępem.",
"go.faq.q3": "Czy Go to to samo co Zen?",
"go.faq.a3":
"Nie. Zen działa w modelu płatności za użycie, natomiast Go kosztuje $5 za pierwszy miesiąc, a następnie $10/miesiąc, oferując hojne limity i niezawodny dostęp do starannie dobranego zestawu modeli.",
"Nie. Zen działa w modelu płatności za użycie, natomiast Go kosztuje $10/miesiąc, oferując hojne limity i niezawodny dostęp do starannie dobranego zestawu modeli.",
"go.faq.q4": "Ile kosztuje Go?",
"go.faq.a4.p1.beforePricing": "Go kosztuje",
"go.faq.a4.p1.pricingLink": "$5 za pierwszy miesiąc",
"go.faq.a4.p1.afterPricing": "potem $10/miesiąc z hojnymi limitami.",
"go.faq.a4.p1.pricingLink": "$10/miesiąc",
"go.faq.a4.p1.afterPricing": "z hojnymi limitami.",
"go.faq.a4.p2.beforeAccount": "Możesz zarządzać subskrypcją na swoim",
"go.faq.a4.p2.accountLink": "koncie",
"go.faq.a4.p3": "Anuluj w dowolnym momencie.",
@@ -680,8 +679,8 @@ export const dict = {
"workspace.lite.other.message":
"Inny członek tego obszaru roboczego już subskrybuje OpenCode Go. Tylko jeden członek na obszar roboczy może subskrybować.",
"workspace.lite.promo.description":
"OpenCode Go zaczyna się od {{price}}, potem $10/miesiąc, i zapewnia niezawodny dostęp do popularnych otwartych modeli kodowania z hojnymi limitami użycia.",
"workspace.lite.promo.price": "$5 za pierwszy miesiąc",
"OpenCode Go kosztuje {{price}} i zapewnia niezawodny dostęp do popularnych otwartych modeli kodowania z hojnymi limitami użycia.",
"workspace.lite.promo.price": "$10/miesiąc",
"workspace.lite.promo.modelsTitle": "Co zawiera",
"workspace.lite.promo.footer":
"Plan został opracowany przede wszystkim z myślą o użytkownikach z całego świata i zapewnia stabilny globalny dostęp. Ceny i limity użycia mogą ulec zmianie w miarę zdobywania doświadczeń na podstawie początkowego korzystania z usługi i otrzymywanych opinii.",
+11 -12
View File
@@ -260,7 +260,7 @@ export const dict = {
"go.title": "OpenCode Go | Недорогие модели для кодинга для всех",
"go.banner.text": "DeepSeek V4 Flash получает 2x лимиты использования на ограниченное время",
"go.meta.description":
"Go стоит $5 за первый месяц, затем $10/месяц и предлагает щедрые лимиты использования и надежный доступ к ведущим моделям для кодинга.",
"Go стоит $10/месяц и предлагает щедрые лимиты использования и надежный доступ к ведущим моделям для кодинга.",
"go.hero.title": "Недорогие модели для кодинга для всех",
"go.hero.body":
"Go открывает доступ к агентам-программистам разработчикам по всему миру. Предлагая щедрые лимиты и надежный доступ к наиболее способным моделям с открытым исходным кодом, вы можете создавать проекты с мощными агентами, не беспокоясь о затратах или доступности.",
@@ -269,9 +269,8 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "Подписаться на Go",
"go.cta.price": "$10/месяц",
"go.cta.promo": "$5 первый месяц",
"go.pricing.body":
"Используйте с любым агентом. $5 за первый месяц, затем $10/месяц. Пополняйте баланс при необходимости. Отменить можно в любое время.",
"Используйте с любым агентом. $10/месяц. Пополняйте баланс при необходимости. Отменить можно в любое время.",
"go.graph.free": "Бесплатно",
"go.graph.freePill": "Big Pickle и бесплатные модели",
"go.graph.go": "Go",
@@ -303,7 +302,7 @@ export const dict = {
"go.testimonials.frank.quote": "Жаль, что я больше не в Nvidia.",
"go.problem.title": "Какую проблему решает Go?",
"go.problem.body":
"Мы стремимся сделать OpenCode доступным для как можно большего числа людей. OpenCode Go - это недорогая подписка: $5 за первый месяц, затем $10/месяц. Она предоставляет щедрые лимиты и надежный доступ к самым мощным моделям с открытым исходным кодом.",
"Мы стремимся сделать OpenCode доступным для как можно большего числа людей. OpenCode Go - это недорогая подписка за $10/месяц. Она предоставляет щедрые лимиты и надежный доступ к самым мощным моделям с открытым исходным кодом.",
"go.problem.subtitle": " ",
"go.problem.item1": "Недорогая подписка",
"go.problem.item2": "Щедрые лимиты и надежный доступ",
@@ -311,13 +310,13 @@ export const dict = {
"go.problem.item4": "Отобранные модели, протестированные для агентного программирования",
"go.how.title": "Как работает Go",
"go.how.body":
"Go начинается с $5 за первый месяц, затем $10/месяц. Вы можете использовать его с OpenCode или любым агентом.",
"Go стоит $10/месяц. Вы можете использовать его с OpenCode или любым агентом.",
"go.how.step1.title": "Создайте аккаунт",
"go.how.step1.beforeLink": "следуйте",
"go.how.step1.link": "инструкциям по настройке",
"go.how.step2.title": "Подпишитесь на Go",
"go.how.step2.link": "$5 за первый месяц",
"go.how.step2.afterLink": "затем $10/месяц с щедрыми лимитами",
"go.how.step2.link": "$10/месяц",
"go.how.step2.afterLink": "с щедрыми лимитами",
"go.how.step3.title": "Начните кодить",
"go.how.step3.body": "с надежным доступом к open-source моделям",
"go.privacy.title": "Ваша приватность важна для нас",
@@ -334,11 +333,11 @@ export const dict = {
"go.faq.a2": "Go включает перечисленные ниже модели с щедрыми лимитами и надежным доступом.",
"go.faq.q3": "Go — это то же самое, что и Zen?",
"go.faq.a3":
"Нет. Zen оплачивается по мере использования, а Go стоит $5 за первый месяц, затем $10/месяц и предлагает щедрые лимиты и надежный доступ к отобранным моделям.",
"Нет. Zen оплачивается по мере использования, а Go стоит $10/месяц и предлагает щедрые лимиты и надежный доступ к отобранным моделям.",
"go.faq.q4": "Сколько стоит Go?",
"go.faq.a4.p1.beforePricing": "Go стоит",
"go.faq.a4.p1.pricingLink": "$5 за первый месяц",
"go.faq.a4.p1.afterPricing": "затем $10/месяц с щедрыми лимитами.",
"go.faq.a4.p1.pricingLink": "$10/месяц",
"go.faq.a4.p1.afterPricing": "с щедрыми лимитами.",
"go.faq.a4.p2.beforeAccount": "Вы можете управлять подпиской в своем",
"go.faq.a4.p2.accountLink": "аккаунте",
"go.faq.a4.p3": "Отмена в любое время.",
@@ -687,8 +686,8 @@ export const dict = {
"workspace.lite.other.message":
"Другой участник в этом рабочем пространстве уже подписан на OpenCode Go. Только один участник в рабочем пространстве может оформить подписку.",
"workspace.lite.promo.description":
"OpenCode Go начинается с {{price}}, затем $10/месяц и предоставляет надежный доступ к популярным открытым моделям кодирования с щедрыми лимитами использования.",
"workspace.lite.promo.price": "$5 за первый месяц",
"OpenCode Go стоит {{price}} и предоставляет надежный доступ к популярным открытым моделям кодирования с щедрыми лимитами использования.",
"workspace.lite.promo.price": "$10/месяц",
"workspace.lite.promo.modelsTitle": "Что включено",
"workspace.lite.promo.footer":
"План предназначен в первую очередь для пользователей по всему миру и обеспечивает стабильный глобальный доступ. Цены и лимиты использования могут меняться по мере изучения первых результатов использования и отзывов.",
+11 -12
View File
@@ -255,7 +255,7 @@ export const dict = {
"go.title": "OpenCode Go | โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน",
"go.banner.text": "DeepSeek V4 Flash เพิ่มโควตาการใช้งานเป็น 2 เท่าในช่วงเวลาจำกัด",
"go.meta.description":
"Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดการใช้งานที่เอื้อเฟื้อและการเข้าถึงโมเดลเขียนโค้ดชั้นนำอย่างเชื่อถือได้",
"Go มีราคา $10/เดือน พร้อมขีดจำกัดการใช้งานที่เอื้อเฟื้อและการเข้าถึงโมเดลเขียนโค้ดชั้นนำอย่างเชื่อถือได้",
"go.hero.title": "โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน",
"go.hero.body":
"Go นำการเขียนโค้ดแบบเอเจนต์มาสู่นักเขียนโปรแกรมทั่วโลก เสนอขีดจำกัดที่กว้างขวางและการเข้าถึงโมเดลโอเพนซอร์สที่มีความสามารถสูงสุดได้อย่างน่าเชื่อถือ เพื่อให้คุณสามารถสร้างสรรค์ด้วยเอเจนต์ที่ทรงพลังโดยไม่ต้องกังวลเรื่องค่าใช้จ่ายหรือความพร้อมใช้งาน",
@@ -264,8 +264,7 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "สมัครสมาชิก Go",
"go.cta.price": "$10/เดือน",
"go.cta.promo": "$5 เดือนแรก",
"go.pricing.body": "ใช้กับเอเจนต์ใดก็ได้ $5 ในเดือนแรก จากนั้น $10/เดือน เติมเครดิตหากจำเป็น ยกเลิกได้ตลอดเวลา",
"go.pricing.body": "ใช้กับเอเจนต์ใดก็ได้ $10/เดือน เติมเครดิตหากจำเป็น ยกเลิกได้ตลอดเวลา",
"go.graph.free": "ฟรี",
"go.graph.freePill": "Big Pickle และโมเดลฟรี",
"go.graph.go": "Go",
@@ -296,20 +295,20 @@ export const dict = {
"go.testimonials.frank.quote": "ผมหวังว่าผมจะยังอยู่ที่ Nvidia",
"go.problem.title": "Go แก้ปัญหาอะไร?",
"go.problem.body":
"เรามุ่งมั่นที่จะนำประสบการณ์ OpenCode ไปสู่ผู้คนให้ได้มากที่สุด OpenCode Go เป็นการสมัครสมาชิกราคาประหยัด: $5 สำหรับเดือนแรก จากนั้น $10/เดือน โดยมอบขีดจำกัดที่เอื้อเฟื้อและการเข้าถึงโมเดลโอเพนซอร์สที่มีความสามารถสูงสุดอย่างเชื่อถือได้",
"เรามุ่งมั่นที่จะนำประสบการณ์ OpenCode ไปสู่ผู้คนให้ได้มากที่สุด OpenCode Go เป็นการสมัครสมาชิกราคาประหยัด $10/เดือน โดยมอบขีดจำกัดที่เอื้อเฟื้อและการเข้าถึงโมเดลโอเพนซอร์สที่มีความสามารถสูงสุดอย่างเชื่อถือได้",
"go.problem.subtitle": " ",
"go.problem.item1": "ราคาการสมัครสมาชิกที่ต่ำ",
"go.problem.item2": "ขีดจำกัดที่กว้างขวางและการเข้าถึงที่เชื่อถือได้",
"go.problem.item3": "สร้างขึ้นเพื่อโปรแกรมเมอร์จำนวนมากที่สุดเท่าที่จะเป็นไปได้",
"go.problem.item4": "ชุดโมเดลที่คัดสรรและผ่านการทดสอบสำหรับการเขียนโค้ดแบบเอเจนต์",
"go.how.title": "Go ทำงานอย่างไร",
"go.how.body": "Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน คุณสามารถใช้กับ OpenCode หรือเอเจนต์ใดก็ได้",
"go.how.body": "Go มีราคา $10/เดือน คุณสามารถใช้กับ OpenCode หรือเอเจนต์ใดก็ได้",
"go.how.step1.title": "สร้างบัญชี",
"go.how.step1.beforeLink": "ทำตาม",
"go.how.step1.link": "คำแนะนำการตั้งค่า",
"go.how.step2.title": "สมัครสมาชิก Go",
"go.how.step2.link": "$5 เดือนแรก",
"go.how.step2.afterLink": "จากนั้น $10/เดือน พร้อมขีดจำกัดที่เอื้อเฟื้อ",
"go.how.step2.link": "$10/เดือน",
"go.how.step2.afterLink": "พร้อมขีดจำกัดที่เอื้อเฟื้อ",
"go.how.step3.title": "เริ่มเขียนโค้ด",
"go.how.step3.body": "ด้วยการเข้าถึงโมเดลโอเพนซอร์สที่เชื่อถือได้",
"go.privacy.title": "ความเป็นส่วนตัวของคุณสำคัญสำหรับเรา",
@@ -326,11 +325,11 @@ export const dict = {
"go.faq.a2": "Go รวมโมเดลด้านล่างนี้ พร้อมขีดจำกัดที่มากและการเข้าถึงที่เชื่อถือได้",
"go.faq.q3": "Go เหมือนกับ Zen หรือไม่?",
"go.faq.a3":
"ไม่ Zen เป็นแบบจ่ายตามการใช้งาน ขณะที่ Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดที่เอื้อเฟื้อและการเข้าถึงชุดโมเดลที่คัดสรรอย่างเชื่อถือได้",
"ไม่ Zen เป็นแบบจ่ายตามการใช้งาน ขณะที่ Go มีราคา $10/เดือน พร้อมขีดจำกัดที่เอื้อเฟื้อและการเข้าถึงชุดโมเดลที่คัดสรรอย่างเชื่อถือได้",
"go.faq.q4": "Go ราคาเท่าไหร่?",
"go.faq.a4.p1.beforePricing": "Go ราคา",
"go.faq.a4.p1.pricingLink": "$5 เดือนแรก",
"go.faq.a4.p1.afterPricing": "จากนั้น $10/เดือน พร้อมขีดจำกัดที่เอื้อเฟื้อ",
"go.faq.a4.p1.pricingLink": "$10/เดือน",
"go.faq.a4.p1.afterPricing": "พร้อมขีดจำกัดที่เอื้อเฟื้อ",
"go.faq.a4.p2.beforeAccount": "คุณสามารถจัดการการสมัครสมาชิกของคุณได้ใน",
"go.faq.a4.p2.accountLink": "บัญชีของคุณ",
"go.faq.a4.p3": "ยกเลิกได้ตลอดเวลา",
@@ -675,8 +674,8 @@ export const dict = {
"workspace.lite.other.message":
"สมาชิกคนอื่นใน Workspace นี้ได้สมัคร OpenCode Go แล้ว สามารถสมัครได้เพียงหนึ่งคนต่อหนึ่ง Workspace เท่านั้น",
"workspace.lite.promo.description":
"OpenCode Go เริ่มต้นที่ {{price}} จากนั้น $10/เดือน และมอบการเข้าถึงโมเดลการเขียนโค้ดแบบเปิดยอดนิยมอย่างเสถียรพร้อมขีดจำกัดการใช้งานที่ให้มาอย่างเหลือเฟือ",
"workspace.lite.promo.price": "$5 สำหรับเดือนแรก",
"OpenCode Go ราคา {{price}} และมอบการเข้าถึงโมเดลการเขียนโค้ดแบบเปิดยอดนิยมอย่างเสถียรพร้อมขีดจำกัดการใช้งานที่ให้มาอย่างเหลือเฟือ",
"workspace.lite.promo.price": "$10/เดือน",
"workspace.lite.promo.modelsTitle": "สิ่งที่รวมอยู่ด้วย",
"workspace.lite.promo.footer":
"แผนนี้ออกแบบมาสำหรับผู้ใช้งานต่างประเทศเป็นหลักและให้การเข้าถึงที่เสถียรทั่วโลก ราคาและขีดจำกัดการใช้งานอาจเปลี่ยนแปลงได้ตามสิ่งที่เราเรียนรู้จากการใช้งานและข้อเสนอแนะในช่วงแรก",
+11 -12
View File
@@ -258,7 +258,7 @@ export const dict = {
"go.title": "OpenCode Go | Herkes için düşük maliyetli kodlama modelleri",
"go.banner.text": "DeepSeek V4 Flash sınırlı bir süre için 2x kullanım limiti sunuyor",
"go.meta.description":
"Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; cömert kullanım limitleri ve önde gelen kodlama modellerine güvenilir erişim sunar.",
"Go ayda 10$'dır; cömert kullanım limitleri ve önde gelen kodlama modellerine güvenilir erişim sunar.",
"go.hero.title": "Herkes için düşük maliyetli kodlama modelleri",
"go.hero.body":
"Go, dünya çapındaki programcılara ajan tabanlı kodlama getiriyor. En yetenekli açık kaynaklı modellere cömert limitler ve güvenilir erişim sunarak, maliyet veya erişilebilirlik konusunda endişelenmeden güçlü ajanlarla geliştirme yapmanızı sağlar.",
@@ -267,9 +267,8 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "Go'ya abone ol",
"go.cta.price": "Ayda 10$",
"go.cta.promo": "İlk ay $5",
"go.pricing.body":
"Herhangi bir ajanla kullanın. İlk ay $5, sonrasında ayda 10$. Gerekirse kredi yükleyin. İstediğiniz zaman iptal edin.",
"Herhangi bir ajanla kullanın. Ayda 10$. Gerekirse kredi yükleyin. İstediğiniz zaman iptal edin.",
"go.graph.free": "Ücretsiz",
"go.graph.freePill": "Big Pickle ve ücretsiz modeller",
"go.graph.go": "Go",
@@ -301,7 +300,7 @@ export const dict = {
"go.testimonials.frank.quote": "Keşke hala Nvidia'da olsaydım.",
"go.problem.title": "Go hangi sorunu çözüyor?",
"go.problem.body":
"OpenCode deneyimini mümkün olduğunca çok kişiye ulaştırmaya odaklandık. OpenCode Go düşük maliyetli bir aboneliktir: İlk ay $5, sonrasında ayda 10$. Cömert limitler ve en yetenekli açık kaynak modellere güvenilir erişim sağlar.",
"OpenCode deneyimini mümkün olduğunca çok kişiye ulaştırmaya odaklandık. OpenCode Go, ayda 10$ olan düşük maliyetli bir aboneliktir. Cömert limitler ve en yetenekli açık kaynak modellere güvenilir erişim sağlar.",
"go.problem.subtitle": " ",
"go.problem.item1": "Düşük maliyetli abonelik fiyatlandırması",
"go.problem.item2": "Cömert limitler ve güvenilir erişim",
@@ -309,13 +308,13 @@ export const dict = {
"go.problem.item4": "Ajan tabanlı kodlama için test edilmiş, özenle seçilmiş model seçenekleri",
"go.how.title": "Go nasıl çalışır?",
"go.how.body":
"Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar. OpenCode veya herhangi bir ajanla kullanabilirsiniz.",
"Go ayda 10$'dır. OpenCode veya herhangi bir ajanla kullanabilirsiniz.",
"go.how.step1.title": "Bir hesap oluşturun",
"go.how.step1.beforeLink": "takip edin",
"go.how.step1.link": "kurulum talimatları",
"go.how.step2.title": "Go'ya abone olun",
"go.how.step2.link": "İlk ay $5",
"go.how.step2.afterLink": "sonrasında cömert limitlerle ayda 10$",
"go.how.step2.link": "Ayda 10$",
"go.how.step2.afterLink": "cömert limitlerle",
"go.how.step3.title": "Kodlamaya başlayın",
"go.how.step3.body": "açık kaynaklı modellere güvenilir erişimle",
"go.privacy.title": "Gizliliğiniz bizim için önemlidir",
@@ -332,11 +331,11 @@ export const dict = {
"go.faq.a2": "Go, aşağıda listelenen modelleri cömert limitler ve güvenilir erişimle sunar.",
"go.faq.q3": "Go, Zen ile aynı mı?",
"go.faq.a3":
"Hayır. Zen kullandıkça öde modelidir; Go ise ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar ve özenle seçilmiş model seçeneklerine cömert limitlerle güvenilir erişim sunar.",
"Hayır. Zen kullandıkça öde modelidir; Go ise ayda 10$'dır ve özenle seçilmiş model seçeneklerine cömert limitlerle güvenilir erişim sunar.",
"go.faq.q4": "Go ne kadar?",
"go.faq.a4.p1.beforePricing": "Go'nun maliyeti",
"go.faq.a4.p1.pricingLink": "İlk ay $5",
"go.faq.a4.p1.afterPricing": "sonrasında cömert limitlerle ayda 10$.",
"go.faq.a4.p1.pricingLink": "ayda 10$",
"go.faq.a4.p1.afterPricing": "cömert limitlerle.",
"go.faq.a4.p2.beforeAccount": "Aboneliğinizi",
"go.faq.a4.p2.accountLink": "hesabınızdan",
"go.faq.a4.p3": "yönetebilirsiniz. İstediğiniz zaman iptal edin.",
@@ -683,8 +682,8 @@ export const dict = {
"workspace.lite.other.message":
"Bu çalışma alanındaki başka bir üye zaten OpenCode Go abonesi. Çalışma alanı başına yalnızca bir üye abone olabilir.",
"workspace.lite.promo.description":
"OpenCode Go {{price}} fiyatından başlar, sonrasında ayda 10$ olur ve cömert kullanım limitleriyle popüler açık kodlama modellerine güvenilir erişim sağlar.",
"workspace.lite.promo.price": "İlk ay $5",
"OpenCode Go {{price}} fiyatıyla cömert kullanım limitleri ve popüler açık kodlama modellerine güvenilir erişim sağlar.",
"workspace.lite.promo.price": "Ayda 10$",
"workspace.lite.promo.modelsTitle": "Neler Dahil",
"workspace.lite.promo.footer":
"Plan öncelikle uluslararası kullanıcılar için tasarlanmıştır ve istikrarlı küresel erişim sağlar. Erken kullanım ve geri bildirimlerden öğrendiklerimiz doğrultusunda fiyatlandırma ve kullanım limitleri değişebilir.",
+11 -12
View File
@@ -257,7 +257,7 @@ export const dict = {
"go.title": "OpenCode Go | Недорогі моделі кодування для всіх",
"go.banner.text": "DeepSeek V4 Flash отримує 2x ліміти використання протягом обмеженого часу",
"go.meta.description":
"Go починається від $5 за перший місяць, потім $10/місяць, зі щедрими лімітами використання та надійним доступом до провідних моделей для кодування.",
"Go коштує $10/місяць, зі щедрими лімітами використання та надійним доступом до провідних моделей для кодування.",
"go.hero.title": "Недорогі моделі кодування для всіх",
"go.hero.body":
"Go надає агентне програмування програмістам у всьому світі, пропонуючи щедрі ліміти та надійний доступ до найкращих моделей з відкритим кодом.",
@@ -266,9 +266,8 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "Підписатися на Go",
"go.cta.price": "$10/місяць",
"go.cta.promo": "$5 перший місяць",
"go.pricing.body":
"Використовуйте з будь-яким агентом. $5 перший місяць, потім $10/місяць. Поповнюйте за потреби. Скасуйте в будь-який час.",
"Використовуйте з будь-яким агентом. $10/місяць. Поповнюйте за потреби. Скасуйте в будь-який час.",
"go.graph.free": "Безкоштовно",
"go.graph.freePill": "Big Pickle та безкоштовні моделі",
"go.graph.go": "Go",
@@ -299,7 +298,7 @@ export const dict = {
"go.testimonials.frank.quote": "Хотів би я досі бути в Nvidia.",
"go.problem.title": "Яку проблему вирішує Go?",
"go.problem.body":
"Ми зосереджені на тому, щоб зробити досвід OpenCode доступним для якомога більшої кількості людей. OpenCode Go — це недорога підписка: $5 за перший місяць, потім $10/місяць. Вона надає щедрі ліміти та надійний доступ до найкращих моделей з відкритим кодом.",
"Ми зосереджені на тому, щоб зробити досвід OpenCode доступним для якомога більшої кількості людей. OpenCode Go — це недорога підписка за $10/місяць. Вона надає щедрі ліміти та надійний доступ до найкращих моделей з відкритим кодом.",
"go.problem.subtitle": " ",
"go.problem.item1": "Недорога підписка",
"go.problem.item2": "Щедрі ліміти та надійний доступ",
@@ -307,13 +306,13 @@ export const dict = {
"go.problem.item4": "Добірка моделей, протестованих для агентного кодування",
"go.how.title": "Як працює Go",
"go.how.body":
"Go починається від $5 за перший місяць, потім $10/місяць. Використовуйте з OpenCode або будь-яким агентом.",
"Go коштує $10/місяць. Використовуйте з OpenCode або будь-яким агентом.",
"go.how.step1.title": "Створіть обліковий запис",
"go.how.step1.beforeLink": "дотримуйтесь",
"go.how.step1.link": "інструкцій з налаштування",
"go.how.step2.title": "Підпишіться на Go",
"go.how.step2.link": "$5 перший місяць",
"go.how.step2.afterLink": "потім $10/місяць із щедрими лімітами",
"go.how.step2.link": "$10/місяць",
"go.how.step2.afterLink": "із щедрими лімітами",
"go.how.step3.title": "Почніть кодувати",
"go.how.step3.body": "з надійним доступом до моделей з відкритим кодом",
"go.privacy.title": "Ваша конфіденційність важлива для нас",
@@ -330,11 +329,11 @@ export const dict = {
"go.faq.a2": "Go включає моделі, перелічені нижче, із щедрими лімітами та надійним доступом.",
"go.faq.q3": "Чи Go те саме, що Zen?",
"go.faq.a3":
"Ні. Zen — це плата за використання, тоді як Go починається від $5 за перший місяць, потім $10/місяць, із щедрими лімітами та надійним доступом до добірки моделей.",
"Ні. Zen — це плата за використання, тоді як Go коштує $10/місяць, із щедрими лімітами та надійним доступом до добірки моделей.",
"go.faq.q4": "Скільки коштує Go?",
"go.faq.a4.p1.beforePricing": "Go коштує",
"go.faq.a4.p1.pricingLink": "$5 за перший місяць",
"go.faq.a4.p1.afterPricing": "потім $10/місяць із щедрими лімітами.",
"go.faq.a4.p1.pricingLink": "$10/місяць",
"go.faq.a4.p1.afterPricing": "із щедрими лімітами.",
"go.faq.a4.p2.beforeAccount": "Ви можете керувати підпискою в",
"go.faq.a4.p2.accountLink": "обліковому записі",
"go.faq.a4.p3": "Скасуйте в будь-який час.",
@@ -676,8 +675,8 @@ export const dict = {
"workspace.lite.black.message":
"Ви вже підписані на OpenCode Black або в списку очікування. Спочатку скасуйте підписку, якщо хочете перейти на Go.",
"workspace.lite.other.message": "Інший учасник цього робочого простору вже підписаний на OpenCode Go.",
"workspace.lite.promo.description": "OpenCode Go починається від {{price}}, потім $10/місяць, із щедрими лімітами.",
"workspace.lite.promo.price": "$5 за перший місяць",
"workspace.lite.promo.description": "OpenCode Go коштує {{price}} і має щедрі ліміти.",
"workspace.lite.promo.price": "$10/місяць",
"workspace.lite.promo.modelsTitle": "Що включено",
"workspace.lite.promo.footer":
"План призначений насамперед для міжнародних користувачів і забезпечує стабільний глобальний доступ. Ціни та ліміти використання можуть змінюватися з урахуванням перших даних про використання та відгуків.",
+11 -12
View File
@@ -245,7 +245,7 @@ export const dict = {
"go.title": "OpenCode Go | 人人可用的低成本编程模型",
"go.banner.text": "DeepSeek V4 Flash 限时享受 2 倍使用额度",
"go.meta.description": "Go 月 $5,之后 $10/月,提供充裕的使用限额,并可可靠访问领先的编程模型。",
"go.meta.description": "Go 月 $10,提供充裕的使用限额,并可可靠访问领先的编程模型。",
"go.hero.title": "人人可用的低成本编程模型",
"go.hero.body":
"Go 将代理编程带给全世界的程序员。提供充裕的限额和对最强大的开源模型的可靠访问,让您可以利用强大的代理进行构建,而无需担心成本或可用性。",
@@ -254,8 +254,7 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "订阅 Go",
"go.cta.price": "$10/月",
"go.cta.promo": "首月 $5",
"go.pricing.body": "可配合任何代理使用。首月 $5,之后 $10/月。如有需要可充值。随时取消。",
"go.pricing.body": "可配合任何代理使用。每月 $10。如有需要可充值。随时取消。",
"go.graph.free": "免费",
"go.graph.freePill": "Big Pickle 和免费模型",
"go.graph.go": "Go",
@@ -286,20 +285,20 @@ export const dict = {
"go.testimonials.frank.quote": "我希望我还在 Nvidia。",
"go.problem.title": "Go 解决了什么问题?",
"go.problem.body":
"我们致力于将 OpenCode 体验带给尽可能多的人。OpenCode Go 是一款低成本订阅服务:首月 $5,之后 $10/月。它提供充裕的额度,并让您能可靠地使用最强大的开源模型。",
"我们致力于将 OpenCode 体验带给尽可能多的人。OpenCode Go 是一款每月 $10 的低成本订阅服务。它提供充裕的额度,并让您能可靠地使用最强大的开源模型。",
"go.problem.subtitle": " ",
"go.problem.item1": "低成本订阅定价",
"go.problem.item2": "充裕的限额和可靠的访问",
"go.problem.item3": "为尽可能多的程序员打造",
"go.problem.item4": "经过代理编程测试的精选模型阵容",
"go.how.title": "Go 如何工作",
"go.how.body": "Go 起价为首月 $5,之后 $10/月。您可以将其与 OpenCode 或任何代理搭配使用。",
"go.how.body": "Go 每月 $10。您可以将其与 OpenCode 或任何代理搭配使用。",
"go.how.step1.title": "创建账户",
"go.how.step1.beforeLink": "遵循",
"go.how.step1.link": "设置说明",
"go.how.step2.title": "订阅 Go",
"go.how.step2.link": "月 $5",
"go.how.step2.afterLink": "之后 $10/月,额度充裕",
"go.how.step2.link": "月 $10",
"go.how.step2.afterLink": "额度充裕",
"go.how.step3.title": "开始编程",
"go.how.step3.body": "可靠访问开源模型",
"go.privacy.title": "您的隐私对我们很重要",
@@ -312,11 +311,11 @@ export const dict = {
"go.faq.q2": "Go 包含哪些模型?",
"go.faq.a2": "Go 包含下方列出的模型,提供充足的限额和可靠的访问。",
"go.faq.q3": "Go 和 Zen 一样吗?",
"go.faq.a3": "不。Zen 是按量付费,而 Go 月 $5,之后 $10/月,提供充裕的限额,并可可靠访问精选模型阵容。",
"go.faq.a3": "不。Zen 是按量付费,而 Go 月 $10,提供充裕的限额,并可可靠访问精选模型阵容。",
"go.faq.q4": "Go 多少钱?",
"go.faq.a4.p1.beforePricing": "Go 费用为",
"go.faq.a4.p1.pricingLink": "月 $5",
"go.faq.a4.p1.afterPricing": "之后 $10/月,额度充裕。",
"go.faq.a4.p1.pricingLink": "月 $10",
"go.faq.a4.p1.afterPricing": "额度充裕。",
"go.faq.a4.p2.beforeAccount": "您可以在您的",
"go.faq.a4.p2.accountLink": "账户",
"go.faq.a4.p3": "中管理订阅。随时取消。",
@@ -650,8 +649,8 @@ export const dict = {
"workspace.lite.black.message": "您当前已订阅 OpenCode Black 或在候补名单中。如需切换到 Go,请先取消订阅。",
"workspace.lite.other.message": "此工作区中的另一位成员已经订阅了 OpenCode Go。每个工作区只有一名成员可以订阅。",
"workspace.lite.promo.description":
"OpenCode Go 起价为 {{price}}之后 $10/月,并提供对流行开放编码模型的可靠访问,同时享有充裕的使用限额。",
"workspace.lite.promo.price": "首月 $5",
"OpenCode Go 每月 {{price}},并提供对流行开放编码模型的可靠访问,同时享有充裕的使用限额。",
"workspace.lite.promo.price": "$10/月",
"workspace.lite.promo.modelsTitle": "包含模型",
"workspace.lite.promo.footer":
"该计划主要面向国际用户,提供稳定的全球访问体验。随着我们持续了解早期使用情况并收集反馈,定价和使用限额可能会有所调整。",
+11 -12
View File
@@ -245,7 +245,7 @@ export const dict = {
"go.title": "OpenCode Go | 低成本全民編碼模型",
"go.banner.text": "DeepSeek V4 Flash 限時享有 2 倍使用額度",
"go.meta.description": "Go 月 $5,之後 $10/月,提供充裕的使用限額,並可穩定存取領先的編碼模型。",
"go.meta.description": "Go 月 $10,提供充裕的使用限額,並可穩定存取領先的編碼模型。",
"go.hero.title": "低成本全民編碼模型",
"go.hero.body":
"Go 將代理編碼帶給全世界的程式設計師。提供寬裕的限額以及對最強大開源模型的穩定存取,讓你可以使用強大的代理進行構建,而無需擔心成本或可用性。",
@@ -254,8 +254,7 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "訂閱 Go",
"go.cta.price": "$10/月",
"go.cta.promo": "首月 $5",
"go.pricing.body": "可搭配任何代理使用。首月 $5,之後 $10/月。如有需要可儲值。隨時取消。",
"go.pricing.body": "可搭配任何代理使用。每月 $10。如有需要可儲值。隨時取消。",
"go.graph.free": "免費",
"go.graph.freePill": "Big Pickle 與免費模型",
"go.graph.go": "Go",
@@ -286,20 +285,20 @@ export const dict = {
"go.testimonials.frank.quote": "我希望我還在 Nvidia。",
"go.problem.title": "Go 正在解決什麼問題?",
"go.problem.body":
"我們致力於將 OpenCode 體驗帶給盡可能多的人。OpenCode Go 是一款低成本訂閱服務:首月 $5,之後 $10/月。它提供充裕的額度,並讓您能可靠地使用最強大的開源模型。",
"我們致力於將 OpenCode 體驗帶給盡可能多的人。OpenCode Go 是一款每月 $10 的低成本訂閱服務。它提供充裕的額度,並讓您能可靠地使用最強大的開源模型。",
"go.problem.subtitle": " ",
"go.problem.item1": "低成本訂閱定價",
"go.problem.item2": "寬裕的限額與穩定存取",
"go.problem.item3": "專為盡可能多的程式設計師打造",
"go.problem.item4": "針對代理編碼測試的精選模型陣容",
"go.how.title": "Go 如何運作",
"go.how.body": "Go 起價為首月 $5,之後 $10/月。您可以將其與 OpenCode 或任何代理搭配使用。",
"go.how.body": "Go 每月 $10。您可以將其與 OpenCode 或任何代理搭配使用。",
"go.how.step1.title": "建立帳號",
"go.how.step1.beforeLink": "遵循",
"go.how.step1.link": "設定說明",
"go.how.step2.title": "訂閱 Go",
"go.how.step2.link": "月 $5",
"go.how.step2.afterLink": "之後 $10/月,額度充裕",
"go.how.step2.link": "月 $10",
"go.how.step2.afterLink": "額度充裕",
"go.how.step3.title": "開始編碼",
"go.how.step3.body": "穩定存取開源模型",
"go.privacy.title": "你的隱私對我們很重要",
@@ -312,11 +311,11 @@ export const dict = {
"go.faq.q2": "Go 包含哪些模型?",
"go.faq.a2": "Go 包含下方列出的模型,提供充足的額度與穩定的存取。",
"go.faq.q3": "Go 與 Zen 一樣嗎?",
"go.faq.a3": "不。Zen 是按量付費,而 Go 月 $5,之後 $10/月,提供充裕的限額,並可穩定存取精選模型陣容。",
"go.faq.a3": "不。Zen 是按量付費,而 Go 月 $10,提供充裕的限額,並可穩定存取精選模型陣容。",
"go.faq.q4": "Go 費用是多少?",
"go.faq.a4.p1.beforePricing": "Go 費用為",
"go.faq.a4.p1.pricingLink": "月 $5",
"go.faq.a4.p1.afterPricing": "之後 $10/月,額度充裕。",
"go.faq.a4.p1.pricingLink": "月 $10",
"go.faq.a4.p1.afterPricing": "額度充裕。",
"go.faq.a4.p2.beforeAccount": "你可以在你的",
"go.faq.a4.p2.accountLink": "帳戶",
"go.faq.a4.p3": "中管理訂閱。隨時取消。",
@@ -650,8 +649,8 @@ export const dict = {
"workspace.lite.black.message": "您目前已訂閱 OpenCode Black 或在候補名單中。若要切換至 Go,請先取消訂閱。",
"workspace.lite.other.message": "此工作區中的另一位成員已訂閱 OpenCode Go。每個工作區只能有一位成員訂閱。",
"workspace.lite.promo.description":
"OpenCode Go 起價為 {{price}}之後 $10/月,並提供對熱門開放編碼模型的可靠存取,同時享有充裕的使用額度。",
"workspace.lite.promo.price": "首月 $5",
"OpenCode Go 每月 {{price}},並提供對熱門開放編碼模型的可靠存取,同時享有充裕的使用額度。",
"workspace.lite.promo.price": "$10/月",
"workspace.lite.promo.modelsTitle": "包含模型",
"workspace.lite.promo.footer":
"此方案主要為國際使用者設計,提供穩定的全球存取服務。隨著我們從初期使用情況和回饋中持續了解需求,價格和使用額度可能會有所調整。",
+1 -6
View File
@@ -364,12 +364,7 @@ export default function Home() {
{(part) => {
if (part === "{{text}}") return <span>{i18n.t("go.cta.text")}</span>
if (part === "{{price}}") {
return (
<span data-slot="cta-price">
<span data-slot="cta-price-old">{i18n.t("go.cta.price")}</span>
<span data-slot="cta-price-new">{i18n.t("go.cta.promo")}</span>
</span>
)
return <span data-slot="cta-price">{i18n.t("go.cta.price")}</span>
}
return part
}}
-1
View File
@@ -328,7 +328,6 @@ export namespace Billing {
return LiteData.threeMonths100Coupon
if (coupons.some((coupon) => coupon.type === "GOFREEMONTH" && !coupon.timeRedeemed))
return LiteData.firstMonth100Coupon
if (!coupons.some((coupon) => coupon.type === "GO1MONTH50")) return LiteData.firstMonth50Coupon
return undefined
})()
const createSession = () =>
+68 -10
View File
@@ -30,10 +30,12 @@ import {
type ToolDefinition,
type UsageInput,
} from "@opencode/ai"
import { Auth, Endpoint, RequestExecutor, type AnyRoute } from "@opencode/ai/route"
import { Auth, Endpoint, RequestExecutor, type AnyRoute, type HttpMiddleware } from "@opencode/ai/route"
import { ProviderShared } from "@opencode/ai/protocols/shared"
import { Cause, Context, Effect, Layer, Option, Schema, Scope, Stream } from "effect"
import { makeParser } from "effect/unstable/encoding/Sse"
import { HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
import { AsyncLocalStorage } from "node:async_hooks"
import type { ID, RuntimeInfo } from "./model.js"
import { Provider } from "./provider.js"
import { State } from "./state.js"
@@ -151,10 +153,11 @@ function prepareOptions(model: RuntimeInfo, pkg: string) {
}
}
const res = await (typeof customFetch === "function" ? customFetch : fetch)(input, {
...opts,
timeout: false,
})
const send: Fetch = typeof customFetch === "function" ? customFetch : fetch
const middleware = httpMiddleware.getStore()
const res = middleware
? await throughMiddleware(middleware, send, input, { ...opts, timeout: false })
: await send(input, { ...opts, timeout: false })
if (!chunkAbortCtl || typeof chunkTimeout !== "number") return res
return wrapSSE(res, chunkTimeout, chunkAbortCtl)
}
@@ -162,6 +165,56 @@ function prepareOptions(model: RuntimeInfo, pkg: string) {
return options
}
type Fetch = (input: Parameters<typeof fetch>[0], init?: BunFetchRequestInit) => Promise<Response>
// HTTP hook middleware is scoped to one model request, but the SDK's fetch is baked into the
// cached language model, so the active middleware rides along in async context instead.
const httpMiddleware = new AsyncLocalStorage<{ http: HttpMiddleware; context: Context.Context<never> }>()
function throughMiddleware(
store: { http: HttpMiddleware; context: Context.Context<never> },
send: Fetch,
input: Parameters<typeof fetch>[0],
init: BunFetchRequestInit,
) {
const toError = (cause: unknown) => (cause instanceof Error ? cause : new Error(String(cause)))
const request = input instanceof Request ? new Request(input, init) : new Request(String(input), init)
return Effect.runPromiseWith(store.context)(
Effect.gen(function* () {
// Hooks see a byte body like on the native route, so they may convert it to a web Request
// as many times as they like without contending for one stream.
const body = request.body ? new Uint8Array(yield* Effect.promise(() => request.arrayBuffer())) : undefined
const response = yield* store.http(
body
? HttpClientRequest.bodyUint8Array(
HttpClientRequest.fromWeb(request),
body,
request.headers.get("content-type") ?? undefined,
)
: HttpClientRequest.fromWeb(request),
(sent) =>
Effect.gen(function* () {
const web = yield* HttpClientRequest.toWeb(sent)
const response = yield* Effect.tryPromise(async () =>
send(web.url, {
...init,
method: web.method,
headers: web.headers,
body: web.body ? await web.arrayBuffer() : undefined,
}),
)
return HttpClientResponse.fromWeb(sent, response)
}).pipe(Effect.mapError(toError)),
)
const stream = [204, 205, 304].includes(response.status)
? null
: yield* Stream.toReadableStreamEffect(response.stream)
return new Response(stream, { status: response.status, headers: response.headers })
}),
{ signal: init.signal ?? undefined },
)
}
export class InitError extends Schema.TaggedError<InitError>()("AISDK.InitError", {
providerID: Provider.ID,
cause: Schema.Defect(),
@@ -344,7 +397,8 @@ function modelFromLanguage(info: RuntimeInfo, language: LanguageModelV3) {
model: (input) =>
LanguageModel.make({ ...input, provider: "provider" in input ? input.provider : providerID, route }),
prepareTransport: (body) => Effect.succeed(body),
streamPrepared: (prepared) => streamLanguage(language, prepared as LanguageModelV3CallOptions),
streamPrepared: (prepared, _request, _runtime, options) =>
streamLanguage(language, prepared as LanguageModelV3CallOptions, options?.http),
}
return LanguageModel.make({
id: info.modelID ?? info.id,
@@ -642,14 +696,18 @@ function metadataProviderOptions(input: ProviderMetadata | undefined): SharedV3P
return Object.fromEntries(Object.entries(input).map(([key, value]) => [key, jsonObject(value)]))
}
function streamLanguage(language: LanguageModelV3, options: LanguageModelV3CallOptions) {
function streamLanguage(language: LanguageModelV3, options: LanguageModelV3CallOptions, http?: HttpMiddleware) {
const state = { step: 0, toolNames: {} as Record<string, string> }
return Stream.concat(
Stream.make(LLMEvent.stepStart({ index: state.step })),
Stream.unwrap(
Effect.tryPromise({
try: () => language.doStream(options),
catch: (error) => llmError(error, "request"),
Effect.gen(function* () {
const context = yield* Effect.context<never>()
return yield* Effect.tryPromise({
try: () =>
http ? httpMiddleware.run({ http, context }, () => language.doStream(options)) : language.doStream(options),
catch: (error) => llmError(error, "request"),
})
}).pipe(
Effect.map((result) =>
Stream.fromReadableStream({
@@ -379,13 +379,13 @@ export function transformSession(input: TransformInput): TransformResult {
return []
const content = owned.flatMap((part): Array<Record<string, unknown>> => {
if (part.type === "text")
return [{ type: "text", text: part.text, ...(part.metadata ? { state: part.metadata } : {}) }]
return [{ type: "text", text: part.text, ...(part.metadata ? { native: part.metadata } : {}) }]
if (part.type === "reasoning")
return [
{
type: "reasoning",
text: part.text,
...(part.metadata ? { state: part.metadata } : {}),
...(part.metadata ? { native: part.metadata } : {}),
time: { created: part.time.start, ...(part.time.end === undefined ? {} : { completed: part.time.end }) },
},
]
@@ -258,7 +258,6 @@ export const GithubCopilotPlugin = define({
evt.sdk = mod.createOpenaiCompatible(evt.options)
}),
)
// Runs for every route, unlike http.request, which the AI SDK route bypasses.
yield* ctx.session.hook(
"model.request",
(evt) =>
+1 -1
View File
@@ -60,7 +60,7 @@ export const latestCompaction = Effect.fnUntraced(function* (
})
export const decodeMessageRow = (row: typeof SessionMessageTable.$inferSelect) =>
decode({ ...row.data, id: row.id, type: row.type }).pipe(
decode(SessionMessage.persisted({ ...row.data, id: row.id, type: row.type })).pipe(
Effect.tap((message) =>
SessionProviderContext.isCheckpoint(message)
? SessionProviderContext.validate(message.providerContext)
+1 -1
View File
@@ -125,7 +125,7 @@ const promotedFromMessage = Effect.fn("SessionInbox.promotedFromMessage")(functi
if (row === undefined) return undefined
if (row.session_id !== sessionID || (row.type !== "user" && row.type !== "synthetic"))
return yield* new LifecycleConflict({ id })
const message = decodeMessage({ ...row.data, id: row.id, type: row.type })
const message = decodeMessage(SessionMessage.persisted({ ...row.data, id: row.id, type: row.type }))
const base = { id, sessionID, time: { created: message.time.created }, delivery }
if (message.type === "user")
return User.make({
+11 -9
View File
@@ -83,7 +83,9 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
"session.message.content.updated": (event) =>
updateOwnedAssistant(event.data.messageID, (draft) => {
draft.content = castDraft(
Schema.decodeUnknownSync(Schema.Array(SessionMessage.AssistantContent))(event.data.content),
Schema.decodeUnknownSync(Schema.Array(SessionMessage.AssistantContent))(
SessionMessage.persistedContent(event.data.content),
),
)
}),
"session.usage.recorded": () => Effect.void,
@@ -222,7 +224,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
draft.error = undefined
draft.finish = undefined
draft.rawFinish = undefined
draft.providerState = undefined
draft.native = undefined
draft.time.created = DateTime.makeUnsafe(event.data.started)
draft.time.streamed = undefined
draft.time.completed = undefined
@@ -263,7 +265,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
draft.time.completed = created
draft.finish = event.data.finish
draft.rawFinish = event.data.rawFinish
draft.providerState = castDraft(event.data.providerState)
draft.native = castDraft(event.data.providerState)
draft.cost = event.data.cost
draft.tokens = event.data.tokens
projectTerminalSnapshot(draft, event)
@@ -274,7 +276,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
draft.time.completed = created
draft.finish = event.data.finish ?? "error"
draft.rawFinish = event.data.rawFinish
draft.providerState = castDraft(event.data.providerState)
draft.native = castDraft(event.data.providerState)
draft.error = castDraft(event.data.error)
draft.retry = undefined
if (event.data.cost !== undefined && event.data.tokens !== undefined) {
@@ -294,7 +296,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
const match = latestText(draft)
if (match) {
match.text = event.data.text
match.state = castDraft(event.data.state)
match.native = castDraft(event.data.state)
}
})
},
@@ -382,7 +384,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
SessionMessage.AssistantReasoning.make({
type: "reasoning",
text: "",
state: event.data.state,
native: event.data.state,
time: { created },
}),
),
@@ -395,7 +397,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
if (match) {
match.text = event.data.text
match.time = { created: match.time?.created ?? created, completed: created }
if (event.data.state !== undefined) match.state = event.data.state
if (event.data.state !== undefined) match.native = event.data.state
}
})
},
@@ -431,7 +433,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
metadata: event.metadata ? { ...current.metadata, ...event.metadata } : current.metadata,
reason: event.data.reason,
model: event.data.model,
providerState: event.data.providerState,
native: event.data.providerState,
summary: event.data.text,
providerContext: event.data.providerContext,
recent: event.data.recent,
@@ -448,7 +450,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
metadata: event.metadata,
reason: event.data.reason,
model: event.data.model,
providerState: event.data.providerState,
native: event.data.providerState,
summary: event.data.text,
providerContext: event.data.providerContext,
recent: event.data.recent,
+2 -2
View File
@@ -229,7 +229,7 @@ export const layer = Layer.effect(
const entries = Object.entries(shaped.options)
const generation = Object.fromEntries(entries.filter(([k]) => GENERATION_KEYS.has(k))) as GenerationOptionsFields
const providerOptions = Object.fromEntries(entries.filter(([k]) => !GENERATION_KEYS.has(k)))
const root = session.fork?.sessionID ?? session.id
const affinity = session.parentID ?? session.fork?.sessionID ?? session.id
const base = LLM.request({
model: model.model,
http: {
@@ -244,7 +244,7 @@ export const layer = Layer.effect(
},
},
// TODO: Persist cache lineage so nested forks reuse the root session's cache key.
promptCacheKey: /^ses_[0-9a-f]{64}$/.test(root) ? root.slice(4) : root,
promptCacheKey: /^ses_[0-9a-f]{64}$/.test(affinity) ? affinity.slice(4) : affinity,
system: shaped.system,
messages: boundImages(unsupportedParts(shaped.messages, model.capabilities)),
tools: Array.from(hooked, ([name, t]) => ({ ...t, name })),
+1 -1
View File
@@ -228,7 +228,7 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
function run(db: DatabaseService, event: MessageEvent) {
return Effect.gen(function* () {
const decodeRow = (row: typeof SessionMessageTable.$inferSelect) =>
decodeMessage({ ...row.data, id: row.id, type: row.type })
decodeMessage(SessionMessage.persisted({ ...row.data, id: row.id, type: row.type }))
const updateMessage = (message: SessionMessage.Info) => {
const encoded = encodeMessage(message)
const { id, type, ...data } = encoded
+3 -1
View File
@@ -107,7 +107,9 @@ const plan = Effect.fn("SessionRevert.plan")(function* (db: Database.Interface["
const decode = Schema.decodeUnknownEffect(SessionMessage.Info)
const files = new Map<RelativePath, Snapshot.ID>()
for (const row of rows) {
const message = yield* decode({ ...row.data, id: row.id, type: row.type }).pipe(Effect.orDie)
const message = yield* decode(SessionMessage.persisted({ ...row.data, id: row.id, type: row.type })).pipe(
Effect.orDie,
)
if (message.type !== "assistant" || !message.snapshot?.start) continue
for (const file of message.snapshot.files ?? [])
if (!files.has(file)) files.set(file, Snapshot.ID.make(message.snapshot.start))
@@ -162,7 +162,7 @@ const assistant = (message: SessionMessage.Assistant, model: Model.Ref, provider
text: item.text,
// Text can carry provider-bound state (e.g. Gemini thought signatures),
// which is only replayable against the model that produced it.
providerMetadata: reuseProviderMetadata ? providerMetadata(providerMetadataKey, item.state) : undefined,
providerMetadata: reuseProviderMetadata ? providerMetadata(providerMetadataKey, item.native) : undefined,
},
]
// Let the destination adapter handle readable reasoning after a model/provider switch.
@@ -172,7 +172,7 @@ const assistant = (message: SessionMessage.Assistant, model: Model.Ref, provider
{
type: "reasoning",
text: item.text,
providerMetadata: providerMetadata(providerMetadataKey, item.state),
providerMetadata: providerMetadata(providerMetadataKey, item.native),
},
]
: item.text.length > 0
+3 -3
View File
@@ -269,13 +269,13 @@ function sanitizeMessage(message: SessionMessage.Info): SessionMessage.Info {
return {
...content,
text: redact("text", message.id, content.text),
state: content.state ? { redacted: `text-state:${message.id}` } : undefined,
native: content.native ? { redacted: `text-native:${message.id}` } : undefined,
}
if (content.type === "reasoning")
return {
...content,
text: redact("reasoning", message.id, content.text),
state: content.state ? { redacted: `reasoning-state:${message.id}` } : undefined,
native: content.native ? { redacted: `reasoning-native:${message.id}` } : undefined,
}
return {
...content,
@@ -299,7 +299,7 @@ function sanitizeMessage(message: SessionMessage.Info): SessionMessage.Info {
summary: redact("compaction-summary", message.id, message.summary),
recent: redact("compaction-recent", message.id, message.recent),
...(message.status === "completed"
? { providerState: metadata("compaction-provider-state", message.id, message.providerState) }
? { native: metadata("compaction-native", message.id, message.native) }
: {}),
}
}
+95
View File
@@ -23,6 +23,7 @@ import { LLMClient, RequestExecutor } from "@opencode/ai/route"
import { compileRequest } from "@opencode/ai/route/client"
import { expect } from "bun:test"
import { Effect, Layer } from "effect"
import { HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
import { testEffect } from "./lib/effect"
const it = testEffect(AISDK.locationLayer)
@@ -596,6 +597,100 @@ it.effect("does not treat SSE comment heartbeats as model progress", () =>
}),
)
const chatChunk = (text: string) =>
`data: ${JSON.stringify({
id: "response-1",
object: "chat.completion.chunk",
created: 0,
model: "api-model",
choices: [{ index: 0, delta: { content: text }, finish_reason: "stop" }],
})}\n\ndata: [DONE]\n\n`
const compatibleModel = Effect.fn(function* (customFetch: typeof fetch) {
const aisdk = yield* AISDK.Service
yield* aisdk.hook.sdk((event) => {
event.sdk = createOpenAICompatible({
...event.options,
name: String(event.options.name),
baseURL: String(event.options.baseURL),
})
})
return yield* aisdk.model(
model("@ai-sdk/openai-compatible", { apiKey: "test", baseURL: "https://example.test/v1", fetch: customFetch }),
)
})
it.effect("routes AI SDK requests and responses through HTTP hook middleware", () =>
Effect.gen(function* () {
const sent: Array<{ url: string; headers: Headers; body: string }> = []
const resolved = yield* compatibleModel(
Object.assign(
async (input: Parameters<typeof fetch>[0], init?: RequestInit) => {
sent.push({
url: String(input),
headers: new Headers(init?.headers),
body: new TextDecoder().decode(init?.body as ArrayBuffer),
})
return new Response(chatChunk("upstream"), { headers: { "content-type": "text/event-stream" } })
},
{ preconnect: fetch.preconnect },
),
)
const seen: string[] = []
const response = yield* LLMClient.generate(LLM.request({ model: resolved, prompt: "Hello" }), {
http: (request, handler) =>
Effect.gen(function* () {
// Read the body twice the way session hooks do, to prove it is not a single-use stream.
const first = yield* HttpClientRequest.toWeb(request)
const second = yield* HttpClientRequest.toWeb(request)
seen.push(`${request.method} ${request.url}`)
seen.push(yield* Effect.promise(() => first.text()))
seen.push(yield* Effect.promise(() => second.text()))
const upstream = yield* handler(HttpClientRequest.setHeader(request, "x-hook", "applied"))
seen.push(`status ${upstream.status}`)
return HttpClientResponse.fromWeb(
upstream.request,
new Response(chatChunk("rewritten"), { headers: { "content-type": "text/event-stream" } }),
)
}),
}).pipe(Effect.provide(client))
expect(sent).toHaveLength(1)
expect(sent[0]?.url).toBe("https://example.test/v1/chat/completions")
expect(sent[0]?.headers.get("x-hook")).toBe("applied")
expect(sent[0]?.headers.get("authorization")).toBe("Bearer test")
expect(JSON.parse(sent[0]?.body ?? "")).toMatchObject({ model: "api-model" })
expect(seen).toEqual([
"POST https://example.test/v1/chat/completions",
sent[0]?.body,
sent[0]?.body,
"status 200",
])
expect(response.events.filter(LLMEvent.is.textDelta).map((event) => event.text)).toEqual(["rewritten"])
}),
)
it.effect("sends AI SDK requests directly when no HTTP hook middleware is attached", () =>
Effect.gen(function* () {
const bodies: unknown[] = []
const resolved = yield* compatibleModel(
Object.assign(
async (_input: Parameters<typeof fetch>[0], init?: RequestInit) => {
bodies.push(init?.body)
return new Response(chatChunk("upstream"), { headers: { "content-type": "text/event-stream" } })
},
{ preconnect: fetch.preconnect },
),
)
const response = yield* LLMClient.generate(LLM.request({ model: resolved, prompt: "Hello" })).pipe(
Effect.provide(client),
)
expect(bodies).toHaveLength(1)
expect(typeof bodies[0]).toBe("string")
expect(response.events.filter(LLMEvent.is.textDelta).map((event) => event.text)).toEqual(["upstream"])
}),
)
it.effect("emits malformed AI SDK tool input without executing it", () =>
Effect.gen(function* () {
const aisdk = yield* AISDK.Service
@@ -397,7 +397,7 @@ it.effect("manual compaction summarizes short context instead of no-op", () =>
])
expect(requests).toHaveLength(1)
expect(requests[0]?.promptCacheKey).toBe(sessionID)
expect(requests[0]?.promptCacheKey).toBe(parentID)
expect(requests[0]?.http?.headers).toEqual({
"x-session-affinity": sessionID,
"X-Session-Id": sessionID,
+4 -4
View File
@@ -1272,7 +1272,7 @@ describe("SessionTransfer", () => {
const runningCompactionID = SessionMessage.ID.create()
const completedCompactionID = SessionMessage.ID.create()
const model = Model.Ref.make({ id: Model.ID.make("model"), providerID: Provider.ID.make("provider") })
const providerState = { responseId: "summary-response" }
const native = { responseId: "summary-response" }
yield* transfer.import({
data: {
@@ -1328,7 +1328,7 @@ describe("SessionTransfer", () => {
status: "completed",
reason: "manual",
model,
providerState,
native,
summary: "summary",
recent: "recent",
time: { created: DateTime.makeUnsafe(9) },
@@ -1345,10 +1345,10 @@ describe("SessionTransfer", () => {
completedCompactionID,
])
expect(yield* Bus.latestSequence(db, sessionID)).toBe(4)
expect((yield* transfer.export({ sessionID })).messages.at(-1)).toMatchObject({ model, providerState })
expect((yield* transfer.export({ sessionID })).messages.at(-1)).toMatchObject({ model, native })
expect((yield* transfer.export({ sessionID, sanitize: true })).messages.at(-1)).toMatchObject({
model,
providerState: { redacted: `compaction-provider-state:${completedCompactionID}` },
native: { redacted: `compaction-native:${completedCompactionID}` },
})
}),
)
@@ -426,7 +426,7 @@ it.live("compaction hooks supply the summary instead of provider compaction", ()
status: "completed",
summary: "## Objective\n- hooked summary",
recent: "",
providerState: { responseId: "plugin" },
native: { responseId: "plugin" },
metadata: { plugin: "custom" },
tokens: { input: 10, output: 5 },
})
+2 -2
View File
@@ -690,7 +690,7 @@ describe("SessionProjector", () => {
type: "assistant",
finish: "stop",
rawFinish: "stop_sequence",
providerState: { response: "ended" },
native: { response: "ended" },
cost: Money.USD.make(1),
tokens: { input: 2, output: 3, reasoning: 4, cache: { read: 5, write: 6 } },
snapshot: { end: "snap_ended", files: ["src/ended.ts"] },
@@ -700,7 +700,7 @@ describe("SessionProjector", () => {
type: "assistant",
finish: "content-filter",
rawFinish: "blocked",
providerState: { response: "failed" },
native: { response: "failed" },
error: { type: "provider.invalid-request", message: "Failed" },
snapshot: { end: "snap_failed", files: ["src/failed.ts"] },
time: { completed: created },
@@ -72,7 +72,7 @@ describe("toLLMMessages", () => {
SessionMessage.AssistantReasoning.make({
type: "reasoning",
text: "",
state: { signature: "sig_1" },
native: { signature: "sig_1" },
}),
]),
],
@@ -711,7 +711,7 @@ Recent work
SessionMessage.AssistantReasoning.make({
type: "reasoning",
text: "Think",
state: { signature: "sig_1" },
native: { signature: "sig_1" },
}),
SessionMessage.AssistantTool.make({
type: "tool",
@@ -860,7 +860,7 @@ Recent work
SessionMessage.AssistantReasoning.make({
type: "reasoning",
text: "Think",
state: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" },
native: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" },
}),
],
time: { created, completed: created },
@@ -891,7 +891,7 @@ Recent work
SessionMessage.AssistantReasoning.make({
type: "reasoning",
text: "Think",
state: { signature: "signed" },
native: { signature: "signed" },
}),
],
time: { created, completed: created },
@@ -918,7 +918,7 @@ Recent work
SessionMessage.AssistantReasoning.make({
type: "reasoning",
text: "Partial thought",
state: { itemId: "rs_failed", reasoningEncryptedContent: null },
native: { itemId: "rs_failed", reasoningEncryptedContent: null },
}),
SessionMessage.AssistantTool.make({
type: "tool",
@@ -1016,7 +1016,7 @@ Recent work
SessionMessage.AssistantReasoning.make({
type: "reasoning",
text: "Visible thought",
state: { signature: "sig_old" },
native: { signature: "sig_old" },
}),
SessionMessage.AssistantTool.make({
type: "tool",
@@ -1110,7 +1110,7 @@ Recent work
SessionMessage.AssistantReasoning.make({
type: "reasoning",
text: "Visible thought",
state: { reasoningEncryptedContent: "encrypted" },
native: { reasoningEncryptedContent: "encrypted" },
}),
],
time: { created, completed: created },
@@ -1140,7 +1140,7 @@ Recent work
SessionMessage.AssistantText.make({
type: "text",
text: "Checking.",
state: { phase: "commentary" },
native: { phase: "commentary" },
}),
],
error: { type: "provider.unknown", message: "Interrupted after commentary" },
@@ -1171,7 +1171,7 @@ Recent work
SessionMessage.AssistantText.make({
type: "text",
text: "Checking.",
state: { phase: "commentary" },
native: { phase: "commentary" },
}),
],
time: { created, completed: created },
+10 -9
View File
@@ -2558,7 +2558,7 @@ describe("SessionRunnerLLM", () => {
expect(s.executions).toEqual(["x".repeat(4_000)])
expect((yield* s.messages).find((message) => message.type === "compaction")).toMatchObject({
model: { id: s.currentModel.id, providerID: s.currentModel.provider, variant },
providerState: { responseId: "summary" },
native: { responseId: "summary" },
})
// Compare wire content without the cache breakpoints that move to the new final message.
@@ -3508,12 +3508,12 @@ describe("SessionRunnerLLM", () => {
{
type: "reasoning",
text: "Signed thought",
state: { signature: "sig_1" },
native: { signature: "sig_1" },
},
{
type: "reasoning",
text: "Encrypted thought",
state: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" },
native: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" },
},
]),
])
@@ -3565,7 +3565,7 @@ describe("SessionRunnerLLM", () => {
{
type: "reasoning",
text: "thinking",
state: { reasoningField: "reasoning", reasoningDetails: details },
native: { reasoningField: "reasoning", reasoningDetails: details },
},
{ type: "text", text: "Hello world" },
]),
@@ -3609,7 +3609,7 @@ describe("SessionRunnerLLM", () => {
expect(yield* s.context).toMatchObject([
Expected.user("Check first"),
Expected.assistant({}, [
{ type: "text", text: "Checking.", state: { itemId: "msg_commentary", phase: "commentary" } },
{ type: "text", text: "Checking.", native: { itemId: "msg_commentary", phase: "commentary" } },
]),
])
@@ -4411,7 +4411,7 @@ describe("SessionRunnerLLM", () => {
})
})
scenario("adds the parent session header to child model requests", function* (s) {
scenario("uses parent cache affinity for child model requests", function* (s) {
const parentID = Session.ID.make("ses_runner_parent")
yield* s.db
@@ -4423,6 +4423,7 @@ describe("SessionRunnerLLM", () => {
yield* s.runPrompt("Run child request")
expect(s.requests[0]?.http?.headers?.["x-parent-session-id"]).toBe(parentID)
expect(s.requests[0]?.promptCacheKey).toBe(parentID)
})
scenario("runs different sessions concurrently", function* (s) {
@@ -4965,7 +4966,7 @@ describe("SessionRunnerLLM", () => {
type: "assistant",
finish: "stop",
rawFinish: "end_turn",
providerState: { responseId: "response-1", serviceTier: "priority" },
native: { responseId: "response-1", serviceTier: "priority" },
content: [Expected.text("Complete")],
},
])
@@ -4996,7 +4997,7 @@ describe("SessionRunnerLLM", () => {
type: "assistant",
finish: "content-filter",
rawFinish: "SAFETY",
providerState: {
native: {
responseId: "response-blocked",
refusal: { category: "safety", explanation: "Prompt blocked" },
},
@@ -5443,7 +5444,7 @@ describe("SessionRunnerLLM", () => {
{
type: "reasoning",
text: "",
state: { itemId: "rs_disconnected", reasoningEncryptedContent: "encrypted-state" },
native: { itemId: "rs_disconnected", reasoningEncryptedContent: "encrypted-state" },
},
]),
{ type: "synthetic", text: INCOMPLETE_STREAM_CONTINUATION },
+2 -2
View File
@@ -337,8 +337,8 @@ describe("V1Migration.transformSession", () => {
agent: "build",
model: { id: "model", providerID: "provider", variant: "fast" },
content: [
{ type: "text", text: "", state: { separator: true } },
{ type: "reasoning", text: "think", state: { provider: 1 }, time: { created: 21, completed: 22 } },
{ type: "text", text: "", native: { separator: true } },
{ type: "reasoning", text: "think", native: { provider: 1 }, time: { created: 21, completed: 22 } },
],
snapshot: { start: "snap_start", end: "snap_end", files: ["a.ts", "b.ts", "c.ts"] },
finish: "stop",
@@ -17,6 +17,12 @@ const channels = [
{ channel: "prod", appId: "ai.opencode.desktop" },
] as const
test("signs the macOS app without signing the DMG", async () => {
const config = (await import("./electron-builder.config.ts?mac-signing")).default as Configuration
expect(config.mac?.sign).toBeFunction()
expect(config.dmg?.sign).not.toBe(true)
})
for (const channel of channels) {
test(`disables security code AutoFill by default for ${channel.channel}`, async () => {
const previous = process.env.OPENCODE_CHANNEL
@@ -123,9 +123,6 @@ const getBase = (appId: string): Configuration => ({
notarize: true,
target: ["dmg", "zip"],
},
dmg: {
sign: true,
},
protocols: {
name: "OpenCode",
schemes: ["opencode"],
+4 -4
View File
@@ -17320,7 +17320,7 @@
"rawFinish": {
"type": "string"
},
"providerState": {
"native": {
"$ref": "#/components/schemas/Session.Message.ProviderState_4"
},
"cost": {
@@ -17349,7 +17349,7 @@
"text": {
"type": "string"
},
"state": {
"native": {
"$ref": "#/components/schemas/Session.Message.ProviderState_1"
},
"time": {
@@ -17396,7 +17396,7 @@
"text": {
"type": "string"
},
"state": {
"native": {
"$ref": "#/components/schemas/Session.Message.ProviderState"
}
},
@@ -17509,7 +17509,7 @@
"model": {
"$ref": "#/components/schemas/Model.Ref"
},
"providerState": {
"native": {
"$ref": "#/components/schemas/Session.Message.ProviderState_5"
},
"summary": {
+1 -1
View File
@@ -601,7 +601,7 @@ export namespace Compaction {
...Base,
reason: Started.data.fields.reason,
model: SessionMessage.CompactionCompleted.fields.model,
providerState: SessionMessage.CompactionCompleted.fields.providerState,
providerState: SessionMessage.CompactionCompleted.fields.native,
providerContext: SessionMessage.CompactionCompleted.fields.providerContext,
text: Schema.String,
recent: Schema.String,
+43 -6
View File
@@ -1,6 +1,6 @@
export * as SessionMessage from "./session-message.js"
import { Schema } from "effect"
import { Predicate, Schema, Struct } from "effect"
import { SessionProviderContext } from "./session-provider-context.js"
import { optional } from "./schema.js"
import { Content } from "./tool.js"
@@ -177,14 +177,14 @@ export interface AssistantText extends Schema.Schema.Type<typeof AssistantText>
export const AssistantText = Schema.Struct({
type: Schema.tag("text"),
text: Schema.String,
state: ProviderState.pipe(optional),
native: ProviderState.pipe(optional),
}).annotate({ identifier: "Session.Message.Assistant.Text" })
export interface AssistantReasoning extends Schema.Schema.Type<typeof AssistantReasoning> {}
export const AssistantReasoning = Schema.Struct({
type: Schema.tag("reasoning"),
text: Schema.String,
state: ProviderState.pipe(optional),
native: ProviderState.pipe(optional),
time: Schema.Struct({
created: DateTimeUtcFromMillis,
completed: DateTimeUtcFromMillis.pipe(optional),
@@ -196,7 +196,18 @@ export const AssistantContent = Schema.Union([AssistantText, AssistantReasoning,
)
export type AssistantContent = AssistantText | AssistantReasoning | AssistantTool
export const AssistantContentEncoded = Schema.toEncoded(AssistantContent).annotate({
/**
* Frozen at the shape older releases stored: text and reasoning carried their
* provider blob as `state`. Only replayed durable events still use it; read it
* through `persistedContent` before decoding as `AssistantContent`.
*/
export const AssistantContentEncoded = Schema.toEncoded(
Schema.Union([
Schema.Struct({ ...Struct.omit(AssistantText.fields, ["native"]), state: ProviderState.pipe(optional) }),
Schema.Struct({ ...Struct.omit(AssistantReasoning.fields, ["native"]), state: ProviderState.pipe(optional) }),
AssistantTool,
]).pipe(Schema.toTaggedUnion("type")),
).annotate({
identifier: "Session.Message.AssistantContent.Encoded",
})
export type AssistantContentEncoded = typeof AssistantContentEncoded.Type
@@ -222,7 +233,7 @@ export const Assistant = Schema.Struct({
}).pipe(optional),
finish: FinishReason.pipe(optional),
rawFinish: Schema.String.pipe(optional),
providerState: ProviderState.pipe(optional),
native: ProviderState.pipe(optional),
cost: Money.USD.pipe(optional),
tokens: TokenUsage.Info.pipe(optional),
error: SessionError.Error.pipe(optional),
@@ -258,7 +269,7 @@ export const CompactionCompleted = Schema.Struct({
status: Schema.tag("completed"),
reason: Schema.Literals(["auto", "manual"]),
model: Model.Ref.pipe(optional),
providerState: ProviderState.pipe(optional),
native: ProviderState.pipe(optional),
summary: Schema.String,
recent: Schema.String,
providerContext: SessionProviderContext.Info.pipe(optional),
@@ -318,3 +329,29 @@ export type Info =
| Compaction
| Idle
export type Type = Info["type"]
/** Reads messages stored before provider blobs were renamed to `native`. Tool parts are unchanged. */
export function persisted(input: unknown) {
if (!Predicate.isObject(input)) return input
const message =
input.type === "assistant" || input.type === "compaction" ? rename(input, "providerState", "native") : input
if (message.type !== "assistant" || !Array.isArray(message.content)) return message
const content = persistedContent(message.content)
return content === message.content ? message : { ...message, content }
}
/** Reads assistant content stored before text and reasoning blobs were renamed to `native`. */
export function persistedContent(content: ReadonlyArray<unknown>) {
const next = content.map((part) => {
if (!Predicate.isObject(part) || (part.type !== "text" && part.type !== "reasoning")) return part
return rename(part, "state", "native")
})
return next.every((part, index) => part === content[index]) ? content : next
}
function rename(record: Record<string, unknown>, from: string, to: string) {
if (record[from] === undefined || record[to] !== undefined) return record
const value = record[from]
const rest = Object.fromEntries(Object.entries(record).filter(([key]) => key !== from))
return { ...rest, [to]: value }
}
@@ -257,8 +257,8 @@ describe("contract hygiene", () => {
text: "hello",
})
expect(
SessionMessage.AssistantReasoning.make({ type: "reasoning", text: "thinking", state: { id: "opaque" } }),
).toEqual({ type: "reasoning", text: "thinking", state: { id: "opaque" } })
SessionMessage.AssistantReasoning.make({ type: "reasoning", text: "thinking", native: { id: "opaque" } }),
).toEqual({ type: "reasoning", text: "thinking", native: { id: "opaque" } })
expect(
SessionMessage.AssistantTool.make({
type: "tool",
+36 -2
View File
@@ -23,14 +23,48 @@ test("assistant terminal diagnostics remain optional and round trip", () => {
...assistant,
finish: "content-filter",
rawFinish: "SAFETY",
providerState: { promptFeedback: { blockReason: "SAFETY" } },
native: { promptFeedback: { blockReason: "SAFETY" } },
}),
),
).toMatchObject({
finish: "content-filter",
rawFinish: "SAFETY",
providerState: { promptFeedback: { blockReason: "SAFETY" } },
native: { promptFeedback: { blockReason: "SAFETY" } },
})
const legacy = SessionMessage.persisted({
...assistant,
providerState: { promptFeedback: { blockReason: "SAFETY" } },
content: [{ type: "text", text: "hello", state: { signature: "sig" } }],
})
expect(decode(legacy)).toMatchObject({
native: { promptFeedback: { blockReason: "SAFETY" } },
content: [{ type: "text", native: { signature: "sig" } }],
})
expect(encode(decode(legacy))).not.toHaveProperty("providerState")
expect(SessionMessage.persisted(assistant)).toBe(assistant)
})
test("replayed content updates keep the stored provider blob shape", () => {
const content = [
{ type: "text", text: "hello", state: { signature: "sig" } },
{ type: "reasoning", text: "think", state: { id: "rs_1" }, time: { created: 1 } },
{ type: "tool", id: "call", name: "read", state: { status: "streaming", input: "" }, time: { created: 1 } },
] as const
const decoded = Schema.decodeUnknownSync(SessionEvent.MessageContentUpdated.data)({
sessionID: "ses_terminal",
messageID: "msg_terminal",
content,
})
expect(decoded.content).toEqual(content)
expect(
Schema.decodeUnknownSync(Schema.Array(SessionMessage.AssistantContent))(
SessionMessage.persistedContent(decoded.content),
),
).toMatchObject([
{ type: "text", native: { signature: "sig" } },
{ type: "reasoning", native: { id: "rs_1" } },
{ type: "tool", state: { status: "streaming" } },
])
})
test("failed steps only override the assistant finish for content filters", () => {
@@ -190,13 +190,13 @@ export const streamingDocument = document(
{
type: "reasoning",
text: "## Checking the current contract\n\nThe assistant content is nested on each current Session message.",
state: { phase: "streaming" },
native: { phase: "streaming" },
time: { created: STORY_TIME + 11_100 },
},
{
type: "text",
text: "I have the typed rows in place. Next I am checking the streaming presentation",
state: { phase: "streaming" },
native: { phase: "streaming" },
},
],
}),
@@ -47,7 +47,7 @@ function MermaidTimeline(props: { streaming: boolean }) {
"```mermaid\nsequenceDiagram\n Client->>Server: Send prompt\n Server->>Model: Generate response\n Model-->>Client: Response\n" +
(completed() ? "```" : ""),
].join("\n\n"),
...(completed() ? {} : { state: { phase: "streaming" } }),
...(completed() ? {} : { native: { phase: "streaming" } }),
},
],
},
+4 -5
View File
@@ -121,12 +121,11 @@ export const settings: Setting[] = [
keywords: ["approve", "accept", "permission requests"],
},
{
title: "Enabled",
title: "Mode",
category: "Tabs",
path: ["tabs", "enabled"],
default: true,
values: [false, true],
labels: ["off", "on"],
path: ["tabs", "mode"],
default: "auto",
values: ["off", "on", "auto"],
},
{
title: "Scope",

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