Compare commits

..
419 changed files with 5356 additions and 15833 deletions
-72
View File
@@ -29,78 +29,6 @@ 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.
## Alibaba Cloud Model Studio
`Alibaba` provides standard Model Studio inference. Configure a region explicitly, then select
Chat Completions (`.model` or `.chat`), Anthropic-compatible Messages (`.messages`), or OpenAI-compatible
Responses (`.responses`). These routes use HTTP/SSE.
```ts
import { LLM } from "@opencode/ai"
import { Alibaba } from "@opencode/ai/providers"
const alibaba = Alibaba.configure({
region: "ap-southeast-1", // Singapore
apiKey: process.env.DASHSCOPE_API_KEY,
// workspaceID: "llm-your-workspace", // use a workspace-dedicated endpoint
})
const request = LLM.request({
model: alibaba.model("qwen3.8-max"),
prompt: "Explain this design.",
providerOptions: { reasoningEffort: "medium" },
})
```
### Regions and credentials
| Region | `region` | Shared host when `workspaceID` is omitted |
| ------------------- | ---------------- | ----------------------------------------- |
| Singapore | `ap-southeast-1` | `dashscope-intl.aliyuncs.com` |
| China (Beijing) | `cn-beijing` | `dashscope.aliyuncs.com` |
| China (Hong Kong) | `cn-hongkong` | `cn-hongkong.dashscope.aliyuncs.com` |
| US (Virginia) | `us-east-1` | `dashscope-us.aliyuncs.com` |
| Germany (Frankfurt) | `eu-central-1` | Supply `workspaceID` or `baseURL` |
| Japan (Tokyo) | `ap-northeast-1` | Supply `workspaceID` or `baseURL` |
With `workspaceID`, the host is `{workspaceID}.{region}.maas.aliyuncs.com`. A complete `baseURL`
overrides regional setup, including the API prefix: `/compatible-mode/v1` for Chat/Responses,
or `/apps/anthropic/v1` for Messages. The selector appends its operation path.
Keys and model availability are region-specific. Auth resolves from explicit `auth` or `apiKey`,
then `DASHSCOPE_API_KEY`, then `ALIBABA_API_KEY`.
The access region and inference scope differ: Virginia's `-us` model IDs request US-only inference;
some regions select scope through their workspace. Model IDs pass through unchanged.
Alibaba's [regional guide](https://www.alibabacloud.com/help/en/model-studio/regions) and
[base URL table](https://www.alibabacloud.com/help/en/model-studio/base-url) disagree about Virginia's
shared host; the entry above follows the base URL table. Dedicated hosts can be copied from the console.
### Native options
- **Chat:** `reasoningEffort``reasoning_effort`, `enableThinking``enable_thinking`,
`thinkingBudget``thinking_budget`, and `preserveThinking``preserve_thinking`.
Replay complete `response.message` values to retain `reasoning_content` separately from answer text.
Qwen 3.8 defaults to preserving thinking; older models have different defaults.
Additional options include `toolStream`, `parallelToolCalls`, `repetitionPenalty`, `responseFormat`,
`enableSearch`, and native `searchOptions`. `generation.topK` lowers to `top_k`.
`clearThinking` is a hosted GLM control, and `thinking.type` is available for hosted MiniMax models.
- **Messages:** `effort``output_config.effort`. `thinking.type` accepts enabled/disabled with an
optional `budgetTokens` (or native `budget_tokens`). `outputConfig.format` accepts a JSON schema.
Model Studio's empty thinking signatures are accepted; supplied signatures are replayed unchanged.
- **Responses:** `reasoningEffort``reasoning.effort`, plus `enableThinking`, `store`,
`previousResponseId`, and `conversation`. Omitted `store` retains the API's default (`true`);
set it to `false` for client-managed history. `previousResponseId` requires a stored response.
Hosted tools are `Alibaba.webSearch()`, `Alibaba.webExtractor()`, and `Alibaba.codeInterpreter()`.
Web extraction is used together with web search. Hosted calls/results carry `providerExecuted: true`.
Omitted options preserve provider defaults. Effort values pass through unchanged and accept future
strings. Qwen 3.8 Chat rejects requests combining a thinking budget with effort.
Package entrypoints are `@opencode/ai/providers/alibaba`, `alibaba/chat`, `alibaba/messages`,
and `alibaba/responses`. Live recordings cover all three APIs in Singapore; regional URL construction
is unit-tested for all six regions.
## Z.AI
`ZAI` uses the standard API. Chat Completions is the default language-model API;
-92
View File
@@ -1,92 +0,0 @@
import { Effect, Schema } from "effect"
import { Protocol } from "../route/protocol.js"
import type { LanguageModelCompatibility } from "../schema/index.js"
import { OpenAIChat } from "./openai-chat.js"
import { JsonObject, ProviderShared } from "./shared.js"
import { OpenResponsesOptions } from "./utils/open-responses-options.js"
export type ReasoningEffort = OpenResponsesOptions.ReasoningEffort
const Options = Schema.Struct({
reasoningEffort: OpenResponsesOptions.Options.fields.reasoningEffort,
enableThinking: Schema.optional(Schema.Boolean),
thinkingBudget: Schema.optional(Schema.Int),
preserveThinking: Schema.optional(Schema.Boolean),
clearThinking: Schema.optional(Schema.Boolean),
thinking: Schema.optional(
Schema.Struct({
type: Schema.declare<"adaptive" | "disabled" | (string & {})>(Schema.is(Schema.String)),
}),
),
toolStream: Schema.optional(Schema.Boolean),
parallelToolCalls: OpenResponsesOptions.Options.fields.parallelToolCalls,
repetitionPenalty: Schema.optional(Schema.Number),
responseFormat: Schema.optional(
Schema.Struct({
type: Schema.declare<"text" | "json_object" | "json_schema" | (string & {})>(Schema.is(Schema.String)),
json_schema: Schema.optional(JsonObject),
}),
),
enableSearch: Schema.optional(Schema.Boolean),
searchOptions: Schema.optional(
Schema.Struct({
forced_search: Schema.optional(Schema.Boolean),
search_strategy: Schema.optional(
Schema.declare<"turbo" | "max" | "agent" | "agent_max" | (string & {})>(Schema.is(Schema.String)),
),
enable_search_extension: Schema.optional(Schema.Boolean),
}),
),
})
export type OptionsInput = typeof Options.Type
export const compatibility = {
maxTokensField: "max_completion_tokens",
supportsStore: false,
supportsStrictMode: false,
reasoningField: "reasoning_content",
zaiToolStream: false,
} satisfies LanguageModelCompatibility
export const protocol = Protocol.make({
id: "alibaba-chat",
body: {
schema: Schema.Struct({
...OpenAIChat.bodyFields,
enable_thinking: Options.fields.enableThinking,
thinking_budget: Options.fields.thinkingBudget,
preserve_thinking: Options.fields.preserveThinking,
clear_thinking: Options.fields.clearThinking,
thinking: Options.fields.thinking,
parallel_tool_calls: Options.fields.parallelToolCalls,
repetition_penalty: Options.fields.repetitionPenalty,
top_k: Schema.optional(Schema.Int),
response_format: Options.fields.responseFormat,
enable_search: Options.fields.enableSearch,
search_options: Options.fields.searchOptions,
}),
from: Effect.fn("AlibabaChat.fromRequest")(function* (req) {
const opts = yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(Options))(req.providerOptions ?? {})
return {
...(yield* OpenAIChat.protocol.body.from(req)),
enable_thinking: opts.enableThinking,
thinking_budget: opts.thinkingBudget,
preserve_thinking: opts.preserveThinking,
clear_thinking: opts.clearThinking,
thinking: opts.thinking,
tool_stream: opts.toolStream,
parallel_tool_calls:
opts.parallelToolCalls ??
(req.toolChoice?.disableParallelToolUse === undefined ? undefined : !req.toolChoice.disableParallelToolUse),
repetition_penalty: opts.repetitionPenalty,
top_k: req.generation?.topK,
response_format: opts.responseFormat,
enable_search: opts.enableSearch,
search_options: opts.searchOptions,
}
}),
},
stream: OpenAIChat.protocol.stream,
})
export * as AlibabaChat from "./alibaba-chat.js"
@@ -1,48 +0,0 @@
import { Effect, Schema } from "effect"
import { Protocol } from "../route/protocol.js"
import { LLMRequest } from "../schema/index.js"
import { AnthropicMessages } from "./anthropic-messages.js"
import { ProviderShared } from "./shared.js"
import { OpenResponsesOptions } from "./utils/open-responses-options.js"
const Options = Schema.Struct({
effort: Schema.optional(OpenResponsesOptions.ReasoningEffort),
thinking: Schema.optional(
Schema.Struct({
type: Schema.declare<"enabled" | "disabled" | (string & {})>(Schema.is(Schema.String)),
budgetTokens: Schema.optional(Schema.Int),
budget_tokens: Schema.optional(Schema.Int),
}),
),
})
export type OptionsInput = typeof Options.Type & Pick<AnthropicMessages.OptionsInput, "outputConfig">
export const protocol = Protocol.make({
id: "alibaba-messages",
body: {
schema: Schema.Struct({
...AnthropicMessages.AnthropicMessagesBody.fields,
thinking: Schema.optional(Schema.Struct({ type: Schema.String, budget_tokens: Schema.optional(Schema.Int) })),
}),
from: Effect.fn("AlibabaMessages.fromRequest")(function* (req) {
const opts = yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(Options))(req.providerOptions ?? {})
// Model Studio accepts enabled thinking without Anthropic's mandatory token budget.
return {
...(yield* AnthropicMessages.protocol.body.from(
LLMRequest.update(req, {
providerOptions: { ...req.providerOptions, thinking: undefined },
}),
)),
thinking:
opts.thinking === undefined
? undefined
: {
type: opts.thinking.type,
budget_tokens: opts.thinking.budgetTokens ?? opts.thinking.budget_tokens,
},
}
}),
},
stream: AnthropicMessages.protocol.stream,
})
export * as AlibabaMessages from "./alibaba-messages.js"
@@ -1,98 +0,0 @@
import { Effect, Schema } from "effect"
import { Protocol } from "../route/protocol.js"
import { OpenResponses } from "./open-responses.js"
import { JsonObject, optionalArray, ProviderShared } from "./shared.js"
import { OpenResponsesOptions } from "./utils/open-responses-options.js"
import { ResponsesHostedTools } from "./utils/responses-hosted-tools.js"
const Options = Schema.Struct({
reasoningEffort: OpenResponsesOptions.Options.fields.reasoningEffort,
enableThinking: Schema.optional(Schema.Boolean),
store: OpenResponsesOptions.Options.fields.store,
previousResponseId: Schema.optional(Schema.String),
conversation: Schema.optional(Schema.String),
})
export type OptionsInput = typeof Options.Type
const NativeTool = Schema.Struct({ type: Schema.Literals(["web_search", "web_extractor", "code_interpreter"]) })
const WebExtractorItem = Schema.StructWithRest(
Schema.Struct({
type: Schema.Literal("web_extractor_call"),
id: Schema.String,
urls: Schema.optional(Schema.Array(Schema.String)),
goal: Schema.optional(Schema.String),
}),
[JsonObject],
)
const Body = Schema.Struct({
...OpenResponses.coreFields,
input: Schema.Array(Schema.Union([OpenResponses.InputItem, WebExtractorItem])),
tools: optionalArray(Schema.Union([OpenResponses.Tool, NativeTool])),
enable_thinking: Options.fields.enableThinking,
previous_response_id: Options.fields.previousResponseId,
conversation: Options.fields.conversation,
stream: Schema.Literal(true),
})
const adapter = {
id: "alibaba-responses",
name: "Alibaba Responses",
nativeTool: (native) => ProviderShared.validateWith(Schema.decodeUnknownEffect(NativeTool))(native.alibaba),
restoreHostedToolItem: (item: unknown) => (Schema.is(WebExtractorItem)(item) ? item : undefined),
} satisfies OpenResponses.ProviderAdapter
const tools = {
web_search_call: { name: "web_search", input: (item) => item.action ?? {} },
code_interpreter_call: { name: "code_interpreter", input: (item) => ({ code: item.code }) },
} satisfies ResponsesHostedTools.Definitions
export const protocol = Protocol.make({
id: adapter.id,
body: {
schema: Body,
from: Effect.fn("AlibabaResponses.fromRequest")(function* (req) {
const opts = yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(Options))(req.providerOptions ?? {})
const body = yield* OpenResponses.fromRequestWithAdapter(req, adapter)
const choice = body.tool_choice
return yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(Body))({
...body,
enable_thinking: opts.enableThinking,
previous_response_id: opts.previousResponseId,
conversation: opts.conversation,
// Model Studio expresses named selection through allowed_tools.
tool_choice:
typeof choice === "object" && choice.type === "function"
? { type: "allowed_tools" as const, mode: "required" as const, tools: [choice] }
: choice,
})
}),
},
stream: {
event: OpenResponses.protocol.stream.event,
initial: (req) => OpenResponses.initial(req, adapter),
step: (state, input) =>
Effect.gen(function* () {
const event = OpenResponses.normalize(state, input)
if (event.type !== "response.output_item.done" || !event.item) return yield* OpenResponses.step(state, event)
if (event.item.type === "web_extractor_call") {
const item = yield* Schema.decodeUnknownEffect(WebExtractorItem)(event.item).pipe(
Effect.mapError((cause) =>
ProviderShared.eventError(
adapter.id,
"Alibaba returned an invalid web extraction item",
ProviderShared.encodeJson(event),
cause,
),
),
)
return yield* ResponsesHostedTools.onDone(state, item, {
web_extractor_call: { name: "web_extractor", input: () => ({ urls: item.urls, goal: item.goal }) },
})
}
if (ResponsesHostedTools.isItem(event.item, tools))
return yield* ResponsesHostedTools.onDone(state, event.item, tools)
return yield* OpenResponses.step(state, event)
}),
terminal: OpenResponses.terminal,
},
})
export * as AlibabaResponses from "./alibaba-responses.js"
@@ -153,15 +153,6 @@ export const driver = (input: DriverInput): WebSocketChannelDriver => {
const rejection = code(event)
if (rejection === "previous_response_not_found") return rejected(observation, "retry-full")
if (rejection === "websocket_connection_limit_reached") return rejected(observation, "rotate-and-retry-full")
// Only the continuation distinguishes an incremental send from a full one, so an unclassified
// invalid request there is retried full; Codex reports a stale previous_response_id that way, with
// no code. Classified failures such as context overflow keep their runner-owned recovery.
if (
create.mode === "incremental" &&
observation.error.reason._tag === "InvalidRequest" &&
observation.error.reason.classification === undefined
)
return rejected(observation, "retry-full")
}
if (observation.type !== "completed") return observation
// A trigger installs a different context window. Clear the append baseline, retaining the socket.
+6 -12
View File
@@ -408,9 +408,6 @@ export type NormalizedEvent = Event & { readonly item?: OutputItem | null }
export interface ProviderAdapter {
readonly id: string
readonly name: string
readonly nativeTool?: (
native: NonNullable<ToolDefinition["native"]>,
) => Effect.Effect<{ readonly type: string }, AIError>
readonly lowerMedia?: (input: {
readonly part: MediaPart
readonly media: ProviderShared.NormalizedMedia
@@ -655,8 +652,7 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (
type: "message" as const,
...(group.id === undefined ? {} : { id: group.id }),
role: "assistant" as const,
// Replayed text is a finished input item, even if generation was cut short.
status: "completed",
status: metadata?.status,
content: group.parts.map((part) => ({ type: "output_text" as const, text: part.text })),
...(group.phase === undefined ? {} : { phase: group.phase }),
})),
@@ -823,13 +819,11 @@ export const fromRequestWithAdapter = Effect.fn("OpenResponses.fromRequestWithAd
projected.tools.length === 0
? undefined
: yield* Effect.forEach(projected.tools, (tool) =>
tool.native !== undefined && adapter.nativeTool
? adapter.nativeTool(tool.native)
: lowerTool(
adapter.name,
tool,
ToolSchemaProjection.modelCompatibility(tool.inputSchema, toolSchemaCompatibility),
),
lowerTool(
adapter.name,
tool,
ToolSchemaProjection.modelCompatibility(tool.inputSchema, toolSchemaCompatibility),
),
),
tool_choice:
allowedToolChoice(request) ??
-134
View File
@@ -1,134 +0,0 @@
import type { ProviderPackage } from "../provider-package.js"
import { AlibabaChat } from "../protocols/alibaba-chat.js"
import { AlibabaMessages } from "../protocols/alibaba-messages.js"
import { AlibabaResponses } from "../protocols/alibaba-responses.js"
import { AuthOptions, type AtLeastOne, type ProviderAuthOption } from "../route/auth-options.js"
import { Route, type RouteDefaultsInput } from "../route/client.js"
import { Endpoint } from "../route/endpoint.js"
import { Framing } from "../route/framing.js"
import { ProviderID, ToolDefinition, type ModelID } from "../schema/index.js"
export const id = ProviderID.make("alibaba")
export type Region =
| "ap-southeast-1"
| "cn-beijing"
| "cn-hongkong"
| "us-east-1"
| "eu-central-1"
| "ap-northeast-1"
| (string & {})
export type ChatOptionsInput = AlibabaChat.OptionsInput
export type MessagesOptionsInput = AlibabaMessages.OptionsInput
export type ResponsesOptionsInput = AlibabaResponses.OptionsInput
type Location = AtLeastOne<{
readonly region: Region
/** Overrides the selected API's complete base URL, including its version prefix. */
readonly baseURL: string
}> & { readonly workspaceID?: string }
export type Config = Location &
Omit<RouteDefaultsInput, "providerOptions"> &
ProviderAuthOption<"optional"> & {
readonly providerOptions?: ChatOptionsInput | MessagesOptionsInput | ResponsesOptionsInput
}
export type Settings<Options = ChatOptionsInput> = Location &
ProviderPackage.Settings & {
readonly apiKey?: string
readonly providerOptions?: Options
}
const hosts = new Map<string, string>([
["ap-southeast-1", "dashscope-intl.aliyuncs.com"],
["cn-beijing", "dashscope.aliyuncs.com"],
["cn-hongkong", "cn-hongkong.dashscope.aliyuncs.com"],
["us-east-1", "dashscope-us.aliyuncs.com"],
])
const chatRoute = Route.make({
id: "alibaba-chat",
provider: id,
providerMetadataKey: "alibaba",
protocol: AlibabaChat.protocol,
endpoint: Endpoint.path("/chat/completions"),
framing: Framing.sse,
})
const messagesRoute = Route.make({
id: "alibaba-messages",
provider: id,
providerMetadataKey: "alibaba",
protocol: AlibabaMessages.protocol,
endpoint: Endpoint.path("/messages"),
framing: Framing.sse,
headers: () => ({ "anthropic-version": "2023-06-01" }),
})
const responsesRoute = Route.make({
id: "alibaba-responses",
provider: id,
providerMetadataKey: "alibaba",
protocol: AlibabaResponses.protocol,
endpoint: Endpoint.path("/responses"),
framing: Framing.sse,
})
export const routes = [chatRoute, messagesRoute, responsesRoute]
export const configure = (input: Config) => {
const { apiKey: _key, auth: _auth, region, workspaceID, baseURL, ...rest } = input
const host =
region === undefined
? undefined
: workspaceID === undefined
? hosts.get(region)
: `${workspaceID}.${region}.maas.aliyuncs.com`
if (baseURL === undefined) {
if (region === undefined) throw new Error("Alibaba requires region or baseURL")
if (host === undefined) throw new Error(`Alibaba region ${region} requires workspaceID or baseURL`)
}
const opts = { ...rest, auth: AuthOptions.bearer(input, ["DASHSCOPE_API_KEY", "ALIBABA_API_KEY"]) }
const common = { ...opts, endpoint: { baseURL: baseURL ?? `https://${host}/compatible-mode/v1` } }
const chat = (id: string | ModelID) =>
chatRoute.with(common).model<ChatOptionsInput>({ id, compatibility: AlibabaChat.compatibility })
const messages = (id: string | ModelID) =>
messagesRoute
.with({
...opts,
endpoint: { baseURL: baseURL ?? `https://${host}/apps/anthropic/v1` },
})
.model<MessagesOptionsInput>({ id, compatibility: { requireSignature: false } })
const responses = (id: string | ModelID) => responsesRoute.with(common).model<ResponsesOptionsInput>({ id })
return { id, model: chat, chat, messages, responses, configure }
}
export const provider = { id, configure }
export const model: ProviderPackage.Definition<Settings, ChatOptionsInput>["model"] = (id, input) =>
fromSettings(input).chat(id)
export const messagesModel: ProviderPackage.Definition<
Settings<MessagesOptionsInput>,
MessagesOptionsInput
>["model"] = (id, input) => fromSettings(input).messages(id)
export const responsesModel: ProviderPackage.Definition<
Settings<ResponsesOptionsInput>,
ResponsesOptionsInput
>["model"] = (id, input) => fromSettings(input).responses(id)
function fromSettings(input: Settings<Config["providerOptions"]>) {
const { body, ...rest } = input
return configure({ ...rest, http: body === undefined ? undefined : { body } })
}
export const webSearch = () => hostedTool("web_search", "Search the web with Alibaba's hosted search tool.")
export const webExtractor = () => hostedTool("web_extractor", "Extract web page content with Alibaba's hosted tool.")
export const codeInterpreter = () => hostedTool("code_interpreter", "Execute code with Alibaba's hosted interpreter.")
function hostedTool(type: "web_search" | "web_extractor" | "code_interpreter", description: string) {
return ToolDefinition.make({
name: type,
description,
inputSchema: { type: "object", properties: {} },
native: { alibaba: { type } },
})
}
export * as Alibaba from "./alibaba.js"
@@ -1 +0,0 @@
export { model, type Settings } from "../alibaba.js"
@@ -1,3 +0,0 @@
import type { Alibaba } from "../alibaba.js"
export { messagesModel as model } from "../alibaba.js"
export type Settings = Alibaba.Settings<Alibaba.MessagesOptionsInput>
@@ -1,3 +0,0 @@
import type { Alibaba } from "../alibaba.js"
export { responsesModel as model } from "../alibaba.js"
export type Settings = Alibaba.Settings<Alibaba.ResponsesOptionsInput>
-1
View File
@@ -1,4 +1,3 @@
export * as Alibaba from "./alibaba.js"
export * as Anthropic from "./anthropic.js"
export * as AnthropicCompatible from "./anthropic-compatible.js"
export * as AmazonBedrock from "./amazon-bedrock.js"
+2 -5
View File
@@ -115,11 +115,8 @@ const waitOpen = (ws: globalThis.WebSocket, input: WebSocketRequest) => {
}
const onAbort = () => {
cleanup()
if (ws.readyState === globalThis.WebSocket.CLOSED || ws.readyState === globalThis.WebSocket.CLOSING) return
// Node's ws reports an aborted handshake as an error event on the next tick; with no listener left
// after cleanup, EventEmitter would throw it as an uncaught exception.
ws.addEventListener("error", () => {}, { once: true })
ws.close(1000)
if (ws.readyState !== globalThis.WebSocket.CLOSED && ws.readyState !== globalThis.WebSocket.CLOSING)
ws.close(1000)
}
const onOpen = () => {
cleanup()
@@ -1,35 +0,0 @@
{
"version": 1,
"metadata": {
"tags": [
"prefix:alibaba-chat",
"provider:alibaba",
"protocol:alibaba-chat",
"region:ap-southeast-1",
"thinking",
"usage"
],
"name": "alibaba-chat/qwen-3-7-plus-streams-thinking-disabled",
"recordedAt": "2026-09-08T03:10:42.782Z"
},
"interactions": [
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"qwen3.7-plus\",\"messages\":[{\"role\":\"user\",\"content\":\"What is 173 multiplied by 219? Reply with only the final integer.\"}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_completion_tokens\":4096,\"enable_thinking\":false}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream;charset=utf-8"
},
"body": "data: {\"model\":\"qwen3.7-plus\",\"id\":\"chatcmpl-047bcb67-b193-9a4f-9d77-ea0325be6d7c\",\"created\":1788837041,\"object\":\"chat.completion.chunk\",\"usage\":null,\"choices\":[{\"logprobs\":null,\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\"},\"finish_reason\":null}]}\n\ndata: {\"model\":\"qwen3.7-plus\",\"id\":\"chatcmpl-047bcb67-b193-9a4f-9d77-ea0325be6d7c\",\"choices\":[{\"delta\":{\"content\":\"3\"},\"index\":0,\"finish_reason\":null,\"logprobs\":null}],\"created\":1788837041,\"object\":\"chat.completion.chunk\",\"usage\":null}\n\ndata: {\"model\":\"qwen3.7-plus\",\"id\":\"chatcmpl-047bcb67-b193-9a4f-9d77-ea0325be6d7c\",\"choices\":[{\"delta\":{\"content\":\"7887\"},\"index\":0,\"finish_reason\":null,\"logprobs\":null}],\"created\":1788837041,\"object\":\"chat.completion.chunk\",\"usage\":null}\n\ndata: {\"model\":\"qwen3.7-plus\",\"id\":\"chatcmpl-047bcb67-b193-9a4f-9d77-ea0325be6d7c\",\"choices\":[{\"delta\":{\"content\":\"\"},\"index\":0,\"finish_reason\":\"stop\",\"logprobs\":null}],\"created\":1788837041,\"object\":\"chat.completion.chunk\",\"usage\":null}\n\ndata: {\"choices\":[],\"created\":1788837041,\"id\":\"chatcmpl-047bcb67-b193-9a4f-9d77-ea0325be6d7c\",\"model\":\"qwen3.7-plus\",\"object\":\"chat.completion.chunk\",\"usage\":{\"completion_tokens\":5,\"prompt_tokens\":32,\"prompt_tokens_details\":{\"cached_tokens\":0,\"text_tokens\":32},\"total_tokens\":37}}\n\ndata: [DONE]\n\n"
}
}
]
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,35 +0,0 @@
{
"version": 1,
"metadata": {
"tags": [
"prefix:alibaba-chat",
"provider:alibaba",
"protocol:alibaba-chat",
"region:ap-southeast-1",
"tool",
"tool-choice"
],
"name": "alibaba-chat/qwen-3-8-max-obeys-named-tool-choice",
"recordedAt": "2026-09-08T03:10:56.576Z"
},
"interactions": [
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"qwen3.8-max\",\"messages\":[{\"role\":\"user\",\"content\":\"Find the current weather in Paris.\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"description\":\"Get weather in a city\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\",\"enum\":[\"Paris\"]}},\"required\":[\"city\"]}}}],\"tool_choice\":{\"type\":\"function\",\"function\":{\"name\":\"get_weather\"}},\"stream\":true,\"stream_options\":{\"include_usage\":true},\"reasoning_effort\":\"none\",\"max_completion_tokens\":4096}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream;charset=utf-8"
},
"body": "data: {\"model\":\"qwen3.8-max\",\"id\":\"chatcmpl-681dfa5a-ae08-98da-ba0d-4b1d0d364b6b\",\"created\":1788837055,\"object\":\"chat.completion.chunk\",\"usage\":null,\"choices\":[{\"logprobs\":null,\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"tool_calls\":[{\"index\":0,\"id\":\"call_4eb823cb28d141c8befbb331\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"\"}}]},\"finish_reason\":null}]}\n\ndata: {\"model\":\"qwen3.8-max\",\"id\":\"chatcmpl-681dfa5a-ae08-98da-ba0d-4b1d0d364b6b\",\"choices\":[{\"delta\":{\"content\":\"\",\"tool_calls\":[{\"index\":0,\"id\":\"\",\"type\":\"function\",\"function\":{\"arguments\":\"\"}}]},\"index\":0,\"finish_reason\":null,\"logprobs\":null}],\"created\":1788837055,\"object\":\"chat.completion.chunk\",\"usage\":null}\n\ndata: {\"model\":\"qwen3.8-max\",\"id\":\"chatcmpl-681dfa5a-ae08-98da-ba0d-4b1d0d364b6b\",\"choices\":[{\"delta\":{\"content\":\"\",\"tool_calls\":[{\"type\":\"function\",\"index\":0,\"function\":{\"arguments\":\"{\\\"city\\\": \\\"Paris\"}}]},\"index\":0,\"finish_reason\":null,\"logprobs\":null}],\"created\":1788837055,\"object\":\"chat.completion.chunk\",\"usage\":null}\n\ndata: {\"model\":\"qwen3.8-max\",\"id\":\"chatcmpl-681dfa5a-ae08-98da-ba0d-4b1d0d364b6b\",\"choices\":[{\"delta\":{\"content\":\"\",\"tool_calls\":[{\"type\":\"function\",\"index\":0,\"function\":{\"arguments\":\"\\\"\"}}]},\"index\":0,\"finish_reason\":null,\"logprobs\":null}],\"created\":1788837055,\"object\":\"chat.completion.chunk\",\"usage\":null}\n\ndata: {\"model\":\"qwen3.8-max\",\"id\":\"chatcmpl-681dfa5a-ae08-98da-ba0d-4b1d0d364b6b\",\"choices\":[{\"delta\":{\"content\":\"\",\"tool_calls\":[{\"type\":\"function\",\"index\":0,\"function\":{\"arguments\":\"}\"}}]},\"index\":0,\"finish_reason\":null,\"logprobs\":null}],\"created\":1788837055,\"object\":\"chat.completion.chunk\",\"usage\":null}\n\ndata: {\"model\":\"qwen3.8-max\",\"id\":\"chatcmpl-681dfa5a-ae08-98da-ba0d-4b1d0d364b6b\",\"choices\":[{\"delta\":{\"content\":\"\",\"tool_calls\":[{\"type\":\"function\",\"index\":0,\"function\":{\"arguments\":\"\"}}]},\"index\":0,\"finish_reason\":null,\"logprobs\":null}],\"created\":1788837055,\"object\":\"chat.completion.chunk\",\"usage\":null}\n\ndata: {\"model\":\"qwen3.8-max\",\"id\":\"chatcmpl-681dfa5a-ae08-98da-ba0d-4b1d0d364b6b\",\"choices\":[{\"delta\":{\"tool_calls\":[{\"function\":{\"arguments\":\"\"},\"index\":0,\"id\":null,\"type\":\"function\"}],\"content\":\"\"},\"index\":0,\"finish_reason\":null,\"logprobs\":null}],\"created\":1788837055,\"object\":\"chat.completion.chunk\",\"usage\":null}\n\ndata: {\"model\":\"qwen3.8-max\",\"id\":\"chatcmpl-681dfa5a-ae08-98da-ba0d-4b1d0d364b6b\",\"choices\":[{\"delta\":{},\"index\":0,\"finish_reason\":\"stop\",\"logprobs\":null}],\"created\":1788837055,\"object\":\"chat.completion.chunk\",\"usage\":null}\n\ndata: {\"choices\":[],\"created\":1788837055,\"id\":\"chatcmpl-681dfa5a-ae08-98da-ba0d-4b1d0d364b6b\",\"model\":\"qwen3.8-max\",\"object\":\"chat.completion.chunk\",\"usage\":{\"completion_tokens\":19,\"prompt_tokens\":288,\"prompt_tokens_details\":{\"cached_tokens\":0,\"text_tokens\":288},\"total_tokens\":307}}\n\ndata: [DONE]\n\n"
}
}
]
}
File diff suppressed because one or more lines are too long
@@ -1,34 +0,0 @@
{
"version": 1,
"metadata": {
"tags": [
"prefix:alibaba-chat",
"provider:alibaba",
"protocol:alibaba-chat",
"region:ap-southeast-1",
"structured-output"
],
"name": "alibaba-chat/qwen-3-8-max-returns-a-json-object",
"recordedAt": "2026-09-08T03:11:29.462Z"
},
"interactions": [
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"qwen3.8-max\",\"messages\":[{\"role\":\"user\",\"content\":\"Return a JSON object with one key \\\"city\\\" set to the capital city of France.\"}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"reasoning_effort\":\"none\",\"max_completion_tokens\":1024,\"response_format\":{\"type\":\"json_object\"}}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream;charset=utf-8"
},
"body": "data: {\"model\":\"qwen3.8-max\",\"id\":\"chatcmpl-de34af77-7b1e-9999-a48f-e564c03d4b6b\",\"created\":1788837088,\"object\":\"chat.completion.chunk\",\"usage\":null,\"choices\":[{\"logprobs\":null,\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\"},\"finish_reason\":null}]}\n\ndata: {\"model\":\"qwen3.8-max\",\"id\":\"chatcmpl-de34af77-7b1e-9999-a48f-e564c03d4b6b\",\"choices\":[{\"delta\":{\"content\":\"{\\\"\"},\"index\":0,\"finish_reason\":null,\"logprobs\":null}],\"created\":1788837088,\"object\":\"chat.completion.chunk\",\"usage\":null}\n\ndata: {\"model\":\"qwen3.8-max\",\"id\":\"chatcmpl-de34af77-7b1e-9999-a48f-e564c03d4b6b\",\"choices\":[{\"delta\":{\"content\":\"city\\\":\"},\"index\":0,\"finish_reason\":null,\"logprobs\":null}],\"created\":1788837088,\"object\":\"chat.completion.chunk\",\"usage\":null}\n\ndata: {\"model\":\"qwen3.8-max\",\"id\":\"chatcmpl-de34af77-7b1e-9999-a48f-e564c03d4b6b\",\"choices\":[{\"delta\":{\"content\":\" \\\"Paris\\\"}\"},\"index\":0,\"finish_reason\":null,\"logprobs\":null}],\"created\":1788837088,\"object\":\"chat.completion.chunk\",\"usage\":null}\n\ndata: {\"model\":\"qwen3.8-max\",\"id\":\"chatcmpl-de34af77-7b1e-9999-a48f-e564c03d4b6b\",\"choices\":[{\"delta\":{\"content\":\"\"},\"index\":0,\"finish_reason\":\"stop\",\"logprobs\":null}],\"created\":1788837088,\"object\":\"chat.completion.chunk\",\"usage\":null}\n\ndata: {\"choices\":[],\"created\":1788837088,\"id\":\"chatcmpl-de34af77-7b1e-9999-a48f-e564c03d4b6b\",\"model\":\"qwen3.8-max\",\"object\":\"chat.completion.chunk\",\"usage\":{\"completion_tokens\":6,\"prompt_tokens\":32,\"prompt_tokens_details\":{\"cached_tokens\":0,\"text_tokens\":32},\"total_tokens\":38}}\n\ndata: [DONE]\n\n"
}
}
]
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,36 +0,0 @@
{
"version": 1,
"metadata": {
"tags": [
"prefix:alibaba-chat",
"provider:alibaba",
"protocol:alibaba-chat",
"region:ap-southeast-1",
"text",
"reasoning",
"usage"
],
"name": "alibaba-chat/qwen-3-8-max-streams-none-effort",
"recordedAt": "2026-09-08T03:09:27.584Z"
},
"interactions": [
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"qwen3.8-max\",\"messages\":[{\"role\":\"user\",\"content\":\"What is 173 multiplied by 219? Reply with only the final integer.\"}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"reasoning_effort\":\"none\",\"max_completion_tokens\":4096}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream;charset=utf-8"
},
"body": "data: {\"model\":\"qwen3.8-max\",\"id\":\"chatcmpl-d8fbc8fe-4d5a-9dc9-84c6-ed0c7b9dd3bb\",\"created\":1788836967,\"object\":\"chat.completion.chunk\",\"usage\":null,\"choices\":[{\"logprobs\":null,\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\"},\"finish_reason\":null}]}\n\ndata: {\"model\":\"qwen3.8-max\",\"id\":\"chatcmpl-d8fbc8fe-4d5a-9dc9-84c6-ed0c7b9dd3bb\",\"choices\":[{\"delta\":{\"content\":\"3\"},\"index\":0,\"finish_reason\":null,\"logprobs\":null}],\"created\":1788836967,\"object\":\"chat.completion.chunk\",\"usage\":null}\n\ndata: {\"model\":\"qwen3.8-max\",\"id\":\"chatcmpl-d8fbc8fe-4d5a-9dc9-84c6-ed0c7b9dd3bb\",\"choices\":[{\"delta\":{\"content\":\"78\"},\"index\":0,\"finish_reason\":null,\"logprobs\":null}],\"created\":1788836967,\"object\":\"chat.completion.chunk\",\"usage\":null}\n\ndata: {\"model\":\"qwen3.8-max\",\"id\":\"chatcmpl-d8fbc8fe-4d5a-9dc9-84c6-ed0c7b9dd3bb\",\"choices\":[{\"delta\":{\"content\":\"87\"},\"index\":0,\"finish_reason\":null,\"logprobs\":null}],\"created\":1788836967,\"object\":\"chat.completion.chunk\",\"usage\":null}\n\ndata: {\"model\":\"qwen3.8-max\",\"id\":\"chatcmpl-d8fbc8fe-4d5a-9dc9-84c6-ed0c7b9dd3bb\",\"choices\":[{\"delta\":{\"content\":\"\"},\"index\":0,\"finish_reason\":\"stop\",\"logprobs\":null}],\"created\":1788836967,\"object\":\"chat.completion.chunk\",\"usage\":null}\n\ndata: {\"choices\":[],\"created\":1788836967,\"id\":\"chatcmpl-d8fbc8fe-4d5a-9dc9-84c6-ed0c7b9dd3bb\",\"model\":\"qwen3.8-max\",\"object\":\"chat.completion.chunk\",\"usage\":{\"completion_tokens\":5,\"prompt_tokens\":32,\"prompt_tokens_details\":{\"cached_tokens\":0,\"text_tokens\":32},\"total_tokens\":37}}\n\ndata: [DONE]\n\n"
}
}
]
}
File diff suppressed because one or more lines are too long
@@ -1,35 +0,0 @@
{
"version": 1,
"metadata": {
"tags": [
"prefix:alibaba-messages",
"provider:alibaba",
"protocol:alibaba-messages",
"region:ap-southeast-1",
"thinking",
"usage"
],
"name": "alibaba-messages/qwen-3-7-plus-streams-thinking-disabled",
"recordedAt": "2026-09-08T03:10:57.819Z"
},
"interactions": [
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://dashscope-intl.aliyuncs.com/apps/anthropic/v1/messages",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"qwen3.7-plus\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"What is 173 multiplied by 219? Reply with only the final integer.\"}]}],\"stream\":true,\"max_tokens\":4096,\"thinking\":{\"type\":\"disabled\"}}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream"
},
"body": "event:ping\ndata:{\"type\":\"ping\"}\n\nevent:message_start\ndata:{\"message\":{\"model\":\"qwen3.7-plus\",\"id\":\"msg_c4d58b4f-a61d-9d0c-a9e4-cb45d34b1120\",\"role\":\"assistant\",\"type\":\"message\",\"content\":[],\"usage\":{\"input_tokens\":20,\"output_tokens\":0}},\"type\":\"message_start\"}\n\nevent:content_block_start\ndata:{\"type\":\"content_block_start\",\"content_block\":{\"type\":\"text\",\"text\":\"\"},\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"text_delta\",\"text\":\"3\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"text_delta\",\"text\":\"7887\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_stop\ndata:{\"type\":\"content_block_stop\",\"index\":0}\n\nevent:message_delta\ndata:{\"delta\":{\"stop_reason\":\"end_turn\"},\"type\":\"message_delta\",\"usage\":{\"output_tokens\":5,\"cache_creation_input_tokens\":0,\"input_tokens\":32,\"cache_read_input_tokens\":0,\"prompt_tokens_details\":{\"cached_tokens\":0}}}\n\nevent:message_stop\ndata:{\"type\":\"message_stop\"}\n\n"
}
}
]
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,34 +0,0 @@
{
"version": 1,
"metadata": {
"tags": [
"prefix:alibaba-messages",
"provider:alibaba",
"protocol:alibaba-messages",
"region:ap-southeast-1",
"structured-output"
],
"name": "alibaba-messages/qwen-3-8-max-follows-a-json-schema",
"recordedAt": "2026-09-08T03:11:30.530Z"
},
"interactions": [
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://dashscope-intl.aliyuncs.com/apps/anthropic/v1/messages",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"qwen3.8-max\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Return a JSON object with one key \\\"city\\\" set to the capital city of France.\"}]}],\"stream\":true,\"max_tokens\":1024,\"thinking\":{\"type\":\"disabled\"},\"output_config\":{\"format\":{\"type\":\"json_schema\",\"schema\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}}}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream"
},
"body": "event:ping\ndata:{\"type\":\"ping\"}\n\nevent:message_start\ndata:{\"message\":{\"model\":\"qwen3.8-max\",\"id\":\"msg_16e067c1-984b-94d3-9abb-11792d794271\",\"role\":\"assistant\",\"type\":\"message\",\"content\":[],\"usage\":{\"input_tokens\":18,\"output_tokens\":0}},\"type\":\"message_start\"}\n\nevent:content_block_start\ndata:{\"type\":\"content_block_start\",\"content_block\":{\"type\":\"text\",\"text\":\"\"},\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"text_delta\",\"text\":\"{\\\"\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"text_delta\",\"text\":\"city\\\":\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"text_delta\",\"text\":\" \\\"Paris\\\"}\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_stop\ndata:{\"type\":\"content_block_stop\",\"index\":0}\n\nevent:message_delta\ndata:{\"delta\":{\"stop_reason\":\"end_turn\"},\"type\":\"message_delta\",\"usage\":{\"output_tokens\":6,\"cache_creation_input_tokens\":0,\"input_tokens\":32,\"cache_read_input_tokens\":0,\"prompt_tokens_details\":{\"cached_tokens\":0}}}\n\nevent:message_stop\ndata:{\"type\":\"message_stop\"}\n\n"
}
}
]
}
@@ -1,35 +0,0 @@
{
"version": 1,
"metadata": {
"tags": [
"prefix:alibaba-messages",
"provider:alibaba",
"protocol:alibaba-messages",
"region:ap-southeast-1",
"tool",
"tool-choice"
],
"name": "alibaba-messages/qwen-3-8-max-obeys-named-tool-choice",
"recordedAt": "2026-09-08T03:11:06.590Z"
},
"interactions": [
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://dashscope-intl.aliyuncs.com/apps/anthropic/v1/messages",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"qwen3.8-max\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Find the current weather in Paris.\"}]}],\"tools\":[{\"name\":\"get_weather\",\"description\":\"Get weather in a city\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\",\"enum\":[\"Paris\"]}},\"required\":[\"city\"]}}],\"tool_choice\":{\"type\":\"tool\",\"name\":\"get_weather\"},\"stream\":true,\"max_tokens\":4096,\"thinking\":{\"type\":\"disabled\"}}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream"
},
"body": "event:ping\ndata:{\"type\":\"ping\"}\n\nevent:message_start\ndata:{\"message\":{\"model\":\"qwen3.8-max\",\"id\":\"msg_0e8f1abf-f2bc-9a86-a6a7-14804ac17eac\",\"role\":\"assistant\",\"type\":\"message\",\"content\":[],\"usage\":{\"input_tokens\":45,\"output_tokens\":0}},\"type\":\"message_start\"}\n\nevent:content_block_start\ndata:{\"type\":\"content_block_start\",\"content_block\":{\"name\":\"get_weather\",\"input\":{},\"id\":\"toolu_cf9cab33261f4709ae096d8a\",\"type\":\"tool_use\"},\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"partial_json\":\"\",\"type\":\"input_json_delta\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"partial_json\":\"{\\\"city\\\": \\\"Paris\",\"type\":\"input_json_delta\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"partial_json\":\"\\\"\",\"type\":\"input_json_delta\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"partial_json\":\"}\",\"type\":\"input_json_delta\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_stop\ndata:{\"type\":\"content_block_stop\",\"index\":0}\n\nevent:message_delta\ndata:{\"delta\":{\"stop_reason\":\"end_turn\"},\"type\":\"message_delta\",\"usage\":{\"output_tokens\":19,\"cache_creation_input_tokens\":0,\"input_tokens\":288,\"cache_read_input_tokens\":0,\"prompt_tokens_details\":{\"cached_tokens\":0}}}\n\nevent:message_stop\ndata:{\"type\":\"message_stop\"}\n\n"
}
}
]
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,36 +0,0 @@
{
"version": 1,
"metadata": {
"tags": [
"prefix:alibaba-messages",
"provider:alibaba",
"protocol:alibaba-messages",
"region:ap-southeast-1",
"text",
"reasoning",
"usage"
],
"name": "alibaba-messages/qwen-3-8-max-streams-max-effort",
"recordedAt": "2026-09-08T03:12:36.479Z"
},
"interactions": [
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://dashscope-intl.aliyuncs.com/apps/anthropic/v1/messages",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"qwen3.8-max\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"What is 173 multiplied by 219? Reply with only the final integer.\"}]}],\"stream\":true,\"max_tokens\":4096,\"output_config\":{\"effort\":\"max\"}}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream"
},
"body": "event:ping\ndata:{\"type\":\"ping\"}\n\nevent:message_start\ndata:{\"message\":{\"model\":\"qwen3.8-max\",\"id\":\"msg_7952446b-0176-9526-ad7d-b1f0184bc106\",\"role\":\"assistant\",\"type\":\"message\",\"content\":[],\"usage\":{\"input_tokens\":20,\"output_tokens\":0}},\"type\":\"message_start\"}\n\nevent:content_block_start\ndata:{\"type\":\"content_block_start\",\"content_block\":{\"type\":\"thinking\",\"signature\":\"\",\"thinking\":\"\"},\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"We\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" need answer user\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"'s simple multiplication with\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" only final integer.\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" Need compute 1\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"73*2\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"19. \"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"173*\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"200=\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"3460\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"0; 1\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"73*1\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"9=32\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"87 (\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"173*\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"20=3\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"460-\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"173=\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"3287\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"); sum=3\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"7887\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\". Final only integer\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\".\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"signature_delta\",\"signature\":\"\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_stop\ndata:{\"type\":\"content_block_stop\",\"index\":0}\n\nevent:content_block_start\ndata:{\"type\":\"content_block_start\",\"content_block\":{\"type\":\"text\",\"text\":\"\"},\"index\":1}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"text_delta\",\"text\":\"37\"},\"type\":\"content_block_delta\",\"index\":1}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"text_delta\",\"text\":\"887\"},\"type\":\"content_block_delta\",\"index\":1}\n\nevent:content_block_stop\ndata:{\"type\":\"content_block_stop\",\"index\":1}\n\nevent:message_delta\ndata:{\"delta\":{\"stop_reason\":\"end_turn\"},\"type\":\"message_delta\",\"usage\":{\"output_tokens\":92,\"cache_creation_input_tokens\":0,\"input_tokens\":81,\"cache_read_input_tokens\":0,\"prompt_tokens_details\":{\"cached_tokens\":0}}}\n\nevent:message_stop\ndata:{\"type\":\"message_stop\"}\n\n"
}
}
]
}
File diff suppressed because one or more lines are too long
@@ -1,36 +0,0 @@
{
"version": 1,
"metadata": {
"tags": [
"prefix:alibaba-messages",
"provider:alibaba",
"protocol:alibaba-messages",
"region:ap-southeast-1",
"text",
"reasoning",
"usage"
],
"name": "alibaba-messages/qwen-3-8-max-streams-xhigh-effort",
"recordedAt": "2026-09-08T03:10:04.633Z"
},
"interactions": [
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://dashscope-intl.aliyuncs.com/apps/anthropic/v1/messages",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"qwen3.8-max\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"What is 173 multiplied by 219? Reply with only the final integer.\"}]}],\"stream\":true,\"max_tokens\":4096,\"output_config\":{\"effort\":\"xhigh\"}}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream"
},
"body": "event:ping\ndata:{\"type\":\"ping\"}\n\nevent:message_start\ndata:{\"message\":{\"model\":\"qwen3.8-max\",\"id\":\"msg_a57b8563-71fb-9248-a31e-c49d6ba38717\",\"role\":\"assistant\",\"type\":\"message\",\"content\":[],\"usage\":{\"input_tokens\":20,\"output_tokens\":0}},\"type\":\"message_start\"}\n\nevent:content_block_start\ndata:{\"type\":\"content_block_start\",\"content_block\":{\"type\":\"thinking\",\"signature\":\"\",\"thinking\":\"\"},\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"We\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" need answer simple multiplication\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\". We\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" already call\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\". Need compute \"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"173*\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"219.\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" 173\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"*200\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"=346\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"00; *\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"19=3\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"287;\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" sum 37\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"88\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"7. Final only\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" integer. Ensure\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" no extra.\\n\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"signature_delta\",\"signature\":\"\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_stop\ndata:{\"type\":\"content_block_stop\",\"index\":0}\n\nevent:content_block_start\ndata:{\"type\":\"content_block_start\",\"content_block\":{\"type\":\"text\",\"text\":\"\"},\"index\":1}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"text_delta\",\"text\":\"378\"},\"type\":\"content_block_delta\",\"index\":1}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"text_delta\",\"text\":\"87\"},\"type\":\"content_block_delta\",\"index\":1}\n\nevent:content_block_stop\ndata:{\"type\":\"content_block_stop\",\"index\":1}\n\nevent:message_delta\ndata:{\"delta\":{\"stop_reason\":\"end_turn\"},\"type\":\"message_delta\",\"usage\":{\"output_tokens\":69,\"cache_creation_input_tokens\":0,\"input_tokens\":81,\"cache_read_input_tokens\":0,\"prompt_tokens_details\":{\"cached_tokens\":0}}}\n\nevent:message_stop\ndata:{\"type\":\"message_stop\"}\n\n"
}
}
]
}
@@ -1,35 +0,0 @@
{
"version": 1,
"metadata": {
"tags": [
"prefix:alibaba-responses",
"provider:alibaba",
"protocol:alibaba-responses",
"region:ap-southeast-1",
"thinking",
"usage"
],
"name": "alibaba-responses/qwen-3-7-plus-streams-thinking-disabled",
"recordedAt": "2026-09-08T03:11:07.495Z"
},
"interactions": [
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1/responses",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"qwen3.7-plus\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"What is 173 multiplied by 219? Reply with only the final integer.\"}]}],\"max_output_tokens\":4096,\"enable_thinking\":false,\"stream\":true}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream;charset=UTF-8"
},
"body": "id:1\nevent:response.created\n:HTTP_STATUS/200\ndata:{\"sequence_number\":0,\"type\":\"response.created\",\"response\":{\"top_logprobs\":0,\"metadata\":{},\"presence_penalty\":0.0,\"created_at\":1788837067,\"store\":true,\"tools\":[],\"output\":[],\"top_p\":1.0,\"completed_at\":1788837067,\"frequency_penalty\":0.0,\"parallel_tool_calls\":true,\"background\":false,\"temperature\":1.0,\"tool_choice\":\"auto\",\"model\":\"qwen3.7-plus\",\"service_tier\":\"default\",\"id\":\"resp_493607bf-baef-9ced-9e36-2e2a583b6177\",\"max_output_tokens\":4096,\"object\":\"response\",\"status\":\"queued\"}}\n\nid:2\nevent:response.in_progress\n:HTTP_STATUS/200\ndata:{\"sequence_number\":1,\"type\":\"response.in_progress\",\"response\":{\"top_logprobs\":0,\"metadata\":{},\"presence_penalty\":0.0,\"created_at\":1788837067,\"store\":true,\"tools\":[],\"output\":[],\"top_p\":1.0,\"completed_at\":1788837067,\"frequency_penalty\":0.0,\"parallel_tool_calls\":true,\"background\":false,\"temperature\":1.0,\"tool_choice\":\"auto\",\"model\":\"qwen3.7-plus\",\"service_tier\":\"default\",\"id\":\"resp_493607bf-baef-9ced-9e36-2e2a583b6177\",\"max_output_tokens\":4096,\"object\":\"response\",\"status\":\"in_progress\"}}\n\nid:3\nevent:response.output_item.added\n:HTTP_STATUS/200\ndata:{\"sequence_number\":2,\"item\":{\"id\":\"msg_509c6dec-e527-41b5-a9c9-7f02fecb7a3f\",\"role\":\"assistant\",\"type\":\"message\",\"content\":[],\"status\":\"in_progress\"},\"output_index\":0,\"type\":\"response.output_item.added\"}\n\nid:4\nevent:response.content_part.added\n:HTTP_STATUS/200\ndata:{\"sequence_number\":3,\"output_index\":0,\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_509c6dec-e527-41b5-a9c9-7f02fecb7a3f\",\"part\":{\"type\":\"output_text\",\"annotations\":[],\"text\":\"\"}}\n\nid:5\nevent:response.output_text.delta\n:HTTP_STATUS/200\ndata:{\"sequence_number\":4,\"content_index\":0,\"item_id\":\"msg_509c6dec-e527-41b5-a9c9-7f02fecb7a3f\",\"delta\":\"3\",\"output_index\":0,\"type\":\"response.output_text.delta\",\"logprobs\":[]}\n\nid:6\nevent:response.output_text.delta\n:HTTP_STATUS/200\ndata:{\"sequence_number\":5,\"content_index\":0,\"item_id\":\"msg_509c6dec-e527-41b5-a9c9-7f02fecb7a3f\",\"delta\":\"7887\",\"output_index\":0,\"type\":\"response.output_text.delta\",\"logprobs\":[]}\n\nid:7\nevent:response.output_text.delta\n:HTTP_STATUS/200\ndata:{\"sequence_number\":6,\"content_index\":0,\"item_id\":\"msg_509c6dec-e527-41b5-a9c9-7f02fecb7a3f\",\"delta\":\"\",\"output_index\":0,\"type\":\"response.output_text.delta\",\"logprobs\":[]}\n\nid:8\nevent:response.output_text.done\n:HTTP_STATUS/200\ndata:{\"sequence_number\":7,\"content_index\":0,\"item_id\":\"msg_509c6dec-e527-41b5-a9c9-7f02fecb7a3f\",\"text\":\"37887\",\"output_index\":0,\"type\":\"response.output_text.done\",\"logprobs\":[]}\n\nid:9\nevent:response.content_part.done\n:HTTP_STATUS/200\ndata:{\"sequence_number\":8,\"output_index\":0,\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_509c6dec-e527-41b5-a9c9-7f02fecb7a3f\",\"part\":{\"type\":\"output_text\",\"annotations\":[],\"text\":\"37887\"}}\n\nid:10\nevent:response.output_item.done\n:HTTP_STATUS/200\ndata:{\"sequence_number\":9,\"item\":{\"id\":\"msg_509c6dec-e527-41b5-a9c9-7f02fecb7a3f\",\"role\":\"assistant\",\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"text\":\"37887\"}],\"status\":\"completed\"},\"output_index\":0,\"type\":\"response.output_item.done\"}\n\nid:11\nevent:response.completed\n:HTTP_STATUS/200\ndata:{\"sequence_number\":10,\"type\":\"response.completed\",\"response\":{\"top_logprobs\":0,\"metadata\":{},\"presence_penalty\":0.0,\"usage\":{\"total_tokens\":73,\"input_tokens_details\":{\"cached_tokens\":0},\"output_tokens\":5,\"input_tokens\":68,\"output_tokens_details\":{\"reasoning_tokens\":0},\"x_details\":[{\"total_tokens\":73,\"x_billing_type\":\"response_api\",\"output_tokens\":5,\"input_tokens\":68,\"prompt_tokens_details\":{\"cached_tokens\":0}}]},\"created_at\":1788837067,\"store\":true,\"tools\":[],\"output\":[{\"id\":\"msg_509c6dec-e527-41b5-a9c9-7f02fecb7a3f\",\"role\":\"assistant\",\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"text\":\"37887\"}],\"status\":\"completed\"}],\"top_p\":1.0,\"completed_at\":1788837067,\"frequency_penalty\":0.0,\"parallel_tool_calls\":true,\"background\":false,\"temperature\":1.0,\"tool_choice\":\"auto\",\"model\":\"qwen3.7-plus\",\"service_tier\":\"default\",\"id\":\"resp_493607bf-baef-9ced-9e36-2e2a583b6177\",\"max_output_tokens\":4096,\"object\":\"response\",\"status\":\"completed\"}}\n\n"
}
}
]
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,53 +0,0 @@
{
"version": 1,
"metadata": {
"tags": [
"prefix:alibaba-responses",
"provider:alibaba",
"protocol:alibaba-responses",
"region:ap-southeast-1",
"continuation",
"storage"
],
"name": "alibaba-responses/qwen-3-8-max-continues-a-stored-response",
"recordedAt": "2026-09-08T03:12:42.429Z"
},
"interactions": [
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1/responses",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"qwen3.8-max\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Remember the password word apricot. Reply OK.\"}]}],\"store\":true,\"reasoning\":{\"effort\":\"none\"},\"max_output_tokens\":1024,\"stream\":true}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream;charset=UTF-8"
},
"body": "id:1\nevent:response.created\n:HTTP_STATUS/200\ndata:{\"sequence_number\":0,\"type\":\"response.created\",\"response\":{\"top_logprobs\":0,\"metadata\":{},\"presence_penalty\":0.0,\"reasoning\":{\"effort\":\"none\"},\"created_at\":1788837161,\"store\":true,\"tools\":[],\"output\":[],\"top_p\":1.0,\"completed_at\":1788837161,\"frequency_penalty\":0.0,\"parallel_tool_calls\":true,\"background\":false,\"temperature\":1.0,\"tool_choice\":\"auto\",\"model\":\"qwen3.8-max\",\"service_tier\":\"default\",\"id\":\"resp_97f8cd28-c7ab-9d51-9c14-4fc531decdc8\",\"max_output_tokens\":1024,\"object\":\"response\",\"status\":\"queued\"}}\n\nid:2\nevent:response.in_progress\n:HTTP_STATUS/200\ndata:{\"sequence_number\":1,\"type\":\"response.in_progress\",\"response\":{\"top_logprobs\":0,\"metadata\":{},\"presence_penalty\":0.0,\"reasoning\":{\"effort\":\"none\"},\"created_at\":1788837161,\"store\":true,\"tools\":[],\"output\":[],\"top_p\":1.0,\"completed_at\":1788837161,\"frequency_penalty\":0.0,\"parallel_tool_calls\":true,\"background\":false,\"temperature\":1.0,\"tool_choice\":\"auto\",\"model\":\"qwen3.8-max\",\"service_tier\":\"default\",\"id\":\"resp_97f8cd28-c7ab-9d51-9c14-4fc531decdc8\",\"max_output_tokens\":1024,\"object\":\"response\",\"status\":\"in_progress\"}}\n\nid:3\nevent:response.output_item.added\n:HTTP_STATUS/200\ndata:{\"sequence_number\":2,\"item\":{\"id\":\"msg_a908ad8e-6220-444b-880c-02aecbaf8fac\",\"role\":\"assistant\",\"type\":\"message\",\"content\":[],\"status\":\"in_progress\"},\"output_index\":0,\"type\":\"response.output_item.added\"}\n\nid:4\nevent:response.content_part.added\n:HTTP_STATUS/200\ndata:{\"sequence_number\":3,\"output_index\":0,\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_a908ad8e-6220-444b-880c-02aecbaf8fac\",\"part\":{\"type\":\"output_text\",\"annotations\":[],\"text\":\"\"}}\n\nid:5\nevent:response.output_text.delta\n:HTTP_STATUS/200\ndata:{\"sequence_number\":4,\"content_index\":0,\"item_id\":\"msg_a908ad8e-6220-444b-880c-02aecbaf8fac\",\"delta\":\"OK\",\"output_index\":0,\"type\":\"response.output_text.delta\",\"logprobs\":[]}\n\nid:6\nevent:response.output_text.delta\n:HTTP_STATUS/200\ndata:{\"sequence_number\":5,\"content_index\":0,\"item_id\":\"msg_a908ad8e-6220-444b-880c-02aecbaf8fac\",\"delta\":\".\",\"output_index\":0,\"type\":\"response.output_text.delta\",\"logprobs\":[]}\n\nid:7\nevent:response.output_text.delta\n:HTTP_STATUS/200\ndata:{\"sequence_number\":6,\"content_index\":0,\"item_id\":\"msg_a908ad8e-6220-444b-880c-02aecbaf8fac\",\"delta\":\"\",\"output_index\":0,\"type\":\"response.output_text.delta\",\"logprobs\":[]}\n\nid:8\nevent:response.output_text.done\n:HTTP_STATUS/200\ndata:{\"sequence_number\":7,\"content_index\":0,\"item_id\":\"msg_a908ad8e-6220-444b-880c-02aecbaf8fac\",\"text\":\"OK.\",\"output_index\":0,\"type\":\"response.output_text.done\",\"logprobs\":[]}\n\nid:9\nevent:response.content_part.done\n:HTTP_STATUS/200\ndata:{\"sequence_number\":8,\"output_index\":0,\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_a908ad8e-6220-444b-880c-02aecbaf8fac\",\"part\":{\"type\":\"output_text\",\"annotations\":[],\"text\":\"OK.\"}}\n\nid:10\nevent:response.output_item.done\n:HTTP_STATUS/200\ndata:{\"sequence_number\":9,\"item\":{\"id\":\"msg_a908ad8e-6220-444b-880c-02aecbaf8fac\",\"role\":\"assistant\",\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"text\":\"OK.\"}],\"status\":\"completed\"},\"output_index\":0,\"type\":\"response.output_item.done\"}\n\nid:11\nevent:response.completed\n:HTTP_STATUS/200\ndata:{\"sequence_number\":10,\"type\":\"response.completed\",\"response\":{\"top_logprobs\":0,\"metadata\":{},\"presence_penalty\":0.0,\"reasoning\":{\"effort\":\"none\"},\"usage\":{\"total_tokens\":60,\"input_tokens_details\":{\"cached_tokens\":0},\"output_tokens\":2,\"input_tokens\":58,\"output_tokens_details\":{\"reasoning_tokens\":0},\"x_details\":[{\"total_tokens\":60,\"x_billing_type\":\"response_api\",\"output_tokens\":2,\"input_tokens\":58,\"prompt_tokens_details\":{\"cached_tokens\":0}}]},\"created_at\":1788837161,\"store\":true,\"tools\":[],\"output\":[{\"id\":\"msg_a908ad8e-6220-444b-880c-02aecbaf8fac\",\"role\":\"assistant\",\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"text\":\"OK.\"}],\"status\":\"completed\"}],\"top_p\":1.0,\"completed_at\":1788837161,\"frequency_penalty\":0.0,\"parallel_tool_calls\":true,\"background\":false,\"temperature\":1.0,\"tool_choice\":\"auto\",\"model\":\"qwen3.8-max\",\"service_tier\":\"default\",\"id\":\"resp_97f8cd28-c7ab-9d51-9c14-4fc531decdc8\",\"max_output_tokens\":1024,\"object\":\"response\",\"status\":\"completed\"}}\n\n"
}
},
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1/responses",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"qwen3.8-max\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"What word did I ask you to remember? Reply with only the word.\"}]}],\"store\":true,\"reasoning\":{\"effort\":\"none\"},\"max_output_tokens\":1024,\"previous_response_id\":\"resp_97f8cd28-c7ab-9d51-9c14-4fc531decdc8\",\"stream\":true}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream;charset=UTF-8"
},
"body": "id:1\nevent:response.created\n:HTTP_STATUS/200\ndata:{\"sequence_number\":0,\"type\":\"response.created\",\"response\":{\"top_logprobs\":0,\"metadata\":{},\"presence_penalty\":0.0,\"reasoning\":{\"effort\":\"none\"},\"created_at\":1788837162,\"store\":true,\"tools\":[],\"output\":[],\"top_p\":1.0,\"completed_at\":1788837162,\"previous_response_id\":\"resp_97f8cd28-c7ab-9d51-9c14-4fc531decdc8\",\"frequency_penalty\":0.0,\"parallel_tool_calls\":true,\"background\":false,\"temperature\":1.0,\"tool_choice\":\"auto\",\"model\":\"qwen3.8-max\",\"service_tier\":\"default\",\"id\":\"resp_1d81d34c-d3ec-9f98-ab59-5d4641d4217a\",\"max_output_tokens\":1024,\"object\":\"response\",\"status\":\"queued\"}}\n\nid:2\nevent:response.in_progress\n:HTTP_STATUS/200\ndata:{\"sequence_number\":1,\"type\":\"response.in_progress\",\"response\":{\"top_logprobs\":0,\"metadata\":{},\"presence_penalty\":0.0,\"reasoning\":{\"effort\":\"none\"},\"created_at\":1788837162,\"store\":true,\"tools\":[],\"output\":[],\"top_p\":1.0,\"completed_at\":1788837162,\"previous_response_id\":\"resp_97f8cd28-c7ab-9d51-9c14-4fc531decdc8\",\"frequency_penalty\":0.0,\"parallel_tool_calls\":true,\"background\":false,\"temperature\":1.0,\"tool_choice\":\"auto\",\"model\":\"qwen3.8-max\",\"service_tier\":\"default\",\"id\":\"resp_1d81d34c-d3ec-9f98-ab59-5d4641d4217a\",\"max_output_tokens\":1024,\"object\":\"response\",\"status\":\"in_progress\"}}\n\nid:3\nevent:response.output_item.added\n:HTTP_STATUS/200\ndata:{\"sequence_number\":2,\"item\":{\"id\":\"msg_7bc50c33-8fbd-4b10-91fc-4f2533bb2091\",\"role\":\"assistant\",\"type\":\"message\",\"content\":[],\"status\":\"in_progress\"},\"output_index\":0,\"type\":\"response.output_item.added\"}\n\nid:4\nevent:response.content_part.added\n:HTTP_STATUS/200\ndata:{\"sequence_number\":3,\"output_index\":0,\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_7bc50c33-8fbd-4b10-91fc-4f2533bb2091\",\"part\":{\"type\":\"output_text\",\"annotations\":[],\"text\":\"\"}}\n\nid:5\nevent:response.output_text.delta\n:HTTP_STATUS/200\ndata:{\"sequence_number\":4,\"content_index\":0,\"item_id\":\"msg_7bc50c33-8fbd-4b10-91fc-4f2533bb2091\",\"delta\":\"ap\",\"output_index\":0,\"type\":\"response.output_text.delta\",\"logprobs\":[]}\n\nid:6\nevent:response.output_text.delta\n:HTTP_STATUS/200\ndata:{\"sequence_number\":5,\"content_index\":0,\"item_id\":\"msg_7bc50c33-8fbd-4b10-91fc-4f2533bb2091\",\"delta\":\"ricot\",\"output_index\":0,\"type\":\"response.output_text.delta\",\"logprobs\":[]}\n\nid:7\nevent:response.output_text.delta\n:HTTP_STATUS/200\ndata:{\"sequence_number\":6,\"content_index\":0,\"item_id\":\"msg_7bc50c33-8fbd-4b10-91fc-4f2533bb2091\",\"delta\":\"\",\"output_index\":0,\"type\":\"response.output_text.delta\",\"logprobs\":[]}\n\nid:8\nevent:response.output_text.done\n:HTTP_STATUS/200\ndata:{\"sequence_number\":7,\"content_index\":0,\"item_id\":\"msg_7bc50c33-8fbd-4b10-91fc-4f2533bb2091\",\"text\":\"apricot\",\"output_index\":0,\"type\":\"response.output_text.done\",\"logprobs\":[]}\n\nid:9\nevent:response.content_part.done\n:HTTP_STATUS/200\ndata:{\"sequence_number\":8,\"output_index\":0,\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_7bc50c33-8fbd-4b10-91fc-4f2533bb2091\",\"part\":{\"type\":\"output_text\",\"annotations\":[],\"text\":\"apricot\"}}\n\nid:10\nevent:response.output_item.done\n:HTTP_STATUS/200\ndata:{\"sequence_number\":9,\"item\":{\"id\":\"msg_7bc50c33-8fbd-4b10-91fc-4f2533bb2091\",\"role\":\"assistant\",\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"text\":\"apricot\"}],\"status\":\"completed\"},\"output_index\":0,\"type\":\"response.output_item.done\"}\n\nid:11\nevent:response.completed\n:HTTP_STATUS/200\ndata:{\"sequence_number\":10,\"type\":\"response.completed\",\"response\":{\"top_logprobs\":0,\"metadata\":{},\"presence_penalty\":0.0,\"reasoning\":{\"effort\":\"none\"},\"usage\":{\"total_tokens\":92,\"input_tokens_details\":{\"cached_tokens\":0},\"output_tokens\":3,\"input_tokens\":89,\"output_tokens_details\":{\"reasoning_tokens\":0},\"x_details\":[{\"total_tokens\":92,\"x_billing_type\":\"response_api\",\"output_tokens\":3,\"input_tokens\":89,\"prompt_tokens_details\":{\"cached_tokens\":0}}]},\"created_at\":1788837162,\"store\":true,\"tools\":[],\"output\":[{\"id\":\"msg_7bc50c33-8fbd-4b10-91fc-4f2533bb2091\",\"role\":\"assistant\",\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"text\":\"apricot\"}],\"status\":\"completed\"}],\"top_p\":1.0,\"completed_at\":1788837162,\"previous_response_id\":\"resp_97f8cd28-c7ab-9d51-9c14-4fc531decdc8\",\"frequency_penalty\":0.0,\"parallel_tool_calls\":true,\"background\":false,\"temperature\":1.0,\"tool_choice\":\"auto\",\"model\":\"qwen3.8-max\",\"service_tier\":\"default\",\"id\":\"resp_1d81d34c-d3ec-9f98-ab59-5d4641d4217a\",\"max_output_tokens\":1024,\"object\":\"response\",\"status\":\"completed\"}}\n\n"
}
}
]
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -81,7 +81,7 @@
{
"direction": "client",
"kind": "text",
"body": "{\"type\":\"response.create\",\"model\":\"gpt-5.5\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Reply exactly: Alpha.\"}]},{\"type\":\"message\",\"id\":\"msg_ws_reconnect_1\",\"role\":\"assistant\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"text\":\"Alpha.\"}]},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Reply exactly: Beta.\"}]}],\"store\":false,\"max_output_tokens\":30,\"include\":[\"reasoning.encrypted_content\"],\"reasoning\":{\"effort\":\"medium\",\"summary\":\"auto\"},\"text\":{\"verbosity\":\"low\"},\"instructions\":\"Follow the user's exact reply instruction.\"}"
"body": "{\"type\":\"response.create\",\"model\":\"gpt-5.5\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Reply exactly: Alpha.\"}]},{\"type\":\"message\",\"id\":\"msg_ws_reconnect_1\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"Alpha.\"}]},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Reply exactly: Beta.\"}]}],\"store\":false,\"max_output_tokens\":30,\"include\":[\"reasoning.encrypted_content\"],\"reasoning\":{\"effort\":\"medium\",\"summary\":\"auto\"},\"text\":{\"verbosity\":\"low\"},\"instructions\":\"Follow the user's exact reply instruction.\"}"
},
{
"direction": "server",
@@ -91,7 +91,7 @@
{
"direction": "client",
"kind": "text",
"body": "{\"type\":\"response.create\",\"model\":\"gpt-5.5\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Reply exactly: Ready.\"}]},{\"type\":\"message\",\"id\":\"msg_ws_rejection_1\",\"role\":\"assistant\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"text\":\"Ready.\"}]},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Reply exactly: Recovered.\"}]}],\"store\":false,\"max_output_tokens\":30,\"include\":[\"reasoning.encrypted_content\"],\"reasoning\":{\"effort\":\"medium\",\"summary\":\"auto\"},\"text\":{\"verbosity\":\"low\"},\"instructions\":\"Follow the user's exact reply instruction.\"}"
"body": "{\"type\":\"response.create\",\"model\":\"gpt-5.5\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Reply exactly: Ready.\"}]},{\"type\":\"message\",\"id\":\"msg_ws_rejection_1\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"Ready.\"}]},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Reply exactly: Recovered.\"}]}],\"store\":false,\"max_output_tokens\":30,\"include\":[\"reasoning.encrypted_content\"],\"reasoning\":{\"effort\":\"medium\",\"summary\":\"auto\"},\"text\":{\"verbosity\":\"low\"},\"instructions\":\"Follow the user's exact reply instruction.\"}"
},
{
"direction": "server",
File diff suppressed because one or more lines are too long
@@ -1,39 +0,0 @@
import { LLM } from "../../src/index.js"
import { Alibaba } from "../../src/providers.js"
const provider = Alibaba.configure({ region: "ap-southeast-1" })
Alibaba.configure({ baseURL: "https://gateway.example/v1" })
Alibaba.configure({ region: "eu-central-1", workspaceID: "llm-workspace" })
LLM.request({
model: provider.chat("qwen3.8-max"),
providerOptions: { reasoningEffort: "future", enableThinking: true, preserveThinking: false, toolStream: true },
})
LLM.request({
model: provider.messages("qwen3.8-max"),
providerOptions: { effort: "xhigh", thinking: { type: "enabled" } },
})
LLM.request({
model: provider.messages("qwen3.7-plus"),
providerOptions: { thinking: { type: "enabled", budgetTokens: 512 } },
})
LLM.request({
model: provider.responses("qwen3.8-max"),
providerOptions: { reasoningEffort: "low", enableThinking: true, store: true, previousResponseId: "resp_previous" },
})
// @ts-expect-error Region or complete base URL is required.
Alibaba.configure({ apiKey: "fixture" })
LLM.request({
model: provider.chat("qwen3.8-max"),
// @ts-expect-error Thinking toggle is a boolean.
providerOptions: { enableThinking: "true" },
})
LLM.request({
model: provider.messages("qwen3.8-max"),
// @ts-expect-error Messages uses effort.
providerOptions: { reasoningEffort: "high" },
})
LLM.request({
model: provider.responses("qwen3.8-max"),
// @ts-expect-error Responses does not use Chat thinking budgets.
providerOptions: { thinkingBudget: 512 },
})
-32
View File
@@ -51,10 +51,6 @@ describe("provider package entrypoints", () => {
import("@opencode/ai/providers/zai-coding-plan/chat"),
import("@opencode/ai/providers/zai-coding-plan/messages"),
import("@opencode/ai/providers/zai-coding-plan/responses"),
import("@opencode/ai/providers/alibaba"),
import("@opencode/ai/providers/alibaba/chat"),
import("@opencode/ai/providers/alibaba/messages"),
import("@opencode/ai/providers/alibaba/responses"),
])
for (const module of modules) expect(module.model).toBeFunction()
@@ -65,34 +61,6 @@ describe("provider package entrypoints", () => {
expect(modules[19].model).not.toBe(modules[20].model)
})
test("maps Alibaba API entrypoints onto explicit regional routes", async () => {
const modules = await Promise.all([
import("@opencode/ai/providers/alibaba"),
import("@opencode/ai/providers/alibaba/chat"),
import("@opencode/ai/providers/alibaba/messages"),
import("@opencode/ai/providers/alibaba/responses"),
])
expect(modules[0].model).toBe(modules[1].model)
const settings = {
region: "eu-central-1",
workspaceID: "llm-fixture",
apiKey: "fixture",
headers: { "x-test": "fixture" },
body: { extension: true },
}
const routes = ["alibaba-chat", "alibaba-chat", "alibaba-messages", "alibaba-responses"]
modules.forEach((module, index) => {
const model = module.model("qwen3.8-max", settings)
expect(model.provider).toBe("alibaba")
expect(model.route.id).toBe(routes[index])
expect(model.route.endpoint.baseURL).toBe(
`https://llm-fixture.eu-central-1.maas.aliyuncs.com/${index === 2 ? "apps/anthropic/v1" : "compatible-mode/v1"}`,
)
expect(model.route.defaults.headers).toEqual(settings.headers)
expect(model.route.defaults.http?.body).toEqual(settings.body)
})
})
test("maps Moonshot API entrypoints onto provider-owned routes", async () => {
const modules = await Promise.all([
import("@opencode/ai/providers/moonshot"),
@@ -1,254 +0,0 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { LLM, LLMEvent, LLMRequest, Message, ToolDefinition } from "../../src/index.js"
import { Alibaba } from "../../src/providers.js"
import { LLMClient } from "../../src/route.js"
import { compileRequest } from "../../src/route/client.js"
import { recordedTests } from "../recorded-test.js"
const alibaba = Alibaba.configure({ region: "ap-southeast-1", apiKey: process.env.ALIBABA_API_KEY ?? "fixture" })
const record = (api: "chat" | "messages" | "responses") =>
recordedTests({
prefix: `alibaba-${api}`,
provider: "alibaba",
protocol: `alibaba-${api}`,
requires: ["ALIBABA_API_KEY"],
tags: ["region:ap-southeast-1"],
})
for (const api of ["chat", "messages", "responses"] as const) {
const recorded = record(api)
describe(`Alibaba ${api} capabilities`, () => {
for (const enabled of [false, true]) {
recorded.effect.with(
`Qwen 3.7 Plus streams thinking ${enabled ? "enabled" : "disabled"}`,
{ tags: ["thinking", "usage"] },
() =>
Effect.gen(function* () {
const request = LLM.request({
model: alibaba[api]("qwen3.7-plus"),
providerOptions:
api === "messages"
? { thinking: { type: enabled ? "enabled" : "disabled", ...(enabled ? { budgetTokens: 1024 } : {}) } }
: api === "chat"
? { enableThinking: enabled, ...(enabled ? { thinkingBudget: 1024 } : {}) }
: { enableThinking: enabled },
prompt: "What is 173 multiplied by 219? Reply with only the final integer.",
generation: { maxTokens: 4096 },
})
const compiled = yield* compileRequest(request)
expect(api === "messages" ? compiled.body.thinking.type : compiled.body.enable_thinking).toBe(
api === "messages" ? (enabled ? "enabled" : "disabled") : enabled,
)
const response = yield* LLMClient.generate(request)
expect(response.text.replaceAll(",", "")).toContain("37887")
expect(response.reasoning.length > 0).toBe(enabled)
expect(response.finishReason.normalized).toBe("stop")
expect(response.usage?.inputTokens).toBeGreaterThan(0)
expect(response.usage?.outputTokens).toBeGreaterThan(0)
}),
120_000,
)
}
recorded.effect.with(
"Qwen 3.8 Flash reads image bytes",
{ tags: ["image"] },
() =>
Effect.gen(function* () {
const bytes = yield* Effect.promise(() =>
Bun.file(new URL("../fixtures/media/restroom.png", import.meta.url)).bytes(),
)
const response = yield* LLMClient.generate(
LLM.request({
model: alibaba[api]("qwen3.8-flash"),
providerOptions: api === "messages" ? { thinking: { type: "disabled" } } : { enableThinking: false },
messages: [
Message.user([
{ type: "text", text: "Read the three words in this image. Reply with only the words in order." },
{ type: "media", mediaType: "image/png", data: bytes },
]),
],
generation: { maxTokens: 4096 },
}),
)
expect(response.text.toLowerCase()).toContain("jiggling restroom prison")
expect(response.finishReason.normalized).toBe("stop")
}),
120_000,
)
recorded.effect.with(
"Qwen 3.8 Max obeys named tool choice",
{ tags: ["tool", "tool-choice"] },
() =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(
LLM.request({
model: alibaba[api]("qwen3.8-max"),
prompt: "Find the current weather in Paris.",
providerOptions: api === "messages" ? { thinking: { type: "disabled" } } : { reasoningEffort: "none" },
tools: [
ToolDefinition.make({
name: "get_weather",
description: "Get weather in a city",
inputSchema: {
type: "object",
properties: { city: { type: "string", enum: ["Paris"] } },
required: ["city"],
},
}),
],
toolChoice: { type: "tool", name: "get_weather" },
generation: { maxTokens: 4096 },
}),
)
expect(response.toolCalls).toMatchObject([{ name: "get_weather", input: { city: "Paris" } }])
expect(response.finishReason.normalized).toBe(api === "messages" ? "stop" : "tool-calls")
if (api === "messages") expect(response.finishReason.raw).toBe("end_turn")
}),
120_000,
)
})
}
record("chat").effect.with(
"Qwen 3.8 Max returns a JSON object",
{ tags: ["structured-output"] },
() =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(
LLM.request({
model: alibaba.chat("qwen3.8-max"),
prompt: 'Return a JSON object with one key "city" set to the capital city of France.',
providerOptions: { reasoningEffort: "none", responseFormat: { type: "json_object" } },
generation: { maxTokens: 1024 },
}),
)
expect(JSON.parse(response.text)).toEqual({ city: "Paris" })
expect(response.finishReason.normalized).toBe("stop")
}),
120_000,
)
record("messages").effect.with(
"Qwen 3.8 Max follows a JSON schema",
{ tags: ["structured-output"] },
() =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(
LLM.request({
model: alibaba.messages("qwen3.8-max"),
prompt: 'Return a JSON object with one key "city" set to the capital city of France.',
providerOptions: {
thinking: { type: "disabled" },
outputConfig: {
format: {
type: "json_schema",
schema: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
additionalProperties: false,
},
},
},
},
generation: { maxTokens: 1024 },
}),
)
expect(JSON.parse(response.text)).toEqual({ city: "Paris" })
expect(response.finishReason.normalized).toBe("stop")
}),
120_000,
)
const responses = record("responses")
responses.effect.with(
"Qwen 3.8 Max continues a stored response",
{ tags: ["continuation", "storage"] },
() =>
Effect.gen(function* () {
const first = yield* LLMClient.generate(
LLM.request({
model: alibaba.responses("qwen3.8-max"),
prompt: "Remember the password word apricot. Reply OK.",
providerOptions: { store: true, reasoningEffort: "none" },
generation: { maxTokens: 1024 },
}),
)
const id = first.events.find(LLMEvent.is.finish)?.providerMetadata?.alibaba?.responseId
expect(id).toBeString()
if (typeof id !== "string") throw new Error("Missing Alibaba response ID")
const second = yield* LLMClient.generate(
LLM.request({
model: alibaba.responses("qwen3.8-max"),
prompt: "What word did I ask you to remember? Reply with only the word.",
providerOptions: { previousResponseId: id, store: true, reasoningEffort: "none" },
generation: { maxTokens: 1024 },
}),
)
expect(second.text.toLowerCase()).toContain("apricot")
expect(second.finishReason.normalized).toBe("stop")
}),
120_000,
)
responses.effect.with(
"Qwen 3.8 Max uses hosted web search and extraction",
{ tags: ["hosted-tool", "web-search", "web-extractor"] },
() =>
Effect.gen(function* () {
const request = LLM.request({
model: alibaba.responses("qwen3.8-max"),
prompt:
"Use web search to find Alibaba Cloud Model Studio's official documentation, then use web_extractor to read the page. Give a brief summary with the source URL.",
tools: [Alibaba.webSearch(), Alibaba.webExtractor()],
providerOptions: { reasoningEffort: "low", store: false },
generation: { maxTokens: 4096 },
})
const response = yield* LLMClient.generate(request)
expect(response.toolCalls).toEqual(
expect.arrayContaining([
expect.objectContaining({ name: "web_search", providerExecuted: true }),
expect.objectContaining({ name: "web_extractor", providerExecuted: true }),
]),
)
expect(response.text.toLowerCase()).toContain("alibaba")
expect(response.events.some(LLMEvent.is.toolResult)).toBe(true)
const replay = yield* compileRequest(
LLMRequest.update(request, {
messages: [...request.messages, response.message, Message.user("Summarize in one sentence.")],
}),
)
expect(replay.body.input).toEqual(
expect.arrayContaining([
expect.objectContaining({ type: "web_search_call" }),
expect.objectContaining({ type: "web_extractor_call" }),
]),
)
}),
180_000,
)
responses.effect.with(
"Qwen 3.8 Max uses hosted code interpreter",
{ tags: ["hosted-tool", "code-interpreter"] },
() =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(
LLM.request({
model: alibaba.responses("qwen3.8-max"),
prompt:
"Use the code interpreter to compute the SHA-256 hash of the UTF-8 string hello (no newline). Reply with only the hash.",
tools: [Alibaba.codeInterpreter()],
providerOptions: { reasoningEffort: "low" },
generation: { maxTokens: 4096 },
}),
)
expect(response.toolCalls).toEqual(
expect.arrayContaining([expect.objectContaining({ name: "code_interpreter", providerExecuted: true })]),
)
expect(response.text).toContain("2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824")
expect(response.events.some(LLMEvent.is.toolResult)).toBe(true)
}),
180_000,
)
@@ -1,163 +0,0 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { LLM, LLMEvent, LLMRequest, Message, ToolDefinition, type LLMResponse } from "../../src/index.js"
import { Alibaba } from "../../src/providers.js"
import { LLMClient } from "../../src/route.js"
import { compileRequest } from "../../src/route/client.js"
import { recordedTests } from "../recorded-test.js"
const alibaba = Alibaba.configure({ region: "ap-southeast-1", apiKey: process.env.ALIBABA_API_KEY ?? "fixture" })
const weather = ToolDefinition.make({
name: "get_weather",
description: "Get the current weather in a city",
inputSchema: {
type: "object",
properties: { city: { type: "string", enum: ["Paris"] } },
required: ["city"],
additionalProperties: false,
},
})
for (const api of ["chat", "messages", "responses"] as const) {
const recorded = recordedTests({
prefix: `alibaba-${api}`,
provider: "alibaba",
protocol: `alibaba-${api}`,
requires: ["ALIBABA_API_KEY"],
tags: ["region:ap-southeast-1"],
})
describe(`Alibaba ${api}`, () => {
for (const effort of api === "messages"
? [undefined, "low", "medium", "high", "xhigh", "max"]
: [undefined, "none", "minimal", "low", "medium", "high", "xhigh", "max"]) {
recorded.effect.with(
`Qwen 3.8 Max streams ${effort ?? "default"} effort`,
{ tags: ["text", "reasoning", "usage"] },
() =>
Effect.gen(function* () {
const request = LLM.request({
model: alibaba[api]("qwen3.8-max"),
prompt: "What is 173 multiplied by 219? Reply with only the final integer.",
providerOptions: api === "messages" ? { effort } : { reasoningEffort: effort },
generation: { maxTokens: 4096 },
})
const compiled = yield* compileRequest(request)
expect(compiled.body.enable_thinking).toBeUndefined()
expect(compiled.body.thinking).toBeUndefined()
expect(
api === "chat"
? compiled.body.reasoning_effort
: api === "messages"
? compiled.body.output_config?.effort
: compiled.body.reasoning?.effort,
).toBe(effort)
const response = yield* LLMClient.generate(request)
expect(response.text.replaceAll(",", "")).toContain("37887")
expect(response.reasoning.length > 0).toBe(effort !== "none")
expect(response.finishReason.normalized).toBe("stop")
expectUsage(response)
}),
120_000,
)
}
recorded.effect.with(
"Qwen 3.8 Max replays reasoning through a tool loop and follow-up",
{ tags: ["tool", "tool-loop", "reasoning", "continuation"] },
() =>
Effect.gen(function* () {
const request = LLM.request({
model: alibaba[api]("qwen3.8-max"),
providerOptions:
api === "messages"
? { effort: "medium" }
: api === "chat"
? { reasoningEffort: "medium", preserveThinking: true, toolStream: true }
: { reasoningEffort: "medium", store: false },
prompt:
"We have a budget of 38000 dollars for 219 trips costing 173 dollars each. Calculate whether that is affordable. If it is, use get_weather to look up the current weather in Paris. After receiving the result, report the weather in one short sentence.",
tools: [weather],
generation: { maxTokens: 4096 },
})
const first = yield* LLMClient.generate(request)
expect(first.toolCalls).toMatchObject([{ name: "get_weather", input: { city: "Paris" } }])
expect(first.finishReason.normalized).toBe("tool-calls")
expect(first.reasoning.length).toBeGreaterThan(0)
expect(first.events.some(LLMEvent.is.toolInputDelta)).toBe(true)
expectUsage(first)
const continuation = LLMRequest.update(request, {
messages: [
...request.messages,
first.message,
...first.toolCalls.map((call) =>
Message.tool({ id: call.id, name: call.name, result: { condition: "sunny", temperature: "18C" } }),
),
],
})
expectReasoning(api, (yield* compileRequest(continuation)).body, first)
const second = yield* LLMClient.generate(continuation)
expect(second.text.toLowerCase()).toContain("sunny")
expect(second.toolCalls).toHaveLength(0)
expect(second.finishReason.normalized).toBe("stop")
expectUsage(second)
const followUp = LLMRequest.update(continuation, {
messages: [
...continuation.messages,
second.message,
Message.user("What temperature did the tool report? Reply with only the temperature."),
],
})
expectReasoning(api, (yield* compileRequest(followUp)).body, first)
const third = yield* LLMClient.generate(followUp)
expect(third.text).toContain("18")
expect(third.toolCalls).toHaveLength(0)
expect(third.finishReason.normalized).toBe("stop")
expectUsage(third)
}),
180_000,
)
})
}
function expectUsage(response: LLMResponse) {
expect(response.usage?.inputTokens).toBeGreaterThan(0)
expect(response.usage?.outputTokens).toBeGreaterThan(0)
expect(response.events.filter(LLMEvent.is.finish)).toHaveLength(1)
}
function expectReasoning(
api: "chat" | "messages" | "responses",
body: Readonly<Record<string, unknown>>,
response: LLMResponse,
) {
if (api === "chat") {
expect(body.messages).toEqual(
expect.arrayContaining([expect.objectContaining({ role: "assistant", reasoning_content: response.reasoning })]),
)
return
}
for (const part of response.message.content.filter((part) => part.type === "reasoning")) {
if (api === "messages") {
expect(body.messages).toEqual(
expect.arrayContaining([
expect.objectContaining({
role: "assistant",
content: expect.arrayContaining([
{ type: "thinking", thinking: part.text, signature: part.providerMetadata?.alibaba?.signature ?? "" },
]),
}),
]),
)
continue
}
expect(body.input).toEqual(
expect.arrayContaining([
expect.objectContaining({
type: "reasoning",
id: part.providerMetadata?.alibaba?.itemId,
summary: expect.arrayContaining([{ type: "summary_text", text: part.text }]),
}),
]),
)
}
}
-376
View File
@@ -1,376 +0,0 @@
import { expect, test } from "bun:test"
import { ConfigProvider, Effect } from "effect"
import { Headers } from "effect/unstable/http"
import { Auth, LLM, LLMClient, LLMRequest, Message, ReasoningPart, ToolDefinition } from "../../src/index.js"
import { Alibaba } from "../../src/providers.js"
import { compileRequest } from "../../src/route/client.js"
import { Endpoint } from "../../src/route/endpoint.js"
import { it } from "../lib/effect.js"
import { dynamicResponse } from "../lib/http.js"
import { sseEvents } from "../lib/sse.js"
const tool = ToolDefinition.make({
name: "lookup",
description: "Look up a value",
inputSchema: { type: "object", properties: { key: { type: "string" } } },
})
const paths = {
chat: "/compatible-mode/v1/chat/completions",
messages: "/apps/anthropic/v1/messages",
responses: "/compatible-mode/v1/responses",
}
it.effect("Alibaba owns regional shared and workspace-specific endpoints", () =>
Effect.gen(function* () {
for (const region of [
"ap-southeast-1",
"cn-beijing",
"cn-hongkong",
"us-east-1",
"eu-central-1",
"ap-northeast-1",
]) {
const provider = Alibaba.configure({ region, workspaceID: "llm-fixture", apiKey: "fixture" })
expect(provider.model).toBe(provider.chat)
for (const api of ["chat", "messages", "responses"] as const) {
const model = provider[api]("qwen-plus-us")
const request = LLM.request({ model, prompt: "Hello" })
const compiled = yield* compileRequest(request)
expect(Endpoint.render(model.route.endpoint, { request, body: compiled.body }).toString()).toBe(
`https://llm-fixture.${region}.maas.aliyuncs.com${paths[api]}`,
)
expect(model.provider).toBe("alibaba")
expect(model.route.id).toBe(`alibaba-${api}`)
expect(compiled.body.model).toBe("qwen-plus-us")
for (const field of [
"thinking",
"enable_thinking",
"thinking_budget",
"preserve_thinking",
"reasoning_effort",
"reasoning",
"output_config",
"store",
"tool_stream",
])
expect(compiled.body[field]).toBeUndefined()
}
}
for (const [region, host] of [
["ap-southeast-1", "dashscope-intl.aliyuncs.com"],
["cn-beijing", "dashscope.aliyuncs.com"],
["cn-hongkong", "cn-hongkong.dashscope.aliyuncs.com"],
["us-east-1", "dashscope-us.aliyuncs.com"],
]) {
const provider = Alibaba.configure({ region, apiKey: "fixture" })
for (const api of ["chat", "messages", "responses"] as const) {
const request = LLM.request({ model: provider[api]("qwen3.8-max") })
const compiled = yield* compileRequest(request)
expect(Endpoint.render(request.model.route.endpoint, { request, body: compiled.body }).toString()).toBe(
`https://${host}${paths[api]}`,
)
}
}
}),
)
test("Alibaba requires explicit placement and supports complete base URL overrides", () => {
for (const region of ["eu-central-1", "ap-northeast-1", "future-region"])
expect(() => Alibaba.configure({ region })).toThrow("requires workspaceID or baseURL")
for (const config of [
{ baseURL: "https://gateway.example/prefix" },
{ region: "future-region", workspaceID: "ignored", baseURL: "https://gateway.example/prefix" },
]) {
const provider = Alibaba.configure(config)
for (const api of ["chat", "messages", "responses"] as const)
expect(provider[api]("unchanged-id").route.endpoint.baseURL).toBe(config.baseURL)
}
expect(
Alibaba.configure({ region: "future-region", workspaceID: "llm-fixture" }).chat("new-model").route.endpoint.baseURL,
).toBe("https://llm-fixture.future-region.maas.aliyuncs.com/compatible-mode/v1")
})
it.effect("Alibaba resolves explicit auth, API keys, and regional environment credentials", () =>
Effect.gen(function* () {
for (const item of [
{
config: {},
env: { DASHSCOPE_API_KEY: "primary", ALIBABA_API_KEY: "fallback" },
headers: { authorization: "Bearer primary" },
},
{ config: {}, env: { ALIBABA_API_KEY: "fallback" }, headers: { authorization: "Bearer fallback" } },
{
config: { apiKey: "explicit" },
env: { DASHSCOPE_API_KEY: "primary" },
headers: { authorization: "Bearer explicit" },
},
{
config: { auth: Auth.header("x-custom-key", "custom") },
env: { DASHSCOPE_API_KEY: "primary" },
headers: { "x-custom-key": "custom" },
},
]) {
const provider = Alibaba.configure({ region: "ap-southeast-1", ...item.config })
for (const api of ["chat", "messages", "responses"] as const) {
const request = LLM.request({ model: provider[api]("qwen3.8-max") })
const headers = yield* request.model.route.auth
.apply({ request, method: "POST", url: "https://fixture", body: "{}", headers: Headers.empty })
.pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: item.env }))))
expect(headers).toEqual(expect.objectContaining(item.headers))
}
}
}),
)
it.effect("Alibaba keeps native reasoning controls and future efforts on their selected API", () =>
Effect.gen(function* () {
const provider = Alibaba.configure({ region: "ap-southeast-1", apiKey: "fixture" })
for (const effort of ["none", "minimal", "low", "medium", "high", "xhigh", "max", "future-effort"]) {
const chat = yield* compileRequest(
LLM.request({ model: provider.chat("qwen3.8-max"), providerOptions: { reasoningEffort: effort } }),
)
const messages = yield* compileRequest(
LLM.request({ model: provider.messages("qwen3.8-max"), providerOptions: { effort } }),
)
const responses = yield* compileRequest(
LLM.request({ model: provider.responses("qwen3.8-max"), providerOptions: { reasoningEffort: effort } }),
)
expect(chat.body.reasoning_effort).toBe(effort)
expect(messages.body.output_config).toEqual({ effort })
expect(responses.body.reasoning).toEqual({ effort })
for (const result of [chat, messages, responses]) {
expect(result.body.thinking).toBeUndefined()
expect(result.body.enable_thinking).toBeUndefined()
}
}
for (const enableThinking of [true, false]) {
const chat = yield* compileRequest(
LLM.request({
model: provider.chat("qwen3.7-plus"),
tools: [tool],
generation: { maxTokens: 1234, topK: 20 },
providerOptions: {
enableThinking,
thinkingBudget: 512,
preserveThinking: false,
clearThinking: false,
toolStream: false,
parallelToolCalls: false,
repetitionPenalty: 1.1,
responseFormat: { type: "json_object" },
enableSearch: true,
searchOptions: { forced_search: true, search_strategy: "future-strategy", enable_search_extension: false },
},
}),
)
expect(chat.body).toMatchObject({
enable_thinking: enableThinking,
thinking_budget: 512,
preserve_thinking: false,
clear_thinking: false,
tool_stream: false,
parallel_tool_calls: false,
repetition_penalty: 1.1,
max_completion_tokens: 1234,
top_k: 20,
response_format: { type: "json_object" },
enable_search: true,
search_options: { forced_search: true, search_strategy: "future-strategy", enable_search_extension: false },
})
expect(chat.body.max_tokens).toBeUndefined()
expect(chat.body.tools).toEqual([
expect.objectContaining({ function: expect.not.objectContaining({ strict: expect.anything() }) }),
])
const responses = yield* compileRequest(
LLM.request({
model: provider.responses("qwen3.7-plus"),
providerOptions: { enableThinking, store: false, previousResponseId: "resp_previous" },
}),
)
expect(responses.body).toMatchObject({
enable_thinking: enableThinking,
store: false,
previous_response_id: "resp_previous",
})
}
for (const thinking of [
{ type: "enabled" },
{ type: "disabled" },
{ type: "enabled", budgetTokens: 512 },
{ type: "future", budget_tokens: 4096 },
]) {
const messages = yield* compileRequest(
LLM.request({ model: provider.messages("qwen3.7-plus"), providerOptions: { thinking } }),
)
expect(messages.body.thinking).toEqual({
type: thinking.type,
budget_tokens: thinking.budgetTokens ?? thinking.budget_tokens,
})
}
}),
)
it.effect("Alibaba validates malformed options before execution", () =>
Effect.gen(function* () {
const provider = Alibaba.configure({ region: "ap-southeast-1", apiKey: "fixture" })
for (const [api, providerOptions] of [
["chat", { preserveThinking: "false" }],
["messages", { thinking: { type: "enabled", budgetTokens: "512" } }],
["responses", { enableThinking: "false" }],
] as const) {
const model = provider[api]("qwen3.8-max").route.with({ providerOptions }).model({ id: "qwen3.8-max" })
const error = yield* compileRequest(LLM.request({ model })).pipe(Effect.flip)
expect(error.reason._tag).toBe("InvalidRequest")
}
}),
)
it.effect("Alibaba preserves unsigned and signed Messages thinking without a budget requirement", () =>
Effect.gen(function* () {
const result = yield* compileRequest(
LLM.request({
model: Alibaba.configure({ region: "ap-southeast-1", apiKey: "fixture" }).messages("qwen3.8-max"),
providerOptions: { thinking: { type: "enabled" }, effort: "low" },
messages: [
Message.user("Hello"),
Message.assistant([
ReasoningPart.make({ type: "reasoning", text: "unsigned" }),
ReasoningPart.make({
type: "reasoning",
text: "signed",
providerMetadata: { alibaba: { signature: "opaque" } },
}),
]),
],
}),
)
expect(result.body.thinking).toEqual({ type: "enabled" })
expect(result.body.messages).toEqual(
expect.arrayContaining([
expect.objectContaining({
role: "assistant",
content: [
{ type: "thinking", thinking: "unsigned", signature: "" },
{ type: "thinking", thinking: "signed", signature: "opaque" },
],
}),
]),
)
}),
)
it.effect("Alibaba serializes Messages configuration, per-request controls, and final HTTP overlays", () =>
LLMClient.generate(
LLM.request({
model: Alibaba.configure({
baseURL: "https://gateway.example/v1",
apiKey: "fixture",
providerOptions: { thinking: { type: "enabled", budgetTokens: 512 }, effort: "high" },
}).messages("qwen3.8-max"),
prompt: "Hello",
providerOptions: { effort: "low" },
http: { body: { output_config: { effort: "medium" }, extension: true } },
}),
).pipe(
Effect.provide(
dynamicResponse((input) =>
Effect.sync(() => {
expect(input.request.url).toBe("https://gateway.example/v1/messages")
expect(input.request.headers.authorization).toBe("Bearer fixture")
expect(input.request.headers["anthropic-version"]).toBe("2023-06-01")
expect(JSON.parse(input.text)).toMatchObject({
thinking: { type: "enabled", budget_tokens: 512 },
output_config: { effort: "medium" },
extension: true,
})
return input.respond(
sseEvents(
{
type: "message_start",
message: { id: "msg_fixture", content: [], usage: { input_tokens: 1, output_tokens: 0 } },
},
{ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 1 } },
{ type: "message_stop" },
),
{ headers: { "content-type": "text/event-stream" } },
)
}),
),
),
),
)
it.effect("Alibaba Responses lowers hosted tools and named selection using HTTP even with a WebSocket executor", () =>
Effect.gen(function* () {
const request = LLM.request({
model: Alibaba.configure({ region: "ap-southeast-1", apiKey: "fixture" }).responses("qwen3.8-max"),
tools: [Alibaba.webSearch(), Alibaba.webExtractor(), Alibaba.codeInterpreter(), tool],
prompt: "Hello",
})
const compiled = yield* compileRequest(request)
expect(compiled.body.tools).toMatchObject([
{ type: "web_search" },
{ type: "web_extractor" },
{ type: "code_interpreter" },
{ type: "function", name: "lookup" },
])
const named = yield* compileRequest(
LLMRequest.update(request, { tools: [tool], toolChoice: { type: "tool", name: "lookup" } }),
)
expect(named.body.tool_choice).toEqual({
type: "allowed_tools",
mode: "required",
tools: [{ type: "function", name: "lookup" }],
})
const response = yield* LLMClient.generate(request, {
webSocket: { execute: () => Effect.die("Unexpected WebSocket") },
})
expect(response.toolCalls).toMatchObject([
{
name: "web_extractor",
providerExecuted: true,
input: { urls: ["https://example.com"], goal: "Read the page" },
},
])
const replay = yield* compileRequest(
LLMRequest.update(request, { messages: [...request.messages, response.message, Message.user("Continue")] }),
)
expect(replay.body.input).toEqual(
expect.arrayContaining([
expect.objectContaining({
type: "web_extractor_call",
id: "extract_1",
urls: ["https://example.com"],
goal: "Read the page",
result: { text: "fixture" },
}),
]),
)
}).pipe(
Effect.provide(
dynamicResponse((input) =>
Effect.sync(() => {
expect(input.request.url).toBe("https://dashscope-intl.aliyuncs.com/compatible-mode/v1/responses")
return input.respond(
sseEvents(
{
type: "response.output_item.done",
output_index: 0,
item: {
type: "web_extractor_call",
id: "extract_1",
status: "completed",
urls: ["https://example.com"],
goal: "Read the page",
result: { text: "fixture" },
},
},
{ type: "response.completed", response: {} },
),
{ headers: { "content-type": "text/event-stream" } },
)
}),
),
),
),
)
@@ -23,7 +23,7 @@ it.effect("conversation lowering excludes generation settings and tool definitio
instructions: "Keep the context",
input: [
{ role: "user", content: [{ type: "input_text", text: "hello" }] },
{ type: "message", role: "assistant", status: "completed", content: [{ type: "output_text", text: "hi" }] },
{ type: "message", role: "assistant", content: [{ type: "output_text", text: "hi" }] },
],
})
}),
@@ -1,120 +0,0 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { LLM, LLMEvent, Message } from "../../src/index.js"
import { OpenAI } from "../../src/providers.js"
import { configure } from "../../src/providers/openai-compatible-responses.js"
import { compileRequest, LLMClient } from "../../src/route/client.js"
import { it } from "../lib/effect.js"
import { fixedResponse } from "../lib/http.js"
import { sseEvents } from "../lib/sse.js"
for (const model of [
OpenAI.configure({ apiKey: "test-key" }).responses("example-model"),
configure({ apiKey: "test-key", baseURL: "https://responses.example.test/v1" }).model("example-model"),
]) {
describe(`${model.route.protocol} message replay`, () => {
const key = model.route.providerMetadataKey ?? "openresponses"
it.effect("marks assistant text completed regardless of stored status", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
...[undefined, "in_progress", "incomplete", "completed"].map((status, index) =>
Message.make({
role: "assistant",
providerMetadata: { [key]: { status } },
content: [
{
type: "text",
text: `Saved ${index}`,
providerMetadata: { [key]: { itemId: `msg_${index}`, phase: "commentary", status } },
},
{
type: "text",
text: `Final ${index}`,
providerMetadata: { [key]: { itemId: `msg_final_${index}`, phase: "final_answer", status } },
},
],
}),
),
Message.make({
role: "user",
content: [{ type: "text", text: "Continue" }],
providerMetadata: { [key]: { status: "incomplete" } },
}),
],
}),
)
expect(prepared.body.input).toEqual([
...[0, 1, 2, 3].flatMap((index) => [
{
type: "message",
role: "assistant",
id: `msg_${index}`,
phase: "commentary",
status: "completed",
content: [{ type: "output_text", text: `Saved ${index}` }],
},
{
type: "message",
role: "assistant",
id: `msg_final_${index}`,
phase: "final_answer",
status: "completed",
content: [{ type: "output_text", text: `Final ${index}` }],
},
]),
{ role: "user", status: "incomplete", content: [{ type: "input_text", text: "Continue" }] },
])
}),
)
it.effect("replays truncated text as completed while retaining the response finish reason", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(LLM.request({ model, prompt: "Respond" })).pipe(
Effect.provide(
fixedResponse(
sseEvents(
{
type: "response.output_item.added",
item: { type: "message", id: "msg_partial", status: "in_progress" },
},
{ type: "response.output_text.delta", item_id: "msg_partial", delta: "The next step is" },
{
type: "response.output_item.done",
item: {
type: "message",
id: "msg_partial",
status: "incomplete",
content: [{ type: "output_text", text: "The next step is" }],
},
},
{
type: "response.incomplete",
response: { status: "incomplete", incomplete_details: { reason: "max_output_tokens" } },
},
),
),
),
)
expect(response.finishReason.normalized).toBe("length")
expect(response.events.filter(LLMEvent.is.textEnd)).toHaveLength(1)
const prepared = yield* compileRequest(
LLM.request({ model, messages: [response.message, Message.user("Continue")] }),
)
expect(prepared.body.input).toEqual([
{
type: "message",
role: "assistant",
id: "msg_partial",
status: "completed",
content: [{ type: "output_text", text: "The next step is" }],
},
{ role: "user", content: [{ type: "input_text", text: "Continue" }] },
])
}),
)
})
}
@@ -91,7 +91,7 @@ describe("Open Responses-compatible route", () => {
expect(prepared.body.input).toEqual([
{ role: "user", content: [{ type: "input_text", text: "Before." }] },
{ role: "developer", content: "Operator update." },
{ type: "message", role: "assistant", status: "completed", content: [{ type: "output_text", text: "After." }] },
{ type: "message", role: "assistant", content: [{ type: "output_text", text: "After." }] },
])
}),
)
@@ -299,27 +299,23 @@ describe("Open Responses-compatible route", () => {
type: "message",
id: "history_1",
role: "assistant",
status: "completed",
content: [{ type: "output_text", text: "Kept." }],
},
{
type: "message",
id: `history_${"a".repeat(64)}`,
role: "assistant",
status: "completed",
content: [{ type: "output_text", text: "Long." }],
},
{
type: "message",
id: "provider_value/with+symbols",
role: "assistant",
status: "completed",
content: [{ type: "output_text", text: "Opaque." }],
},
{
type: "message",
role: "assistant",
status: "completed",
content: [
{ type: "output_text", text: "No suffix." },
{ type: "output_text", text: "No prefix." },
@@ -860,7 +856,6 @@ describe("Open Responses-compatible route", () => {
type: "message",
id: "msg_refusal",
role: "assistant",
status: "completed",
content: [{ type: "output_text", text: "I can't help with that." }],
},
])
@@ -171,7 +171,7 @@ describe("OpenAI Responses WebSocket recorded", () => {
instructions: "Follow the user's exact reply instruction.",
input: [
{ role: "user", content: [{ type: "input_text", text: "Reply exactly: Alpha." }] },
{ role: "assistant", status: "completed", content: [{ type: "output_text", text: "Alpha." }] },
{ role: "assistant", content: [{ type: "output_text", text: "Alpha." }] },
{ role: "user", content: [{ type: "input_text", text: "Reply exactly: Beta." }] },
],
})
@@ -208,7 +208,7 @@ describe("OpenAI Responses WebSocket recorded", () => {
instructions: "Follow the user's exact reply instruction.",
input: [
{ role: "user", content: [{ type: "input_text", text: "Reply exactly: Ready." }] },
{ role: "assistant", status: "completed", content: [{ type: "output_text", text: "Ready." }] },
{ role: "assistant", content: [{ type: "output_text", text: "Ready." }] },
{ role: "user", content: [{ type: "input_text", text: "Reply exactly: Recovered." }] },
],
})
@@ -1,5 +1,5 @@
import { describe, expect } from "bun:test"
import { ConfigProvider, Effect, Layer, Ref, Schema, Stream } from "effect"
import { ConfigProvider, Effect, Layer, Ref, Stream } from "effect"
import { Headers, HttpClientRequest } from "effect/unstable/http"
import {
LLM,
@@ -30,7 +30,6 @@ import * as Azure from "../../src/providers/azure.js"
import * as OpenAI from "../../src/providers/openai.js"
import * as XAI from "../../src/providers/xai.js"
import * as OpenAIResponses from "../../src/protocols/openai-responses.js"
import { OpenResponses } from "../../src/protocols/open-responses.js"
import { OpenResponsesContinuation } from "../../src/protocols/open-responses-continuation.js"
import * as ProviderShared from "../../src/protocols/shared.js"
import { continuationRequest, nativeOpenAIResponsesContinuation } from "../continuation-scenarios.js"
@@ -70,34 +69,14 @@ const baseChannelDriver = (message: string): WebSocketChannelDriver => ({
},
})
/** Classifies error frames the way the production channel does, so recovery can read the canonical reason. */
const classifyingChannelDriver = (message: string): WebSocketChannelDriver => {
const base = baseChannelDriver(message)
const decodeEvent = Schema.decodeUnknownSync(OpenResponses.protocol.stream.event)
return {
...base,
observe: (create, frame) =>
base.observe(create, frame).pipe(
Effect.map((observation) =>
observation.type === "provider-failure"
? {
...observation,
error: OpenResponses.providerFailure(decodeEvent(frame), "stream error", frame),
}
: observation,
),
),
}
}
const continuationDriver = (request: Readonly<Record<string, unknown>>, base = baseChannelDriver) => {
const continuationDriver = (request: Readonly<Record<string, unknown>>) => {
const message = ProviderShared.encodeJson(request)
return OpenResponsesContinuation.driver({
id: "openai-responses",
name: "OpenAI Responses",
request,
message,
base: base(message),
base: baseChannelDriver(message),
})
}
@@ -406,7 +385,7 @@ describe("OpenAI Responses route", () => {
expect(prepared.body.input).toEqual([
{ role: "user", content: [{ type: "input_text", text: "Before." }] },
{ role: "developer", content: "Operator update." },
{ type: "message", role: "assistant", status: "completed", content: [{ type: "output_text", text: "After." }] },
{ type: "message", role: "assistant", content: [{ type: "output_text", text: "After." }] },
])
}),
)
@@ -873,53 +852,6 @@ describe("OpenAI Responses route", () => {
}),
)
it.effect("retries an incremental send in full when the provider rejects it without a code", () =>
Effect.gen(function* () {
const firstRequest = {
type: "response.create",
model: "gpt-5.2",
store: false,
input: [{ role: "user", content: [{ type: "input_text", text: "First" }] }],
}
const first = continuationDriver(firstRequest, classifyingChannelDriver)
const saved = checkpoint(
yield* first.observe(
yield* first.create(undefined),
ProviderShared.encodeJson({ type: "response.completed", response: { id: "resp_1" } }),
),
)
const second = continuationDriver(
{
...firstRequest,
input: [...firstRequest.input, { role: "user", content: [{ type: "input_text", text: "Second" }] }],
},
classifyingChannelDriver,
)
// Codex reports a stale previous_response_id as a plain invalid_request_error.
const stale = ProviderShared.encodeJson({
type: "error",
error: { type: "invalid_request_error", message: "Invalid `previous_response_id`." },
})
const incremental = yield* second.create(saved)
expect(incremental.mode).toBe("incremental")
expect(yield* second.observe(incremental, stale)).toMatchObject({ type: "rejected", recovery: "retry-full" })
// A full send has no continuation to blame, so the same error stays a provider failure.
const full = yield* second.create(undefined)
expect(yield* second.observe(full, stale)).toMatchObject({ type: "provider-failure" })
// A classified failure keeps its runner-owned recovery instead of resending the whole context.
const overflow = ProviderShared.encodeJson({
type: "error",
error: { type: "invalid_request_error", code: "context_length_exceeded", message: "Too long" },
})
expect(yield* second.observe(yield* second.create(saved), overflow)).toMatchObject({
type: "provider-failure",
error: { reason: { _tag: "InvalidRequest", classification: "context-overflow" } },
})
}),
)
it.effect("builds WebSocket and HTTP fallback from the same final request", () =>
Effect.gen(function* () {
const attempts = yield* Ref.make(0)
@@ -2118,7 +2050,6 @@ describe("OpenAI Responses route", () => {
type: "message",
id: "msg_refusal",
role: "assistant",
status: "completed",
content: [{ type: "output_text", text: "I can't help with that." }],
phase: "final_answer",
},
@@ -2201,7 +2132,6 @@ describe("OpenAI Responses route", () => {
type: "message",
id: "msg_commentary",
role: "assistant",
status: "completed",
content: [{ type: "output_text", text: "Checking." }],
phase: "commentary",
},
@@ -2209,7 +2139,6 @@ describe("OpenAI Responses route", () => {
type: "message",
id: "msg_final",
role: "assistant",
status: "completed",
content: [{ type: "output_text", text: "Finished." }],
phase: "final_answer",
},
@@ -2217,7 +2146,6 @@ describe("OpenAI Responses route", () => {
type: "message",
id: "msg_null",
role: "assistant",
status: "completed",
content: [{ type: "output_text", text: "Unclassified." }],
phase: null,
},
@@ -3348,19 +3276,14 @@ describe("OpenAI Responses route", () => {
)
expect(prepared.body.input).toEqual([
{
type: "message",
role: "assistant",
status: "completed",
content: [{ type: "output_text", text: "Before." }],
},
{ type: "message", role: "assistant", content: [{ type: "output_text", text: "Before." }] },
{
type: "reasoning",
id: "rs_1",
encrypted_content: "encrypted-state",
summary: [{ type: "summary_text", text: "Checked order." }],
},
{ type: "message", role: "assistant", status: "completed", content: [{ type: "output_text", text: "After." }] },
{ type: "message", role: "assistant", content: [{ type: "output_text", text: "After." }] },
])
}),
)
@@ -3624,14 +3547,12 @@ describe("OpenAI Responses route", () => {
type: "message",
id: "history_1",
role: "assistant",
status: "completed",
content: [{ type: "output_text", text: "Hello" }],
},
{
type: "message",
id: `message_${"a".repeat(64)}`,
role: "assistant",
status: "completed",
content: [{ type: "output_text", text: "World" }],
},
{
@@ -1,103 +0,0 @@
import { DialogProvider } from "@opencode/ui/context/dialog"
import { Browser } from "@opencode/plugin-browser/rpc"
import { For, Show } from "solid-js"
import { createStore } from "solid-js/store"
import { render } from "solid-js/web"
import { LanguageProvider, UiI18nBridge } from "../src/runtime/i18n/language"
import type { BrowserPaneLayout, BrowserPaneRegistration } from "../src/runtime/platform/browser-pane"
import type { createSessionBrowser } from "../src/session/browser/model"
import { SessionBrowserPane } from "../src/session/browser/pane"
export function mountBrowserPane() {
const host = document.createElement("main")
host.dataset.testid = "browser-pane-fixture"
host.style.cssText = "position:fixed;inset:0;z-index:1000;background:#181818;color:#eee;padding:24px"
document.body.appendChild(host)
function Fixture() {
const [store, setStore] = createStore({
session: "Alpha",
mounted: true,
visible: true,
layouts: {} as Record<string, BrowserPaneLayout | undefined>,
})
const tabs = ["Alpha", "Beta"].map((name) => ({
id: Browser.TabID.make(`tab_${name === "Alpha" ? "11111111" : "22222222"}-1111-1111-1111-111111111111`),
title: name,
url: `https://${name.toLowerCase()}.example/`,
loading: false,
canGoBack: false,
canGoForward: false,
generation: 0,
}))
// Record the native boundary per registration: hiding Beta cannot hide Alpha's view.
const registrations = new Map<string, BrowserPaneRegistration>(
tabs.map((tab) => [
tab.title,
{
setLayout: (layout) => setStore("layouts", tab.title, layout),
command: async () => undefined,
close: () => undefined,
},
]),
)
const browser: ReturnType<typeof createSessionBrowser> = {
available: () => true,
attached: () => !!registrations.get(store.session),
opened: () => !!registrations.get(store.session),
state: () => ({ tabs: tabs.filter((tab) => tab.title === store.session), focusedTabID: null }),
tabs: () => tabs.filter((tab) => tab.title === store.session),
active: () => tabs.find((tab) => tab.title === store.session) ?? tabs[0],
registration: () => registrations.get(store.session),
error: () => undefined,
suspended: () => false,
close: () => undefined,
open: () => undefined,
command: () => undefined,
}
return (
<>
<h1 style={{ "font-size": "24px", "margin-bottom": "16px" }}>Browser pane lifecycle</h1>
<p>Selected session: {store.session}</p>
<nav style={{ display: "flex", gap: "20px", margin: "16px 0" }}>
<For each={["Alpha", "Beta", "Empty"]}>
{(name) => <button onClick={() => setStore({ session: name, mounted: name !== "Empty" })}>{name}</button>}
</For>
<button onClick={() => setStore("mounted", false)}>Unmount pane</button>
<button onClick={() => setStore("visible", (visible) => !visible)}>Toggle Review tab</button>
</nav>
<div style={{ width: "640px", height: "360px", border: "1px solid #555" }}>
<Show when={store.mounted}>
<SessionBrowserPane browser={browser} visible={store.visible} />
</Show>
</div>
<h2 style={{ "font-size": "18px", margin: "20px 0 12px" }}>Native layout recorder</h2>
<p>The desktop boundary keeps each session's page visible until its registration is hidden.</p>
<For each={tabs}>
{(tab) => (
<div
data-testid={`native-${tab.title}`}
data-visible={!!store.layouts[tab.title]?.visible}
style={{ padding: "12px", margin: "8px 0", border: "1px solid #555" }}
>
{tab.title}: {store.layouts[tab.title]?.visible ? "visible" : "hidden"}
</div>
)}
</For>
</>
)
}
return render(
() => (
<LanguageProvider locale="en">
<UiI18nBridge>
<DialogProvider>
<Fixture />
</DialogProvider>
</UiI18nBridge>
</LanguageProvider>
),
host,
)
}
@@ -1,44 +0,0 @@
import { fileURLToPath } from "node:url"
import { expect, story } from "../../storybook/playwright/story"
const fixture = `/@fs/${fileURLToPath(new URL("./browser-pane.fixture.tsx", import.meta.url)).replaceAll("\\", "/")}`
story.beforeEach(async ({ mount, page }) => {
const component = await mount("opencode-composer-flow--mixed-attachments")
await expect(component.getByRole("textbox", { name: "Prompt", exact: true })).toBeVisible()
await page.evaluate(async (fixture) => {
const { mountBrowserPane } = await import(fixture)
mountBrowserPane()
}, fixture)
await expect(page.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
})
story("hides the previous registration when the mounted pane switches sessions", async ({ page }, testInfo) => {
const root = page.getByTestId("browser-pane-fixture")
await root.getByRole("button", { name: "Beta", exact: true }).click()
await expect(root.getByTestId("native-Beta")).toHaveAttribute("data-visible", "true")
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "false")
await page.screenshot({ path: testInfo.outputPath("session-switch.png") })
await root.getByRole("button", { name: "Alpha", exact: true }).click()
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
await expect(root.getByTestId("native-Beta")).toHaveAttribute("data-visible", "false")
})
story("hides the outgoing browser when the destination has no browser pane", async ({ page }) => {
const root = page.getByTestId("browser-pane-fixture")
await root.getByRole("button", { name: "Empty", exact: true }).click()
await expect(root.locator("#browser-panel")).toHaveCount(0)
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "false")
await root.getByRole("button", { name: "Alpha", exact: true }).click()
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
})
story("hides and restores the same registration for Review tabs and unmount", async ({ page }) => {
const root = page.getByTestId("browser-pane-fixture")
await root.getByRole("button", { name: "Toggle Review tab", exact: true }).click()
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "false")
await root.getByRole("button", { name: "Toggle Review tab", exact: true }).click()
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
await root.getByRole("button", { name: "Unmount pane", exact: true }).click()
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "false")
})
@@ -1,67 +0,0 @@
import { expect, story } from "../../storybook/playwright/story"
story("settings menu reconnect retains its prompt handler across server updates", async ({ mount, page }) => {
const component = await mount("app-dialog-ssh--settings-reconnect")
await component.getByRole("button", { name: "More options" }).click()
await page.getByRole("menuitem", { name: "Connect", exact: true }).click()
const dialog = page.getByRole("dialog")
await expect(dialog.getByRole("textbox")).toBeVisible()
await dialog.getByRole("textbox").fill("fixture-password")
await dialog.getByRole("button", { name: "Continue" }).click()
await expect(dialog.getByRole("textbox", { name: "Verification code:" })).toBeVisible()
await dialog.getByRole("textbox").fill("123456")
await dialog.getByRole("button", { name: "Continue" }).click()
await expect(dialog).toHaveCount(0)
await expect(component.getByRole("button", { name: "Authenticate", exact: true })).toHaveCount(0)
})
story("cancelling a version mismatch permits reconnecting again", async ({ mount, page }) => {
const component = await mount("app-dialog-ssh--incompatible-session")
await component.getByRole("button", { name: "Reconnect", exact: true }).click()
const dialog = page.getByRole("dialog")
await expect(dialog.getByRole("alert")).toBeVisible()
await dialog.getByRole("button", { name: "Cancel", exact: true }).click()
await expect(dialog).toHaveCount(0)
await component.getByRole("button", { name: "Reconnect", exact: true }).click()
await expect(dialog.getByRole("alert")).toBeVisible()
await dialog.getByRole("button", { name: "Cancel", exact: true }).click()
await expect(dialog).toHaveCount(0)
})
story("adding a server keeps all SSH challenges in the original connection dialog", async ({ mount, page }) => {
await mount("app-dialog-ssh--host")
const dialog = page.getByRole("dialog")
await dialog.getByRole("textbox", { name: "Host or SSH command" }).fill("ssh devbox")
await dialog.getByRole("button", { name: "Add server", exact: true }).click()
await expect(dialog).toHaveCount(1)
await expect(dialog.getByRole("button", { name: "Cancel", exact: true })).toBeFocused()
await dialog.getByRole("button", { name: "Trust and connect" }).click()
await dialog.getByRole("textbox").fill("fixture-password")
await dialog.getByRole("button", { name: "Continue", exact: true }).click()
await dialog.getByRole("textbox", { name: "Verification code:" }).fill("123456")
await dialog.getByRole("button", { name: "Continue", exact: true }).click()
await expect(dialog).toHaveCount(0)
})
story("updating an incompatible connection continues authentication in the same dialog", async ({ mount, page }) => {
const component = await mount("app-dialog-ssh--incompatible-session")
await component.getByRole("button", { name: "Reconnect", exact: true }).click()
const dialog = page.getByRole("dialog")
await dialog.getByRole("button", { name: "Update and reconnect", exact: true }).click()
await expect(dialog).toHaveCount(1)
await dialog.getByRole("textbox").fill("fixture-password")
await dialog.getByRole("button", { name: "Continue", exact: true }).click()
await dialog.getByRole("textbox", { name: "Verification code:" }).fill("123456")
await dialog.getByRole("button", { name: "Continue", exact: true }).click()
await expect(dialog).toHaveCount(0)
await expect(component.getByText("Session connected")).toBeVisible()
})
story("key-based reconnect completes without opening an authentication dialog", async ({ mount, page }) => {
const component = await mount("app-dialog-ssh--key-reconnect")
await component.getByRole("button", { name: "Reconnect", exact: true }).click()
await expect(component.getByRole("button", { name: "Connecting to SSH server" })).toBeDisabled()
await expect(page.getByRole("dialog")).toHaveCount(0)
await expect(component.getByText("Session connected")).toBeVisible()
await expect(page.getByRole("dialog")).toHaveCount(0)
})
@@ -1,27 +0,0 @@
# Session-export load benchmark
Replay an exported session against a production app build. The two cases compare the default Compact preset with every category ungrouped and details still collapsed.
From `packages/app` in PowerShell:
```powershell
$env:PLAYWRIGHT_BUILD = '1'
$env:PLAYWRIGHT_BASE_URL = 'http://127.0.0.1:4398' # Existing production preview
$env:LAGGY_SESSION_FILE = 'C:\path\session.json'
$env:LAGGY_SESSION_OUTPUT = 'C:\tmp\opencode\session-load'
$env:LAGGY_SESSION_HISTORY = 'paged' # Or 'full' to supply all exported history
bun x playwright test --config e2e/performance/playwright.config.ts timeline/laggy-session-benchmark.spec.ts --repeat-each=20 --workers=1 --retries=0
bun e2e/performance/timeline/laggy-session-report.ts $env:LAGGY_SESSION_OUTPUT
```
Each test uses a fresh browser context and measures one cold load, switches back to the source session, then measures one warm load. Repetitions therefore interleave `cold → warm` pairs rather than collecting separate cold and warm batches. There are no discarded warm-up switches. The warm member of every pair must issue zero message requests. Each pair is saved in a separate JSON file; compare paired differences as well as the cold and warm distributions when system load varies.
The report writes `summary.json` and prints the median, p95, maximum, and median paired cold-minus-warm difference. It rejects incomplete or cold-only records instead of mixing them into paired results.
`--repeat-each=20` collects 20 pairs per grouping mode. `LAGGY_SESSION_COLD_ONLY=1` remains available for focused cold profiling. Screenshots are taken after a pair finishes, not between its measurements.
The app shell, source session, model control, and fonts are ready before the timed action. These measurements cover session entry, not application startup. `firstCorrectObservedMs` begins at mousedown and ends when the destination is visible at its expected bottom position, including Compact's automatic history fill. `stableObservedMs` includes three-observation confirmation and must not be treated as additional rendering time.
Set `OPENCODE_PERFORMANCE_TRACE_DIR` for Chrome traces. `LAGGY_TRACE_ITERATION=0` traces the cold load; the default (`1`) traces the warm member of the pair. Profile separately from timing runs.
`LAGGY_HTTP=1` disables route interception for an external HTTP replay server containing the same export and source fixture. Keep direct HTTP and Playwright-routed cold series separate: routing adds transport overhead. Raw samples, mode settings, viewport, browser version, and screenshots are retained in the output directory. The export itself is not copied into the repository.
@@ -1,227 +0,0 @@
import { readFileSync, mkdirSync, writeFileSync } from "node:fs"
import type { SessionMessageInfo } from "@opencode/client/promise"
import { base64Encode } from "@opencode/util/encode"
import { timelineCategories, timelinePresets } from "@opencode/session-ui/timeline/detail"
import { mockOpenCodeServer } from "../../utils/mock-server"
import { expectSessionTitle } from "../../utils/waits"
import { benchmark, expect } from "../benchmark"
import { measureSessionSwitch, waitForStableTimeline } from "./session-tab-switch-probe"
import { stressSessionHref } from "./timeline-test-helpers"
import { startChromeTrace } from "../chrome-trace"
const file = process.env.LAGGY_SESSION_FILE
const session = file
? (JSON.parse(readFileSync(file, "utf8")) as {
info: {
id: string
projectID: string
title: string
model?: { id: string; providerID: string }
location: { directory: string }
time: { created: number; updated: number }
}
messages: SessionMessageInfo[]
})
: undefined
const sourceID = "ses_laggy_benchmark_source"
const sourceMessageID = "msg_laggy_benchmark_source"
const history = process.env.LAGGY_SESSION_HISTORY ?? "full"
const viewport = { width: 1440, height: 900 }
benchmark.use({ viewport, video: "off", trace: "off", serviceWorkers: "block", traceScope: "interaction" })
for (const mode of ["compact", "ungrouped"] as const) {
benchmark(`laggy session: ${mode}`, async ({ page, report }, testInfo) => {
benchmark.skip(!session, "Set LAGGY_SESSION_FILE to a session export")
if (!session) return
const output = process.env.LAGGY_SESSION_OUTPUT ?? testInfo.outputPath("session-load")
const model = session.info.model ?? { id: "benchmark-model", providerID: "benchmark" }
const lastID = session.messages.findLast((message) => message.type === "user")!.id
const lastText = session.messages.findLast(
(message) =>
message.type === "assistant" && message.content.some((part) => part.type === "text" && part.text.trim()),
)!
benchmark.setTimeout(Number(process.env.LAGGY_SESSION_TIMEOUT ?? 180_000))
const requests: string[] = []
const errors: string[] = []
page.on("pageerror", (error) => errors.push(error.message))
if (process.env.LAGGY_HTTP === "1")
page.on("request", (request) => {
const match = new URL(request.url()).pathname.match(/^\/api\/session\/([^/]+)\/message$/)
if (request.method() === "GET" && match) requests.push(decodeURIComponent(match[1]))
})
const detail = Object.fromEntries(
timelineCategories.map((category) => [
category,
{
...timelinePresets[2].value[category],
placement: mode === "compact" ? "grouped" : "separate",
},
]),
)
const directory = session.info.location.directory
if (process.env.LAGGY_HTTP !== "1")
await mockOpenCodeServer(page, {
directory,
project: {
id: session.info.projectID,
worktree: directory,
vcs: "git",
name: "session-benchmark",
time: session.info.time,
sandboxes: [],
},
provider: {
all: [
{
id: model.providerID,
name: model.providerID,
models: { [model.id]: { id: model.id, name: model.id, limit: { context: 1_000_000 } } },
},
],
connected: [model.providerID],
default: { providerID: model.providerID, modelID: model.id },
},
sessions: [session.info, { ...session.info, id: sourceID, title: "Benchmark source" }],
pageMessages: (id, limit, before) => {
if (id !== session.info.id)
return {
items: [
{
id: sourceMessageID,
type: "user",
text: "Benchmark source",
time: { created: session.info.time.created },
},
],
}
if (history === "full") return { items: session.messages }
const end = before ? session.messages.findIndex((message) => message.id === before) : session.messages.length
const start = Math.max(0, end - limit)
return {
items: session.messages.slice(start, end),
cursor: start > 0 ? session.messages[start].id : undefined,
}
},
onMessages: (request) => {
if (request.phase === "start") requests.push(request.sessionID)
},
})
await page.addInitScript(
({ detail, directory, server, sessionIDs, dirBase64 }) => {
localStorage.setItem("settings.v3", JSON.stringify({ general: { timelineDetail: detail } }))
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(sessionIDs.map((sessionId) => ({ type: "session", server, dirBase64, sessionId }))),
)
},
{
detail,
directory,
server: process.env.PLAYWRIGHT_BASE_URL!,
sessionIDs: [sourceID, session.info.id],
dirBase64: base64Encode(directory),
},
)
await page.goto(stressSessionHref(sourceID))
await expectSessionTitle(page, "Benchmark source")
await expect(page.locator('[data-slot="user-message-text"]')).toHaveText("Benchmark source")
await expect(page.getByRole("textbox", { name: "Prompt", exact: true })).toBeEditable()
await expect(page.getByRole("button", { name: model.id, exact: true })).toBeVisible()
await page.evaluate(() => document.fonts.ready.then(() => undefined))
expect(requests).toEqual([sourceID])
const startedAt = new Date().toISOString()
const samples = []
const phases = process.env.LAGGY_SESSION_COLD_ONLY === "1" ? (["cold"] as const) : (["cold", "warm"] as const)
for (const [iteration, phase] of phases.entries()) {
const before = requests.length
const stopTrace =
iteration === Number(process.env.LAGGY_TRACE_ITERATION ?? 1)
? await startChromeTrace(page, `laggy-${history}-${mode}`)
: undefined
const result = await measureSessionSwitch(page, {
destinationIDs: session.messages.map((message) => message.id),
sourceIDs: [sourceMessageID],
lastID,
requiredPartID: history === "paged" && mode === "compact" ? `${lastText.id}:text:0` : undefined,
requireBottomAnchor: true,
href: stressSessionHref(session.info.id),
switch: async () => {
await page.locator(`[data-slot="titlebar-tabs"] a[href="${stressSessionHref(session.info.id)}"]`).click()
},
})
await stopTrace?.()
await expectSessionTitle(page, session.info.title)
if (history === "full" || mode === "ungrouped") await waitForStableTimeline(page, lastID)
await expect(
page.locator('[data-timeline-key] [data-component="markdown"]:not([data-markdown-ready])'),
).toHaveCount(0)
expect(result.firstCorrectObservedMs).not.toBeNull()
expect(result.stableObservedMs).not.toBeNull()
if (phase === "warm") expect(requests.length - before).toBe(0)
samples.push({
iteration,
phase,
messageRequests: requests.length - before,
messageResources: await page.evaluate((sessionID) => {
const start = performance.getEntriesByName("session-switch:start").at(-1)!.startTime
return (performance.getEntriesByType("resource") as PerformanceResourceTiming[])
.filter(
(entry) =>
entry.startTime >= start && new URL(entry.name).pathname === `/api/session/${sessionID}/message`,
)
.map((entry) => ({
limit: Number(new URL(entry.name).searchParams.get("limit")),
startMs: entry.startTime - start,
durationMs: entry.duration,
transferBytes: entry.transferSize,
}))
}, session.info.id),
...result,
})
if (iteration === phases.length - 1) {
mkdirSync(output, { recursive: true })
if (testInfo.repeatEachIndex === 0) await page.screenshot({ path: `${output}/${mode}.png` })
break
}
await page.locator(`[data-slot="titlebar-tabs"] a[href="${stressSessionHref(sourceID)}"]`).click()
await expectSessionTitle(page, "Benchmark source")
await expect(page.locator('[data-slot="user-message-text"]')).toHaveText("Benchmark source")
await expect(page.getByRole("textbox", { name: "Prompt", exact: true })).toBeEditable()
}
expect(errors).toEqual([])
const result = {
pair: testInfo.repeatEachIndex,
startedAt,
mode,
history,
file,
messages: session.messages.length,
viewport,
browser: page.context().browser()!.version(),
detail,
samples,
}
writeFileSync(`${output}/${mode}-${testInfo.repeatEachIndex}.json`, JSON.stringify(result, null, 2))
report(
{ samples },
{
mode,
sampling: "cold/warm pair in one browser context",
pair: testInfo.repeatEachIndex,
messages: session.messages.length,
viewport,
data: history === "full" ? "full exported history" : "paginated exported history",
transport: process.env.LAGGY_HTTP === "1" ? "http" : "playwright-route",
inputEvent: "mousedown",
},
)
})
}
@@ -1,63 +0,0 @@
export {}
type Pair = {
mode: "compact" | "ungrouped"
samples: { phase: string; firstCorrectObservedMs: number | null; messageRequests: number }[]
}
const directory = Bun.argv[2]
if (!directory) throw new Error("Pass the directory containing session-load pairs")
const pairs = await Promise.all(
[...new Bun.Glob("{compact,ungrouped}-*.json").scanSync(directory)].map(async (file) => {
const pair = (await Bun.file(`${directory}/${file}`).json()) as Pair
const cold = pair.samples.find((sample) => sample.phase === "cold")
const warm = pair.samples.find((sample) => sample.phase === "warm")
if (cold?.firstCorrectObservedMs == null || warm?.firstCorrectObservedMs == null)
throw new Error(`Expected a completed cold/warm pair in ${file}`)
return {
mode: pair.mode,
cold: cold.firstCorrectObservedMs,
warm: warm.firstCorrectObservedMs,
requests: warm.messageRequests,
}
}),
)
if (!pairs.length) throw new Error(`No session-load pairs found in ${directory}`)
const result = ["compact", "ungrouped"].flatMap((mode) => {
const selected = pairs.filter((pair) => pair.mode === mode)
if (!selected.length) return []
return [
{
mode,
cold: { ...stats(selected.map((pair) => pair.cold)), over50ms: selected.filter((pair) => pair.cold > 50).length },
warm: { ...stats(selected.map((pair) => pair.warm)), over50ms: selected.filter((pair) => pair.warm > 50).length },
pairedColdMinusWarm: stats(selected.map((pair) => pair.cold - pair.warm)),
messageRequestsDuringWarm: selected.reduce((total, pair) => total + pair.requests, 0),
},
]
})
await Bun.write(`${directory}/summary.json`, JSON.stringify(result, null, 2))
console.table(
result.map((row) => ({
mode: row.mode,
pairs: row.cold.n,
coldMedianMs: Math.round(row.cold.median * 10) / 10,
coldP95Ms: Math.round(row.cold.p95 * 10) / 10,
warmMedianMs: Math.round(row.warm.median * 10) / 10,
warmP95Ms: Math.round(row.warm.p95 * 10) / 10,
warmMaxMs: Math.round(row.warm.max * 10) / 10,
pairedDifferenceMs: Math.round(row.pairedColdMinusWarm.median * 10) / 10,
})),
)
function stats(values: number[]) {
const sorted = values.toSorted((left, right) => left - right)
return {
n: sorted.length,
median: (sorted[Math.floor((sorted.length - 1) / 2)] + sorted[Math.floor(sorted.length / 2)]) / 2,
p95: sorted[Math.ceil(sorted.length * 0.95) - 1],
min: sorted[0],
max: sorted.at(-1)!,
}
}
@@ -16,11 +16,6 @@ test("status drawer dismisses and reopens after button, backdrop, Escape, and dr
await more.click()
await page.getByRole("menuitem", { name: "Status", exact: true }).click()
await expect(drawer.getByRole("tab", { name: "MCP", exact: true })).toBeVisible()
// Corvu starts opening after paint; the transition flag is also absent
// before that callback. Wait for the open position before dismissing.
await expect
.poll(() => drawer.evaluate((element) => new DOMMatrixReadOnly(getComputedStyle(element).transform).m42))
.toBe(0)
await expect(drawer).not.toHaveAttribute("data-transitioning")
if (dismissal === "button") await drawer.getByRole("button", { name: "Close", exact: true }).click()
if (dismissal === "backdrop") await overlay.click({ position: { x: 10, y: 10 } })
@@ -22,10 +22,6 @@ test("selects a base branch for a new workspace", async ({ page }) => {
pageMessages: () => ({ items: [] }),
vcsBranches: ["feature/api", "main", "origin/release"],
})
await page.route("**/api/vcs/branches?*", (route) => {
if (new URL(route.request().url()).searchParams.get("search") !== "feature") return route.fallback()
return route.fulfill({ json: { location: { directory }, data: ["feature/api"] } })
})
await page.addInitScript(
({ directory, draftID, server }) => {
localStorage.setItem(
@@ -48,28 +44,10 @@ test("selects a base branch for a new workspace", async ({ page }) => {
await page.getByRole("button", { name: "Local", exact: true }).click()
await page.getByRole("menuitem", { name: "New worktree", exact: true }).click()
await page.getByRole("button", { name: "from main", exact: true }).click()
const search = page.getByRole("textbox", { name: "Search branches", exact: true })
await expect(search).toBeFocused()
await page.keyboard.type("feature")
await expect(search).toHaveValue("feature")
await expect(page.getByRole("menuitemradio")).toHaveText(["feature/api"])
await expect(search).toBeFocused()
await page.getByRole("menuitemradio", { name: "feature/api", exact: true }).click()
const selected = page.getByRole("button", { name: "from feature/api", exact: true })
await expect(selected).toBeVisible()
await selected.click()
await expect(search).toBeFocused()
await expect(search).toHaveValue("")
await expect(page.getByRole("menuitemradio", { name: "feature/api", exact: true })).toBeChecked()
await page.keyboard.press("Escape")
await expect(selected).toBeFocused()
await page.keyboard.press("Enter")
await expect(search).toBeFocused()
await page.keyboard.type("feature")
await expect(search).toHaveValue("feature")
await expect(page.getByRole("menuitemradio")).toHaveText(["feature/api"])
await page.getByRole("button", { name: "Clear", exact: true }).click()
await expect(search).toHaveValue("")
await expect(page.getByRole("menuitemradio")).toHaveText(["feature/api", "main", "origin/release"])
})
@@ -39,51 +39,6 @@ test("restores review mode and selected file per session", async ({ page }) => {
await expectSelectedFile(page, "gamma.ts")
})
for (const tab of ["Context", "Open file", "README.md"]) {
test(`restores the selected ${tab} pane tab after switching sessions and reloading`, async ({ page }) => {
await setup(page)
await page.goto(sessionHref(sessionA))
await expectSessionTitle(page, titleA)
await page.getByRole("button", { name: "Toggle review" }).click()
const panel = page.locator("#review-panel")
if (tab === "Context") await page.getByRole("button", { name: "View context usage" }).click()
if (tab !== "Context") await panel.getByRole("button", { name: "Open file" }).click()
if (tab === "README.md") await panel.getByRole("button", { name: "README.md" }).click()
await expect(panel.getByRole("tab", { name: tab, selected: true })).toBeVisible()
await switchSession(page, titleB)
await page.getByRole("button", { name: "Toggle review" }).click()
await expect(panel.locator("#session-side-panel-review-tab")).toHaveAttribute("aria-selected", "true")
await switchSession(page, titleA)
await expect(panel.getByRole("tab", { name: tab, selected: true })).toBeVisible()
await page.reload()
await expectSessionTitle(page, titleA)
await expect(panel.getByRole("tab", { name: tab, selected: true })).toBeVisible()
const selected = panel.getByRole("tab", { name: tab })
const review = panel.locator("#session-side-panel-review-tab")
await selected.press("Home")
await expect(review).toHaveAttribute("aria-selected", "true")
await review.press("End")
await expect(selected).toHaveAttribute("aria-selected", "true")
await review.click()
await expect(review).toHaveAttribute("aria-selected", "true")
await selected.click()
await expect(selected).toHaveAttribute("aria-selected", "true")
await switchSession(page, titleB)
await expect(review).toHaveAttribute("aria-selected", "true")
await switchSession(page, titleA)
await expect(selected).toHaveAttribute("aria-selected", "true")
await selected.press("Control+w")
await expect(selected).toHaveCount(0)
await expect(review).toHaveAttribute("aria-selected", "true")
})
}
async function selectFile(page: Page, file: string) {
await page.getByRole("button", { name: file }).click()
await expectSelectedFile(page, file)
@@ -121,10 +76,6 @@ async function setup(page: Page) {
default: { providerID: "opencode", modelID: "test" },
},
sessions: [session(sessionA, titleA, 1700000000000), session(sessionB, titleB, 1700000001000)],
fileList: () => [
{ name: "README.md", path: "README.md", absolute: `${directory}/README.md`, type: "file", ignored: false },
],
fileContent: (path) => ({ type: "text", content: `contents:${path}` }),
pageMessages: () => ({ items: [] }),
})
await page.route(/\/api\/vcs(?:\?.*)?$/, (route) =>
@@ -1,5 +1,5 @@
import { base64Encode } from "@opencode/util/encode"
import { expect, test, type Locator, type Page } from "@playwright/test"
import { expect, test, type Locator } from "@playwright/test"
import { mockOpenCodeServer } from "../utils/mock-server"
import { expectSessionTitle } from "../utils/waits"
@@ -85,38 +85,6 @@ for (const width of [1000, 1440]) {
await expect.poll(() => toggle.boundingBox()).toEqual(closed)
})
test(`keeps moving header content out of the toggle area (${width}px, ${direction})`, async ({ page }) => {
await page.setViewportSize({ width, height: 900 })
await page.goto(`/server/${base64Encode(server)}/session/${sessionID}`)
await expectSessionTitle(page, "Review toggle position")
await page.locator("html").evaluate((element, dir) => element.setAttribute("dir", dir), direction)
const toggle = page.getByRole("button", { name: "Toggle review", exact: true })
await expect(toggle).toHaveAttribute("aria-expanded", "false")
for (const opened of [true, false]) {
// Pause in the same task as the click so even the first painted state can be inspected.
await toggle.evaluate((element) => {
;(element as HTMLButtonElement).click()
document
.getAnimations()
.filter((animation) => animation.timeline instanceof DocumentTimeline)
.forEach((animation) => animation.pause())
})
await expect(toggle).toHaveAttribute("aria-expanded", String(opened))
await expect(page.locator("#review-panel")).toHaveAttribute("aria-hidden", String(!opened))
for (const progress of [0.08, 0.16, 0.25, 0.5, 0.8, 0.96]) {
await expectHeaderClearOfToggle(page, toggle, progress)
}
await page.evaluate(() => {
document
.getAnimations()
.filter((animation) => animation.timeline instanceof DocumentTimeline)
.forEach((animation) => animation.finish())
})
}
await expect(page.locator("#review-panel")).toBeHidden()
})
test(`keeps terminal controls clear of the review toggle (${width}px, ${direction})`, async ({ page }) => {
await page.setViewportSize({ width, height: 900 })
const ptys: { id: string; title: string }[] = []
@@ -198,75 +166,10 @@ for (const width of [1000, 1440]) {
await expect(toggle).toBeFocused()
await expect.poll(() => toggle.boundingBox()).toEqual(position)
await expectTerminalControlsAligned(terminal, toggle)
// Closing the terminal clears the region's animation flag while retaining the review contents.
await page.keyboard.press("Control+Backquote")
await expect(terminal).toBeHidden()
await expect
.poll(() =>
page
.locator('[data-slot="session-chat-panel"]')
.evaluate((element) => element.getAnimations().every((animation) => animation.playState === "finished")),
)
.toBe(true)
await expect(page.locator('[data-slot="session-review-content"]')).toHaveCSS("opacity", "0")
await toggle.evaluate((element) => {
;(element as HTMLButtonElement).click()
document
.getAnimations()
.filter((animation) => animation.timeline instanceof DocumentTimeline)
.forEach((animation) => animation.pause())
})
await expect(toggle).toHaveAttribute("aria-expanded", "true")
await expectHeaderClearOfToggle(page, toggle, 0.25)
})
}
}
async function expectHeaderClearOfToggle(page: Page, toggle: Locator, progress: number) {
const geometry = await page.locator('[data-slot="session-chat-panel"]').evaluate((chat, progress) => {
const row = chat.parentElement!
const animations = row
.getAnimations({ subtree: true })
.filter((animation) => animation.timeline instanceof DocumentTimeline)
const width = animations.find(
(animation) => animation instanceof CSSTransition && animation.transitionProperty === "width",
)!
animations.forEach((animation) => {
animation.pause()
animation.currentTime = Number(width.effect!.getTiming().duration) * progress
})
const chatBounds = chat.getBoundingClientRect()
const panelBounds = document.querySelector("#review-panel")!.getBoundingClientRect()
return {
row: row.getBoundingClientRect().width,
panelWidth: panelBounds.width,
gap:
getComputedStyle(row).direction === "rtl"
? chatBounds.left - panelBounds.right
: panelBounds.left - chatBounds.right,
contentOpacity: Number(getComputedStyle(document.querySelector('[data-slot="session-review-content"]')!).opacity),
panels: chatBounds.width + panelBounds.width + parseFloat(getComputedStyle(row).columnGap),
}
}, progress)
expect(geometry.gap).toBeCloseTo(8, 1)
if (geometry.panelWidth > 0) expect(Math.abs(geometry.row - geometry.panels)).toBeLessThanOrEqual(1)
if (progress === 0.25) {
expect(geometry.contentOpacity).toBeGreaterThan(0)
expect(geometry.contentOpacity).toBeLessThan(1)
}
const clip = await toggle.boundingBox()
if (!clip) throw new Error("Review toggle bounds are unavailable")
// Header contents must make no difference to the pixels behind the fixed toggle.
expect(await page.screenshot({ clip })).toEqual(
await page.screenshot({
clip,
style: ".session-review-v2-tabs-bar { visibility: hidden !important; }",
}),
)
}
async function expectTerminalControlsAligned(terminal: Locator, toggle: Locator) {
await expect
.poll(async () => {
@@ -1,44 +0,0 @@
import { expect, test } from "@playwright/test"
import { setupTimeline } from "../performance/timeline-stability/fixture"
for (const reducedMotion of [false, true]) {
test(`suppresses the scrollbar from toggle press until timeline interaction (reduced motion: ${reducedMotion})`, async ({
page,
}) => {
await setupTimeline(page, { seedHistory: true, reducedMotion })
const chat = page.locator('[data-slot="session-chat-panel"]')
const scroll = page.locator('[data-slot="session-timeline-scroll"]')
const viewport = scroll.locator(".scroll-view__viewport")
const thumb = scroll.locator('.scroll-view__thumb[data-orientation="vertical"]')
const toggle = page.getByRole("button", { name: "Toggle review", exact: true })
await expect(thumb).toHaveCount(1)
await scroll.hover()
await expect(thumb).toHaveAttribute("data-visible", "true")
await expect(thumb).toHaveCSS("visibility", "visible")
for (const opened of [true, false]) {
await toggle.hover()
await page.mouse.down()
await expect(thumb).toHaveCSS("visibility", "hidden")
await page.mouse.up()
await expect(toggle).toHaveAttribute("aria-expanded", String(opened))
await chat.evaluate(async (element) => {
await Promise.all(element.getAnimations().map((animation) => animation.finished))
})
await expect(chat).toHaveAttribute("data-width-animating", "false")
await expect(thumb).toHaveCSS("visibility", "hidden")
// Late scroll anchoring must not bring the thumb back after the panel has settled.
await viewport.evaluate(
(element) =>
new Promise<void>((resolve) => {
element.addEventListener("scroll", () => resolve(), { once: true })
element.scrollTop += element.scrollTop > 0 ? -1 : 1
}),
)
await expect(thumb).toHaveCSS("visibility", "hidden")
await scroll.hover()
await expect(thumb).toHaveAttribute("data-visible", "true")
await expect(thumb).toHaveCSS("visibility", "visible")
}
})
}
@@ -308,8 +308,8 @@ for (const delivery of ["steer", "queue"] as const) {
})
const tools = page.locator('[data-timeline-part-ids="tool_queue_read,tool_queue_grep"]')
await expect(tools).toBeVisible()
await expect(tools).toHaveText(/^Used\s*2\s*Read, Grep$/)
await expect(tools.locator('[data-slot="basic-tool-tool-title"]')).toHaveText("Read, Grep")
await expect(tools).toHaveText(/^Used\s*1 Read, 1 Grep$/)
await expect(tools.locator('[data-slot="basic-tool-tool-title"]')).toHaveText("1 Read, 1 Grep")
await expect(thinking).toHaveCount(0)
await expect(pending).toBeVisible()
expect(mock.rows.map((row) => ({ id: row.id, delivery: row.delivery }))).toEqual([
@@ -318,7 +318,7 @@ for (const delivery of ["steer", "queue"] as const) {
await transcript.screenshot({ path: testInfo.outputPath("pending-steer.png") })
// Soft assertions let delivery run too, even when the pending ordering regresses.
await expect.soft(tools.or(pending)).toHaveText([/^Used\s*2\s*Read, Grep$/, /U2: Also check the retry path\./])
await expect.soft(tools.or(pending)).toHaveText([/^Used\s*1 Read, 1 Grep$/, /U2: Also check the retry path\./])
await expect
.soft(transcript.locator('[data-timeline-row="AssistantPart"]').filter({ has: tools }))
.toHaveAttribute("data-message-id", userID)
@@ -350,7 +350,7 @@ for (const delivery of ["steer", "queue"] as const) {
await expect(response).toHaveAttribute("data-message-id", inboxID)
await expect(thinking).toHaveCount(0)
await expect(tools.or(pending).or(response)).toHaveText([
/^Used\s*2\s*Read, Grep$/,
/^Used\s*1 Read, 1 Grep$/,
/U2: Also check the retry path\./,
/A3: Now checking the retry path for U2\./,
])
@@ -1,315 +0,0 @@
import { expect, test } from "@playwright/test"
import type { OpenCodeEvent, SessionMessageInfo } from "@opencode/client/promise"
import { timelinePresets } from "@opencode/session-ui/timeline/detail"
import { mockOpenCodeServer } from "../utils/mock-server"
import { fixture } from "../performance/timeline/session-timeline-stress.fixture"
import { installStressSessionTabs, stressSessionHref } from "../performance/timeline/timeline-test-helpers"
import { waitForStableTimeline } from "../performance/timeline/session-tab-switch-probe"
test.use({ viewport: { width: 1440, height: 900 }, serviceWorkers: "block" })
test("recovers from a failed cold history load when another session is selected", async ({ page }) => {
await mockOpenCodeServer(page, {
...fixture,
pageMessages: (id) => ({
items: [{ id: `msg_${id}`, type: "user", text: `History for ${id}`, time: { created: 1 } }],
}),
})
await page.route(`**/api/session/${fixture.targetID}/message?*`, (route) =>
route.fulfill({ status: 500, json: { message: "History unavailable" } }),
)
await installStressSessionTabs(page)
await page.goto(stressSessionHref(fixture.sourceID))
await expect(page.getByText(`History for ${fixture.sourceID}`, { exact: true })).toBeVisible()
await page.locator(`[data-titlebar-tab-link][href="${stressSessionHref(fixture.targetID)}"]`).click()
await expect(page.getByRole("heading", { name: "Something went wrong", exact: true })).toBeVisible()
await page.locator(`[data-titlebar-tab-link][href="${stressSessionHref(fixture.sourceID)}"]`).click()
await expect(page.getByText(`History for ${fixture.sourceID}`, { exact: true })).toBeVisible()
await expect(page.getByRole("heading", { name: "Something went wrong", exact: true })).toHaveCount(0)
})
test("focuses Find in the selected cached timeline", async ({ page }) => {
await mockOpenCodeServer(page, {
...fixture,
pageMessages: (id) => ({
items: [{ id: `msg_${id}`, type: "user", text: `History for ${id}`, time: { created: 1 } }],
}),
})
await installStressSessionTabs(page)
await page.goto(stressSessionHref(fixture.sourceID))
await expect(page.getByText(`History for ${fixture.sourceID}`, { exact: true })).toBeVisible()
await page.locator(`[data-titlebar-tab-link][href="${stressSessionHref(fixture.targetID)}"]`).click()
await expect(page.getByText(`History for ${fixture.targetID}`, { exact: true })).toBeVisible()
await page.locator(`[data-titlebar-tab-link][href="${stressSessionHref(fixture.sourceID)}"]`).click()
await expect(page.getByText(`History for ${fixture.sourceID}`, { exact: true })).toBeVisible()
await page.keyboard.press("ControlOrMeta+f")
const search = page.locator('[data-component="timeline-search-bar"] input')
await expect(search).toBeFocused()
await page.keyboard.type("History")
await expect(search).toHaveValue("History")
await page.locator(`[data-titlebar-tab-link][href="${stressSessionHref(fixture.targetID)}"]`).click()
await expect(page.getByText(`History for ${fixture.targetID}`, { exact: true })).toBeVisible()
await page.keyboard.press("ControlOrMeta+f")
await expect(search).toBeFocused()
await search.press("Escape")
await expect(search).toHaveCount(0)
})
test("disposes the old workspace's shell while destination history is loading", async ({ page }) => {
const destination = "C:/OpenCode/OtherProject"
const requested = Promise.withResolvers<void>()
const release = Promise.withResolvers<void>()
const reads: string[] = []
const output = { text: "Initial shell output\n" }
await mockOpenCodeServer(page, {
...fixture,
sessions: fixture.sessions.map((session) =>
session.id === fixture.targetID ? { ...session, directory: destination } : session,
),
pageMessages: (id) => ({
items:
id === fixture.sourceID
? ([
{ id: "msg_workspace_source", type: "user", text: "Follow the shell", time: { created: 1 } },
{
id: "msg_workspace_shell",
type: "assistant",
agent: "build",
model: { id: "claude-opus-4-6", providerID: "opencode" },
time: { created: 2 },
content: [
{
type: "tool",
id: "call_workspace_shell",
name: "shell",
time: { created: 2 },
state: {
status: "running",
input: { command: "run checks" },
metadata: { shellID: "sh_workspace_source" },
},
},
],
},
] satisfies SessionMessageInfo[])
: [],
}),
beforeMessagesResponse: async ({ sessionID }) => {
if (sessionID !== fixture.targetID) return
requested.resolve()
await release.promise
},
})
await page.route("**/api/shell/sh_workspace_source/output?*", (route) => {
const url = new URL(route.request().url())
const directory = url.searchParams.get("location[directory]")!
reads.push(directory)
if (directory !== fixture.directory)
return route.fulfill({ status: 404, json: { _tag: "ShellNotFoundError", id: "sh_workspace_source" } })
return route.fulfill({
json: {
location: { directory },
data: {
output: output.text.slice(Number(url.searchParams.get("cursor") ?? 0)),
cursor: output.text.length,
size: output.text.length,
truncated: false,
},
},
})
})
await installStressSessionTabs(page)
await page.addInitScript(
(detail) =>
localStorage.setItem(
"settings.v3",
JSON.stringify({
general: {
timelineDetail: { ...detail, shell: { placement: "separate", details: "expanded" } },
},
}),
),
timelinePresets[2].value,
)
await page.goto(stressSessionHref(fixture.sourceID))
const shell = page.locator('[data-timeline-part-id="call_workspace_shell"]')
await expect(shell.locator('[data-slot="bash-result"]')).toContainText("Initial shell output")
const original = await page.locator("[data-timeline-virtual-content]").elementHandle()
try {
await page.locator(`[data-titlebar-tab-link][href="${stressSessionHref(fixture.targetID)}"]`).click()
await requested.promise
await expect(page.locator("[data-session-title]")).toHaveText(fixture.expected.targetTitle)
output.text += "Output after returning\n"
await page.locator(`[data-titlebar-tab-link][href="${stressSessionHref(fixture.sourceID)}"]`).click()
await expect(shell.locator('[data-slot="bash-result"]')).toContainText("Output after returning")
expect(await original!.evaluate((element) => element.isConnected)).toBe(false)
expect(reads.length).toBeGreaterThan(1)
expect(reads.every((directory) => directory === fixture.directory)).toBe(true)
} finally {
release.resolve()
}
})
test("loads the transcript code font before opening rich history", async ({ page }) => {
const font = page.waitForResponse((response) => /IBMPlexMono-Text[^/]*\.woff2/.test(response.url()))
await mockOpenCodeServer(page, {
directory: fixture.directory,
project: fixture.project,
provider: fixture.provider,
sessions: fixture.sessions,
pageMessages: () => ({
items: [{ id: "msg_font_source", type: "user", text: "A transcript with no code", time: { created: 1 } }],
}),
})
await installStressSessionTabs(page)
await page.goto(stressSessionHref(fixture.sourceID))
await expect(page.getByText("A transcript with no code", { exact: true })).toBeVisible()
expect((await font).ok()).toBe(true)
await expect.poll(() => page.evaluate(() => document.fonts.check('440 13px "IBM Plex Mono"'))).toBe(true)
})
test("waits for the requested session's history before constructing its cold timeline", async ({ page }) => {
const requested = Promise.withResolvers<void>()
const release = Promise.withResolvers<void>()
await mockOpenCodeServer(page, {
directory: fixture.directory,
project: fixture.project,
provider: fixture.provider,
sessions: fixture.sessions,
pageMessages: (id) => ({ items: fixture.messages[id] ?? [] }),
beforeMessagesResponse: async ({ sessionID }) => {
if (sessionID !== fixture.targetID) return
requested.resolve()
await release.promise
},
})
await installStressSessionTabs(page)
await page.goto(stressSessionHref(fixture.sourceID))
await waitForStableTimeline(page, fixture.expected.sourceMessageIDs.at(-1)!)
try {
await page.locator(`[data-titlebar-tab-link][href="${stressSessionHref(fixture.targetID)}"]`).click()
await requested.promise
await expect(page.locator("[data-timeline-virtual-content]")).toHaveCount(0)
release.resolve()
await waitForStableTimeline(page, fixture.expected.targetMessageIDs.at(-1)!)
await expect(page.locator("[data-timeline-virtual-content]")).toHaveCount(1)
} finally {
release.resolve()
}
})
for (const grouped of [true, false]) {
test(`restores a ${grouped ? "grouped" : "separate"} timeline after inactive updates and a resize`, async ({
page,
}) => {
const events: OpenCodeEvent[] = []
const messages: Record<string, SessionMessageInfo[]> = Object.fromEntries(
[fixture.sourceID, fixture.targetID].map((id) => [
id,
[
{ id: `msg_user_${id}`, type: "user", text: `Prompt for ${id}`, time: { created: 1 } },
{
id: `msg_assistant_${id}`,
type: "assistant",
agent: "build",
model: { id: "claude-opus-4-6", providerID: "opencode" },
time: { created: 2, completed: 3 },
content: [
{
type: "tool",
id: `tool_${id}`,
name: "shell",
time: { created: 2, completed: 3 },
state: {
status: "completed",
input: { command: `echo ${id}` },
metadata: {},
content: [{ type: "text", text: `Output for ${id}` }],
},
},
{ type: "text", text: `Answer for ${id}` },
],
},
] satisfies SessionMessageInfo[],
]),
)
await mockOpenCodeServer(page, {
directory: fixture.directory,
project: fixture.project,
provider: fixture.provider,
sessions: fixture.sessions,
pageMessages: (id) => ({ items: messages[id] ?? [] }),
events: () => events.splice(0),
})
await installStressSessionTabs(page)
await page.addInitScript(
({ grouped, detail }) => {
localStorage.setItem(
"settings.v3",
JSON.stringify({
general: {
timelineDetail: {
...detail,
shell: { placement: grouped ? "grouped" : "separate", details: "collapsed" },
},
},
}),
)
},
{ grouped, detail: timelinePresets[2].value },
)
await page.goto(stressSessionHref(fixture.sourceID))
await expect(page.getByText(`Answer for ${fixture.sourceID}`, { exact: true })).toBeVisible()
if (grouped)
await page
.locator(
'[data-component="collapsed-tool-group"] > [data-component="collapsible"] > [data-slot="collapsible-trigger"]',
)
.click()
const shell = page.locator(`[data-timeline-part-id="tool_${fixture.sourceID}"]`)
const trigger = shell.locator('[data-slot="collapsible-trigger"]')
await trigger.click()
await expect(shell.locator('[data-slot="bash-result"]')).toHaveText(`Output for ${fixture.sourceID}`)
const original = await page.locator("[data-timeline-virtual-content]").elementHandle()
await page.locator(`[data-titlebar-tab-link][href="${stressSessionHref(fixture.targetID)}"]`).click()
await expect(page.getByText(`Answer for ${fixture.targetID}`, { exact: true })).toBeVisible()
await expect(shell).toHaveCount(0)
expect(await original!.evaluate((element) => element.isConnected)).toBe(false)
await expect(page.locator("[data-timeline-virtual-content]")).toHaveCount(1)
events.push({
id: "evt_cached_text",
created: 4,
type: "session.text.ended",
location: { directory: fixture.directory },
durable: { aggregateID: fixture.sourceID, seq: 0, version: 1 },
data: {
sessionID: fixture.sourceID,
assistantMessageID: `msg_assistant_${fixture.sourceID}`,
ordinal: 0,
text: "Updated while inactive",
},
})
await page.setViewportSize({ width: 900, height: 650 })
await page.locator(`[data-titlebar-tab-link][href="${stressSessionHref(fixture.sourceID)}"]`).click()
await expect(page.getByText("Updated while inactive", { exact: true })).toBeVisible()
await expect(trigger).toHaveAttribute("aria-expanded", "true")
await expect(shell.locator('[data-slot="bash-result"]')).toHaveText(`Output for ${fixture.sourceID}`)
expect(await original!.evaluate((element) => element.isConnected)).toBe(true)
await expect(page.locator("[data-timeline-virtual-content]")).toHaveCount(1)
await expect
.poll(() =>
page
.locator("[data-timeline-key]")
.evaluateAll((rows) =>
rows.every(
(row) =>
(row.firstElementChild?.getBoundingClientRect().height ?? 0) <= row.getBoundingClientRect().height + 1,
),
),
)
.toBe(true)
await trigger.click()
await expect(trigger).toHaveAttribute("aria-expanded", "false")
})
}
@@ -111,7 +111,7 @@ test.describe("regression: session timeline local row state", () => {
const id = `prt_patch_${count}`
events.push(...toolEvents({ ...part, id, callID: id }))
await expect(summary).toHaveAccessibleName(`Used ${count} Patch`)
await expect(summary.locator('[data-slot="basic-tool-tool-title"]')).toHaveText("Patch")
await expect(summary.locator('[data-slot="basic-tool-tool-title"]')).toHaveText(`${count} Patch`)
await expect(group).toHaveAttribute("data-timeline-part-ids", new RegExp(`${id}$`))
await expect(trigger).toHaveAttribute("aria-expanded", String(count === 2))
await expect(summary).toHaveAttribute("aria-expanded", "true")
@@ -55,7 +55,7 @@ test.describe("regression: session timeline context group resize", () => {
await devtools.send("Emulation.setCPUThrottlingRate", { rate: 4 })
const context = page.locator(`[data-timeline-part-ids="${contextIDs.join(",")}"]`).first()
await expectAppVisible(context)
await expect(context.getByRole("button")).toHaveAccessibleName("Used 4 Read, Glob, Grep, List")
await expect(context.getByRole("button")).toHaveAccessibleName("Used 1 Read, 1 Glob, 1 Grep, 1 List")
const contextSelector = `[data-timeline-part-ids="${contextIDs.join(",")}"]`
const regions = defineVisualRegions({
@@ -88,7 +88,7 @@ test.describe("regression: session timeline context group resize", () => {
await page.waitForTimeout(delay)
}
await expect(context.getByRole("button")).toHaveAccessibleName("Used 4 Read, Glob, Grep, List")
await expect(context.getByRole("button")).toHaveAccessibleName("Used 1 Read, 1 Glob, 1 Grep, 1 List")
await page.waitForTimeout(700)
const trace = await stopVisualProbe<keyof typeof regions>(page)
const labels = trace.samples
@@ -107,7 +107,7 @@ test.describe("regression: session timeline context group resize", () => {
]),
)
expect(labels).toEqual(["Used 4 Read, Glob, Grep, List"])
expect(labels).toEqual(["Used 1 Read, 1 Glob, 1 Grep, 1 List"])
expect(issues, JSON.stringify(trace.samples, null, 2)).toEqual([])
})
})
@@ -17,7 +17,7 @@ import { mockOpenCodeServer } from "../utils/mock-server"
import { installSseTransport } from "../utils/sse-transport"
import { expectSessionTitle } from "../utils/waits"
const messagePageSize = 40
const messagePageSize = 20
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
const messages = Array.from({ length: messagePageSize / 2 + 1 }, (_, index) => {
const id = `msg_${String(index + 1001).padStart(4, "0")}_history_root_user`
@@ -188,7 +188,7 @@ for (const scenario of scenarios) {
await waitForProbeSamples(page, beforeHistory)
expect(pages).toEqual([
{ before: undefined, limit: messagePageSize },
{ before: messages.at(-messagePageSize)!.id, limit: 20 },
{ before: messages.at(-messagePageSize)!.id, limit: messagePageSize },
])
expect(roots).toEqual([])
@@ -9,11 +9,11 @@ test.use({ viewport: { width: 1440, height: 900 }, serviceWorkers: "block" })
for (const window of ["assistant-only", "mixed"] as const) {
test(`renders the ${window} latest page before parent hydration and preserves it afterward`, async ({ page }) => {
const session = { ...fixture.sessions[0]!, id: `ses_hydration_${window}` }
// Compact's initial 40 and the next 20 begin with an assistant; page three supplies its parent.
const messages = Array.from({ length: 61 }, (_, index): SessionMessageInfo => {
// Both 20-message pages begin with an assistant; only page three supplies its parent.
const messages = Array.from({ length: 41 }, (_, index): SessionMessageInfo => {
const id = `msg_hydration_${index}`
const time = { created: 1700000000000 + index * 1_000 }
if (index === 0 || (window === "mixed" && index === 59))
if (index === 0 || (window === "mixed" && index === 39))
return { id, type: "user", time, text: `Prompt ${index}` }
return {
id,
@@ -21,7 +21,7 @@ for (const window of ["assistant-only", "mixed"] as const) {
time: { ...time, completed: time.created + 500 },
model: { id: "claude-opus-4-6", providerID: "opencode" },
agent: "build",
content: [{ type: "text", text: index === 60 ? "## Hydrated tail\n\n**Ready.**" : `Answer ${index}` }],
content: [{ type: "text", text: index === 40 ? "## Hydrated tail\n\n**Ready.**" : `Answer ${index}` }],
}
})
const gates = [21, 1].map((index) => ({
@@ -43,18 +43,18 @@ for (const window of ["assistant-only", "mixed"] as const) {
await gate.release.promise
},
pageMessages: (_, limit, before) => {
expect(limit).toBe(before ? 20 : 40)
expect(limit).toBe(20)
const end = before ? messages.findIndex((message) => message.id === before) : messages.length
const start = Math.max(0, end - limit)
return { items: messages.slice(start, end), cursor: start > 0 ? messages[start]!.id : undefined }
},
})
const tail = page.locator('[data-timeline-part-id="msg_hydration_60:text:0"]')
const tail = page.locator('[data-timeline-part-id="msg_hydration_40:text:0"]')
const markdown = tail.locator('[data-component="markdown"]')
const content = page.locator("[data-timeline-virtual-content]", { has: tail })
const viewport = page.locator(".scroll-view__viewport", { has: tail })
const orphan = page.locator('[data-timeline-row="AssistantPart"]', {
has: page.locator('[data-timeline-part-id="msg_hydration_58:text:0"]'),
has: page.locator('[data-timeline-part-id="msg_hydration_38:text:0"]'),
})
const expectReadyTail = async () => {
await expect(content).toHaveCSS("visibility", "visible")
@@ -75,7 +75,7 @@ for (const window of ["assistant-only", "mixed"] as const) {
await expect(orphan).toHaveAttribute("data-message-id", "msg_hydration_21")
if (window === "mixed")
await expect(
page.locator('[data-timeline-row="UserMessage"][data-message-id="msg_hydration_59"]'),
page.locator('[data-timeline-row="UserMessage"][data-message-id="msg_hydration_39"]'),
).toBeInViewport()
const original = await markdown.elementHandle()
@@ -16,8 +16,8 @@ for (const locale of ["de", "ar"] as const) {
})
const group = page.locator(`[data-timeline-part-ids="${ids.join(",")}"]`)
const names = locale === "de" ? "Lesen, Glob" : "\u0642\u0631\u0627\u0621\u0629, Glob"
await expect(group.getByRole("button")).toHaveAccessibleName(`Used 2 ${names}`)
const names = locale === "de" ? "1 Lesen, 1 Glob" : "1 \u0642\u0631\u0627\u0621\u0629, 1 Glob"
await expect(group.getByRole("button")).toHaveAccessibleName(`Used ${names}`)
await expect(group.locator('[data-slot="basic-tool-tool-title"]')).toHaveText(names)
await expect(page.locator("html")).toHaveAttribute("lang", locale)
})
@@ -7,6 +7,7 @@ import {
compactionFailed,
compactionStarted,
directory,
event,
session,
sessionID,
setupTimeline,
@@ -86,13 +87,12 @@ test("renders current protocol notices in CLI order", async ({ page }) => {
expect(ownerWarnings).toEqual([])
})
test("renders compaction progress, summary, and outcome in order", async ({ page }) => {
test("renders a compaction summary while it streams and after completion", async ({ page }) => {
const timeline = await setupTimeline(page, {
settings: {
timelineDetail: { ...timelinePresets[2].value, notices: { placement: "separate" } },
},
sessionMessages: [user, assistant(true)],
sessionStatus: { [sessionID]: { type: "busy" } },
})
await timeline.send(
@@ -104,15 +104,7 @@ test("renders compaction progress, summary, and outcome in order", async ({ page
)
const compaction = page.locator('[data-component="session-compaction-message"]')
await expect(compaction.getByText("Session compaction started", { exact: true })).toBeVisible()
await expect(compaction.getByRole("status").getByLabel("Compacting", { exact: true })).toBeVisible()
await expect(compaction.locator('[data-component="text-shimmer"]')).toHaveAttribute("data-active", "true")
await expect(compaction.getByText("Session compacted", { exact: true })).toHaveCount(0)
await expect(page.getByRole("button", { name: "Stop", exact: true })).toBeVisible()
await expect(page.locator('[data-component="session-working"]')).toHaveCount(0)
await page.setViewportSize({ width: 480, height: 900 })
await expect(compaction.getByText("Session compaction started", { exact: true })).toBeInViewport()
await expect(compaction.getByText("Session compacted", { exact: true })).toBeVisible()
await timeline.send(
compactionDelta({
@@ -122,8 +114,6 @@ test("renders compaction progress, summary, and outcome in order", async ({ page
)
await expect(compaction.getByRole("heading", { name: "Checkpoint" })).toBeVisible()
await expect(compaction).toContainText("Streamed implementation details.")
await expect(compaction.getByRole("status").getByLabel("Compacting", { exact: true })).toBeVisible()
await expect(compaction.getByText("Session compacted", { exact: true })).toHaveCount(0)
await timeline.send(
compactionEnded({
@@ -135,18 +125,6 @@ test("renders compaction progress, summary, and outcome in order", async ({ page
)
await expect(compaction).toContainText("Final implementation details.")
await expect(compaction).not.toContainText("Streamed implementation details.")
await expect(compaction.getByText("Session compaction started", { exact: true })).toBeVisible()
await expect(compaction.getByText("Session compacted", { exact: true })).toBeVisible()
await expect
.poll(async () => {
const summary = await compaction.locator('[data-component="text-part"]').boundingBox()
const completed = await compaction.getByText("Session compacted", { exact: true }).boundingBox()
return !!summary && !!completed && completed.y >= summary.y + summary.height
})
.toBe(true)
await expect(compaction.getByRole("status")).toHaveCount(0)
await expect(page.getByRole("button", { name: "Stop", exact: true })).toBeVisible()
await expect(page.locator('[data-component="session-working"]')).toBeVisible()
})
test("updates running compactions to failed and cancelled boundaries", async ({ page }) => {
@@ -168,10 +146,7 @@ test("updates running compactions to failed and cancelled boundaries", async ({
const compactions = page.locator('[data-component="session-compaction-message"]')
const failed = compactions.filter({ hasText: "The provider rejected the summary." })
await expect(failed.getByText("Session compaction started", { exact: true })).toBeVisible()
await expect(failed.getByText("Session compaction failed", { exact: true })).toBeVisible()
await expect(failed.getByText("Session compacted", { exact: true })).toHaveCount(0)
await expect(failed.getByRole("status")).toHaveCount(0)
await expect(failed.getByText("Session compacted", { exact: true })).toBeVisible()
await expect(failed.getByText("ProviderError: The provider rejected the summary.", { exact: true })).toBeVisible()
await expect(failed).not.toContainText("Partial summary that should be discarded.")
@@ -189,48 +164,11 @@ test("updates running compactions to failed and cancelled boundaries", async ({
await expect(compactions).toHaveCount(2)
const cancelled = compactions.filter({ hasNotText: "The provider rejected the summary." })
await expect(cancelled.getByText("Session compaction started", { exact: true })).toBeVisible()
await expect(cancelled.getByText("Session compaction cancelled", { exact: true })).toBeVisible()
await expect(cancelled.getByText("Session compacted", { exact: true })).toHaveCount(0)
await expect(cancelled.getByRole("status")).toHaveCount(0)
await expect(cancelled.getByText("Session compacted", { exact: true })).toBeVisible()
await expect(cancelled).not.toContainText("Cancellation detail should stay hidden.")
await expect(cancelled).not.toContainText("Summary before cancellation.")
})
test("shows an interrupted outcome when stopping automatic compaction", async ({ page }) => {
const timeline = await setupTimeline(page, {
sessionMessages: [user, assistant(true)],
sessionStatus: { [sessionID]: { type: "busy" } },
})
await timeline.send(compactionStarted({ sessionID, reason: "auto", recent: "" }))
await timeline.send(compactionDelta({ sessionID, text: "Partial automatic summary." }))
const compaction = page.locator('[data-component="session-compaction-message"]')
await expect(compaction.getByRole("status").getByLabel("Compacting", { exact: true })).toBeVisible()
await expect(compaction).toContainText("Partial automatic summary.")
const request = page.waitForRequest(
(request) =>
request.method() === "POST" && new URL(request.url()).pathname === `/api/session/${sessionID}/interrupt`,
)
await page.getByRole("button", { name: "Stop", exact: true }).click()
await request
await timeline.send(
compactionFailed({
sessionID,
reason: "auto",
error: { type: "compaction.interrupted", message: "Compaction was interrupted" },
}),
)
await expect(compaction.getByText("Session compaction started", { exact: true })).toBeVisible()
await expect(compaction.getByText("Session compaction interrupted", { exact: true })).toBeVisible()
await expect(compaction.getByText("Session compaction failed", { exact: true })).toHaveCount(0)
await expect(compaction.getByText("Session compacted", { exact: true })).toHaveCount(0)
await expect(compaction.getByRole("status")).toHaveCount(0)
await expect(compaction).not.toContainText("Partial automatic summary.")
await expect(compaction).not.toContainText("Compaction was interrupted")
})
test("moves blocking work to the background with Ctrl+B", async ({ page }) => {
await setupTimeline(page, {
settings: {
@@ -448,7 +386,7 @@ test("separates blocking and already-backgrounded work into two rows", async ({
const used = page
.locator('[data-timeline-part-ids="call_backgrounded,call_shell_backgrounded,call_blocking"]')
.locator(':scope > [data-component="collapsible"] > [data-slot="collapsible-trigger"]')
await expect(used).toHaveText(/^Used\s*3\s*Agent, Shell$/)
await expect(used).toHaveText(/^Used\s*2 Agent, 1 Shell$/)
await expect(used).toHaveAttribute("aria-expanded", "false")
await used.click()
await expect(used).toHaveAttribute("aria-expanded", "true")
@@ -45,10 +45,10 @@ test("expands a mixed collapsed tool stack without expanding its individual call
const group = page.locator(
'[data-timeline-part-ids="prt_stack_shell_1,prt_stack_explore,prt_stack_patch,prt_stack_shell_2"]',
)
const summary = group.getByRole("button", { name: "Used 4 Shell, Agent, Patch", exact: true })
const summary = group.getByRole("button", { name: "Used 2 Shell, 1 Agent, 1 Patch", exact: true })
await expect(summary).toHaveAttribute("aria-expanded", "false")
await expect(summary).toHaveCSS("height", "28px")
await expect(summary.locator('[data-slot="basic-tool-tool-title"]')).toHaveText("Shell, Agent, Patch")
await expect(summary.locator('[data-slot="basic-tool-tool-title"]')).toHaveText("2 Shell, 1 Agent, 1 Patch")
await expect(summary.locator('[data-component="tag"]')).toHaveCount(0)
await summary.click()
await expect(summary).toHaveAttribute("aria-expanded", "true")
@@ -75,8 +75,8 @@ test("leaves tools expanded by settings outside the collapsed stack", async ({ p
await expect(page.locator('[data-timeline-part-id="prt_expanded_shell"]')).toBeVisible()
const group = page.locator('[data-timeline-part-ids="prt_collapsed_patch,prt_collapsed_read"]')
await expect(group.getByRole("button", { name: "Used 2 Patch, Read", exact: true })).toBeVisible()
await expect(group.locator('[data-slot="basic-tool-tool-title"]')).toHaveText("Patch, Read")
await expect(group.getByRole("button", { name: "Used 1 Patch, 1 Read", exact: true })).toBeVisible()
await expect(group.locator('[data-slot="basic-tool-tool-title"]')).toHaveText("1 Patch, 1 Read")
await expect(page.locator('[data-timeline-spacing="tool"]')).toHaveCSS("padding-top", "8px")
})
@@ -114,7 +114,7 @@ test("combines follow-up patches into one three-file stack inside Used", async (
],
})
const group = page.locator('[data-component="collapsed-tool-group"]')
await group.getByRole("button", { name: "Used 2 Shell, Patch", exact: true }).click()
await group.getByRole("button", { name: "Used 1 Shell, 1 Patch", exact: true }).click()
await expect(group.getByText("2 files", { exact: true })).toBeVisible()
await timeline.send(
partUpdated(
@@ -129,7 +129,7 @@ test("combines follow-up patches into one three-file stack inside Used", async (
),
),
)
await expect(group.getByRole("button", { name: "Used 3 Shell, Patch", exact: true })).toHaveAttribute(
await expect(group.getByRole("button", { name: "Used 1 Shell, 2 Patch", exact: true })).toHaveAttribute(
"aria-expanded",
"true",
)
@@ -162,7 +162,7 @@ test("keeps failed search calls and their error cards inside the collapsed stack
await setupTimeline(page, { messages: [userMessage(), assistantMessage(parts)] })
const group = page.locator('[data-component="collapsed-tool-group"]')
const summary = group.getByRole("button", { name: "Used 2 Glob, Grep", exact: true })
const summary = group.getByRole("button", { name: "Used 1 Glob, 1 Grep", exact: true })
await expect(summary).toHaveAttribute("aria-expanded", "false")
await summary.click()
await expect(group.locator('[data-kind="tool-error-card"]')).toHaveCount(2)
@@ -1,86 +0,0 @@
import { expect, test } from "@playwright/test"
import { mockOpenCodeServer } from "../utils/mock-server"
for (const colorScheme of ["light", "dark"] as const) {
test.describe(colorScheme, () => {
test.use({ colorScheme, contextOptions: { reducedMotion: "reduce" } })
test("project card edges stay inside the settings scrollport", async ({ page }, info) => {
const projects = ["rebase", "dinocms", "opencode", "Playground"].map((name, index) => ({
id: `project-${index}`,
name,
canonical: `/projects/${name}`,
vcs: "git",
time: { created: 1, updated: 1 },
sandboxes: [],
}))
await mockOpenCodeServer(page, {
directory: "/projects/rebase",
project: projects[0],
sessions: [],
pageMessages: () => ({ items: [] }),
provider: { all: [], connected: [], default: {} },
})
await page.route("**/api/project", (route) =>
route.fulfill({ json: projects, headers: { "access-control-allow-origin": "*" } }),
)
await page.addInitScript((projects) => {
localStorage.setItem(
"opencode.global.dat:server",
JSON.stringify({
projects: { local: projects.map((project) => ({ worktree: project.canonical, expanded: true })) },
}),
)
}, projects)
await page.goto("/")
await expect(page.getByRole("button", { name: "Settings", exact: true })).toBeEnabled()
await page.getByRole("button", { name: "Settings", exact: true }).click()
const settings = page.getByTestId("settings-screen")
await settings.getByRole("tab", { name: "Projects", exact: true }).click()
const panel = settings.getByRole("tabpanel")
await expect(panel.getByText("rebase", { exact: true })).toBeVisible()
await expect(panel.getByText("Playground", { exact: true })).toBeVisible()
await page.evaluate(() => document.fonts.ready)
for (const width of [1280, 1050, 960, 720, 600]) {
await page.setViewportSize({ width, height: 720 })
await page.mouse.move(0, 0)
await page.screenshot({ path: info.outputPath(`projects-${width}.png`), animations: "disabled" })
// Raised cards paint a half-pixel border outside their box. The scrollport
// must leave room for that border and the soft shadow on both sides.
await expect
.poll(() =>
panel.getByText("rebase", { exact: true }).evaluate((label) => {
const row = label.parentElement!.parentElement!
const bounds = row.getBoundingClientRect()
const clips = []
for (let parent = row.parentElement; parent; parent = parent.parentElement) {
if (getComputedStyle(parent).overflowX === "visible") continue
const clip = parent.getBoundingClientRect()
clips.push(bounds.left - clip.left, clip.right - bounds.right)
}
return Math.min(...clips)
}),
)
.toBeGreaterThanOrEqual(4)
await expect(panel).toHaveJSProperty("scrollWidth", await panel.evaluate((el) => el.clientWidth))
}
await page.setViewportSize({ width: 1280, height: 720 })
await panel.getByText("rebase", { exact: true }).hover()
await panel.getByText("rebase", { exact: true }).click()
const dialog = page.getByRole("dialog")
await expect(dialog.getByRole("textbox")).toHaveValue("rebase")
await expect(dialog.getByRole("textbox")).toBeFocused()
await dialog.getByRole("button", { name: "Cancel", exact: true }).click()
await expect(dialog).toBeHidden()
await expect(panel.getByText("rebase", { exact: true })).toBeVisible()
await page.setViewportSize({ width: 1280, height: 260 })
await panel.getByText("rebase", { exact: true }).hover()
await page.mouse.wheel(0, 400)
await expect(panel.getByText("Playground", { exact: true })).toBeInViewport({ ratio: 1 })
await expect(panel.getByRole("heading", { name: "Projects", exact: true })).toBeInViewport({ ratio: 1 })
})
})
}
@@ -99,16 +99,13 @@ test("creates a session in a new project and selects its model", async ({ page }
const modelControl = page.locator('[data-action="composer-model"]')
await expect(modelControl).toContainText("Go Model 1")
await modelControl.click()
const modelSearch = page.getByPlaceholder("Search models", { exact: true })
await expect(modelSearch).toBeFocused()
await modelSearch.press("ArrowDown")
await modelSearch.press("Enter")
await page.locator('[data-option-key="opencode:free-model"]').click()
await expect(modelControl).toContainText("Free Model")
await modelControl.click()
await expect(modelSearch).toBeFocused()
await modelSearch.press("ArrowUp")
await modelSearch.press("Enter")
const goModel = page.locator('[data-option-key="opencode-go:go-model-1"]')
await expect(goModel).toBeVisible()
await goModel.click()
await expect(modelControl).toContainText("Go Model 1")
})
-6
View File
@@ -80,7 +80,6 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
const id = state.connections
let ended = false
let own: ReadableStreamDefaultController<Uint8Array> | undefined
let keepalive: ReturnType<typeof setInterval> | undefined
const stream = new ReadableStream<Uint8Array>({
start(controller) {
own = controller
@@ -90,15 +89,11 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
encoder.encode(frame({ id: `evt_mock_connected_${id}`, type: "server.connected", data: {} })),
)
state.buffer.splice(0).forEach((item) => controller.enqueue(encoder.encode(item)))
// Match the real server's idle stream so long scenarios do not
// trigger the client's 45-second stall watchdog and reload history.
keepalive = setInterval(() => controller.enqueue(encoder.encode(": keepalive\n\n")), 15_000)
request.signal.addEventListener(
"abort",
() => {
if (ended) return
ended = true
clearInterval(keepalive)
if (state.controller === controller) state.controller = undefined
controller.error(request.signal.reason ?? new DOMException("The operation was aborted", "AbortError"))
},
@@ -108,7 +103,6 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
cancel() {
if (ended) return
ended = true
clearInterval(keepalive)
if (state.controller === own) state.controller = undefined
},
})
-1
View File
@@ -10,7 +10,6 @@
"./i18n/desktop-native": "./src/runtime/i18n/desktop-native.ts",
"./updater": "./src/shell/updates/types.ts",
"./wsl/types": "./src/servers/wsl/types.ts",
"./ssh": "./src/servers/ssh/types.ts",
"./vite": "./vite.js",
"./index.css": "./src/index.css"
},
+1 -6
View File
@@ -17,8 +17,6 @@ import { ServerConnection, ServersProvider } from "@/runtime/server/registry"
import { SettingsProvider } from "@/settings/model"
import { TabsProvider } from "@/shell/tabs/tabs"
import { WslServersProvider } from "@/servers/wsl/context"
import { SshProvider } from "@/servers/ssh/context"
import { SshRestore } from "@/servers/ssh/restore"
import { ErrorPage } from "@/shell/errors/error"
import { AppRoutes, File, preloadRoute } from "@/shell/routes/routes"
@@ -83,9 +81,7 @@ export function AppBaseProviders(
<QueryProvider>
<WslServersProvider>
<DialogProvider>
<SshProvider>
<FileComponentProvider component={File}>{props.children}</FileComponentProvider>
</SshProvider>
<FileComponentProvider component={File}>{props.children}</FileComponentProvider>
</DialogProvider>
</WslServersProvider>
</QueryProvider>
@@ -113,7 +109,6 @@ export function AppInterface(props: {
<BodyTypography />
<CommandProvider>
<DesktopCommands />
<SshRestore />
<HighlightsProvider>
{props.children}
{rootProps.children}
+2 -2
View File
@@ -841,8 +841,8 @@ export function ComposerEditorSubmitButton(props: {
disabled={!props.stopping && props.disabled}
tabIndex={props.mode === "normal" ? undefined : -1}
icon={<Icon name={props.stopping ? "stop" : props.mode === "shell" ? "arrow-undo-down" : "arrow-up"} />}
variant="submit"
class="size-7 rounded-md p-[6px]"
variant="contrast"
class="size-7 rounded-md p-[6px] disabled:opacity-50"
aria-label={props.stopping ? props.stopLabel : props.sendLabel}
onClick={(event) => {
event.preventDefault()
@@ -37,7 +37,6 @@ export type ComposerEditorView = {
agent?: ComposerSelectControl
variant?: ComposerSelectControl
submit: {
available?: Accessor<boolean>
stopping: Accessor<boolean>
working?: Accessor<boolean>
queue?: ComposerQueue
@@ -334,7 +333,6 @@ export function createComposerEditor(input: {
draft.removeAttachment(id)
},
canSubmit() {
if (input.view.submit.available?.() === false) return false
if (input.view.draftOnly) return false
const persisted = draft.state
if (state.mode === "shell") {
@@ -367,7 +365,6 @@ export function createComposerEditor(input: {
dispatch({ type: "mode.shell" })
},
submit(options?: { alternate?: boolean }) {
if (input.view.submit.available?.() === false) return
if (input.view.draftOnly) return
input.view.submit.onSubmit(options)
dispatch({ type: "popover.close" })
+1 -5
View File
@@ -11,7 +11,7 @@ import { useLanguage } from "@/runtime/i18n/language"
import { useLayout } from "@/shell/state/layout"
import { usePlatform } from "@/runtime/platform/platform"
import { useWorkspaceLocation } from "@/workspaces/location"
import { useData, useServer } from "@/runtime/server/current"
import { useData } from "@/runtime/server/current"
import { createSessionTabs } from "@/session/helpers"
import { showToast } from "@/shell/notifications/toast"
import { formatServerError } from "@/runtime/server/errors"
@@ -30,8 +30,6 @@ export type ComposerModel = ComposerEditorModel & {
export function createComposerModel(adapter: ComposerAdapter, options?: { queue?: ComposerQueue }): ComposerModel {
const sdk = useWorkspaceLocation()
const data = useData()
const server = useServer()
const available = () => server.conn.type !== "ssh" || server.ctx.sdk.connection.status() === "connected"
const files = useFile()
const layout = useLayout()
const comments = useComments()
@@ -396,12 +394,10 @@ export function createComposerModel(adapter: ComposerAdapter, options?: { queue?
keybind: () => command.keybindParts("model.variant.cycle"),
},
submit: {
available,
stopping,
working: adapter.working,
queue: options?.queue,
onSubmit: (submitOptions) => {
if (!available()) return
const queue = options?.queue
// Confirming an edit re-admits the queued prompt instead of sending
// the composer value as a new prompt. Enter keeps it queued in
-1
View File
@@ -20,5 +20,4 @@ export { createDraftStore } from "./runtime/persistence/drafts"
export { createNamespaceStorage, type NamespaceStorage } from "./runtime/persistence/namespace"
export { flushPersisted } from "./runtime/persistence/persist"
export { useWslServers } from "./servers/wsl/context"
export { useSsh } from "./servers/ssh/context"
export { type UpdaterPlatform, type UpdaterState } from "./shell/updates/types"
+9 -26
View File
@@ -15,7 +15,6 @@ import { Persistence } from "@/runtime/persistence/schema"
import type { HomeController } from "../model"
import { useGlobal } from "@/runtime/server/runtime"
import { SessionTransfer } from "@opencode/schema/session-transfer"
import { useSshAuthenticate } from "@/servers/ssh/authenticate"
export const HomeServersSchema = Schema.Struct({
collapsed: Persistence.record(Persistence.fallback(Schema.Boolean, () => false)),
@@ -29,7 +28,6 @@ export function createHomeProjectsController(home: HomeController) {
const openSettings = useSettingsCommand()
const serverManagement = useServerActionsController()
const global = useGlobal()
const authenticate = useSshAuthenticate()
const [_state, setState, _, ready] = persisted(Persist.global("home.servers"), HomeServersSchema, { collapsed: {} })
const [state] = createResource(
() => ready.promise ?? Promise.resolve(),
@@ -44,15 +42,6 @@ export function createHomeProjectsController(home: HomeController) {
return platform.platform === "desktop" && !!platform.openPath && ServerConnection.local(conn)
}
function choose(conn: ServerConnection.Any) {
pickDirectory({
server: conn,
title: language.t("command.project.open"),
multiple: true,
onSelect: (result) => home.project.add(conn, homeProjectDirectories(result)),
})
}
return {
copy: {
language,
@@ -82,25 +71,15 @@ export function createHomeProjectsController(home: HomeController) {
void dialog.show(() => <DialogServer mode="edit" server={conn} />)
})
},
authenticate: (conn: ServerConnection.Any) => authenticate(conn),
focus: (conn: ServerConnection.Any) => {
if (authenticate(conn, () => home.selection.focusServer(conn))) return
home.selection.focusServer(conn)
},
focus: home.selection.focusServer,
},
project: {
list: home.project.list,
recentlyClosed: home.project.recentlyClosed,
homedir: home.project.homedir,
select: (conn: ServerConnection.Any, directory: string) => {
if (authenticate(conn, () => home.project.select(conn, directory))) return
home.project.select(conn, directory)
},
select: home.project.select,
add: home.project.add,
openNewSession: (conn: ServerConnection.Any, directory: string) => {
if (authenticate(conn, () => home.project.openProjectNewSession(conn, directory))) return
home.project.openProjectNewSession(conn, directory)
},
openNewSession: home.project.openProjectNewSession,
canImportSession: !!platform.openAttachmentPickerDialog,
importSession: (conn: ServerConnection.Any, project: LocalProject) => {
if (!platform.openAttachmentPickerDialog) return
@@ -146,9 +125,13 @@ export function createHomeProjectsController(home: HomeController) {
.forEach((directory) => notification.project.markViewed(directory))
},
choose: (conn: ServerConnection.Any) => {
if (authenticate(conn, () => choose(conn))) return
if (home.server.health(conn)?.healthy === false) return
choose(conn)
pickDirectory({
server: conn,
title: language.t("command.project.open"),
multiple: true,
onSelect: (result) => home.project.add(conn, homeProjectDirectories(result)),
})
},
close: (conn: ServerConnection.Any, directory: string) => {
const next = closeHomeProject(

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