mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-22 08:37:36 +00:00
Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3178cf10dd | ||
|
|
dcfe1ec7bd | ||
|
|
ceace24a3e | ||
|
|
19e1357a06 | ||
|
|
4b381ac6a1 | ||
|
|
07d48e1ffb | ||
|
|
9fdcb8da41 | ||
|
|
ba61ac6730 | ||
|
|
94b9133910 | ||
|
|
6f8c5ae0aa | ||
|
|
60673aaef3 | ||
|
|
651529d64e | ||
|
|
532f25d0d4 | ||
|
|
643c4c3500 | ||
|
|
9d531435b4 | ||
|
|
5b9dc35eec | ||
|
|
1814dd9799 | ||
|
|
1e1cd042ea | ||
|
|
02566f6219 | ||
|
|
f488aa3f79 |
@@ -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 }}
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-/jah4P2a0aGJNJ0aMdlFEbGHAXx33lqxHbc7UmpLFDg=",
|
||||
"aarch64-linux": "sha256-L3SoZ24qNXicsE2FK6LATQjOmjPxY679RjugrUyO/1Y=",
|
||||
"aarch64-darwin": "sha256-pI9NT8KWUPi+JCk6DYMqIAYmBqTbi13uL4VdNV3WS6Y=",
|
||||
"x86_64-darwin": "sha256-rMAGhTTz46KA5Ya7E5J0af7Bn1QzTDhTaNfNJm8qfsw="
|
||||
"x86_64-linux": "sha256-8hc0Typ9cA1NDpToM0Pq7q3AutSp+I80Sakthq10F4c=",
|
||||
"aarch64-linux": "sha256-7bzI4zWOdxuoMdMHMuqIAOgnuzWiHmpdCMYCYPbs+3c=",
|
||||
"aarch64-darwin": "sha256-EWDHUSVH2AjsNvoKvzC392H6GjCWVh07cOg2x0mAsng=",
|
||||
"x86_64-darwin": "sha256-7b2BRdRRVG+PMCSd/cm4E+slziwWIKLVttYkotdSrK4="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"))),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
@@ -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,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"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
+8
-4
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
+33
@@ -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}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+33
@@ -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" },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
}),
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
},
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import { base64Encode } from "@opencode/util/encode"
|
||||
import { expect, test } from "@playwright/test"
|
||||
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||
import { expectSessionTitle } from "../utils/waits"
|
||||
|
||||
const directory = "C:/OpenCode/BtwSidebar"
|
||||
const projectID = "proj_btw_sidebar"
|
||||
const sessionID = "ses_btw_sidebar"
|
||||
const otherSessionID = "ses_btw_sidebar_other"
|
||||
const title = "Side question session"
|
||||
const otherTitle = "Other side question session"
|
||||
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
|
||||
const sessionHref = (id: string) => `/server/${base64Encode(server)}/session/${id}`
|
||||
|
||||
test.use({ viewport: { width: 1440, height: 900 } })
|
||||
|
||||
test("answers /btw in the side panel without admitting a prompt", async ({ page }) => {
|
||||
const generations: { sessionID: string; prompt: string }[] = []
|
||||
const prompts: unknown[] = []
|
||||
const generated = Promise.withResolvers<void>()
|
||||
await mockOpenCodeServer(page, {
|
||||
directory,
|
||||
project: {
|
||||
id: projectID,
|
||||
worktree: directory,
|
||||
vcs: "git",
|
||||
name: "btw-sidebar",
|
||||
time: { created: 1700000000000, updated: 1700000000000 },
|
||||
sandboxes: [],
|
||||
},
|
||||
provider: {
|
||||
all: [
|
||||
{
|
||||
id: "opencode",
|
||||
name: "OpenCode",
|
||||
models: { test: { id: "test", name: "Test", limit: { context: 200_000 } } },
|
||||
},
|
||||
],
|
||||
connected: ["opencode"],
|
||||
default: { providerID: "opencode", modelID: "test" },
|
||||
},
|
||||
sessions: [
|
||||
{
|
||||
id: sessionID,
|
||||
slug: sessionID,
|
||||
projectID,
|
||||
directory,
|
||||
title,
|
||||
version: "dev",
|
||||
time: { created: 1700000000000, updated: 1700000000000 },
|
||||
},
|
||||
{
|
||||
id: otherSessionID,
|
||||
slug: otherSessionID,
|
||||
projectID,
|
||||
directory,
|
||||
title: otherTitle,
|
||||
version: "dev",
|
||||
time: { created: 1700000001000, updated: 1700000001000 },
|
||||
},
|
||||
],
|
||||
pageMessages: () => ({ items: [] }),
|
||||
vcsDiff: [],
|
||||
onPrompt: (input) => prompts.push(input),
|
||||
generate: async (input) => {
|
||||
generations.push(input)
|
||||
if (input.sessionID === otherSessionID) return { text: "This answer belongs to the **other session**." }
|
||||
await generated.promise
|
||||
return {
|
||||
text: "The retry loop uses **exponential backoff** and stops after three attempts.\n\n```ts\nconst delay = 2 ** attempt\n```",
|
||||
}
|
||||
},
|
||||
})
|
||||
await page.addInitScript(
|
||||
({ directory, server, sessionID, otherSessionID }) => {
|
||||
localStorage.setItem(
|
||||
"opencode.global.dat:server",
|
||||
JSON.stringify({
|
||||
projects: { local: [{ worktree: directory, expanded: true }] },
|
||||
lastProject: { local: directory },
|
||||
}),
|
||||
)
|
||||
localStorage.setItem(
|
||||
"opencode.window.browser.dat:tabs",
|
||||
JSON.stringify([
|
||||
{ type: "session", server, sessionId: sessionID },
|
||||
{ type: "session", server, sessionId: otherSessionID },
|
||||
]),
|
||||
)
|
||||
},
|
||||
{ directory, server, sessionID, otherSessionID },
|
||||
)
|
||||
|
||||
await page.goto(sessionHref(sessionID))
|
||||
await expectSessionTitle(page, title)
|
||||
const editor = page.locator('[data-component="composer-editor"]')
|
||||
await expect(editor).toBeEditable()
|
||||
|
||||
await editor.fill("/btw")
|
||||
const suggestion = page.locator('[data-suggestion-id="session.btw"]')
|
||||
await expect(suggestion).toBeVisible()
|
||||
await suggestion.click()
|
||||
await expect(editor).toHaveText("/btw ")
|
||||
await editor.press("Enter")
|
||||
|
||||
const panel = page.locator('[data-slot="session-btw-panel"]')
|
||||
await expect(panel).toBeHidden()
|
||||
await expect(page.getByText("Add a question after /btw", { exact: true })).toBeVisible()
|
||||
expect(generations).toEqual([])
|
||||
expect(prompts).toEqual([])
|
||||
|
||||
await editor.fill("/btw how does the retry loop work?")
|
||||
await editor.press("Enter")
|
||||
|
||||
const tab = page.getByRole("tab", { name: "/btw" })
|
||||
await expect(panel).toBeVisible()
|
||||
await expect(panel.getByRole("textbox")).toHaveCount(0)
|
||||
await expect(panel.getByRole("status")).toContainText("Working")
|
||||
await expect(tab).toHaveAttribute("data-selected", "")
|
||||
generated.resolve()
|
||||
await expect(panel.getByText("how does the retry loop work?", { exact: true })).toBeVisible()
|
||||
await expect(panel.getByText("exponential backoff", { exact: false })).toBeVisible()
|
||||
await expect(panel.getByText("const delay = 2 ** attempt", { exact: true })).toBeVisible()
|
||||
expect(generations).toHaveLength(1)
|
||||
expect(generations[0]?.sessionID).toBe(sessionID)
|
||||
expect(generations[0]?.prompt).toContain("how does the retry loop work?")
|
||||
expect(prompts).toEqual([])
|
||||
await expect(editor).toHaveText("")
|
||||
|
||||
await page.locator(`[data-titlebar-tab-link][href="${sessionHref(otherSessionID)}"]`).click()
|
||||
await expectSessionTitle(page, otherTitle)
|
||||
await editor.fill("/btw what belongs here?")
|
||||
await editor.press("Enter")
|
||||
await expect(panel.getByText("other session", { exact: false })).toBeVisible()
|
||||
|
||||
await page.locator(`[data-titlebar-tab-link][href="${sessionHref(sessionID)}"]`).click()
|
||||
await expectSessionTitle(page, title)
|
||||
await expect(panel.getByText("exponential backoff", { exact: false })).toBeVisible()
|
||||
await expect(panel.getByText("other session", { exact: false })).toHaveCount(0)
|
||||
|
||||
await page.reload()
|
||||
await expectSessionTitle(page, title)
|
||||
await expect(page.getByRole("tab", { name: "/btw" })).toHaveCount(0)
|
||||
await expect(page.locator('[data-slot="session-btw-panel"]')).toHaveCount(0)
|
||||
})
|
||||
@@ -2,6 +2,8 @@ import { expect, test } from "@playwright/test"
|
||||
import { fixture, pageMessages } from "../smoke/session-timeline.fixture"
|
||||
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||
|
||||
test.use({ permissions: ["clipboard-read", "clipboard-write"] })
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
const sessions = fixture.sessions.map((session) => ({ ...session }))
|
||||
await mockOpenCodeServer(page, {
|
||||
@@ -95,7 +97,7 @@ test("renames and closes the session tab from its context menu", async ({ page }
|
||||
await expect(tab).toBeFocused()
|
||||
await tab.press("Shift+F10")
|
||||
await page.getByRole("menuitem", { name: "Rename", exact: true }).click()
|
||||
const input = page.locator('[data-slot="tab-title"][contenteditable="true"]')
|
||||
const input = page.locator('[data-slot="tab-title"][contenteditable="plaintext-only"]')
|
||||
await expect(input).toBeFocused()
|
||||
await input.fill("Renamed from tab")
|
||||
await input.press("Enter")
|
||||
@@ -112,6 +114,28 @@ test("renames and closes the session tab from its context menu", async ({ page }
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test("pastes rich text into the session tab title as plain text", async ({ page }) => {
|
||||
const tab = page.locator('[data-slot="titlebar-tabs"] a').filter({ hasText: fixture.expected.targetTitle })
|
||||
await tab.click({ button: "right" })
|
||||
await page.getByRole("menuitem", { name: "Rename", exact: true }).click()
|
||||
const input = page.locator('[data-slot="tab-title"][contenteditable="plaintext-only"]')
|
||||
await expect(input).toBeFocused()
|
||||
await page.evaluate(async () => {
|
||||
await navigator.clipboard.write([
|
||||
new ClipboardItem({
|
||||
"text/html": new Blob(['<span style="font-size: 48px">Rich title</span>'], { type: "text/html" }),
|
||||
"text/plain": new Blob(["Rich title"], { type: "text/plain" }),
|
||||
}),
|
||||
])
|
||||
})
|
||||
await input.press("ControlOrMeta+A")
|
||||
await input.press("ControlOrMeta+V")
|
||||
await expect(input).toHaveText("Rich title")
|
||||
await expect(input.locator("*")).toHaveCount(0)
|
||||
await input.press("Enter")
|
||||
await expect(page.getByRole("heading", { name: "Rich title", exact: true })).toBeVisible()
|
||||
})
|
||||
|
||||
test("renames an inactive tab without switching sessions", async ({ page }) => {
|
||||
await page.getByRole("button", { name: "Home", exact: true }).click()
|
||||
await page.locator('[data-component="home-session-row"]').filter({ hasText: fixture.expected.sourceTitle }).click()
|
||||
@@ -119,7 +143,7 @@ test("renames an inactive tab without switching sessions", async ({ page }) => {
|
||||
const tab = page.locator('[data-slot="titlebar-tabs"] a').filter({ hasText: fixture.expected.targetTitle })
|
||||
await tab.click({ button: "right" })
|
||||
await page.getByRole("menuitem", { name: "Rename", exact: true }).click()
|
||||
const input = page.locator('[data-slot="tab-title"][contenteditable="true"]')
|
||||
const input = page.locator('[data-slot="tab-title"][contenteditable="plaintext-only"]')
|
||||
await expect(input).toBeFocused()
|
||||
await input.fill("Inactive tab renamed")
|
||||
await input.press("Tab")
|
||||
|
||||
@@ -197,6 +197,13 @@ const Group = HttpApiGroup.make("mock")
|
||||
success: Json,
|
||||
}),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("sessionGenerate", "/api/session/:sessionID/generate", {
|
||||
params: SessionParams,
|
||||
payload: Schema.Struct({ prompt: Schema.String }),
|
||||
success: Json,
|
||||
}),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("sessionSwitchAgent", "/api/session/:sessionID/agent", {
|
||||
params: SessionParams,
|
||||
|
||||
@@ -42,6 +42,7 @@ export interface MockServerConfig {
|
||||
sessionStatus?: Record<string, unknown> | (() => Record<string, unknown>)
|
||||
inbox?: unknown[] | (() => unknown[])
|
||||
onPrompt?: (input: { sessionID: string; body: Record<string, unknown> }) => void
|
||||
generate?: (input: { sessionID: string; prompt: string }) => { text: string } | Promise<{ text: string }>
|
||||
onInboxChange?: (input: { sessionID: string; inboxID: string; action: "cancel" | "steer" | "queue" }) => void
|
||||
}
|
||||
|
||||
@@ -456,6 +457,12 @@ function mockHandlers(config: MockServerConfig, state: { cursors: Map<string, st
|
||||
},
|
||||
}
|
||||
}),
|
||||
sessionGenerate: (ctx) =>
|
||||
Effect.promise(async () => ({
|
||||
data: (await config.generate?.({ sessionID: ctx.params.sessionID, prompt: ctx.payload.prompt })) ?? {
|
||||
text: "Side-question answer",
|
||||
},
|
||||
})),
|
||||
sessionInboxCancel: (ctx) =>
|
||||
Effect.sync(() =>
|
||||
config.onInboxChange?.({ sessionID: ctx.params.sessionID, inboxID: ctx.params.inboxID, action: "cancel" }),
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { parseClientSlashCommand } from "./client-slash-command"
|
||||
|
||||
const options = [
|
||||
{ id: "session.btw", trigger: "btw", arguments: true, type: "builtin" as const },
|
||||
{ id: "custom.btw", trigger: "custom", type: "custom" as const },
|
||||
{ id: "model.choose", trigger: "model", type: "builtin" as const },
|
||||
]
|
||||
|
||||
describe("parseClientSlashCommand", () => {
|
||||
test("parses inline and multiline arguments", () => {
|
||||
expect(parseClientSlashCommand(options, "/btw why this approach?")).toEqual({
|
||||
id: "session.btw",
|
||||
input: "why this approach?",
|
||||
})
|
||||
expect(parseClientSlashCommand(options, "/btw\nwhy this approach?")).toEqual({
|
||||
id: "session.btw",
|
||||
input: "why this approach?",
|
||||
})
|
||||
})
|
||||
|
||||
test("accepts a bare argument command", () => {
|
||||
expect(parseClientSlashCommand(options, "/btw")).toEqual({ id: "session.btw", input: "" })
|
||||
})
|
||||
|
||||
test("rejects prefixes, custom commands, and ordinary slash commands", () => {
|
||||
expect(parseClientSlashCommand(options, "/btwx nope")).toBeUndefined()
|
||||
expect(parseClientSlashCommand(options, "/custom nope")).toBeUndefined()
|
||||
expect(parseClientSlashCommand(options, "/model opus")).toBeUndefined()
|
||||
expect(parseClientSlashCommand(options, "ask /btw later")).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,24 @@
|
||||
type ClientSlashCommand = {
|
||||
id: string
|
||||
trigger: string
|
||||
arguments?: boolean
|
||||
type: "builtin" | "custom"
|
||||
}
|
||||
|
||||
export function parseSlashCommand(text: string) {
|
||||
if (!text.startsWith("/")) return
|
||||
const separator = text.search(/\s/)
|
||||
const name = text.slice(1, separator === -1 ? undefined : separator)
|
||||
return { name, input: separator === -1 ? "" : text.slice(separator).trim() }
|
||||
}
|
||||
|
||||
export function parseClientSlashCommand(options: readonly ClientSlashCommand[], text: string) {
|
||||
const command = parseSlashCommand(text)
|
||||
if (!command) return
|
||||
const option = options.find((item) => item.type === "builtin" && item.arguments && item.trigger === command.name)
|
||||
if (!option) return
|
||||
return {
|
||||
id: option.id,
|
||||
input: command.input,
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import { createComposerHistory } from "./history/store"
|
||||
import { composerPlaceholder } from "./placeholder"
|
||||
import { createComposerSubmit } from "./submit"
|
||||
import { useAttachmentDestination } from "./attachments/destination"
|
||||
import { parseClientSlashCommand } from "./client-slash-command"
|
||||
|
||||
export type ComposerModel = ComposerEditorModel & {
|
||||
readonly model: ComposerControls["model"]
|
||||
@@ -73,9 +74,7 @@ export function createComposerModel(adapter: ComposerAdapter, options?: { queue?
|
||||
return [...result, path]
|
||||
}, [])
|
||||
})
|
||||
const attachments = createMemo(() =>
|
||||
prompt.current().filter(isAttachment),
|
||||
)
|
||||
const attachments = createMemo(() => prompt.current().filter(isAttachment))
|
||||
const commentCount = createMemo(() => {
|
||||
if (mode() === "shell") return 0
|
||||
return prompt.context.items().filter((item) => !!item.comment?.trim()).length
|
||||
@@ -242,6 +241,7 @@ export function createComposerModel(adapter: ComposerAdapter, options?: { queue?
|
||||
trigger: item.slash!,
|
||||
title: item.title,
|
||||
description: item.description,
|
||||
arguments: item.slashArguments,
|
||||
type: "builtin" as const,
|
||||
})),
|
||||
])
|
||||
@@ -299,6 +299,11 @@ export function createComposerModel(adapter: ComposerAdapter, options?: { queue?
|
||||
clear: comments.clear,
|
||||
restore: restoreHistoryComments,
|
||||
},
|
||||
clientCommand: (text) => {
|
||||
const selected = parseClientSlashCommand(slashCommands(), text)
|
||||
if (!selected) return
|
||||
return () => command.trigger(selected.id, "slash", selected.input)
|
||||
},
|
||||
})
|
||||
const controller = createComposerEditor({
|
||||
store: prompt.store,
|
||||
@@ -340,6 +345,7 @@ export function createComposerModel(adapter: ComposerAdapter, options?: { queue?
|
||||
if (item.kind !== "command") return
|
||||
const selected = slashCommands().find((entry) => entry.id === item.id)
|
||||
if (!selected || selected.type === "custom") return
|
||||
if (selected.arguments) return
|
||||
return () => command.trigger(selected.id, "slash")
|
||||
},
|
||||
attachments: {
|
||||
|
||||
@@ -54,14 +54,17 @@ function submitInput(
|
||||
mode: "normal" | "shell" = "normal",
|
||||
commands: () => readonly { name: string }[] | undefined = () => [],
|
||||
history: string[] = [],
|
||||
clientCommand?: (text: string) => (() => void | Promise<void>) | undefined,
|
||||
) {
|
||||
return createComposerSubmit({
|
||||
adapter,
|
||||
mode: () => mode,
|
||||
commands,
|
||||
clientCommand,
|
||||
editor: () => undefined,
|
||||
queueScroll() {},
|
||||
addToHistory: (prompt) => history.push(`add:${prompt.map((part) => ("content" in part ? part.content : part.type)).join("")}`),
|
||||
addToHistory: (prompt) =>
|
||||
history.push(`add:${prompt.map((part) => ("content" in part ? part.content : part.type)).join("")}`),
|
||||
removeFromHistory: (prompt) =>
|
||||
history.push(`remove:${prompt.map((part) => ("content" in part ? part.content : part.type)).join("")}`),
|
||||
resetHistory() {},
|
||||
@@ -118,6 +121,61 @@ function session(input: {
|
||||
}
|
||||
|
||||
describe("Composer submission", () => {
|
||||
test("runs a client argument command without admitting it to the session", async () => {
|
||||
const state = createMemoryComposerState().capture()
|
||||
state.set([
|
||||
{ type: "text", content: "/btw why this approach?", start: 0, end: 23 },
|
||||
{
|
||||
type: "image",
|
||||
id: "attachment",
|
||||
filename: "diagram.png",
|
||||
mime: "image/png",
|
||||
blob: { id: "attachment", url: "data:image/png;base64,YQ==" },
|
||||
},
|
||||
])
|
||||
state.context.add({ type: "file", path: "src/retry.ts" })
|
||||
const calls: string[] = []
|
||||
const target = session({
|
||||
calls,
|
||||
prompt: async () => {
|
||||
throw new Error("client command must not call prompt")
|
||||
},
|
||||
})
|
||||
const adapter: ActiveComposerAdapter = {
|
||||
kind: "active-session",
|
||||
state,
|
||||
ready: () => true,
|
||||
controls,
|
||||
working: () => false,
|
||||
session: () => target,
|
||||
interrupt: async () => undefined,
|
||||
submitted() {},
|
||||
setEditor() {},
|
||||
}
|
||||
|
||||
const history: string[] = []
|
||||
await submitInput(adapter, undefined, "normal", undefined, history, (text) => {
|
||||
expect(text).toBe("/btw why this approach?")
|
||||
return () => {
|
||||
calls.push("btw")
|
||||
}
|
||||
}).submit(new Event("submit"))
|
||||
|
||||
expect(calls).toEqual(["btw"])
|
||||
expect(history).toEqual([])
|
||||
expect(state.current()).toEqual([
|
||||
{ type: "text", content: "", start: 0, end: 0 },
|
||||
{
|
||||
type: "image",
|
||||
id: "attachment",
|
||||
filename: "diagram.png",
|
||||
mime: "image/png",
|
||||
blob: { id: "attachment", url: "data:image/png;base64,YQ==" },
|
||||
},
|
||||
])
|
||||
expect(state.context.items()).toHaveLength(1)
|
||||
})
|
||||
|
||||
test("applies the captured agent and model before a custom command without passing over its overrides", async () => {
|
||||
const state = createMemoryComposerState({ prompt: "/review changes" }).capture()
|
||||
const calls: string[] = []
|
||||
@@ -625,12 +683,7 @@ describe("Composer submission", () => {
|
||||
},
|
||||
}
|
||||
|
||||
await submitInput(
|
||||
adapter,
|
||||
undefined,
|
||||
"normal",
|
||||
() => catalog,
|
||||
).submit(new Event("submit"))
|
||||
await submitInput(adapter, undefined, "normal", () => catalog).submit(new Event("submit"))
|
||||
|
||||
expect(await sent.promise).toBe("command")
|
||||
expect(requests).toEqual([
|
||||
|
||||
@@ -11,6 +11,7 @@ import { setCursorPosition } from "./editor/dom"
|
||||
import { blobDataUrl, resolveBlobUrl } from "@/runtime/persistence/drafts"
|
||||
import { isAttachment } from "./prompt-parts"
|
||||
import type { ModelSelection } from "@/providers/models/selection"
|
||||
import { parseSlashCommand } from "./client-slash-command"
|
||||
|
||||
const submitting = new WeakSet<object>()
|
||||
|
||||
@@ -37,6 +38,7 @@ type ComposerSubmitInput = {
|
||||
setMode: (mode: "normal" | "shell") => void
|
||||
closePopover: () => void
|
||||
delivery?: (alternate: boolean) => ComposerDelivery
|
||||
clientCommand?: (text: string) => (() => void | Promise<void>) | undefined
|
||||
notify: {
|
||||
missingSelection: () => void
|
||||
failed: (kind: "shell" | "command" | "prompt", error: unknown) => void
|
||||
@@ -52,15 +54,31 @@ export function createComposerSubmit(input: ComposerSubmitInput) {
|
||||
const submit = async (event: globalThis.Event, options?: { alternate?: boolean }) => {
|
||||
event.preventDefault()
|
||||
|
||||
const prompt = clonePrompt(input.adapter.state.current())
|
||||
const text = submissionText(prompt)
|
||||
const clientCommand = input.mode() === "normal" ? input.clientCommand?.(text) : undefined
|
||||
if (clientCommand) {
|
||||
if (submitting.has(input.adapter.state)) return
|
||||
submitting.add(input.adapter.state)
|
||||
try {
|
||||
clearClientCommand(input, prompt)
|
||||
await clientCommand()
|
||||
} catch (error) {
|
||||
input.notify.failed("command", error)
|
||||
} finally {
|
||||
submitting.delete(input.adapter.state)
|
||||
}
|
||||
return
|
||||
}
|
||||
const submission = createComposerSubmission({
|
||||
target: input.adapter.state,
|
||||
prompt: clonePrompt(input.adapter.state.current()),
|
||||
prompt,
|
||||
context: input.adapter.state.context.items().map((item) => ({
|
||||
...item,
|
||||
selection: item.selection ? { ...item.selection } : undefined,
|
||||
})),
|
||||
})
|
||||
const read = readSubmission(input, submission.prompt, submission.context, options?.alternate ?? false)
|
||||
const read = readSubmission(input, submission.prompt, submission.context, text, options?.alternate ?? false)
|
||||
if (!read) {
|
||||
if (input.adapter.working() && input.adapter.kind === "active-session") void input.adapter.interrupt()
|
||||
return
|
||||
@@ -150,6 +168,17 @@ export function createComposerSubmit(input: ComposerSubmitInput) {
|
||||
}
|
||||
}
|
||||
|
||||
function clearClientCommand(input: ComposerSubmitInput, prompt: Prompt) {
|
||||
input.adapter.state.set([{ type: "text", content: "", start: 0, end: 0 }, ...prompt.filter(isAttachment)], 0)
|
||||
input.adapter.state.mode.set("normal")
|
||||
input.setMode("normal")
|
||||
input.closePopover()
|
||||
}
|
||||
|
||||
function submissionText(prompt: Prompt) {
|
||||
return prompt.map((part) => ("content" in part ? part.content : "")).join("")
|
||||
}
|
||||
|
||||
function handoffMessage(value: ComposerSubmission): SessionMessageUser {
|
||||
return {
|
||||
id: value.id,
|
||||
@@ -193,9 +222,9 @@ function readSubmission(
|
||||
input: ComposerSubmitInput,
|
||||
prompt: Prompt,
|
||||
context: ComposerSubmission["context"],
|
||||
text: string,
|
||||
alternate: boolean,
|
||||
): ComposerSubmission | undefined {
|
||||
const text = prompt.map((part) => ("content" in part ? part.content : "")).join("")
|
||||
const mode = input.mode()
|
||||
if (mode === "shell" && !text.trim()) return
|
||||
const images = prompt.filter((part): part is ImageAttachmentPart => part.type === "image")
|
||||
@@ -298,14 +327,11 @@ async function sendShell(session: ComposerSession, value: ComposerSubmission) {
|
||||
}
|
||||
|
||||
function findCommand(commands: ReturnType<ComposerSubmitInput["commands"]>, text: string) {
|
||||
if (!text.startsWith("/")) return
|
||||
const [name, ...arguments_] = text.split(" ")
|
||||
const command = name.slice(1)
|
||||
if (!commands?.some((item) => item.name === command)) return
|
||||
return { command, arguments: arguments_.join(" ") }
|
||||
const parsed = parseSlashCommand(text)
|
||||
if (!parsed || !commands?.some((item) => item.name === parsed.name)) return
|
||||
return { command: parsed.name, arguments: parsed.input }
|
||||
}
|
||||
|
||||
|
||||
async function sendCommand(
|
||||
session: ComposerSession,
|
||||
value: ComposerSubmission,
|
||||
@@ -390,7 +416,10 @@ async function sendPrompt(
|
||||
|
||||
async function buildSubmissionRequest(session: ComposerSession, value: ComposerSubmission) {
|
||||
const images = await Promise.all(
|
||||
value.images.map(async (attachment) => ({ ...attachment, dataUrl: await blobDataUrl(attachment.blob, attachment.mime) })),
|
||||
value.images.map(async (attachment) => ({
|
||||
...attachment,
|
||||
dataUrl: await blobDataUrl(attachment.blob, attachment.mime),
|
||||
})),
|
||||
)
|
||||
return buildPromptRequest({
|
||||
prompt: value.prompt,
|
||||
|
||||
@@ -47,7 +47,7 @@ export function HomeCommandPalette(props: {
|
||||
state.cleanup = undefined
|
||||
dialog.close()
|
||||
if (item.type === "command") {
|
||||
item.option?.onSelect?.("palette")
|
||||
void item.option?.onSelect?.("palette")
|
||||
return
|
||||
}
|
||||
if (item.type === "session") props.onSelectSession(item)
|
||||
|
||||
@@ -151,6 +151,8 @@ export const dict = {
|
||||
"command.session.compact.description": "Summarize the session to reduce context size",
|
||||
"command.session.fork": "Fork from message",
|
||||
"command.session.fork.description": "Create a new session from a previous message",
|
||||
"command.session.btw": "Ask a side question",
|
||||
"command.session.btw.description": "Get a one-shot answer without adding to the conversation",
|
||||
"command.session.export": "Export session",
|
||||
"command.session.export.description": "Export the full session transcript as JSON",
|
||||
"command.session.import": "Import session",
|
||||
@@ -756,6 +758,7 @@ export const dict = {
|
||||
"session.tab.browser": "Browser",
|
||||
"session.tab.add": "Add tab",
|
||||
"session.tab.context": "Context",
|
||||
"session.tab.btw": "/btw",
|
||||
"session.tab.unknown": "Unknown Session",
|
||||
"session.panel.reviewAndFiles": "Review and files",
|
||||
"session.error.notFound": "This session cannot be found",
|
||||
@@ -933,6 +936,10 @@ export const dict = {
|
||||
"common.dismiss": "Dismiss",
|
||||
"common.moreCountSuffix": " (+{{count}} more)",
|
||||
"common.requestFailed": "Request failed",
|
||||
"session.btw.questionRequired": "Add a question after /btw",
|
||||
"session.btw.error": "Couldn’t answer that question",
|
||||
"session.btw.retry": "Retry",
|
||||
"session.btw.copy": "Copy answer",
|
||||
"common.moreOptions": "More options",
|
||||
"common.learnMore": "Learn more",
|
||||
"common.rename": "Rename",
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import { createEffect, onCleanup } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useLanguage } from "@/runtime/i18n/language"
|
||||
import { useServerSDK } from "@/runtime/server/client"
|
||||
import { useCommand } from "@/shell/commands/command"
|
||||
import { showToast } from "@/shell/notifications/toast"
|
||||
import { SESSION_BTW_TAB } from "@/session/helpers"
|
||||
import type { SessionModel } from "../model"
|
||||
|
||||
const instructions = [
|
||||
"The user is asking a quick side question about the conversation so far.",
|
||||
"Answer directly and concisely in markdown from what you already know.",
|
||||
"Do not call any tools and do not take any actions.",
|
||||
].join(" ")
|
||||
|
||||
const empty = {
|
||||
question: "",
|
||||
answer: "",
|
||||
error: false,
|
||||
pending: false,
|
||||
}
|
||||
|
||||
export function createSessionBtw(session: SessionModel) {
|
||||
const command = useCommand()
|
||||
const language = useLanguage()
|
||||
const server = useServerSDK()
|
||||
const [states, setStates] = createStore<Record<string, typeof empty>>({})
|
||||
const requests = new Map<string, number>()
|
||||
const controllers = new Map<string, AbortController>()
|
||||
const state = () => states[session.identity.sessionKey()] ?? empty
|
||||
|
||||
createEffect(() => {
|
||||
const key = session.identity.sessionKey()
|
||||
onCleanup(() => {
|
||||
const controller = controllers.get(key)
|
||||
if (!controller) return
|
||||
controller.abort()
|
||||
controllers.delete(key)
|
||||
if (states[key]?.pending) setStates(key, { pending: false, error: true })
|
||||
})
|
||||
})
|
||||
|
||||
const open = () => {
|
||||
session.layout.view().reviewPanel.open()
|
||||
const tabs = session.layout.tabs()
|
||||
if (tabs.active() !== SESSION_BTW_TAB) tabs.open(SESSION_BTW_TAB)
|
||||
}
|
||||
const ask = (value?: string) => {
|
||||
const question = value?.trim()
|
||||
if (!question) {
|
||||
showToast({ title: language.t("session.btw.questionRequired") })
|
||||
return
|
||||
}
|
||||
open()
|
||||
const sessionID = session.identity.sessionID()
|
||||
if (!sessionID) return
|
||||
|
||||
const key = session.identity.sessionKey()
|
||||
const request = (requests.get(key) ?? 0) + 1
|
||||
requests.set(key, request)
|
||||
controllers.get(key)?.abort()
|
||||
const controller = new AbortController()
|
||||
controllers.set(key, controller)
|
||||
const owner = session.ownership.capture()
|
||||
setStates(key, { question, answer: "", error: false, pending: true })
|
||||
return server.api.session
|
||||
.generate(
|
||||
{
|
||||
sessionID,
|
||||
prompt: [instructions, question].join("\n\n"),
|
||||
},
|
||||
{ signal: controller.signal },
|
||||
)
|
||||
.then((result) => {
|
||||
owner.run(() => {
|
||||
if (requests.get(key) !== request) return
|
||||
setStates(key, { answer: result.text.trim(), pending: false })
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
owner.run(() => {
|
||||
if (controller.signal.aborted || requests.get(key) !== request) return
|
||||
setStates(key, { error: true, pending: false })
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
if (controllers.get(key) === controller) controllers.delete(key)
|
||||
})
|
||||
}
|
||||
|
||||
command.register("session.btw", () => [
|
||||
{
|
||||
id: "session.btw",
|
||||
title: language.t("command.session.btw"),
|
||||
description: language.t("command.session.btw.description"),
|
||||
category: language.t("command.category.session"),
|
||||
slash: "btw",
|
||||
slashArguments: true,
|
||||
hidden: true,
|
||||
disabled: !session.isDesktop(),
|
||||
onSelect: (_source, input) => ask(input),
|
||||
},
|
||||
])
|
||||
|
||||
return {
|
||||
answer: () => state().answer,
|
||||
error: () => state().error,
|
||||
pending: () => state().pending,
|
||||
question: () => state().question,
|
||||
retry: () => ask(state().question),
|
||||
}
|
||||
}
|
||||
|
||||
export type SessionBtwModel = ReturnType<typeof createSessionBtw>
|
||||
@@ -0,0 +1,80 @@
|
||||
import { createEffect, createSignal, Match, Show, Switch } from "solid-js"
|
||||
import { Button } from "@opencode/ui/button"
|
||||
import { Icon } from "@opencode/ui/icon"
|
||||
import { IconButton } from "@opencode/ui/icon-button"
|
||||
import { ScrollView } from "@opencode/ui/scroll-view"
|
||||
import { TextShimmer } from "@opencode/ui/text-shimmer"
|
||||
import { Tooltip } from "@opencode/ui/tooltip"
|
||||
import { Markdown } from "@opencode/session-ui/markdown"
|
||||
import { useLanguage } from "@/runtime/i18n/language"
|
||||
import { usePlatform } from "@/runtime/platform/platform"
|
||||
import { showToast } from "@/shell/notifications/toast"
|
||||
import type { SessionBtwModel } from "./model"
|
||||
|
||||
export function SessionBtwPanel(props: { btw: SessionBtwModel }) {
|
||||
const language = useLanguage()
|
||||
const platform = usePlatform()
|
||||
const [copied, setCopied] = createSignal(false)
|
||||
|
||||
createEffect(() => {
|
||||
props.btw.answer()
|
||||
setCopied(false)
|
||||
})
|
||||
|
||||
const copy = () => {
|
||||
const answer = props.btw.answer()
|
||||
if (!answer) return
|
||||
void (platform.writeClipboardText?.(answer) ?? navigator.clipboard.writeText(answer)).then(
|
||||
() => setCopied(true),
|
||||
() => showToast({ title: language.t("common.requestFailed") }),
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="flex h-full min-h-0 flex-col bg-v2-background-bg-base" data-slot="session-btw-panel">
|
||||
<div class="flex shrink-0 items-start justify-between gap-3 border-b border-v2-border-border-base px-5 py-4">
|
||||
<div class="min-w-0 text-13-regular text-text-weak">{props.btw.question()}</div>
|
||||
<Show when={props.btw.answer()}>
|
||||
<Tooltip value={copied() ? language.t("common.copied") : language.t("session.btw.copy")}>
|
||||
<IconButton
|
||||
size="small"
|
||||
variant="ghost-muted"
|
||||
icon={<Icon name={copied() ? "check" : "outline-copy"} />}
|
||||
aria-label={copied() ? language.t("common.copied") : language.t("session.btw.copy")}
|
||||
onClick={copy}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="relative min-h-0 flex-1">
|
||||
<Switch>
|
||||
<Match when={props.btw.pending()}>
|
||||
<div
|
||||
data-component="session-working"
|
||||
role="status"
|
||||
class="flex h-9 items-center px-5 pt-3 text-[13px] font-[530] leading-text-compact"
|
||||
>
|
||||
<TextShimmer text={language.t("session.timeline.working")} active />
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={props.btw.error()}>
|
||||
<div class="flex h-full flex-col items-center justify-center gap-3 px-8 pb-24 text-center">
|
||||
<div class="text-13-regular text-text-weak">{language.t("session.btw.error")}</div>
|
||||
<Button size="small" variant="outline" onClick={props.btw.retry}>
|
||||
{language.t("session.btw.retry")}
|
||||
</Button>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={props.btw.answer()}>
|
||||
<ScrollView class="absolute inset-0">
|
||||
<div class="px-5 py-4 pb-8">
|
||||
<Markdown text={props.btw.answer()} class="text-14-regular" />
|
||||
</div>
|
||||
</ScrollView>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -37,6 +37,7 @@ import { useSettings } from "@/settings/model"
|
||||
import { createFileTabListSync } from "@/session/files/file-tab-scroll"
|
||||
import {
|
||||
SESSION_OPEN_FILE_TAB,
|
||||
SESSION_BTW_TAB,
|
||||
isSessionBrowserTab,
|
||||
sessionBrowserTab,
|
||||
createOpenSessionFileTab,
|
||||
@@ -74,6 +75,7 @@ export function SessionSidePanel(props: {
|
||||
size: Sizing
|
||||
stacked?: boolean
|
||||
browser: ReturnType<typeof createSessionBrowser>
|
||||
btwPanel: () => JSX.Element
|
||||
}) {
|
||||
const layout = useLayout()
|
||||
const settings = useSettings()
|
||||
@@ -227,7 +229,7 @@ export function SessionSidePanel(props: {
|
||||
})
|
||||
const fileBrowserVisible = createMemo(() => {
|
||||
const active = activeTab()
|
||||
return active !== "review" && active !== "context" && active !== "empty" && !isSessionBrowserTab(active)
|
||||
return active === SESSION_OPEN_FILE_TAB || active === activeFileTab()
|
||||
})
|
||||
const openFileKeybind = createMemo(() => command.keybindParts("file.open"))
|
||||
const openBrowserKeybind = createMemo(() => command.keybindParts("browser.open"))
|
||||
@@ -385,6 +387,14 @@ export function SessionSidePanel(props: {
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Match when={tab === SESSION_BTW_TAB}>
|
||||
<SortableTab tab={tab} index={tabs().all().indexOf(tab)} onTabClose={tabs().close}>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<Icon name="bubble-5" size="small" />
|
||||
<span>{language.t("session.tab.btw")}</span>
|
||||
</div>
|
||||
</SortableTab>
|
||||
</Match>
|
||||
<Match when={isSessionBrowserTab(tab)}>
|
||||
<Show when={props.browser.tabs().find((item) => sessionBrowserTab(item.id) === tab)}>
|
||||
{(item) => (
|
||||
@@ -583,6 +593,12 @@ export function SessionSidePanel(props: {
|
||||
</Tabs.Content>
|
||||
</Show>
|
||||
|
||||
<Show when={activeTab() === SESSION_BTW_TAB}>
|
||||
<Tabs.Content value={SESSION_BTW_TAB} class="flex h-full min-h-0 flex-col overflow-hidden">
|
||||
{props.btwPanel()}
|
||||
</Tabs.Content>
|
||||
</Show>
|
||||
|
||||
<Show when={props.browser.opened()}>
|
||||
<div
|
||||
id={browserTabPanelID}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"
|
||||
import { createMemo, createRoot } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import {
|
||||
SESSION_BTW_TAB,
|
||||
SESSION_BROWSER_TAB,
|
||||
sessionBrowserTab,
|
||||
SESSION_OPEN_FILE_TAB,
|
||||
@@ -236,6 +237,24 @@ describe("createSessionTabs", () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("exposes the BTW tab without treating it as a file tab", () => {
|
||||
createRoot((dispose) => {
|
||||
const tabs = createMemo(() => ({ active: () => SESSION_BTW_TAB, all: () => [SESSION_BTW_TAB] }))
|
||||
const result = createSessionTabs({
|
||||
tabs,
|
||||
pathFromTab: () => undefined,
|
||||
normalizeTab: (tab) => tab,
|
||||
})
|
||||
|
||||
expect(result.panelTabs()).toEqual([SESSION_BTW_TAB])
|
||||
expect(result.openedTabs()).toEqual([])
|
||||
expect(result.activeTab()).toBe(SESSION_BTW_TAB)
|
||||
expect(result.activeFileTab()).toBeUndefined()
|
||||
expect(result.closableTab()).toBe(SESSION_BTW_TAB)
|
||||
dispose()
|
||||
})
|
||||
})
|
||||
|
||||
test("exposes one browser tab without treating it as a file tab", () => {
|
||||
createRoot((dispose) => {
|
||||
const tabs = createMemo(() => ({ active: () => SESSION_BROWSER_TAB, all: () => [SESSION_BROWSER_TAB] }))
|
||||
|
||||
@@ -2,10 +2,11 @@ import { batch, createMemo, onCleanup, onMount, type Accessor } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||
import { same } from "@/runtime/persistence/equality"
|
||||
import { isSessionBrowserTab, SESSION_OPEN_FILE_TAB } from "@/shell/state/session-tabs"
|
||||
import { isSessionBrowserTab, SESSION_BTW_TAB, SESSION_OPEN_FILE_TAB } from "@/shell/state/session-tabs"
|
||||
|
||||
export {
|
||||
SESSION_BROWSER_TAB,
|
||||
SESSION_BTW_TAB,
|
||||
SESSION_OPEN_FILE_TAB,
|
||||
sessionBrowserTab,
|
||||
isSessionBrowserTab,
|
||||
@@ -63,13 +64,17 @@ export const createSessionTabs = (input: TabsInput) => {
|
||||
{ equals: same },
|
||||
)
|
||||
const openedTabs = createMemo(
|
||||
() => panelTabs().filter((tab) => tab !== SESSION_OPEN_FILE_TAB && !isSessionBrowserTab(tab)),
|
||||
() =>
|
||||
panelTabs().filter(
|
||||
(tab) => tab !== SESSION_OPEN_FILE_TAB && tab !== SESSION_BTW_TAB && !isSessionBrowserTab(tab),
|
||||
),
|
||||
emptyTabs,
|
||||
{ equals: same },
|
||||
)
|
||||
const activeTab = createMemo(() => {
|
||||
const active = input.tabs().active()
|
||||
if (active === "context") return active
|
||||
if (active === SESSION_BTW_TAB) return active
|
||||
if (active === SESSION_OPEN_FILE_TAB && openFileOpen()) return active
|
||||
if (active && isSessionBrowserTab(active) && browser()) return active
|
||||
if (active === "review" && review()) return active
|
||||
@@ -89,6 +94,7 @@ export const createSessionTabs = (input: TabsInput) => {
|
||||
const closableTab = createMemo<string | undefined>(() => {
|
||||
const active = activeTab()
|
||||
if (active === "context") return active
|
||||
if (active === SESSION_BTW_TAB) return active
|
||||
if (active === SESSION_OPEN_FILE_TAB && openFileOpen()) return active
|
||||
if (active && isSessionBrowserTab(active) && browser()) return active
|
||||
if (!openedTabs().includes(active)) return
|
||||
|
||||
@@ -14,6 +14,8 @@ import { ReviewPanel } from "./panel"
|
||||
import { SessionReviewTab } from "./review-tab"
|
||||
import type { ChangeMode, SessionReviewModel } from "./model"
|
||||
import type { createSessionBrowser } from "../browser/model"
|
||||
import type { SessionBtwModel } from "../btw/model"
|
||||
import { SessionBtwPanel } from "../btw/panel"
|
||||
|
||||
const MobilePanelDrawer = lazy(async () => {
|
||||
const { MobilePanelDrawer } = await import("@/shell/mobile-panel-drawer")
|
||||
@@ -127,6 +129,7 @@ export function SessionMobileReview(props: { review: SessionReviewModel }) {
|
||||
export function SessionDesktopReview(props: {
|
||||
review: SessionReviewModel
|
||||
browser: ReturnType<typeof createSessionBrowser>
|
||||
btw: SessionBtwModel
|
||||
present?: boolean
|
||||
}) {
|
||||
return (
|
||||
@@ -153,6 +156,7 @@ export function SessionDesktopReview(props: {
|
||||
size={props.review.screen.size}
|
||||
stacked={props.review.screen.side.layout().stacked}
|
||||
browser={props.browser}
|
||||
btwPanel={() => <SessionBtwPanel btw={props.btw} />}
|
||||
/>
|
||||
</Suspense>
|
||||
)
|
||||
|
||||
@@ -39,6 +39,7 @@ import { createAnimatedPresence } from "@/runtime/animated-presence"
|
||||
import { createSessionBrowser } from "./browser/model"
|
||||
import { createTimelineCache } from "./timeline/cache"
|
||||
import { ArtifactMarkdownProvider, ArtifactOpenerProvider } from "./files/open-artifact"
|
||||
import { createSessionBtw } from "./btw/model"
|
||||
|
||||
const SessionMobileFiles = lazy(async () => {
|
||||
const { SessionMobileFiles } = await import("./files/session-mobile-files")
|
||||
@@ -71,6 +72,7 @@ function SessionScreenContent(props: { session: SessionModel; browser: ReturnTyp
|
||||
return info ? projectForSession(info, server.ctx.sync.data.project) : undefined
|
||||
})
|
||||
const isDesktop = session.isDesktop
|
||||
const btw = createSessionBtw(session)
|
||||
const screen = createSessionScreenLayout(session)
|
||||
const timeline = createSessionTimelineInteraction(session)
|
||||
const timelineSearch = createTimelineSearchController({
|
||||
@@ -451,7 +453,12 @@ function SessionScreenContent(props: { session: SessionModel; browser: ReturnTyp
|
||||
setStore("sideReviewPresent", false)
|
||||
}}
|
||||
>
|
||||
<SessionDesktopReview review={review} browser={browser} present={store.sideReviewPresent} />
|
||||
<SessionDesktopReview
|
||||
review={review}
|
||||
browser={browser}
|
||||
btw={btw}
|
||||
present={store.sideReviewPresent}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
@@ -88,11 +88,12 @@ export interface CommandOption {
|
||||
category?: string
|
||||
keybind?: KeybindConfig
|
||||
slash?: string
|
||||
slashArguments?: boolean
|
||||
suggested?: boolean
|
||||
disabled?: boolean
|
||||
hidden?: boolean
|
||||
when?: (event: KeyboardEvent) => boolean
|
||||
onSelect?: (source?: "palette" | "keybind" | "slash") => void
|
||||
onSelect?: (source?: "palette" | "keybind" | "slash", input?: string) => void | Promise<void>
|
||||
onHighlight?: () => (() => void) | void
|
||||
}
|
||||
|
||||
@@ -389,9 +390,9 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
||||
return map
|
||||
})
|
||||
|
||||
const run = (id: string, source?: CommandSource) => {
|
||||
const run = (id: string, source?: CommandSource, input?: string) => {
|
||||
const option = optionMap().get(id)
|
||||
option?.onSelect?.(source)
|
||||
return option?.onSelect?.(source, input)
|
||||
}
|
||||
|
||||
const showPalette = () => {
|
||||
@@ -420,7 +421,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
||||
if (!option) return
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
option.onSelect?.("keybind")
|
||||
void option.onSelect?.("keybind")
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
@@ -454,8 +455,8 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
||||
|
||||
return {
|
||||
register,
|
||||
trigger(id: string, source?: CommandSource) {
|
||||
run(id, source)
|
||||
trigger(id: string, source?: CommandSource, input?: string) {
|
||||
return run(id, source, input)
|
||||
},
|
||||
keybind(id: string) {
|
||||
const config = keybindConfig(id)
|
||||
|
||||
@@ -166,7 +166,7 @@ export function createCommandPaletteModel(props: { filesOnly?: () => boolean; on
|
||||
state.cleanup = undefined
|
||||
dialog.close()
|
||||
if (item.type === "command") {
|
||||
item.option?.onSelect?.("palette")
|
||||
void item.option?.onSelect?.("palette")
|
||||
return
|
||||
}
|
||||
if (item.type === "session") {
|
||||
|
||||
@@ -96,7 +96,7 @@ describe("layout persistence", () => {
|
||||
test("keeps scoped state and salvages valid tab entries", () => {
|
||||
const key = "local\u0000L3Byb2plY3Q/session"
|
||||
const value = decode({
|
||||
sessionTabs: { old: { all: ["old"] }, [key]: { all: ["a", null, "a", "b"], active: 12 } },
|
||||
sessionTabs: { old: { all: ["old"] }, [key]: { all: ["a", null, "a", "b", "btw"], active: "btw" } },
|
||||
sessionView: { old: { scroll: {} }, [key]: { scroll: {}, reviewOpen: ["a", null, "b"] } },
|
||||
})
|
||||
expect(value.sessionTabs).toEqual({ [key]: { all: ["a", "b"], active: undefined } })
|
||||
|
||||
@@ -18,7 +18,7 @@ import type { ProjectAvatarVariant } from "@opencode/ui/project-avatar"
|
||||
import { SessionStateKey } from "@/runtime/server/scope"
|
||||
import { createSessionKeyReader, ensureSessionKey, pruneSessionKeys } from "./helpers"
|
||||
import { requireServerKey } from "@/shell/routes/session"
|
||||
import { closeSessionTab, openSessionTab, previewSessionTab, type SessionTabs } from "./session-tabs"
|
||||
import { closeSessionTab, openSessionTab, previewSessionTab, SESSION_BTW_TAB, type SessionTabs } from "./session-tabs"
|
||||
|
||||
export { createSessionKeyReader, ensureSessionKey, pruneSessionKeys }
|
||||
|
||||
@@ -97,8 +97,9 @@ const normalizeSessionTabList = (path: ReturnType<typeof createPathHelpers> | un
|
||||
const normalizeStoredSessionTabs = (key: string, tabs: SessionTabs) => {
|
||||
const path = sessionPath(key)
|
||||
return {
|
||||
all: normalizeSessionTabList(path, tabs.all),
|
||||
active: tabs.active ? normalizeSessionTab(path, tabs.active) : tabs.active,
|
||||
all: normalizeSessionTabList(path, tabs.all).filter((tab) => tab !== SESSION_BTW_TAB),
|
||||
active:
|
||||
tabs.active === SESSION_BTW_TAB ? undefined : tabs.active ? normalizeSessionTab(path, tabs.active) : tabs.active,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export const SESSION_OPEN_FILE_TAB = "open-file"
|
||||
export const SESSION_BROWSER_TAB = "browser"
|
||||
export const SESSION_BTW_TAB = "btw"
|
||||
export const sessionBrowserTab = (tabID: string) => `${SESSION_BROWSER_TAB}:${tabID}`
|
||||
export const isSessionBrowserTab = (tab: string | undefined) =>
|
||||
!!tab && (tab === SESSION_BROWSER_TAB || tab.startsWith(`${SESSION_BROWSER_TAB}:`))
|
||||
|
||||
@@ -276,7 +276,7 @@ export function TabNavItem(props: {
|
||||
"overflow-hidden text-clip whitespace-nowrap": !editing(),
|
||||
"select-text": editing(),
|
||||
}}
|
||||
contenteditable={editing() ? true : undefined}
|
||||
contenteditable={editing() ? "plaintext-only" : undefined}
|
||||
onDblClick={openRename}
|
||||
onKeyDown={(event) => {
|
||||
event.stopPropagation()
|
||||
|
||||
@@ -298,7 +298,7 @@ export function TitlebarTabStrip(props: {
|
||||
: [new PointerActivationConstraints.Distance({ value: 4 })],
|
||||
preventActivation: (event) =>
|
||||
isTabCloseTarget(event.target) ||
|
||||
(event.target instanceof Element && !!event.target.closest('[contenteditable="true"]')),
|
||||
(event.target instanceof Element && !!event.target.closest("[contenteditable]")),
|
||||
}),
|
||||
]}
|
||||
modifiers={[
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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,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 }
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
},
|
||||
|
||||
@@ -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,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 {
|
||||
|
||||
@@ -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>) => {
|
||||
|
||||
@@ -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.")
|
||||
}
|
||||
|
||||
@@ -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,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)
|
||||
|
||||
@@ -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)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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":
|
||||
"صُممت الخطة بشكل أساسي للمستخدمين الدوليين، وتوفر وصولًا عالميًا مستقرًا. قد تتغير الأسعار وحدود الاستخدام بينما نتعلم من الاستخدام المبكر والملاحظات.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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 d’utilisation 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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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":
|
||||
"このプランは主に海外のユーザー向けに設計されており、世界中から安定してご利用いただけます。料金と利用上限は、初期の利用状況やフィードバックを踏まえて変更される場合があります。",
|
||||
|
||||
@@ -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":
|
||||
"이 플랜은 주로 해외 사용자를 위해 설계되었으며, 전 세계에서 안정적으로 이용할 수 있습니다. 초기 이용 현황과 피드백을 반영하는 과정에서 가격과 사용 한도가 변경될 수 있습니다.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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":
|
||||
"План предназначен в первую очередь для пользователей по всему миру и обеспечивает стабильный глобальный доступ. Цены и лимиты использования могут меняться по мере изучения первых результатов использования и отзывов.",
|
||||
|
||||
@@ -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":
|
||||
"แผนนี้ออกแบบมาสำหรับผู้ใช้งานต่างประเทศเป็นหลักและให้การเข้าถึงที่เสถียรทั่วโลก ราคาและขีดจำกัดการใช้งานอาจเปลี่ยนแปลงได้ตามสิ่งที่เราเรียนรู้จากการใช้งานและข้อเสนอแนะในช่วงแรก",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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":
|
||||
"План призначений насамперед для міжнародних користувачів і забезпечує стабільний глобальний доступ. Ціни та ліміти використання можуть змінюватися з урахуванням перших даних про використання та відгуків.",
|
||||
|
||||
@@ -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":
|
||||
"该计划主要面向国际用户,提供稳定的全球访问体验。随着我们持续了解早期使用情况并收集反馈,定价和使用限额可能会有所调整。",
|
||||
|
||||
@@ -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":
|
||||
"此方案主要為國際使用者設計,提供穩定的全球存取服務。隨著我們從初期使用情況和回饋中持續了解需求,價格和使用額度可能會有所調整。",
|
||||
|
||||
@@ -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
|
||||
}}
|
||||
|
||||
@@ -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
@@ -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({
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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 })),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -422,6 +422,15 @@ describe("MCP OAuth", () => {
|
||||
expect(tokenRequests[0]?.get("grant_type")).toBe("refresh_token")
|
||||
})
|
||||
|
||||
test("requests offline_access without forcing a consent prompt", async () => {
|
||||
const { server } = authorizationServer({ scopes_supported: ["read", "offline_access"] })
|
||||
const { url } = await Effect.runPromise(
|
||||
Effect.scoped(start(server, { client_id: "client", scope: "read" })),
|
||||
).finally(() => server.stop(true))
|
||||
expect(url.searchParams.get("scope")).toBe("read offline_access")
|
||||
expect(url.searchParams.has("prompt")).toBe(false)
|
||||
})
|
||||
|
||||
test("forwards iss from the redirect so issuer-advertising servers can complete", async () => {
|
||||
const { server } = authorizationServer({ authorization_response_iss_parameter_supported: true })
|
||||
const result = await Effect.runPromise(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"],
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user