Compare commits

..
247 changed files with 3707 additions and 4495 deletions
+4 -4
View File
@@ -25,7 +25,7 @@ on:
required: false
type: string
concurrency: ${{ github.workflow }}-${{ github.ref }}-${{ (github.ref_name == 'v2' && (inputs.version || inputs.bump) && 'release') || inputs.version || inputs.bump }}
concurrency: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.version || inputs.bump }}
permissions:
id-token: write
@@ -33,7 +33,7 @@ permissions:
packages: write
env:
OPENCODE_CHANNEL: ${{ (github.ref_name == 'v2' && !inputs.bump && !inputs.version && 'dev') || '' }}
OPENCODE_CHANNEL: ${{ (github.ref_name == 'v2' && 'dev') || '' }}
jobs:
version:
@@ -168,7 +168,7 @@ jobs:
fi
found=0
for file in packages/cli/dist/cli-darwin-*/bin/opencode; do
for file in packages/cli/dist/cli-darwin-*/bin/opencode2; do
if [ ! -f "$file" ]; then
continue
fi
@@ -378,7 +378,7 @@ jobs:
needs:
- version
- sign-cli-macos
if: github.repository == 'anomalyco/opencode' && (github.ref_name != 'v2' || needs.version.outputs.release != '')
if: github.repository == 'anomalyco/opencode' && github.ref_name != 'v2'
continue-on-error: false
env:
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
+2 -2
View File
@@ -8,9 +8,9 @@
## Live V2 TUI Testing
- Run `bun run dev:live` from a development worktree to test its TUI against the currently elected `opencode` background server and live sessions.
- Run `bun run dev:live` from a development worktree to test its TUI against the currently elected `opencode2` background server and live sessions.
- Pass a directory after the script when needed, for example `bun run dev:live /path/to/project`.
- The script discovers the server with `opencode service status`, injects its private local credential from `opencode service get password`, and uses the `dev` TUI storage channel so tabs and other client-local state match the installed client.
- The script discovers the server with `opencode2 service status`, injects its private local credential from `opencode2 service get password`, and uses the `dev` TUI storage channel so tabs and other client-local state match the installed client.
- Prefer `dev:live` over plain `bun run dev` for this workflow. An implicit managed-service connection may replace the live server when the worktree client version differs; explicit `--server` warns and continues without replacing it.
## V2 TUI Stories
+35 -35
View File
@@ -31,7 +31,7 @@
},
"packages/ai": {
"name": "@opencode/ai",
"version": "2.0.0",
"version": "1.17.20",
"dependencies": {
"@aws-sdk/credential-providers": "3.1057.0",
"@opencode/schema": "workspace:*",
@@ -53,7 +53,7 @@
},
"packages/app": {
"name": "@opencode/app",
"version": "2.0.0",
"version": "1.18.15",
"dependencies": {
"@corvu/drawer": "catalog:",
"@dnd-kit/abstract": "0.5.0",
@@ -111,7 +111,7 @@
},
"packages/cli": {
"name": "@opencode/cli",
"version": "2.0.0",
"version": "1.18.4",
"bin": {
"opencode2": "./bin/opencode2.cjs",
},
@@ -175,7 +175,7 @@
},
"packages/client": {
"name": "@opencode/client",
"version": "2.0.0",
"version": "1.17.13",
"dependencies": {
"@opencode/protocol": "workspace:*",
"@opencode/schema": "workspace:*",
@@ -201,7 +201,7 @@
},
"packages/codemode": {
"name": "@opencode/codemode",
"version": "2.0.0",
"version": "1.18.4",
"dependencies": {
"acorn": "8.15.0",
"effect": "catalog:",
@@ -215,7 +215,7 @@
},
"packages/console/app": {
"name": "@opencode/console-app",
"version": "2.0.0",
"version": "1.18.15",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -251,7 +251,7 @@
},
"packages/console/core": {
"name": "@opencode/console-core",
"version": "2.0.0",
"version": "1.18.15",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -278,7 +278,7 @@
},
"packages/console/function": {
"name": "@opencode/console-function",
"version": "2.0.0",
"version": "1.18.15",
"dependencies": {
"@openauthjs/openauth": "0.0.0-20250322224806",
"@opencode/console-core": "workspace:*",
@@ -295,7 +295,7 @@
},
"packages/console/mail": {
"name": "@opencode/console-mail",
"version": "2.0.0",
"version": "1.18.15",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -319,7 +319,7 @@
},
"packages/console/support": {
"name": "@opencode/console-support",
"version": "2.0.0",
"version": "1.18.15",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@opencode/console-core": "workspace:*",
@@ -339,7 +339,7 @@
},
"packages/core": {
"name": "@opencode/core",
"version": "2.0.0",
"version": "1.18.4",
"dependencies": {
"@ai-sdk/alibaba": "1.0.17",
"@ai-sdk/anthropic": "3.0.82",
@@ -411,7 +411,7 @@
},
"packages/desktop": {
"name": "@opencode/desktop",
"version": "2.0.0",
"version": "1.18.15",
"dependencies": {
"@zip.js/zip.js": "2.7.62",
"electron-context-menu": "4.1.2",
@@ -463,7 +463,7 @@
},
"packages/enterprise": {
"name": "@opencode/enterprise",
"version": "2.0.0",
"version": "1.18.15",
"dependencies": {
"@hono/standard-validator": "catalog:",
"@opencode-ai/sdk": "1.18.21",
@@ -500,7 +500,7 @@
},
"packages/function": {
"name": "@opencode/function",
"version": "2.0.0",
"version": "1.18.15",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -516,7 +516,7 @@
},
"packages/http-recorder": {
"name": "@opencode/http-recorder",
"version": "2.0.0",
"version": "1.18.15",
"dependencies": {
"@effect/platform-node-shared": "4.0.0-rc.112",
},
@@ -535,7 +535,7 @@
},
"packages/httpapi-codegen": {
"name": "@opencode/httpapi-codegen",
"version": "2.0.0",
"version": "0.0.0",
"dependencies": {
"effect": "catalog:",
"prettier": "3.6.2",
@@ -548,7 +548,7 @@
},
"packages/latex": {
"name": "@opencode/latex",
"version": "2.0.0",
"version": "0.0.0",
"dependencies": {
"@opencode/plugin": "workspace:*",
"@opentui/core": "catalog:",
@@ -562,7 +562,7 @@
},
"packages/merman": {
"name": "@opencode/merman",
"version": "2.0.0",
"version": "0.0.0",
"dependencies": {
"@opencode/plugin": "workspace:*",
"@opentui/core": "catalog:",
@@ -577,7 +577,7 @@
},
"packages/plugin": {
"name": "@opencode/plugin",
"version": "2.0.0",
"version": "1.18.15",
"dependencies": {
"@ai-sdk/provider": "3.0.8",
"@opencode/ai": "workspace:*",
@@ -616,7 +616,7 @@
},
"packages/plugin-browser": {
"name": "@opencode/plugin-browser",
"version": "2.0.0",
"version": "0.0.0",
"dependencies": {
"@opencode/plugin": "workspace:*",
"@opencode/schema": "workspace:*",
@@ -646,7 +646,7 @@
},
"packages/protocol": {
"name": "@opencode/protocol",
"version": "2.0.0",
"version": "1.17.11",
"dependencies": {
"@opencode/schema": "workspace:*",
"effect": "catalog:",
@@ -661,7 +661,7 @@
},
"packages/schema": {
"name": "@opencode/schema",
"version": "2.0.0",
"version": "1.17.11",
"dependencies": {
"@standard-schema/spec": "catalog:",
"effect": "catalog:",
@@ -685,7 +685,7 @@
},
"packages/sdk": {
"name": "@opencode/sdk",
"version": "2.0.0",
"version": "1.18.4",
"dependencies": {
"@opencode/client": "workspace:*",
"@opencode/core": "workspace:*",
@@ -706,7 +706,7 @@
},
"packages/server": {
"name": "@opencode/server",
"version": "2.0.0",
"version": "1.18.4",
"dependencies": {
"@effect/platform-node": "catalog:",
"@effect/platform-node-shared": "catalog:",
@@ -728,7 +728,7 @@
},
"packages/session-ui": {
"name": "@opencode/session-ui",
"version": "2.0.0",
"version": "1.18.15",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode/client": "workspace:*",
@@ -763,7 +763,7 @@
},
"packages/simulation": {
"name": "@opencode/simulation",
"version": "2.0.0",
"version": "1.17.13",
"dependencies": {
"@opencode/ai": "workspace:*",
"@opencode/core": "workspace:*",
@@ -783,7 +783,7 @@
},
"packages/stats/app": {
"name": "@opencode/stats-app",
"version": "2.0.0",
"version": "1.18.15",
"dependencies": {
"@ibm/plex": "6.4.1",
"@kobalte/core": "catalog:",
@@ -817,7 +817,7 @@
},
"packages/stats/core": {
"name": "@opencode/stats-core",
"version": "2.0.0",
"version": "1.18.15",
"dependencies": {
"@aws-sdk/client-athena": "3.933.0",
"@planetscale/database": "1.19.0",
@@ -836,7 +836,7 @@
},
"packages/stats/server": {
"name": "@opencode/stats-server",
"version": "2.0.0",
"version": "1.18.15",
"dependencies": {
"@aws-sdk/client-firehose": "3.933.0",
"@effect/platform-node": "catalog:",
@@ -882,7 +882,7 @@
},
"packages/theme": {
"name": "@opencode/theme",
"version": "2.0.0",
"version": "0.0.0",
"dependencies": {
"@opentui/core": "catalog:",
"effect": "catalog:",
@@ -896,7 +896,7 @@
},
"packages/tui": {
"name": "@opencode/tui",
"version": "2.0.0",
"version": "1.18.4",
"dependencies": {
"@opencode/client": "workspace:*",
"@opencode/core": "workspace:*",
@@ -931,7 +931,7 @@
},
"packages/ui": {
"name": "@opencode/ui",
"version": "2.0.0",
"version": "1.18.15",
"dependencies": {
"@kobalte/core": "catalog:",
"@pierre/diffs": "catalog:",
@@ -966,7 +966,7 @@
},
"packages/util": {
"name": "@opencode/util",
"version": "2.0.0",
"version": "1.18.3",
"dependencies": {
"@effect/opentelemetry": "catalog:",
"@effect/platform-node": "catalog:",
@@ -999,7 +999,7 @@
},
"packages/web": {
"name": "@opencode/web",
"version": "2.0.0",
"version": "1.18.15",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -1040,7 +1040,7 @@
},
"services/update": {
"name": "@opencode/update",
"version": "2.0.0",
"version": "1.18.4",
"dependencies": {
"jose": "6.0.11",
"semver": "catalog:",
+15 -44
View File
@@ -1,7 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
APP=opencode
SOURCE_APP=opencode
APP=opencode2
MUTED='\033[0;2m'
RED='\033[0;31m'
@@ -23,7 +22,7 @@ Options:
Examples:
curl -fsSL https://opencode.ai/v2/install | bash
curl -fsSL https://opencode.ai/v2/install | bash -s -- --version 0.0.0-beta-17236
./install --binary /path/to/opencode
./install --binary /path/to/opencode2
EOF
}
@@ -202,9 +201,9 @@ else
filename="cli-$target-$specific_version.tgz"
url="https://registry.npmjs.org/$package_name/-/$filename"
binary_name="$SOURCE_APP"
binary_name="$APP"
if [ "$os" = "windows" ]; then
binary_name="$SOURCE_APP.exe"
binary_name="$APP.exe"
fi
fi
@@ -231,7 +230,12 @@ check_version() {
installed_version="${installed_version##* }"
installed_version="${installed_version#v}"
print_message info "${MUTED}Installed version: ${NC}$installed_version."
if [[ "$installed_version" != "$specific_version" ]]; then
print_message info "${MUTED}Installed version: ${NC}$installed_version."
else
print_message info "${MUTED}Version ${NC}$specific_version${MUTED} already installed"
exit 0
fi
fi
}
@@ -336,47 +340,15 @@ download_and_install() {
fi
tar -xzf "$tmp_dir/$filename" -C "$tmp_dir"
local installed_binary="$APP"
if [ "$os" = "windows" ]; then
installed_binary="$APP.exe"
fi
mv "$tmp_dir/package/bin/$binary_name" "$INSTALL_DIR/$installed_binary"
chmod 755 "$INSTALL_DIR/$installed_binary"
mv "$tmp_dir/package/bin/$binary_name" "$INSTALL_DIR"
chmod 755 "${INSTALL_DIR}/$binary_name"
rm -rf "$tmp_dir"
}
install_from_binary() {
print_message info "\n${MUTED}Installing ${NC}$APP ${MUTED}from: ${NC}$binary_path"
local installed_binary="$APP"
case "$(uname -s)" in
MINGW*|MSYS*|CYGWIN*) installed_binary="$APP.exe" ;;
esac
cp "$binary_path" "$INSTALL_DIR/$installed_binary"
chmod 755 "$INSTALL_DIR/$installed_binary"
}
install_legacy_shim() {
local shim_os="${os:-}"
if [[ -z "$shim_os" ]]; then
case "$(uname -s)" in
MINGW*|MSYS*|CYGWIN*) shim_os="windows" ;;
esac
fi
rm -f "$INSTALL_DIR/opencode2" "$INSTALL_DIR/opencode2.exe" "$INSTALL_DIR/opencode2.cmd"
if [[ "$shim_os" == "windows" ]]; then
cat > "$INSTALL_DIR/opencode2.cmd" <<'EOF'
@echo off
echo opencode2 is now just opencode. run opencode
exit /b 1
EOF
return
fi
cat > "$INSTALL_DIR/opencode2" <<'EOF'
#!/bin/sh
echo 'opencode2 is now just opencode. run opencode'
exit 1
EOF
chmod 755 "$INSTALL_DIR/opencode2"
cp "$binary_path" "${INSTALL_DIR}/$APP"
chmod 755 "${INSTALL_DIR}/$APP"
}
if [ -n "$binary_path" ]; then
@@ -385,7 +357,6 @@ else
check_version
download_and_install
fi
install_legacy_shim
add_to_path() {
@@ -482,7 +453,7 @@ echo -e ""
echo -e "${MUTED}OpenCode includes free models, to start:${NC}"
echo -e ""
echo -e "cd <project> ${MUTED}# Open directory${NC}"
echo -e "opencode ${MUTED}# Run command${NC}"
echo -e "opencode2 ${MUTED}# Run command${NC}"
echo -e ""
echo -e "${MUTED}For more information visit ${NC}https://opencode.ai/v2/docs"
echo -e ""
+3 -3
View File
@@ -2,15 +2,15 @@
"$schema": "https://json.schemastore.org/package.json",
"name": "opencode",
"description": "AI-powered development tool",
"version": "2.0.0",
"version": "0.0.0",
"private": true,
"type": "module",
"packageManager": "bun@1.4.2",
"scripts": {
"dev": "bun run --cwd packages/cli src/index.ts",
"dev:live": "sh -c 'OPENCODE_TUI_CHANNEL=dev OPENCODE_PASSWORD=\"$(opencode service get password)\" exec bun run dev \"$@\" --server \"$(opencode service status)\"' --",
"dev:live": "sh -c 'OPENCODE_TUI_CHANNEL=dev OPENCODE_PASSWORD=\"$(opencode2 service get password)\" exec bun run dev \"$@\" --server \"$(opencode2 service status)\"' --",
"dev:vite": "bun run --cwd packages/cli --conditions=browser dev/vite.ts",
"dev:vite:live": "sh -c 'OPENCODE_TUI_CHANNEL=dev OPENCODE_PASSWORD=\"$(opencode service get password)\" exec bun run dev:vite \"$@\" --server \"$(opencode service status)\"' --",
"dev:vite:live": "sh -c 'OPENCODE_TUI_CHANNEL=dev OPENCODE_PASSWORD=\"$(opencode2 service get password)\" exec bun run dev:vite \"$@\" --server \"$(opencode2 service status)\"' --",
"dev:desktop": "bun --cwd packages/desktop dev",
"dev:web": "bun --cwd packages/app dev",
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
+5 -2
View File
@@ -122,14 +122,13 @@ Keep provider facades small and explicit:
### Provider Package Entrypoints
Catalog-selected native providers use package-like export paths from `@opencode/ai`. They are internal entrypoints in one npm package, not separately published provider packages. Every entrypoint implements `ProviderPackage.Definition` and exposes `model(modelID, settings)`, where settings are one flat serializable object: the connection keys the entrypoint declares (`apiKey`, `baseURL`, `region`, …), the common `headers` and `body` overlays, and the protocol's request options (`reasoningEffort`, `thinking`, …) side by side. Each entrypoint destructures its own connection keys and passes the rest to the route as `providerOptions`; there is no nested `providerOptions` at the entrypoint.
Catalog-selected native providers use package-like export paths from `@opencode/ai`. They are internal entrypoints in one npm package, not separately published provider packages. Every entrypoint implements `ProviderPackage.Definition` and exposes `model(modelID, settings)`, where settings are serializable provider configuration plus common `headers`, `body`, and `limits` overlays.
```ts
import { model } from "@opencode/ai/providers/openai/responses"
const selected = model("gpt-5", {
apiKey,
reasoningEffort: "high",
})
```
@@ -166,6 +165,10 @@ Native chronological system messages are route/model-specific. Open Responses lo
The wrapped-user fallback preserves ordering while visibly lowering authority. Never silently pass a raw chronological `role: "system"` through a route that might reject it. Do not insert raw retrieved documents, tool output, or web content into privileged chronological system updates; keep untrusted content in ordinary user/tool channels.
### Effort Updates
`Message.effort({ effort, previous })` is a chronological "reasoning effort changed here" marker (`undefined` means the model default). Changing a top-level effort invalidates the whole provider prompt cache, so protocols with a native per-message update (`Protocol.supportsEffortUpdates`) keep the top-level effort at the first marker's `previous` and lower each marker in place: Anthropic Messages emits an empty `role: "system"` message with `output_config.effort` plus the `mid-conversation-output-config-2026-07-01` beta, and OpenAI Responses emits `configuration_update` items. `applyEffortUpdates` runs in `prepareRequest` and strips the markers for every other route, so a protocol without support keeps today's plain top-level behaviour. When the last marker disagrees with the effort the request asks for (reverted or forked history), `resolveEffortUpdates` strips the markers and falls back to a plain top-level change.
### Tools
Tool loops are represented in common messages and events:
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "2.0.0",
"version": "1.17.20",
"name": "@opencode/ai",
"type": "module",
"license": "MIT",
+9 -2
View File
@@ -12,6 +12,7 @@
// count against the four-breakpoint budget; auto only fills remaining slots.
import { CacheHint, type CachePolicy, type CachePolicyObject } from "./schema/options.js"
import { LLMRequest, Message, ToolDefinition, type ContentPart, type ToolEntry } from "./schema/messages.js"
import { effortUpdate } from "./effort-updates.js"
const AUTO: CachePolicyObject = {
tools: true,
@@ -121,9 +122,15 @@ const markMessages = (
return markMessageAt(messages, lastIndexOfRole(messages, "user"), hint, budget)
if (strategy === "latest-assistant")
return markMessageAt(messages, lastIndexOfRole(messages, "assistant"), hint, budget)
const start = Math.max(0, messages.length - strategy.tail)
let start = messages.length
let remaining = strategy.tail
while (remaining > 0 && start > 0) {
start -= 1
if (effortUpdate(messages[start]!) === undefined) remaining -= 1
}
let next = messages
for (let i = start; i < messages.length; i++) next = markMessageAt(next, i, hint, budget)
for (let i = start; i < messages.length; i++)
if (effortUpdate(messages[i]!) === undefined) next = markMessageAt(next, i, hint, budget)
return next
}
+27
View File
@@ -0,0 +1,27 @@
// A top-level reasoning effort change invalidates the whole provider prompt cache, so a
// mid-conversation switch travels as a `Message.effort(...)` marker: protocols with a native
// per-message update freeze the top-level effort and lower the markers; every other route strips them.
import { LLMRequest, type EffortPart, type Message } from "./schema/messages.js"
export const effortUpdate = (message: Message): EffortPart | undefined => {
if (message.role !== "system" || message.content.length !== 1) return undefined
const part = message.content[0]
return part.type === "effort" ? part : undefined
}
export const stripEffortUpdates = (request: LLMRequest) => {
const messages = request.messages.filter((message) => effortUpdate(message) === undefined)
return messages.length === request.messages.length ? request : LLMRequest.update(request, { messages })
}
export const applyEffortUpdates = (request: LLMRequest): LLMRequest =>
request.model.route.supportsEffortUpdates?.(request) ? request : stripEffortUpdates(request)
// The markers must end at `current`: `revert.ts` never touches `session.model` and forks may select
// another variant, so on disagreement fall back to a plain top-level change instead of misreporting effort.
export const resolveEffortUpdates = (request: LLMRequest, current: string | undefined) => {
const updates = request.messages.flatMap((message) => effortUpdate(message) ?? [])
if (updates.length === 0) return { request, effort: current }
if (updates.at(-1)?.effort !== current) return { request: stripEffortUpdates(request), effort: current }
return { request, effort: updates[0]?.previous }
}
+51 -22
View File
@@ -27,6 +27,7 @@ import {
} from "../schema/index.js"
import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js"
import { classifyProviderFailure } from "../provider-error.js"
import { effortUpdate, resolveEffortUpdates } from "../effort-updates.js"
import * as Cache from "./utils/cache.js"
import { Lifecycle } from "./utils/lifecycle.js"
import { ToolSchemaProjection } from "./utils/tool-schema.js"
@@ -36,6 +37,7 @@ const ADAPTER = "anthropic-messages"
export const DEFAULT_BASE_URL = "https://api.anthropic.com/v1"
export const PATH = "/messages"
export const DEFAULT_MAX_TOKENS = 32_000
const DEFAULT_EFFORT = "high"
const SSE_EVENTS = new Set([
"message",
@@ -286,7 +288,11 @@ type AnthropicToolResultBlock = Schema.Schema.Type<typeof AnthropicToolResultBlo
const AnthropicMessage = Schema.Union([
Schema.Struct({ role: Schema.Literal("user"), content: Schema.Array(AnthropicUserBlock) }),
Schema.Struct({ role: Schema.Literal("assistant"), content: Schema.Array(AnthropicAssistantBlock) }),
Schema.Struct({ role: Schema.Literal("system"), content: Schema.Array(AnthropicTextBlock) }),
Schema.Struct({
role: Schema.Literal("system"),
content: Schema.Array(AnthropicTextBlock),
output_config: Schema.optional(Schema.Struct({ effort: Schema.String })),
}),
]).pipe(Schema.toTaggedUnion("role"))
type AnthropicMessage = Schema.Schema.Type<typeof AnthropicMessage>
@@ -877,6 +883,12 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (
for (const [index, message] of request.messages.entries()) {
if (message.role === "system") {
const update = effortUpdate(message)
if (update) {
// Accepted at any position, so the text-update placement rules do not apply.
messages.push({ role: "system", content: [], output_config: { effort: update.effort ?? DEFAULT_EFFORT } })
continue
}
if (splitsLocalToolResults(request.messages, index))
return yield* invalid("Anthropic Messages system updates cannot split a local tool call from its tool result")
if (supportsNativeSystemUpdates(request) && canUseNativeSystemUpdate(request, index)) {
@@ -1034,18 +1046,11 @@ const resolveOptions = Effect.fn("AnthropicMessages.resolveOptions")(function* (
ProviderShared.isRecord(rawOutputConfig) && ProviderShared.isRecord(rawOutputConfig.format)
? (rawOutputConfig.format as { type: "json_schema"; schema: Record<string, unknown> })
: undefined
const output_config =
outputConfigEffort === undefined && outputConfigFormat === undefined
? undefined
: {
...(outputConfigEffort === undefined ? {} : { effort: outputConfigEffort }),
...(outputConfigFormat === undefined ? {} : { format: outputConfigFormat }),
}
const thinking = yield* resolveThinking(input?.thinking)
return {
thinking: applyThinkingBindingDefault(request.model, thinking),
effort: outputConfigEffort,
output_config,
format: outputConfigFormat,
service_tier,
metadata,
container,
@@ -1054,15 +1059,30 @@ const resolveOptions = Effect.fn("AnthropicMessages.resolveOptions")(function* (
}
})
// Accept gateway namespaces and Vertex suffixes without treating a snapshot date as a minor version.
const claudeVersion = (id: string) => {
const match = /(?:^|[./])claude-(?<family>[a-z]+)-(?<major>\d+)(?:[.-](?<minor>\d{1,2}))?(?:$|[-:@])/.exec(
id.toLowerCase(),
)?.groups
if (!match) return undefined
return { family: match.family, major: Number(match.major), minor: Number(match.minor ?? 0) }
}
const supportsThinkingBlockBinding = (model: LLMRequest["model"]) => {
const override = model.compatibility?.supportsThinkingBlockBinding
if (override !== undefined) return override
// Accept gateway namespaces and Vertex suffixes without treating a snapshot date as a minor version.
const version = /(?:^|[./])claude-[a-z]+-(?<major>\d+)(?:[.-](?<minor>\d{1,2}))?(?:$|[-:@])/i.exec(model.id)?.groups
if (!version) return false
const major = Number(version.major)
const minor = Number(version.minor ?? 0)
return major > 5 || (major === 5 && minor >= 1)
const version = claudeVersion(model.id)
return version !== undefined && (version.major > 5 || (version.major === 5 && version.minor >= 1))
}
const supportsEffortUpdates = (model: LLMRequest["model"]) => {
const override = model.compatibility?.supportsEffortUpdates
if (override !== undefined) return override
const version = claudeVersion(model.id)
if (version === undefined) return false
if (version.family === "opus") return version.major >= 5
if (version.family !== "fable" && version.family !== "mythos") return false
return version.major > 5 || (version.major === 5 && version.minor >= 1)
}
const applyThinkingBindingDefault = (model: LLMRequest["model"], thinking: AnthropicThinking | undefined) => {
@@ -1104,13 +1124,15 @@ const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (reques
const management = yield* ProviderShared.validateWith(
Schema.decodeUnknownEffect(Schema.UndefinedOr(ContextManagement)),
)(request.providerOptions?.contextManagement)
const options = yield* resolveOptions(request)
const updates = resolveEffortUpdates(request, options.effort)
const generation = request.generation
const toolSchemaCompatibility = request.model.compatibility?.toolSchema
// Allocate the 4-breakpoint budget in invalidation order: tools → system →
// messages. Tools live highest in the cache hierarchy, so when callers
// over-mark we keep their tool hints and shed the message-tail ones first.
const breakpoints = Cache.newBreakpoints(ANTHROPIC_BREAKPOINT_CAP)
const flattened = ProviderShared.flattenToolRequest(request)
const flattened = ProviderShared.flattenToolRequest(updates.request)
const tools =
flattened.tools.length === 0
? undefined
@@ -1138,7 +1160,13 @@ const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (reques
`Anthropic Messages: dropped ${breakpoints.dropped} cache breakpoint(s); the API allows at most ${ANTHROPIC_BREAKPOINT_CAP} per request.`,
)
}
const options = yield* resolveOptions(request)
const output_config =
updates.effort === undefined && options.format === undefined
? undefined
: {
...(updates.effort === undefined ? {} : { effort: updates.effort }),
...(options.format === undefined ? {} : { format: options.format }),
}
const body = {
model: request.model.id,
system,
@@ -1152,7 +1180,7 @@ const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (reques
top_k: generation?.topK,
stop_sequences: generation?.stop,
thinking: options.thinking,
output_config: options.output_config,
output_config,
// top-level passthrough per SDK MessageCreateParamsBase:4638,4643,4649,4654,4670
cache_control: options.cache_control,
container: options.container,
@@ -1677,6 +1705,7 @@ export const protocol = Protocol.make({
}),
step,
},
supportsEffortUpdates: (request) => supportsEffortUpdates(request.model),
})
export const transport = <
@@ -1708,16 +1737,16 @@ export const transport = <
}
function requiredBetaHeaders(body: Pick<AnthropicMessagesBody, "messages" | "context_management" | "thinking">) {
// Always request interleaved thinking. The API accepts the header on any
// model and ignores it where unsupported, while manual-thinking models need
// it for thinking between tool calls.
const betas: string[] = ["interleaved-thinking-2025-05-14"]
const betas: string[] = []
const requestsCompaction = (body.context_management?.edits.length ?? 0) > 0
const replaysCompaction = body.messages.some((message) =>
message.content.some((block) => block.type === "compaction"),
)
if (requestsCompaction || replaysCompaction) betas.push("compact-2026-01-12")
if (body.messages.some((message) => message.role === "system" && message.output_config !== undefined))
betas.push("mid-conversation-output-config-2026-07-01")
const thinking = body.thinking
if (thinking && thinking.type !== "disabled" && thinking.block_binding)
betas.push("thinking-binding-controls-2026-08-01")
+20 -2
View File
@@ -20,6 +20,7 @@ import {
} from "../schema/index.js"
import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js"
import { classifyProviderFailure } from "../provider-error.js"
import { effortUpdate } from "../effort-updates.js"
import { OpenResponsesOptions } from "./utils/open-responses-options.js"
import { Lifecycle } from "./utils/lifecycle.js"
import { ToolSchemaProjection } from "./utils/tool-schema.js"
@@ -164,6 +165,13 @@ export const CompactionItem = Schema.Struct({
encrypted_content: Schema.String,
})
// Kept out of the baseline `InputItem` union: only the OpenAI extension accepts it.
export const ConfigurationUpdate = Schema.Struct({
type: Schema.Literal("configuration_update"),
reasoning: Schema.Struct({ effort: OpenResponsesOptions.ReasoningEffort }),
})
type ConfigurationUpdate = Schema.Schema.Type<typeof ConfigurationUpdate>
export const InputItem = Schema.Union([
CompactionItem,
Schema.Struct({ role: Schema.tag("system"), content: Schema.String }),
@@ -208,6 +216,7 @@ export type HostedToolReplayItem = {
type LoweredInputItem =
| OpenResponsesInputItem
| HostedToolReplayItem
| ConfigurationUpdate
| {
readonly type: "message"
readonly id?: string
@@ -634,6 +643,8 @@ const lowerToolResultOutput = Effect.fnUntraced(function* (
return yield* Effect.forEach(content, (item) => lowerToolResultContentItem(item, request, adapter))
})
const DEFAULT_EFFORT = "medium"
const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (
request: LLMRequest,
adapter: ProviderAdapter,
@@ -646,6 +657,14 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (
Schema.decodeUnknownEffect(Schema.UndefinedOr(MessageMetadata)),
)(message.providerMetadata?.[providerMetadataKey])
if (message.role === "system") {
const update = effortUpdate(message)
if (update) {
// Consecutive updates are rejected, so a newer one replaces its predecessor.
const last = input.at(-1)
if (last !== undefined && "type" in last && last.type === "configuration_update") input.pop()
input.push({ type: "configuration_update", reasoning: { effort: update.effort ?? DEFAULT_EFFORT } })
continue
}
input.push({
role: "developer",
content: ProviderShared.joinText(yield* ProviderShared.systemUpdateText(adapter.name, message)),
@@ -789,8 +808,7 @@ export const lowerConversation = Effect.fn("OpenResponses.lowerConversation")(fu
}
})
export const lowerGeneration = (request: LLMRequest) => {
const options = OpenResponsesOptions.resolve(request)
export const lowerGeneration = (request: LLMRequest, options = OpenResponsesOptions.resolve(request)) => {
const generation = request.generation
const cacheKey = ProviderShared.promptCacheKey(request)
const parallelToolCalls = resolveParallelToolCalls(request)
+23 -4
View File
@@ -6,7 +6,9 @@ import { Endpoint } from "../route/endpoint.js"
import { Protocol } from "../route/protocol.js"
import { HttpTransport } from "../route/transport/index.js"
import { LLMRequest, mergeJsonRecords, type JsonSchema, type ToolDefinition, type ToolEntry } from "../schema/index.js"
import { resolveEffortUpdates } from "../effort-updates.js"
import { OpenResponses } from "./open-responses.js"
import { OpenResponsesOptions } from "./utils/open-responses-options.js"
import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js"
import { OpenAIImage } from "./utils/openai-image.js"
import { ResponsesHostedTools } from "./utils/responses-hosted-tools.js"
@@ -94,9 +96,15 @@ const OpenAIResponsesToolChoice = Schema.Union([
Schema.Struct({ type: Schema.tag("image_generation") }),
])
const OpenAIResponsesInputItem = Schema.Union([
OpenResponses.InputItem,
OpenAIResponsesHostedToolItem,
OpenResponses.ConfigurationUpdate,
])
const OpenAIResponsesCoreFields = {
...OpenResponses.coreFields,
input: Schema.Array(Schema.Union([OpenResponses.InputItem, OpenAIResponsesHostedToolItem])),
input: Schema.Array(OpenAIResponsesInputItem),
tools: optionalArray(OpenAIResponsesTools),
tool_choice: Schema.optional(OpenAIResponsesToolChoice),
context_management: Schema.optional(
@@ -119,7 +127,7 @@ export type OpenAIResponsesBody = Schema.Schema.Type<typeof OpenAIResponsesBody>
export const CompactionTrigger = Schema.Struct({ type: Schema.Literal("compaction_trigger") })
const CheckpointBody = Schema.Struct({
...OpenAIResponsesBody.fields,
input: Schema.Array(Schema.Union([OpenResponses.InputItem, OpenAIResponsesHostedToolItem, CompactionTrigger])),
input: Schema.Array(Schema.Union([OpenAIResponsesInputItem, CompactionTrigger])),
store: Schema.Literal(false),
prompt_cache_retention: optionalNull(Schema.String),
prompt_cache_options: optionalNull(
@@ -133,6 +141,14 @@ const adapter = {
restoreHostedToolItem: (item: unknown) => (Schema.is(OpenAIResponsesHostedToolItem)(item) ? item : undefined),
} satisfies OpenResponses.ProviderAdapter
// Only GPT-6 Astra accepts `configuration_update`, and never alongside automatic `context_management` compaction.
const supportsEffortUpdates = (request: LLMRequest) => {
if (request.providerOptions?.contextManagement !== undefined) return false
const override = request.model.compatibility?.supportsEffortUpdates
if (override !== undefined) return override
return /(?:^|\/)gpt-6-astra$/i.test(request.model.id)
}
const nativeImageToolInput = (tool: ToolDefinition) => {
const native = tool.native?.openai
return ProviderShared.isRecord(native) && native.type === "image_generation" ? native : undefined
@@ -189,10 +205,12 @@ const fromRequest = Effect.fn("OpenAIResponses.fromRequest")(function* (request:
const management = yield* ProviderShared.validateWith(
Schema.decodeUnknownEffect(Schema.UndefinedOr(ContextManagement)),
)(request.providerOptions?.contextManagement)
const options = OpenResponsesOptions.resolve(request)
const updates = resolveEffortUpdates(request, options.reasoningEffort)
const toolSchemaCompatibility = request.model.compatibility?.toolSchema
return yield* decodeBody({
...(yield* OpenResponses.lowerConversation(request, adapter)),
...OpenResponses.lowerGeneration(request),
...(yield* OpenResponses.lowerConversation(updates.request, adapter)),
...OpenResponses.lowerGeneration(request, { ...options, reasoningEffort: updates.effort }),
context_management: management?.map((edit) => ({ type: edit.type, compact_threshold: edit.compactThreshold })),
tools:
request.tools.length === 0
@@ -295,6 +313,7 @@ export const protocol = Protocol.make({
step,
terminal: OpenResponses.terminal,
},
supportsEffortUpdates,
})
const endpoint = Endpoint.path<OpenAIResponsesBody>(PATH, { baseURL: DEFAULT_BASE_URL })
@@ -1,12 +1,7 @@
import { Option, Schema } from "effect"
import type { LLMRequest } from "../../schema/index.js"
import { ReasoningEffort, ReasoningEfforts, type LLMRequest } from "../../schema/index.js"
export const ReasoningEfforts = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] as const
export type ReasoningEffort = (typeof ReasoningEfforts)[number] | (string & {})
export const ReasoningEffort = Schema.declare<ReasoningEffort>(
(value): value is ReasoningEffort => typeof value === "string",
{ title: "ReasoningEffort" },
)
export { ReasoningEffort, ReasoningEfforts }
export const TextVerbosities = ["low", "medium", "high"] as const
export type TextVerbosity = (typeof TextVerbosities)[number] | (string & {})
@@ -11,6 +11,7 @@ import {
mergeJsonRecords,
} from "../../schema/index.js"
import type { CompactOperation } from "../../route/client.js"
import { stripEffortUpdates } from "../../effort-updates.js"
import { Endpoint } from "../../route/endpoint.js"
import { RequestExecutor } from "../../route/executor.js"
import { HttpTransport } from "../../route/transport/index.js"
@@ -75,7 +76,8 @@ const Response = Schema.Struct({
export const make = (adapter: OpenResponses.ProviderAdapter): CompactOperation =>
Effect.fn("ResponsesCompaction.execute")(function* (request, executor, options) {
const route = request.model.route
const native = yield* OpenResponses.lowerConversation(request, adapter)
// The standalone compaction endpoint rejects histories containing configuration updates.
const native = yield* OpenResponses.lowerConversation(stripEffortUpdates(request), adapter)
const body = yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(Body))(
mergeJsonRecords(
{
-4
View File
@@ -1,10 +1,6 @@
import type { LanguageModel, ProviderOptions } from "./schema/index.js"
import type { CompactionOperations } from "./route/client.js"
/**
* Flat, serializable settings for `model(modelID, settings)`. Each entrypoint declares the connection keys it
* reads; every other key is a request option for the route's protocol.
*/
export interface Settings extends Readonly<Record<string, unknown>> {
readonly baseURL?: string
readonly headers?: Readonly<Record<string, string>>
+3 -8
View File
@@ -1,4 +1,3 @@
import { Struct } from "effect"
import type { ProviderPackage } from "../provider-package.js"
import { AlibabaChat } from "../protocols/alibaba-chat.js"
import { AlibabaMessages } from "../protocols/alibaba-messages.js"
@@ -35,9 +34,9 @@ export type Config = Location &
readonly providerOptions?: ChatOptionsInput | MessagesOptionsInput | ResponsesOptionsInput
}
export type Settings<Options = ChatOptionsInput> = Location &
ProviderPackage.Settings &
Options & {
ProviderPackage.Settings & {
readonly apiKey?: string
readonly providerOptions?: Options
}
const hosts = new Map<string, string>([
@@ -121,11 +120,7 @@ export const responsesModel: ProviderPackage.Definition<
function fromSettings(input: Settings<Config["providerOptions"]>) {
const { body, ...rest } = input
return configure({
...rest,
http: body === undefined ? undefined : { body },
providerOptions: Struct.omit(rest, ["apiKey", "baseURL", "headers", "region", "workspaceID"]),
})
return configure({ ...rest, http: body === undefined ? undefined : { body } })
}
export const webSearch = () => hostedTool("web_search", "Search the web with Alibaba's hosted search tool.")
@@ -23,16 +23,16 @@ export type Config = RouteDefaultsInput & {
readonly providerOptions?: OpenAIProviderOptionsInput
}
export type Settings = ProviderPackage.Settings &
OpenAIProviderOptionsInput & {
readonly apiKey?: string
readonly auth?: "bearer" | "sigv4"
readonly baseURL?: string
readonly credentials?: Credentials
readonly profile?: string
readonly region?: string
readonly topP?: number
}
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly auth?: "bearer" | "sigv4"
readonly baseURL?: string
readonly credentials?: Credentials
readonly profile?: string
readonly region?: string
readonly topP?: number
readonly providerOptions?: OpenAIProviderOptionsInput
}
const responsesRoute = Route.make({
id: "bedrock-mantle-responses",
@@ -108,29 +108,18 @@ export const configure = (input: Config = {}) => {
export const provider = configure()
const fromSettings = ({
apiKey,
auth,
baseURL,
body,
credentials,
headers,
profile,
region,
topP,
...providerOptions
}: Settings) =>
const fromSettings = (settings: Settings) =>
configure({
apiKey,
auth,
baseURL,
credentials,
generation: topP === undefined ? undefined : { topP },
headers: headers === undefined ? undefined : { ...headers },
http: body === undefined ? undefined : { body: { ...body } },
profile,
providerOptions,
region,
apiKey: settings.apiKey,
auth: settings.auth,
baseURL: settings.baseURL,
credentials: settings.credentials,
generation: settings.topP === undefined ? undefined : { topP: settings.topP },
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
profile: settings.profile,
providerOptions: settings.providerOptions,
region: settings.region,
})
export const chatModel: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (
@@ -19,13 +19,13 @@ export type Config = RouteDefaultsInput &
}
export type Settings = ProviderPackage.Settings &
AnthropicMessages.ProviderOptionsInput &
(
| { readonly apiKey?: string; readonly authToken?: never }
| { readonly apiKey?: never; readonly authToken?: string }
) & {
readonly baseURL: string
readonly provider?: string
readonly providerOptions?: AnthropicMessages.ProviderOptionsInput
}
export const routes = [AnthropicMessages.route]
@@ -63,20 +63,22 @@ export const provider = {
export const model: ProviderPackage.Definition<Settings, AnthropicMessages.ProviderOptionsInput>["model"] = (
modelID,
{ apiKey, authToken, baseURL, body, headers, provider, ...providerOptions },
settings,
) => {
if (apiKey !== undefined && authToken !== undefined)
// Read before the exclusivity check narrows a conflicting settings object to `never`.
const provider = ProviderID.make(settings.provider ?? id)
if (settings.apiKey !== undefined && settings.authToken !== undefined)
throw new ProviderConfigurationError({
provider: ProviderID.make(provider ?? id),
provider,
message: "Anthropic-compatible apiKey cannot be combined with authToken",
})
return configure({
...(authToken === undefined ? { apiKey: apiKey } : { auth: Auth.bearer(authToken) }),
baseURL,
headers: headers === undefined ? undefined : { ...headers },
http: body === undefined ? undefined : { body: { ...body } },
provider,
providerOptions,
...(settings.authToken === undefined ? { apiKey: settings.apiKey } : { auth: Auth.bearer(settings.authToken) }),
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
provider: settings.provider,
providerOptions: settings.providerOptions,
}).model(modelID)
}
+8 -8
View File
@@ -21,12 +21,12 @@ export type Config = RouteDefaultsInput &
}
export type Settings = ProviderPackage.Settings &
AnthropicMessages.ProviderOptionsInput &
(
| { readonly apiKey?: string; readonly authToken?: never }
| { readonly apiKey?: never; readonly authToken?: string }
) & {
readonly baseURL?: string
readonly providerOptions?: AnthropicMessages.ProviderOptionsInput
}
const auth = (options: ProviderAuthOption<"optional">) => {
@@ -54,18 +54,18 @@ export const configure = (input: Config = {}) => {
export const provider = configure()
export const model: ProviderPackage.Definition<Settings, AnthropicMessages.ProviderOptionsInput>["model"] = (
modelID,
{ apiKey, authToken, baseURL, body, headers, ...providerOptions },
settings,
) => {
if (apiKey !== undefined && authToken !== undefined)
if (settings.apiKey !== undefined && settings.authToken !== undefined)
throw new ProviderConfigurationError({
provider: id,
message: "Anthropic apiKey cannot be combined with authToken",
})
return configure({
...(authToken === undefined ? { apiKey: apiKey } : { auth: Auth.bearer(authToken) }),
baseURL,
headers: headers === undefined ? undefined : { ...headers },
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
...(settings.authToken === undefined ? { apiKey: settings.apiKey } : { auth: Auth.bearer(settings.authToken) }),
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).model(modelID)
}
+11 -21
View File
@@ -28,12 +28,12 @@ export type LanguageModelOptions = AzureURL &
export type Config = LanguageModelOptions
export type Settings = ProviderPackage.Settings &
OpenAIProviderOptionsInput &
AzureURL & {
readonly apiKey?: string
readonly apiVersion?: string
readonly queryParams?: Readonly<Record<string, string>>
readonly useDeploymentBasedUrls?: boolean
readonly providerOptions?: OpenAIProviderOptionsInput
}
const resourceBaseURL = (resourceName: string) => `https://${resourceName.trim()}.openai.azure.com/openai`
@@ -151,28 +151,18 @@ export const provider = {
configure,
}
const config = ({
apiKey,
apiVersion,
baseURL,
body,
headers,
queryParams,
resourceName,
useDeploymentBasedUrls,
...providerOptions
}: Settings): Config => {
const config = (settings: Settings): Config => {
const common = {
apiKey,
apiVersion,
headers: headers === undefined ? undefined : { ...headers },
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
queryParams: queryParams === undefined ? undefined : { ...queryParams },
useDeploymentBasedUrls,
apiKey: settings.apiKey,
apiVersion: settings.apiVersion,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
queryParams: settings.queryParams === undefined ? undefined : { ...settings.queryParams },
useDeploymentBasedUrls: settings.useDeploymentBasedUrls,
}
if (baseURL !== undefined) return { ...common, baseURL }
if (resourceName !== undefined) return { ...common, resourceName }
if (settings.baseURL !== undefined) return { ...common, baseURL: settings.baseURL }
if (settings.resourceName !== undefined) return { ...common, resourceName: settings.resourceName }
throw new ProviderConfigurationError({ provider: id, message: "Azure requires resourceName or baseURL" })
}
+11 -14
View File
@@ -15,11 +15,11 @@ export type LanguageModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
readonly providerOptions?: OpenAIProviderOptionsInput
}
export type Settings = ProviderPackage.Settings &
OpenAIProviderOptionsInput & {
readonly apiKey?: string
readonly baseURL?: string
}
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL?: string
readonly providerOptions?: OpenAIProviderOptionsInput
}
export const route = Route.make({
id: "baseten-chat",
@@ -48,16 +48,13 @@ export const configure = (input: LanguageModelOptions = {}) => {
export const provider = configure()
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, ...providerOptions },
) =>
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (modelID, settings) =>
configure({
apiKey,
baseURL,
headers: headers === undefined ? undefined : { ...headers },
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).model(modelID)
export * as Baseten from "./baseten.js"
+11 -14
View File
@@ -15,11 +15,11 @@ export type LanguageModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
readonly providerOptions?: OpenAIProviderOptionsInput
}
export type Settings = ProviderPackage.Settings &
OpenAIProviderOptionsInput & {
readonly apiKey?: string
readonly baseURL?: string
}
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL?: string
readonly providerOptions?: OpenAIProviderOptionsInput
}
export const route = Route.make({
id: "cerebras-chat",
@@ -52,14 +52,11 @@ export const configure = (input: LanguageModelOptions = {}) => {
export const provider = configure()
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, ...providerOptions },
) =>
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (modelID, settings) =>
configure({
apiKey,
baseURL,
headers: headers === undefined ? undefined : { ...headers },
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).model(modelID)
@@ -27,10 +27,10 @@ export type LanguageModelOptions = GatewayURL &
}
export type Settings = ProviderPackage.Settings &
OpenAIProviderOptionsInput &
GatewayURL & {
readonly apiKey?: string
readonly gatewayApiKey?: string
readonly providerOptions?: OpenAIProviderOptionsInput
}
export const baseURL = (input: GatewayURL) => {
@@ -89,25 +89,14 @@ export const configure = (input: LanguageModelOptions) => {
export const provider = { id, configure }
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (modelID, settings) => {
const {
accountId: _,
apiKey,
baseURL: _url,
body,
gatewayApiKey,
gatewayId: _id,
headers,
...providerOptions
} = settings
return configure({
apiKey,
gatewayApiKey,
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (modelID, settings) =>
configure({
apiKey: settings.apiKey,
gatewayApiKey: settings.gatewayApiKey,
baseURL: baseURL(settings),
headers: headers === undefined ? undefined : { ...headers },
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).model(modelID)
}
export * as CloudflareAIGateway from "./cloudflare-ai-gateway.js"
@@ -21,9 +21,9 @@ export type LanguageModelOptions = WorkersAIURL &
}
export type Settings = ProviderPackage.Settings &
OpenAIProviderOptionsInput &
WorkersAIURL & {
readonly apiKey?: string
readonly providerOptions?: OpenAIProviderOptionsInput
}
export const baseURL = (input: WorkersAIURL) => {
@@ -63,15 +63,13 @@ export const configure = (input: LanguageModelOptions) => {
export const provider = { id, configure }
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (modelID, settings) => {
const { accountId: _, apiKey, baseURL: _url, body, headers, ...providerOptions } = settings
return configure({
apiKey,
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (modelID, settings) =>
configure({
apiKey: settings.apiKey,
baseURL: baseURL(settings),
headers: headers === undefined ? undefined : { ...headers },
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).model(modelID)
}
export * as CloudflareWorkersAI from "./cloudflare-workers-ai.js"
+11 -14
View File
@@ -15,11 +15,11 @@ export type LanguageModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
readonly providerOptions?: OpenAIProviderOptionsInput
}
export type Settings = ProviderPackage.Settings &
OpenAIProviderOptionsInput & {
readonly apiKey?: string
readonly baseURL?: string
}
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL?: string
readonly providerOptions?: OpenAIProviderOptionsInput
}
export const route = Route.make({
id: "deepinfra-chat",
@@ -55,14 +55,11 @@ export const configure = (input: LanguageModelOptions = {}) => {
export const provider = configure()
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, ...providerOptions },
) =>
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (modelID, settings) =>
configure({
apiKey,
baseURL,
headers: headers === undefined ? undefined : { ...headers },
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).model(modelID)
+11 -14
View File
@@ -15,11 +15,11 @@ export type LanguageModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
readonly providerOptions?: OpenAIProviderOptionsInput
}
export type Settings = ProviderPackage.Settings &
OpenAIProviderOptionsInput & {
readonly apiKey?: string
readonly baseURL?: string
}
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL?: string
readonly providerOptions?: OpenAIProviderOptionsInput
}
export const route = Route.make({
id: "deepseek-chat",
@@ -52,16 +52,13 @@ export const configure = (input: LanguageModelOptions = {}) => {
export const provider = configure()
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, ...providerOptions },
) =>
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (modelID, settings) =>
configure({
apiKey,
baseURL,
headers: headers === undefined ? undefined : { ...headers },
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).model(modelID)
export * as DeepSeek from "./deepseek.js"
+11 -14
View File
@@ -15,11 +15,11 @@ export type LanguageModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
readonly providerOptions?: OpenAIProviderOptionsInput
}
export type Settings = ProviderPackage.Settings &
OpenAIProviderOptionsInput & {
readonly apiKey?: string
readonly baseURL?: string
}
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL?: string
readonly providerOptions?: OpenAIProviderOptionsInput
}
export const route = Route.make({
id: "fireworks-chat",
@@ -48,16 +48,13 @@ export const configure = (input: LanguageModelOptions = {}) => {
export const provider = configure()
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, ...providerOptions },
) =>
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (modelID, settings) =>
configure({
apiKey,
baseURL,
headers: headers === undefined ? undefined : { ...headers },
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).model(modelID)
export * as Fireworks from "./fireworks.js"
+17 -20
View File
@@ -16,14 +16,14 @@ export type Config = RouteDefaultsInput &
readonly providerOptions?: OpenAIProviderOptionsInput
}
export type Settings = ProviderPackage.Settings &
OpenAIProviderOptionsInput & {
readonly accessToken?: string
readonly apiKey?: never
readonly baseURL?: string
readonly location?: string
readonly project?: string
}
export interface Settings extends ProviderPackage.Settings {
readonly accessToken?: string
readonly apiKey?: never
readonly baseURL?: string
readonly location?: string
readonly project?: string
readonly providerOptions?: OpenAIProviderOptionsInput
}
const route = Route.make({
id: "google-vertex-chat",
@@ -74,19 +74,16 @@ export const provider = {
configure,
}
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (
modelID,
{ accessToken, apiKey, baseURL, body, headers, location, project, ...providerOptions },
) => {
if (apiKey !== undefined)
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (modelID, settings) => {
if (settings.apiKey !== undefined)
throw new ProviderConfigurationError({ provider: id, message: "Google Vertex Chat does not support API keys" })
return configure({
accessToken,
baseURL,
headers: headers === undefined ? undefined : { ...headers },
http: body === undefined ? undefined : { body: { ...body } },
location,
project,
providerOptions,
accessToken: settings.accessToken,
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
location: settings.location,
project: settings.project,
providerOptions: settings.providerOptions,
}).model(modelID)
}
@@ -25,14 +25,14 @@ export type Config = RouteDefaultsInput &
readonly providerOptions?: AnthropicMessages.ProviderOptionsInput
}
export type Settings = ProviderPackage.Settings &
AnthropicMessages.ProviderOptionsInput & {
readonly accessToken?: string
readonly apiKey?: never
readonly baseURL?: string
readonly location?: string
readonly project?: string
}
export interface Settings extends ProviderPackage.Settings {
readonly accessToken?: string
readonly apiKey?: never
readonly baseURL?: string
readonly location?: string
readonly project?: string
readonly providerOptions?: AnthropicMessages.ProviderOptionsInput
}
const route = Route.make({
id: "google-vertex-messages",
@@ -105,17 +105,17 @@ export const provider = {
export const model: ProviderPackage.Definition<Settings, AnthropicMessages.ProviderOptionsInput>["model"] = (
modelID,
{ accessToken, apiKey, baseURL, body, headers, location, project, ...providerOptions },
settings,
) => {
if (apiKey !== undefined)
if (settings.apiKey !== undefined)
throw new ProviderConfigurationError({ provider: id, message: "Google Vertex Messages does not support API keys" })
return configure({
accessToken,
baseURL,
headers: headers === undefined ? undefined : { ...headers },
http: body === undefined ? undefined : { body: { ...body } },
location,
project,
providerOptions,
accessToken: settings.accessToken,
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
location: settings.location,
project: settings.project,
providerOptions: settings.providerOptions,
}).model(modelID)
}
@@ -16,14 +16,14 @@ export type Config = RouteDefaultsInput &
readonly providerOptions?: OpenResponsesProviderOptionsInput
}
export type Settings = ProviderPackage.Settings &
OpenResponsesProviderOptionsInput & {
readonly accessToken?: string
readonly apiKey?: never
readonly baseURL?: string
readonly location?: string
readonly project?: string
}
export interface Settings extends ProviderPackage.Settings {
readonly accessToken?: string
readonly apiKey?: never
readonly baseURL?: string
readonly location?: string
readonly project?: string
readonly providerOptions?: OpenResponsesProviderOptionsInput
}
const route = Route.make({
id: "google-vertex-responses",
@@ -77,17 +77,17 @@ export const provider = {
export const model: ProviderPackage.Definition<Settings, OpenResponsesProviderOptionsInput>["model"] = (
modelID,
{ accessToken, apiKey, baseURL, body, headers, location, project, ...providerOptions },
settings,
) => {
if (apiKey !== undefined)
if (settings.apiKey !== undefined)
throw new ProviderConfigurationError({ provider: id, message: "Google Vertex Responses does not support API keys" })
return configure({
accessToken,
baseURL,
headers: headers === undefined ? undefined : { ...headers },
http: body === undefined ? undefined : { body: { ...body } },
location,
project,
providerOptions,
accessToken: settings.accessToken,
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
location: settings.location,
project: settings.project,
providerOptions: settings.providerOptions,
}).model(modelID)
}
+10 -13
View File
@@ -26,7 +26,6 @@ export type Config = RouteDefaultsInput &
}
export type Settings = ProviderPackage.Settings &
GeminiProviderOptionsInput &
(
| { readonly accessToken?: string; readonly apiKey?: never }
| { readonly accessToken?: never; readonly apiKey?: string }
@@ -34,6 +33,7 @@ export type Settings = ProviderPackage.Settings &
readonly baseURL?: string
readonly location?: string
readonly project?: string
readonly providerOptions?: GeminiProviderOptionsInput
}
const fromRequest = Effect.fn("GoogleVertex.fromRequest")(function* (request: LLMRequest) {
@@ -124,22 +124,19 @@ export const provider = {
id,
configure,
}
export const model: ProviderPackage.Definition<Settings, GeminiProviderOptionsInput>["model"] = (
modelID,
{ accessToken, apiKey, baseURL, body, headers, location, project, ...providerOptions },
) => {
if (apiKey !== undefined && accessToken !== undefined)
export const model: ProviderPackage.Definition<Settings, GeminiProviderOptionsInput>["model"] = (modelID, settings) => {
if (settings.apiKey !== undefined && settings.accessToken !== undefined)
throw new ProviderConfigurationError({
provider: id,
message: "Google Vertex apiKey cannot be combined with accessToken or auth",
})
return configure({
...(apiKey === undefined ? { accessToken: accessToken } : { apiKey: apiKey }),
baseURL,
headers: headers === undefined ? undefined : { ...headers },
http: body === undefined ? undefined : { body: { ...body } },
location,
project,
providerOptions,
...(settings.apiKey === undefined ? { accessToken: settings.accessToken } : { apiKey: settings.apiKey }),
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
location: settings.location,
project: settings.project,
providerOptions: settings.providerOptions,
}).model(modelID)
}
+11 -14
View File
@@ -20,11 +20,11 @@ export type Config = RouteDefaultsInput &
readonly providerOptions?: Gemini.ProviderOptionsInput
}
export type Settings = ProviderPackage.Settings &
Gemini.ProviderOptionsInput & {
readonly apiKey?: string
readonly baseURL?: string
}
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL?: string
readonly providerOptions?: Gemini.ProviderOptionsInput
}
const auth = (options: ProviderAuthOption<"optional">) => {
if ("auth" in options && options.auth) return options.auth
@@ -57,16 +57,13 @@ export const configure = (input: Config = {}) => {
}
export const provider = configure()
export const model: ProviderPackage.Definition<Settings, Gemini.ProviderOptionsInput>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, ...providerOptions },
) =>
export const model: ProviderPackage.Definition<Settings, Gemini.ProviderOptionsInput>["model"] = (modelID, settings) =>
configure({
apiKey,
baseURL,
headers: headers === undefined ? undefined : { ...headers },
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).model(modelID)
export const image = provider.image
+11 -14
View File
@@ -26,11 +26,11 @@ export type LanguageModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
readonly providerOptions?: ProviderOptions
}
export type Settings = ProviderPackage.Settings &
ProviderOptions & {
readonly apiKey?: string
readonly baseURL?: string
}
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL?: string
readonly providerOptions?: ProviderOptions
}
const Options = Schema.Struct({
includeReasoning: Schema.optional(Schema.Boolean),
@@ -103,16 +103,13 @@ export const configure = (input: LanguageModelOptions = {}) => {
export const provider = configure()
export const model: ProviderPackage.Definition<Settings, ProviderOptions>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, ...providerOptions },
) =>
export const model: ProviderPackage.Definition<Settings, ProviderOptions>["model"] = (modelID, settings) =>
configure({
apiKey,
baseURL,
headers: headers === undefined ? undefined : { ...headers },
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).model(modelID)
export * as Groq from "./groq.js"
+11 -11
View File
@@ -79,11 +79,11 @@ export type LanguageModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
readonly providerOptions?: ProviderOptionsInput
}
export type Settings = ProviderPackage.Settings &
ProviderOptionsInput & {
readonly apiKey?: string
readonly baseURL?: string
}
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL?: string
readonly providerOptions?: ProviderOptionsInput
}
const responsesRoute = Route.make({
id: "meta-responses",
@@ -169,13 +169,13 @@ export const chatModel: ProviderPackage.Definition<Settings, OpenResponsesProvid
export const messagesModel: ProviderPackage.Definition<Settings, MessagesOptionsInput>["model"] = (modelID, settings) =>
fromSettings(settings).messages(modelID)
function fromSettings({ apiKey, baseURL, body, headers, ...providerOptions }: Settings) {
function fromSettings(settings: Settings) {
return configure({
apiKey,
baseURL,
headers,
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers,
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
})
}
+11 -11
View File
@@ -40,11 +40,11 @@ export type Config = Omit<RouteDefaultsInput, "providerOptions"> &
readonly providerOptions?: ProviderOptionsInput
}
export type Settings<Options = MessagesOptionsInput> = ProviderPackage.Settings &
Options & {
readonly apiKey?: string
readonly baseURL?: string
}
export interface Settings<Options = MessagesOptionsInput> extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL?: string
readonly providerOptions?: Options
}
const ChatOptions = Schema.Struct({
thinking: Schema.optional(Schema.Struct({ type: Schema.String })),
@@ -127,14 +127,14 @@ export const provider = configure()
export const model: ProviderPackage.Definition<Settings<MessagesOptionsInput>, MessagesOptionsInput>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, ...providerOptions },
settings,
) =>
configure({
apiKey,
baseURL,
headers,
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers,
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).model(modelID)
export const messages = provider.messages
+6 -9
View File
@@ -3,14 +3,11 @@ import { MiniMax } from "../minimax.js"
export type Settings = MiniMax.Settings<MiniMax.ChatOptionsInput>
export const model: ProviderPackage.Definition<Settings, MiniMax.ChatOptionsInput>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, ...providerOptions },
) =>
export const model: ProviderPackage.Definition<Settings, MiniMax.ChatOptionsInput>["model"] = (modelID, settings) =>
MiniMax.configure({
apiKey,
baseURL,
headers,
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers,
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).chat(modelID)
@@ -5,12 +5,12 @@ export type Settings = MiniMax.Settings<MiniMax.ResponsesOptionsInput>
export const model: ProviderPackage.Definition<Settings, MiniMax.ResponsesOptionsInput>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, ...providerOptions },
settings,
) =>
MiniMax.configure({
apiKey,
baseURL,
headers,
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers,
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).responses(modelID)
+11 -14
View File
@@ -14,11 +14,11 @@ export type LanguageModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
readonly providerOptions?: ProviderOptions
}
export type Settings = ProviderPackage.Settings &
ProviderOptions & {
readonly apiKey?: string
readonly baseURL?: string
}
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL?: string
readonly providerOptions?: ProviderOptions
}
export const route = MistralChat.route
export const routes = [route]
@@ -39,16 +39,13 @@ export const configure = (input: LanguageModelOptions = {}) => {
export const provider = configure()
export const model: ProviderPackage.Definition<Settings, ProviderOptions>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, ...providerOptions },
) =>
export const model: ProviderPackage.Definition<Settings, ProviderOptions>["model"] = (modelID, settings) =>
configure({
apiKey,
baseURL,
headers: headers === undefined ? undefined : { ...headers },
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).model(modelID)
export * as Mistral from "./mistral.js"
+11 -14
View File
@@ -42,11 +42,11 @@ export type Config = Omit<RouteDefaultsInput, "providerOptions"> &
readonly providerOptions?: ChatOptionsInput | MessagesOptionsInput | ResponsesOptionsInput
}
export type Settings<Options = ChatOptionsInput> = ProviderPackage.Settings &
Options & {
readonly apiKey?: string
readonly baseURL?: string
}
export interface Settings<Options = ChatOptionsInput> extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL?: string
readonly providerOptions?: Options
}
const ChatOptions = Schema.Struct({
reasoningEffort: Schema.optional(Schema.String),
@@ -133,16 +133,13 @@ export const chat = provider.chat
export const messages = provider.messages
export const responses = provider.responses
export const model: ProviderPackage.Definition<Settings, ChatOptionsInput>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, ...providerOptions },
) =>
export const model: ProviderPackage.Definition<Settings, ChatOptionsInput>["model"] = (modelID, settings) =>
configure({
apiKey,
baseURL,
headers,
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers,
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).model(modelID)
export * as Moonshot from "./moonshot.js"
@@ -5,12 +5,12 @@ export type Settings = Moonshot.Settings<Moonshot.MessagesOptionsInput>
export const model: ProviderPackage.Definition<Settings, Moonshot.MessagesOptionsInput>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, ...providerOptions },
settings,
) =>
Moonshot.configure({
apiKey,
baseURL,
headers,
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers,
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).messages(modelID)
@@ -5,12 +5,12 @@ export type Settings = Moonshot.Settings<Moonshot.ResponsesOptionsInput>
export const model: ProviderPackage.Definition<Settings, Moonshot.ResponsesOptionsInput>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, ...providerOptions },
settings,
) =>
Moonshot.configure({
apiKey,
baseURL,
headers,
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers,
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).responses(modelID)
@@ -16,12 +16,12 @@ export type Config = RouteDefaultsInput &
readonly providerOptions?: OpenResponsesProviderOptionsInput
}
export type Settings = ProviderPackage.Settings &
OpenResponsesProviderOptionsInput & {
readonly apiKey?: string
readonly baseURL: string
readonly provider?: string
}
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL: string
readonly provider?: string
readonly providerOptions?: OpenResponsesProviderOptionsInput
}
export const routes = [OpenAICompatibleResponses.route]
@@ -48,13 +48,13 @@ export const provider = {
export const model: ProviderPackage.Definition<Settings, OpenResponsesProviderOptionsInput>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, provider, ...providerOptions },
settings,
) =>
configure({
apiKey,
baseURL,
headers: headers === undefined ? undefined : { ...headers },
http: body === undefined ? undefined : { body: { ...body } },
provider,
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
provider: settings.provider,
providerOptions: settings.providerOptions,
}).model(modelID)
+13 -16
View File
@@ -14,12 +14,12 @@ type GenericModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
readonly providerOptions?: OpenAIProviderOptionsInput
}
export type Settings = ProviderPackage.Settings &
OpenAIProviderOptionsInput & {
readonly apiKey?: string
readonly baseURL: string
readonly provider?: string
}
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL: string
readonly provider?: string
readonly providerOptions?: OpenAIProviderOptionsInput
}
export const routes = [OpenAICompatibleChat.route]
@@ -45,17 +45,14 @@ export const provider = {
configure,
}
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, provider, ...providerOptions },
) =>
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (modelID, settings) =>
configure({
apiKey,
baseURL,
headers: headers === undefined ? undefined : { ...headers },
http: body === undefined ? undefined : { body: { ...body } },
provider,
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
provider: settings.provider,
providerOptions: settings.providerOptions,
}).model(modelID)
export * as OpenAICompatible from "./openai-compatible.js"
+17 -26
View File
@@ -57,14 +57,14 @@ export const imageGeneration = (options: ImageGenerationOptions = {}) =>
},
})
export type Settings = ProviderPackage.Settings &
OpenAIProviderOptionsInput & {
readonly apiKey?: string
readonly baseURL?: string
readonly organization?: string
readonly project?: string
readonly queryParams?: Readonly<Record<string, string>>
}
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL?: string
readonly organization?: string
readonly project?: string
readonly queryParams?: Readonly<Record<string, string>>
readonly providerOptions?: OpenAIProviderOptionsInput
}
const auth = (options: ProviderAuthOption<"optional">) => AuthOptions.bearer(options, "OPENAI_API_KEY")
@@ -116,28 +116,19 @@ export const configure = (input: Config = {}) => {
export const provider = configure()
const config = ({
apiKey,
baseURL,
body,
headers: given,
organization,
project,
queryParams,
...providerOptions
}: Settings): Config => {
const config = (settings: Settings): Config => {
const headers = {
...(organization === undefined ? {} : { "OpenAI-Organization": organization }),
...(project === undefined ? {} : { "OpenAI-Project": project }),
...given,
...(settings.organization === undefined ? {} : { "OpenAI-Organization": settings.organization }),
...(settings.project === undefined ? {} : { "OpenAI-Project": settings.project }),
...settings.headers,
}
return {
apiKey,
baseURL,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: Object.keys(headers).length === 0 ? undefined : headers,
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
queryParams: queryParams === undefined ? undefined : { ...queryParams },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
queryParams: settings.queryParams === undefined ? undefined : { ...settings.queryParams },
}
}
+11 -11
View File
@@ -77,11 +77,11 @@ export type LanguageModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
readonly providerOptions?: OpenRouterProviderOptionsInput
}
export type Settings = ProviderPackage.Settings &
OpenRouterProviderOptionsInput & {
readonly apiKey?: string
readonly baseURL?: string
}
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL?: string
readonly providerOptions?: OpenRouterProviderOptionsInput
}
const OpenRouterBody = Schema.StructWithRest(Schema.Struct(OpenAIChat.bodyFields), [
Schema.Record(Schema.String, Schema.Any),
@@ -191,12 +191,12 @@ export const configure = (input: LanguageModelOptions = {}) => {
export const provider = configure()
export const model: ProviderPackage.Definition<Settings, OpenRouterProviderOptionsInput>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, ...providerOptions },
settings,
) =>
configure({
apiKey,
baseURL,
headers,
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers,
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).model(modelID)
+11 -14
View File
@@ -15,11 +15,11 @@ export type LanguageModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
readonly providerOptions?: OpenAIProviderOptionsInput
}
export type Settings = ProviderPackage.Settings &
OpenAIProviderOptionsInput & {
readonly apiKey?: string
readonly baseURL?: string
}
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL?: string
readonly providerOptions?: OpenAIProviderOptionsInput
}
export const route = Route.make({
id: "togetherai-chat",
@@ -52,14 +52,11 @@ export const configure = (input: LanguageModelOptions = {}) => {
export const provider = configure()
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, ...providerOptions },
) =>
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (modelID, settings) =>
configure({
apiKey,
baseURL,
headers: headers === undefined ? undefined : { ...headers },
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).model(modelID)
+11 -11
View File
@@ -20,11 +20,11 @@ export type LanguageModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
readonly providerOptions?: XAIProviderOptionsInput
}
export type Settings = ProviderPackage.Settings &
XAIProviderOptionsInput & {
readonly apiKey?: string
readonly baseURL?: string
}
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL?: string
readonly providerOptions?: XAIProviderOptionsInput
}
export type { XAIImageOptions } from "../protocols/xai-images.js"
@@ -110,13 +110,13 @@ export const model: ProviderPackage.Definition<
Settings,
XAIProviderOptionsInput,
typeof responsesRoute.compact
>["model"] = (modelID, { apiKey, baseURL, body, headers, ...providerOptions }) =>
>["model"] = (modelID, settings) =>
configure({
apiKey,
baseURL,
headers,
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers,
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).model(modelID)
export const responses = provider.responses
export const chat = provider.chat
+11 -14
View File
@@ -23,11 +23,11 @@ export type Config = Omit<RouteDefaultsInput, "providerOptions"> &
readonly providerOptions?: ChatOptionsInput | MessagesOptionsInput | ResponsesOptionsInput
}
export type Settings<Options = ChatOptionsInput> = ProviderPackage.Settings &
Options & {
readonly apiKey?: string
readonly baseURL?: string
}
export interface Settings<Options = ChatOptionsInput> extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL?: string
readonly providerOptions?: Options
}
const chatRoute = Route.make({
id: "zai-coding-chat",
@@ -80,16 +80,13 @@ export const chat = provider.chat
export const messages = provider.messages
export const responses = provider.responses
export const model: ProviderPackage.Definition<Settings, ChatOptionsInput>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, ...providerOptions },
) =>
export const model: ProviderPackage.Definition<Settings, ChatOptionsInput>["model"] = (modelID, settings) =>
configure({
apiKey,
baseURL,
headers,
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers,
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).model(modelID)
export * as ZAICodingPlan from "./zai-coding-plan.js"
@@ -5,12 +5,12 @@ export type Settings = ZAICodingPlan.Settings<ZAICodingPlan.MessagesOptionsInput
export const model: ProviderPackage.Definition<Settings, ZAICodingPlan.MessagesOptionsInput>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, ...providerOptions },
settings,
) =>
ZAICodingPlan.configure({
apiKey,
baseURL,
headers,
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers,
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).messages(modelID)
@@ -5,12 +5,12 @@ export type Settings = ZAICodingPlan.Settings<ZAICodingPlan.ResponsesOptionsInpu
export const model: ProviderPackage.Definition<Settings, ZAICodingPlan.ResponsesOptionsInput>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, ...providerOptions },
settings,
) =>
ZAICodingPlan.configure({
apiKey,
baseURL,
headers,
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers,
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).responses(modelID)
+11 -14
View File
@@ -17,11 +17,11 @@ export type Config = Omit<RouteDefaultsInput, "providerOptions"> &
readonly providerOptions?: ChatOptionsInput
}
export type Settings = ProviderPackage.Settings &
ChatOptionsInput & {
readonly apiKey?: string
readonly baseURL?: string
}
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL?: string
readonly providerOptions?: ChatOptionsInput
}
export type { ZAIImageOptions } from "../protocols/zai-images.js"
@@ -70,16 +70,13 @@ export const provider = configure()
export const image = provider.image
export const chat = provider.chat
export const model: ProviderPackage.Definition<Settings, ChatOptionsInput>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, ...providerOptions },
) =>
export const model: ProviderPackage.Definition<Settings, ChatOptionsInput>["model"] = (modelID, settings) =>
configure({
apiKey,
baseURL,
headers,
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers,
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).model(modelID)
export * as ZAI from "./zai.js"
+6 -1
View File
@@ -7,6 +7,7 @@ import { HttpTransport } from "./transport/index.js"
import type { HttpMiddleware, Transport, TransportRuntime, WebSocketChannelExecutor } from "./transport/index.js"
import type { Protocol } from "./protocol.js"
import { applyCachePolicy } from "../cache-policy.js"
import { applyEffortUpdates } from "../effort-updates.js"
import { normalizeToolHistory } from "../tool-history.js"
import { sanitizeSurrogates } from "../utils/sanitize.js"
import * as ProviderShared from "../protocols/shared.js"
@@ -55,6 +56,7 @@ export interface Route<
readonly transport: Transport<Body, Prepared, unknown>
readonly defaults: RouteDefaults
readonly body: RouteBody<Body>
readonly supportsEffortUpdates?: (request: LLMRequest) => boolean
readonly with: {
<Next extends CompactionOperations | undefined>(
patch: RoutePatch<Body, Prepared> & { readonly compact: Next },
@@ -388,6 +390,7 @@ function makeFromTransport<Body, Prepared, Frame, Event, State>(
transport: routeInput.transport,
defaults: routeInput.defaults ?? {},
body: protocol.body,
supportsEffortUpdates: protocol.supportsEffortUpdates,
with: (patch: RoutePatch<Body, Prepared>) => {
const { compact, id, provider, providerMetadataKey, auth, transport, endpoint, ...defaults } = patch
return build({
@@ -558,7 +561,9 @@ const prepareRequest = (request: LLMRequest) => {
[...new Map(tools.map((tool) => [`${tool.type}:${tool.name}`, tool])).values()].map((tool) =>
tool.type === "tool" ? tool : { ...tool, tools: dedupe(tool.tools) },
)
const resolved = applyCachePolicy(LLMRequest.update(sanitized, { tools: dedupe(sanitized.tools) }))
const resolved = applyCachePolicy(
applyEffortUpdates(LLMRequest.update(sanitized, { tools: dedupe(sanitized.tools) })),
)
const headers = resolved.model.route.headers?.({ request: resolved })
return headers === undefined
? resolved
+2
View File
@@ -41,6 +41,8 @@ export interface Protocol<Body, Frame, Event, State> {
readonly body: ProtocolBody<Body>
/** Response side: streaming state machine. */
readonly stream: ProtocolStream<Frame, Event, State>
/** Whether `body.from` lowers `Message.effort(...)` markers; wrappers around another `body.from` must forward it. */
readonly supportsEffortUpdates?: (request: LLMRequest) => boolean
}
export interface ProtocolBody<Body> {
+13
View File
@@ -9,6 +9,7 @@ import {
LanguageModelSchema,
type LanguageModel,
ProviderOptions,
ReasoningEffort,
} from "./options.js"
import { ProviderID } from "./ids.js"
@@ -217,6 +218,14 @@ export const CompactionPart = Object.assign(compactionPartSchema, {
Schema.decodeUnknownSync(compactionPartSchema)({ type: "compaction", ...input }),
})
/** Reasoning effort changed here, from `previous` to `effort`; `undefined` is the model default. */
export const EffortPart = Schema.Struct({
type: Schema.Literal("effort"),
effort: Schema.optional(ReasoningEffort),
previous: Schema.optional(ReasoningEffort),
}).annotate({ identifier: "LLM.Content.Effort" })
export type EffortPart = Schema.Schema.Type<typeof EffortPart>
export const ContentPart = Schema.Union([
TextPart,
MediaPart,
@@ -224,6 +233,7 @@ export const ContentPart = Schema.Union([
ToolResultPart,
ReasoningPart,
CompactionPart,
EffortPart,
]).pipe(Schema.toTaggedUnion("type"))
export type ContentPart = Schema.Schema.Type<typeof ContentPart>
@@ -265,6 +275,9 @@ export namespace Message {
*/
export const system = (content: SystemContentInput) => make({ role: "system", content })
export const effort = (input: { readonly effort?: ReasoningEffort; readonly previous?: ReasoningEffort }) =>
make({ role: "system", content: [{ type: "effort", effort: input.effort, previous: input.previous }] })
export const tool = (result: ToolResultPart | Parameters<typeof ToolResultPart.make>[0]) =>
make({ role: "tool", content: ["type" in result ? result : ToolResultPart.make(result)] })
}
+10
View File
@@ -140,6 +140,14 @@ export namespace LanguageModelDefaults {
}
}
/** Ordered lowest to highest. */
export const ReasoningEfforts = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] as const
export type ReasoningEffort = (typeof ReasoningEfforts)[number] | (string & {})
export const ReasoningEffort = Schema.declare<ReasoningEffort>(
(value): value is ReasoningEffort => typeof value === "string",
{ title: "ReasoningEffort" },
)
export const LanguageModelToolSchemaCompatibility = Schema.Literals(["gemini", "moonshot"])
export type LanguageModelToolSchemaCompatibility = Schema.Schema.Type<typeof LanguageModelToolSchemaCompatibility>
@@ -165,6 +173,8 @@ export class LanguageModelCompatibility extends Schema.Class<LanguageModelCompat
requireSignature: Schema.optional(Schema.Boolean),
/** Supports Anthropic's thinking-prefix mismatch controls. Overrides model-ID detection. */
supportsThinkingBlockBinding: Schema.optional(Schema.Boolean),
/** Supports per-message effort updates. Overrides model-ID detection. */
supportsEffortUpdates: Schema.optional(Schema.Boolean),
}) {}
export namespace LanguageModelCompatibility {
+439
View File
@@ -0,0 +1,439 @@
import { describe, expect, test } from "bun:test"
import { Effect } from "effect"
import { LLM, LLMRequest, Message, ToolCallPart } from "../src/index.js"
import { Auth, LLMClient } from "../src/route.js"
import { compileRequest } from "../src/route/client.js"
import { AnthropicMessages } from "../src/protocols/anthropic-messages.js"
import { OpenAIResponses } from "../src/protocols/openai-responses.js"
import { Gemini } from "../src/protocols/gemini.js"
import { GoogleVertexMessages, OpenAI } from "../src/providers.js"
import { applyCachePolicy } from "../src/cache-policy.js"
import { applyEffortUpdates } from "../src/effort-updates.js"
import { it, testEffect } from "./lib/effect.js"
import { dynamicResponse } from "./lib/http.js"
import { sseEvents } from "./lib/sse.js"
const anthropic = (id: string, compatibility?: { readonly supportsEffortUpdates?: boolean }) =>
AnthropicMessages.route
.with({ endpoint: { baseURL: "https://api.anthropic.test/v1/" }, auth: Auth.header("x-api-key", "test") })
.model({ id, compatibility })
const openai = (id: string, compatibility?: { readonly supportsEffortUpdates?: boolean }) =>
OpenAIResponses.route
.with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") })
.model({ id, compatibility })
const opus5 = anthropic("claude-opus-5")
const astra = openai("gpt-6-astra")
const lowFromHigh = Message.effort({ effort: "low", previous: "high" })
const conversation = [Message.user("Before."), lowFromHigh, Message.user("After.")]
const systemMessages = (body: AnthropicMessages.AnthropicMessagesBody) =>
body.messages.filter((message) => message.role === "system")
const updates = (body: OpenAIResponses.OpenAIResponsesBody) =>
body.input.filter((item) => "type" in item && item.type === "configuration_update")
describe("applyEffortUpdates", () => {
test("keeps the request identity without markers and for protocols that lower them", () => {
const plain = LLM.request({ model: opus5, prompt: "hi" })
expect(applyEffortUpdates(plain)).toBe(plain)
const supported = LLM.request({ model: opus5, messages: conversation, providerOptions: { effort: "low" } })
expect(applyEffortUpdates(supported)).toBe(supported)
})
it.effect("compiles markers away for protocols without per-message effort", () =>
Effect.gen(function* () {
const model = Gemini.route
.with({
endpoint: { baseURL: "https://generativelanguage.test/v1beta/" },
auth: Auth.header("x-goog-api-key", "test"),
})
.model({ id: "gemini-3.5-flash" })
const withMarkers = yield* compileRequest(LLM.request({ model, messages: conversation }))
const withoutMarkers = yield* compileRequest(
LLM.request({ model, messages: [Message.user("Before."), Message.user("After.")] }),
)
expect(withMarkers.body).toEqual(withoutMarkers.body)
}),
)
})
describe("cache policy", () => {
it.effect("walks the tail breakpoint back past a trailing effort marker", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model: opus5,
messages: [Message.user("first"), Message.assistant("reply"), Message.user("latest"), lowFromHigh],
providerOptions: { effort: "low" },
cache: "auto",
}),
)
expect(prepared.body.messages).toEqual([
{ role: "user", content: [{ type: "text", text: "first" }] },
{ role: "assistant", content: [{ type: "text", text: "reply" }] },
{ role: "user", content: [{ type: "text", text: "latest", cache_control: { type: "ephemeral" } }] },
{ role: "system", content: [], output_config: { effort: "low" } },
])
}),
)
})
describe("Anthropic Messages effort updates", () => {
it.effect("lowers markers to per-turn system messages and freezes the top-level effort", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({ model: opus5, messages: conversation, providerOptions: { effort: "low" }, cache: "none" }),
)
expect(prepared.body.output_config).toEqual({ effort: "high" })
expect(prepared.body.messages).toEqual([
{ role: "user", content: [{ type: "text", text: "Before." }] },
{ role: "system", content: [], output_config: { effort: "low" } },
{ role: "user", content: [{ type: "text", text: "After." }] },
])
}),
)
it.effect("omits the frozen effort for the model default and sends `high` for a switch back to it", () =>
Effect.gen(function* () {
const format = { type: "json_schema" as const, schema: { type: "object" } }
const prepared = yield* compileRequest(
LLM.request({
model: opus5,
messages: [
Message.user("One."),
Message.effort({ effort: "low" }),
Message.user("Two."),
Message.effort({ previous: "low" }),
Message.user("Three."),
],
providerOptions: { output_config: { format } },
cache: "none",
}),
)
expect(prepared.body.output_config).toEqual({ format })
expect(systemMessages(prepared.body)).toEqual([
{ role: "system", content: [], output_config: { effort: "low" } },
{ role: "system", content: [], output_config: { effort: "high" } },
])
}),
)
it.effect("requests the mid-conversation output config beta only when markers are sent", () =>
Effect.gen(function* () {
for (const [model, betas] of [
[opus5, "existing-beta,mid-conversation-output-config-2026-07-01"],
[anthropic("claude-sonnet-5"), "existing-beta"],
] as const) {
const request = LLM.request({
model,
messages: conversation,
providerOptions: { effort: "low" },
http: { headers: { "anthropic-beta": "existing-beta" } },
})
const compiled = yield* compileRequest(request)
const prepared = yield* AnthropicMessages.route.prepareTransport(compiled.body, request)
expect(prepared.request.headers["anthropic-beta"]).toBe(betas)
}
}),
)
it.effect("accepts a marker between a tool call and its result", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model: opus5,
messages: [
Message.user("Weather?"),
Message.assistant([ToolCallPart.make({ id: "call_1", name: "lookup", input: {} })]),
lowFromHigh,
Message.tool({ id: "call_1", name: "lookup", result: { temp: 72 } }),
],
providerOptions: { effort: "low" },
cache: "none",
}),
)
expect(prepared.body.messages).toEqual([
{ role: "user", content: [{ type: "text", text: "Weather?" }] },
{ role: "assistant", content: [{ type: "tool_use", id: "call_1", name: "lookup", input: {} }] },
{ role: "system", content: [], output_config: { effort: "low" } },
{ role: "user", content: [{ type: "tool_result", tool_use_id: "call_1", content: '{"temp":72}' }] },
])
}),
)
it.effect("falls back to a plain top-level effort when history drifted from the current effort", () =>
Effect.gen(function* () {
const drifted = yield* compileRequest(
LLM.request({ model: opus5, messages: conversation, providerOptions: { effort: "medium" }, cache: "none" }),
)
const plain = yield* compileRequest(
LLM.request({
model: opus5,
messages: [Message.user("Before."), Message.user("After.")],
providerOptions: { effort: "medium" },
cache: "none",
}),
)
expect(drifted.body).toEqual(plain.body)
expect(drifted.body.output_config).toEqual({ effort: "medium" })
}),
)
for (const [id, supported] of [
["claude-opus-5", true],
["claude-opus-5-20260901", true],
["anthropic/claude-opus-5", true],
["claude-fable-5-1", true],
["claude-mythos-5-1", true],
["claude-fable-5", false],
["claude-opus-4-8", false],
["claude-sonnet-5", false],
["kimi-k2.5", false],
] as const) {
it.effect(`${supported ? "lowers" : "strips"} markers for ${id}`, () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({ model: anthropic(id), messages: conversation, providerOptions: { effort: "low" } }),
)
expect(systemMessages(prepared.body)).toHaveLength(supported ? 1 : 0)
expect(prepared.body.output_config).toEqual({ effort: supported ? "high" : "low" })
}),
)
}
it.effect("honors the compatibility override in both directions", () =>
Effect.gen(function* () {
const enabled = yield* compileRequest(
LLM.request({
model: anthropic("claude-sonnet-5", { supportsEffortUpdates: true }),
messages: conversation,
providerOptions: { effort: "low" },
}),
)
const disabled = yield* compileRequest(
LLM.request({
model: anthropic("claude-opus-5", { supportsEffortUpdates: false }),
messages: conversation,
providerOptions: { effort: "low" },
}),
)
expect(systemMessages(enabled.body)).toHaveLength(1)
expect(systemMessages(disabled.body)).toHaveLength(0)
}),
)
it.effect("strips markers on the Vertex Anthropic route, whose protocol wrapper does not forward support", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model: GoogleVertexMessages.configure({ accessToken: "test", location: "global", project: "test" }).model(
"claude-opus-5",
),
messages: conversation,
providerOptions: { effort: "low" },
}),
)
expect(systemMessages(prepared.body)).toHaveLength(0)
expect(prepared.body.output_config).toEqual({ effort: "low" })
}),
)
})
describe("OpenAI Responses effort updates", () => {
it.effect("lowers markers to configuration_update items and freezes reasoning.effort", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({ model: astra, messages: conversation, providerOptions: { reasoningEffort: "low" } }),
)
expect(prepared.body.reasoning).toEqual({ effort: "high" })
expect(prepared.body.input).toEqual([
{ role: "user", content: [{ type: "input_text", text: "Before." }] },
{ type: "configuration_update", reasoning: { effort: "low" } },
{ role: "user", content: [{ type: "input_text", text: "After." }] },
])
}),
)
it.effect("coalesces consecutive updates so the newest wins", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model: astra,
messages: [
Message.user("Before."),
Message.effort({ effort: "low", previous: "medium" }),
Message.effort({ effort: "xhigh", previous: "low" }),
Message.user("After."),
],
providerOptions: { reasoningEffort: "xhigh" },
}),
)
expect(prepared.body.reasoning).toEqual({ effort: "medium" })
expect(prepared.body.input).toEqual([
{ role: "user", content: [{ type: "input_text", text: "Before." }] },
{ type: "configuration_update", reasoning: { effort: "xhigh" } },
{ role: "user", content: [{ type: "input_text", text: "After." }] },
])
}),
)
it.effect("omits reasoning.effort for the model default and sends `medium` for a switch back to it", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model: astra,
messages: [
Message.user("One."),
Message.effort({ effort: "low" }),
Message.user("Two."),
Message.effort({ previous: "low" }),
Message.user("Three."),
],
}),
)
expect(prepared.body.reasoning).toBeUndefined()
expect(updates(prepared.body)).toEqual([
{ type: "configuration_update", reasoning: { effort: "low" } },
{ type: "configuration_update", reasoning: { effort: "medium" } },
])
}),
)
it.effect("falls back to a plain top-level effort when history drifted from the current effort", () =>
Effect.gen(function* () {
const drifted = yield* compileRequest(
LLM.request({ model: astra, messages: conversation, providerOptions: { reasoningEffort: "xhigh" } }),
)
const plain = yield* compileRequest(
LLM.request({
model: astra,
messages: [Message.user("Before."), Message.user("After.")],
providerOptions: { reasoningEffort: "xhigh" },
}),
)
expect(drifted.body).toEqual(plain.body)
expect(drifted.body.reasoning).toEqual({ effort: "xhigh" })
}),
)
it.effect("strips markers when automatic context management is enabled", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model: astra,
messages: conversation,
providerOptions: { reasoningEffort: "low", contextManagement: [{ type: "compaction" }] },
}),
)
expect(updates(prepared.body)).toEqual([])
expect(prepared.body.reasoning).toEqual({ effort: "low" })
expect(prepared.body.context_management).toEqual([{ type: "compaction" }])
}),
)
for (const [id, supported] of [
["gpt-6-astra", true],
["openai/gpt-6-astra", true],
["gpt-6-astra-2026-09-01", false],
["gpt-5.6-sol", false],
] as const) {
it.effect(`${supported ? "lowers" : "strips"} markers for ${id}`, () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({ model: openai(id), messages: conversation, providerOptions: { reasoningEffort: "low" } }),
)
expect(updates(prepared.body)).toHaveLength(supported ? 1 : 0)
expect(prepared.body.reasoning).toEqual({ effort: supported ? "high" : "low" })
}),
)
}
it.effect("honors the compatibility override in both directions", () =>
Effect.gen(function* () {
const enabled = yield* compileRequest(
LLM.request({
model: openai("gpt-5.5", { supportsEffortUpdates: true }),
messages: conversation,
providerOptions: { reasoningEffort: "low" },
}),
)
const disabled = yield* compileRequest(
LLM.request({
model: openai("gpt-6-astra", { supportsEffortUpdates: false }),
messages: conversation,
providerOptions: { reasoningEffort: "low" },
}),
)
expect(updates(enabled.body)).toHaveLength(1)
expect(updates(disabled.body)).toHaveLength(0)
}),
)
const checkpoint = { type: "compaction", id: "cmp_1", encrypted_content: "opaque" }
const compactRequest = LLM.request({
model: OpenAI.configure({ apiKey: "fixture" }).responses("gpt-6-astra"),
messages: conversation,
providerOptions: { reasoningEffort: "low" },
})
testEffect(
dynamicResponse(({ text, respond }) =>
Effect.sync(() => {
const body = JSON.parse(text)
expect(body.reasoning).toEqual({ effort: "high" })
expect(body.input).toEqual([
{ role: "user", content: [{ type: "input_text", text: "Before." }] },
{ type: "configuration_update", reasoning: { effort: "low" } },
{ role: "user", content: [{ type: "input_text", text: "After." }] },
{ type: "compaction_trigger" },
])
return respond(sseEvents({ type: "response.completed", response: { id: "resp_1", output: [checkpoint] } }), {
headers: { "content-type": "text/event-stream" },
})
}),
),
).effect("keeps configuration updates in the checkpoint body", () =>
Effect.gen(function* () {
const result = yield* LLMClient.compact(compactRequest, { mechanism: "trigger" })
expect(result.checkpoint.encrypted).toBe("opaque")
}),
)
testEffect(
dynamicResponse(({ request, text, respond }) =>
Effect.sync(() => {
expect(new URL(request.url).pathname).toEndWith("/responses/compact")
expect(JSON.parse(text).input).toEqual([
{ role: "user", content: [{ type: "input_text", text: "Before." }] },
{ role: "user", content: [{ type: "input_text", text: "After." }] },
])
return respond(JSON.stringify({ object: "response.compaction", output: [checkpoint] }))
}),
),
).effect("drops markers from the compaction endpoint body", () =>
Effect.gen(function* () {
const result = yield* LLMClient.compact(compactRequest, { mechanism: "endpoint" })
expect(result.replacement.map((message) => message.content[0]?.type)).toEqual(["compaction"])
}),
)
})
+9 -10
View File
@@ -188,13 +188,13 @@ describe("provider package entrypoints", () => {
baseURL: "https://provider.example.test/v1/",
headers: { "x-application": "opencode" },
body: { service_tier: "priority" },
reasoningEffort: "high" as const,
providerOptions: { reasoningEffort: "high" as const },
}
const deepinfra = DeepInfra.model("google/gemma-3-27b-it", settings)
expect(deepinfra.route.id).toBe("deepinfra-chat")
expect(deepinfra.route.endpoint.baseURL).toBe("https://provider.example.test/v1/openai")
expect(deepinfra.route.defaults.providerOptions).toEqual({ reasoningEffort: "high" })
expect(deepinfra.route.defaults.providerOptions).toEqual(settings.providerOptions)
expect(deepinfra.route.defaults.headers).toEqual(settings.headers)
expect(deepinfra.route.defaults.http?.body).toEqual(settings.body)
})
@@ -210,7 +210,7 @@ describe("provider package entrypoints", () => {
apiKey: "fixture",
headers: { "x-application": "opencode" },
body: { custom: true },
reasoningEffort: "high",
providerOptions: { reasoningEffort: "high" },
})
expect(selected.provider).toBe(provider.id)
expect(selected.route.endpoint.baseURL).toBe(provider.baseURL({ accountId: "account" }))
@@ -231,11 +231,11 @@ describe("provider package entrypoints", () => {
}
const openrouter = OpenRouter.model("anthropic/claude-sonnet-4", {
...settings,
usage: true,
providerOptions: { usage: true },
})
const xai = XAI.model("grok-4", {
...settings,
reasoningEffort: "high",
providerOptions: { reasoningEffort: "high" },
})
for (const selected of [openrouter, xai]) {
@@ -269,8 +269,7 @@ describe("provider package entrypoints", () => {
provider: "example",
headers: { "x-application": "opencode" },
body: { service_tier: "priority" },
reasoningEffort: "low",
store: true,
providerOptions: { reasoningEffort: "low", store: true },
})
expect(String(selected.provider)).toBe("example")
@@ -296,7 +295,7 @@ describe("provider package entrypoints", () => {
provider: "example",
headers: { "x-application": "opencode" },
body: { metadata: { user_id: "user_1" } },
effort: "low",
providerOptions: { effort: "low" },
})
expect(String(selected.provider)).toBe("example")
@@ -316,7 +315,7 @@ describe("provider package entrypoints", () => {
const Anthropic = await import("@opencode/ai/providers/anthropic")
const selected = Anthropic.model("claude-sonnet-4-6", {
apiKey: "fixture",
thinking: { type: "adaptive" },
providerOptions: { thinking: { type: "adaptive" } },
})
expect(selected.route.defaults.providerOptions).toEqual({ thinking: { type: "adaptive" } })
@@ -410,7 +409,7 @@ describe("provider package entrypoints", () => {
baseURL: "https://generativelanguage.test/v1beta",
headers: { "x-application": "opencode" },
body: { safetySettings: [] },
thinkingConfig: { thinkingBudget: 1_024 },
providerOptions: { thinkingConfig: { thinkingBudget: 1_024 } },
})
expect(selected.route.id).toBe("gemini")
@@ -69,9 +69,7 @@ for (const model of [
dynamicResponse(({ request, text, respond }) =>
Effect.sync(() => {
const body = JSON.parse(text)
expect(request.headers["anthropic-beta"]).toBe(
"existing-beta,interleaved-thinking-2025-05-14,compact-2026-01-12",
)
expect(request.headers["anthropic-beta"]).toBe("existing-beta,compact-2026-01-12")
if (body.messages.length === 1) {
expect(body.context_management.edits).toEqual([
{
@@ -32,9 +32,7 @@ for (const [id, enabled] of [
enabled ? { type: "adaptive", block_binding: { prefix_mismatch_behavior: "drop_block" } } : undefined,
)
expect(prepared.request.headers["anthropic-beta"]).toBe(
enabled
? "existing-beta,interleaved-thinking-2025-05-14,thinking-binding-controls-2026-08-01"
: "existing-beta,interleaved-thinking-2025-05-14",
enabled ? "existing-beta,thinking-binding-controls-2026-08-01" : "existing-beta",
)
}),
)
@@ -55,9 +53,7 @@ it.effect("preserves explicit thinking settings and combines required beta heade
const prepared = yield* AnthropicMessages.route.prepareTransport(compiled.body, request)
expect(compiled.body.thinking).toEqual(thinking)
expect(prepared.request.headers["anthropic-beta"]).toBe(
thinking.type === "disabled"
? "interleaved-thinking-2025-05-14,compact-2026-01-12"
: "interleaved-thinking-2025-05-14,compact-2026-01-12,thinking-binding-controls-2026-08-01",
thinking.type === "disabled" ? "compact-2026-01-12" : "compact-2026-01-12,thinking-binding-controls-2026-08-01",
)
}
}),
+6 -4
View File
@@ -34,10 +34,12 @@ it.effect("Groq lowers its own options for custom catalog identities and endpoin
baseURL: "https://gateway.example/v1",
headers: { "x-client": "test" },
body: { custom: "value" },
reasoningEffort: "default",
parallelToolCalls: true,
serviceTier: "flex",
user: "test-user",
providerOptions: {
reasoningEffort: "default",
parallelToolCalls: true,
serviceTier: "flex",
user: "test-user",
},
}),
{ provider: "custom-groq" },
)
+1 -1
View File
@@ -87,7 +87,7 @@ it.effect("Meta package selectors preserve overrides and Chat token policy on cu
baseURL: "https://gateway.example/v1",
headers: { "x-client": "test" },
body: { custom: "value" },
reasoningEffort: "future-effort",
providerOptions: { reasoningEffort: "future-effort" },
})
expect(model.route.endpoint.baseURL).toBe("https://gateway.example/v1")
expect(model.route.defaults.headers).toEqual({ "x-client": "test" })
@@ -99,7 +99,7 @@ describe("native OpenAI-compatible providers", () => {
const settings = {
apiKey: "fixture",
baseURL: "https://gateway.example/v1",
reasoningEffort: "high",
providerOptions: { reasoningEffort: "high" },
}
const selected = provider.configure(settings).model("test-model")
expect(selected.provider).toBe(provider.id)
@@ -143,7 +143,7 @@ describe("native OpenAI-compatible providers", () => {
tools: [
ToolDefinition.make({ name: "lookup", description: "Look up data", inputSchema: { type: "object" } }),
],
store: true,
providerOptions: { store: true },
}),
)
@@ -168,7 +168,7 @@ describe("native OpenAI-compatible providers", () => {
]),
Message.user("Continue."),
],
store: true,
providerOptions: { store: true },
}),
)
@@ -206,7 +206,7 @@ describe("native OpenAI-compatible providers", () => {
baseURL: "https://gateway.example/v1",
headers: { "x-application": "opencode" },
body: { service_tier: "priority" },
reasoningEffort: "high",
providerOptions: { reasoningEffort: "high" },
})
expect(selected.route.endpoint.baseURL).toBe("https://gateway.example/v1")
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/app",
"version": "2.0.0",
"version": "1.18.15",
"description": "",
"type": "module",
"exports": {
@@ -285,7 +285,7 @@ export const DESKTOP_NATIVE_ENGLISH = {
"desktop.updater.dialog.later": "Later",
"desktop.cli.installed.title": "CLI Installed",
"desktop.cli.installed.message": "CLI installed to {{path}}\n\nRestart your terminal to use the 'opencode' command.",
"desktop.cli.installed.message": "CLI installed to {{path}}\n\nRestart your terminal to use the 'opencode2' command.",
"desktop.cli.failed.title": "Installation Failed",
"desktop.cli.failed.message": "Failed to install CLI: {{error}}",
-16
View File
@@ -1,16 +0,0 @@
FROM alpine AS base
ARG BUN_RUNTIME_TRANSPILER_CACHE_PATH=0
ENV BUN_RUNTIME_TRANSPILER_CACHE_PATH=${BUN_RUNTIME_TRANSPILER_CACHE_PATH}
RUN apk add --no-cache libgcc libstdc++ ripgrep
FROM base AS build-amd64
COPY dist/opencode-linux-x64-baseline-musl/bin/opencode /usr/local/bin/opencode
FROM base AS build-arm64
COPY dist/opencode-linux-arm64-musl/bin/opencode /usr/local/bin/opencode
ARG TARGETARCH
FROM build-${TARGETARCH}
RUN opencode --version
ENTRYPOINT ["opencode"]
-135
View File
@@ -1,135 +0,0 @@
#!/usr/bin/env node
const childProcess = require("child_process")
const fs = require("fs")
const path = require("path")
const os = require("os")
const forwardedSignals =
process.platform === "win32" ? ["SIGINT", "SIGTERM", "SIGHUP"] : ["SIGINT", "SIGTERM", "SIGHUP", "SIGUSR1"]
function run(target) {
const child = childProcess.spawn(target, process.argv.slice(2), { stdio: "inherit" })
child.on("error", (error) => {
console.error(error.message)
process.exit(1)
})
const forwarders = {}
for (const signal of forwardedSignals) {
forwarders[signal] = () => {
try {
child.kill(signal)
} catch {}
}
process.on(signal, forwarders[signal])
}
child.on("exit", (code, signal) => {
for (const forwardedSignal of forwardedSignals) process.removeListener(forwardedSignal, forwarders[forwardedSignal])
if (signal) return process.kill(process.pid, signal)
process.exit(typeof code === "number" ? code : 0)
})
}
const envPath = process.env.OPENCODE_BIN_PATH
const scriptDir = path.dirname(fs.realpathSync(__filename))
const command = path.basename(__filename).replace(/\.cjs$/, "")
const nodeBuild = command === "opencode-node"
const sourceCommand = nodeBuild ? "opencode2-node" : "opencode"
const cached = path.join(scriptDir, `.${command}`)
const platform = { darwin: "darwin", linux: "linux", win32: "windows" }[os.platform()] || os.platform()
const arch = { x64: "x64", arm64: "arm64", arm: "arm" }[os.arch()] || os.arch()
const base = `@opencode/cli${nodeBuild ? "-node" : ""}-` + platform + "-" + arch
const binary = platform === "windows" ? `${sourceCommand}.exe` : sourceCommand
function supportsAvx2() {
if (arch !== "x64") return false
if (platform === "linux") {
try {
return /(^|\s)avx2(\s|$)/i.test(fs.readFileSync("/proc/cpuinfo", "utf8"))
} catch {
return false
}
}
if (platform === "darwin") {
try {
const result = childProcess.spawnSync("sysctl", ["-n", "hw.optional.avx2_0"], { encoding: "utf8", timeout: 1500 })
return result.status === 0 && (result.stdout || "").trim() === "1"
} catch {
return false
}
}
if (platform === "windows") {
const command =
'(Add-Type -MemberDefinition "[DllImport(""kernel32.dll"")] public static extern bool IsProcessorFeaturePresent(int ProcessorFeature);" -Name Kernel32 -Namespace Win32 -PassThru)::IsProcessorFeaturePresent(40)'
for (const executable of ["powershell.exe", "pwsh.exe", "pwsh", "powershell"]) {
try {
const result = childProcess.spawnSync(executable, ["-NoProfile", "-NonInteractive", "-Command", command], {
encoding: "utf8",
timeout: 3000,
windowsHide: true,
})
if (result.status !== 0) continue
const output = (result.stdout || "").trim().toLowerCase()
if (output === "true" || output === "1") return true
if (output === "false" || output === "0") return false
} catch {
continue
}
}
}
return false
}
const names = (() => {
if (nodeBuild) return [base]
const baseline = arch === "x64" && !supportsAvx2()
if (platform === "linux") {
const musl = (() => {
try {
if (fs.existsSync("/etc/alpine-release")) return true
const result = childProcess.spawnSync("ldd", ["--version"], { encoding: "utf8" })
return ((result.stdout || "") + (result.stderr || "")).toLowerCase().includes("musl")
} catch {
return false
}
})()
if (musl)
return arch === "x64"
? baseline
? [`${base}-baseline-musl`, `${base}-musl`, `${base}-baseline`, base]
: [`${base}-musl`, `${base}-baseline-musl`, base, `${base}-baseline`]
: [`${base}-musl`, base]
return arch === "x64"
? baseline
? [`${base}-baseline`, base, `${base}-baseline-musl`, `${base}-musl`]
: [base, `${base}-baseline`, `${base}-musl`, `${base}-baseline-musl`]
: [base, `${base}-musl`]
}
return arch === "x64" ? (baseline ? [`${base}-baseline`, base] : [base, `${base}-baseline`]) : [base]
})()
function findBinary(startDir) {
let current = startDir
for (;;) {
const modules = path.join(current, "node_modules")
if (fs.existsSync(modules))
for (const name of names) {
const candidate = path.join(modules, name, "bin", binary)
if (fs.existsSync(candidate)) return candidate
}
const parent = path.dirname(current)
if (parent === current) return
current = parent
}
}
const resolved = envPath || (fs.existsSync(cached) ? cached : findBinary(scriptDir))
if (!resolved) {
console.error(
`It seems that your package manager failed to install the right ${command} CLI package. Try manually installing ` +
names.map((name) => `"${name}"`).join(" or ") +
" package",
)
process.exit(1)
}
run(resolved)
+132 -2
View File
@@ -1,4 +1,134 @@
#!/usr/bin/env node
console.log("opencode2 is now just opencode. run opencode")
process.exit(1)
const childProcess = require("child_process")
const fs = require("fs")
const path = require("path")
const os = require("os")
const forwardedSignals =
process.platform === "win32" ? ["SIGINT", "SIGTERM", "SIGHUP"] : ["SIGINT", "SIGTERM", "SIGHUP", "SIGUSR1"]
function run(target) {
const child = childProcess.spawn(target, process.argv.slice(2), { stdio: "inherit" })
child.on("error", (error) => {
console.error(error.message)
process.exit(1)
})
const forwarders = {}
for (const signal of forwardedSignals) {
forwarders[signal] = () => {
try {
child.kill(signal)
} catch {}
}
process.on(signal, forwarders[signal])
}
child.on("exit", (code, signal) => {
for (const forwardedSignal of forwardedSignals) process.removeListener(forwardedSignal, forwarders[forwardedSignal])
if (signal) return process.kill(process.pid, signal)
process.exit(typeof code === "number" ? code : 0)
})
}
const envPath = process.env.OPENCODE_BIN_PATH
const scriptDir = path.dirname(fs.realpathSync(__filename))
const command = path.basename(__filename).replace(/\.cjs$/, "")
const nodeBuild = command === "opencode2-node"
const cached = path.join(scriptDir, `.${command}`)
const platform = { darwin: "darwin", linux: "linux", win32: "windows" }[os.platform()] || os.platform()
const arch = { x64: "x64", arm64: "arm64", arm: "arm" }[os.arch()] || os.arch()
const base = `@opencode/cli${nodeBuild ? "-node" : ""}-` + platform + "-" + arch
const binary = platform === "windows" ? `${command}.exe` : command
function supportsAvx2() {
if (arch !== "x64") return false
if (platform === "linux") {
try {
return /(^|\s)avx2(\s|$)/i.test(fs.readFileSync("/proc/cpuinfo", "utf8"))
} catch {
return false
}
}
if (platform === "darwin") {
try {
const result = childProcess.spawnSync("sysctl", ["-n", "hw.optional.avx2_0"], { encoding: "utf8", timeout: 1500 })
return result.status === 0 && (result.stdout || "").trim() === "1"
} catch {
return false
}
}
if (platform === "windows") {
const command =
'(Add-Type -MemberDefinition "[DllImport(""kernel32.dll"")] public static extern bool IsProcessorFeaturePresent(int ProcessorFeature);" -Name Kernel32 -Namespace Win32 -PassThru)::IsProcessorFeaturePresent(40)'
for (const executable of ["powershell.exe", "pwsh.exe", "pwsh", "powershell"]) {
try {
const result = childProcess.spawnSync(executable, ["-NoProfile", "-NonInteractive", "-Command", command], {
encoding: "utf8",
timeout: 3000,
windowsHide: true,
})
if (result.status !== 0) continue
const output = (result.stdout || "").trim().toLowerCase()
if (output === "true" || output === "1") return true
if (output === "false" || output === "0") return false
} catch {
continue
}
}
}
return false
}
const names = (() => {
if (nodeBuild) return [base]
const baseline = arch === "x64" && !supportsAvx2()
if (platform === "linux") {
const musl = (() => {
try {
if (fs.existsSync("/etc/alpine-release")) return true
const result = childProcess.spawnSync("ldd", ["--version"], { encoding: "utf8" })
return ((result.stdout || "") + (result.stderr || "")).toLowerCase().includes("musl")
} catch {
return false
}
})()
if (musl)
return arch === "x64"
? baseline
? [`${base}-baseline-musl`, `${base}-musl`, `${base}-baseline`, base]
: [`${base}-musl`, `${base}-baseline-musl`, base, `${base}-baseline`]
: [`${base}-musl`, base]
return arch === "x64"
? baseline
? [`${base}-baseline`, base, `${base}-baseline-musl`, `${base}-musl`]
: [base, `${base}-baseline`, `${base}-musl`, `${base}-baseline-musl`]
: [base, `${base}-musl`]
}
return arch === "x64" ? (baseline ? [`${base}-baseline`, base] : [base, `${base}-baseline`]) : [base]
})()
function findBinary(startDir) {
let current = startDir
for (;;) {
const modules = path.join(current, "node_modules")
if (fs.existsSync(modules))
for (const name of names) {
const candidate = path.join(modules, name, "bin", binary)
if (fs.existsSync(candidate)) return candidate
}
const parent = path.dirname(current)
if (parent === current) return
current = parent
}
}
const resolved = envPath || (fs.existsSync(cached) ? cached : findBinary(scriptDir))
if (!resolved) {
console.error(
`It seems that your package manager failed to install the right ${command} CLI package. Try manually installing ` +
names.map((name) => `"${name}"`).join(" or ") +
" package",
)
process.exit(1)
}
run(resolved)
+1 -2
View File
@@ -1,11 +1,10 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/cli",
"version": "2.0.0",
"version": "1.18.4",
"type": "module",
"license": "MIT",
"bin": {
"opencode": "./bin/opencode.cjs",
"opencode2": "./bin/opencode2.cjs"
},
"files": [
+2 -2
View File
@@ -12,7 +12,7 @@ import { verifyArtifact, verifySimulationGraph } from "./verify-artifact"
import { resolveOpencodePty } from "./opencode-pty"
const dir = path.resolve(import.meta.dirname, "..")
const binary = "opencode"
const binary = "opencode2"
const outdir = path.resolve(
dir,
process.argv.find((arg) => arg.startsWith("--outdir="))?.slice("--outdir=".length) ?? "dist",
@@ -149,7 +149,7 @@ export default { path: file, version: ${JSON.stringify(opencodePty.version)}, sh
},
define: {
OPENCODE_VERSION: `'${Script.version}'`,
OPENCODE_CLI_NAME: "'opencode'",
OPENCODE_CLI_NAME: `'${binary}'`,
OPENCODE_CHANNEL: `'${Script.channel}'`,
OPENCODE_ARTIFACT: `'cli'`,
OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "undefined",
+1 -2
View File
@@ -12,11 +12,10 @@ const require = createRequire(import.meta.url)
const packageJson = JSON.parse(fs.readFileSync(path.join(directory, "package.json"), "utf8"))
const command = Object.keys(packageJson.bin ?? {})[0]
if (!command) throw new Error("OpenCode package does not declare a binary")
const sourceCommand = packageJson.opencodeSourceBinary ?? command
const platform = { darwin: "darwin", linux: "linux", win32: "windows" }[os.platform()] ?? os.platform()
const arch = { x64: "x64", arm64: "arm64", arm: "arm" }[os.arch()] ?? os.arch()
const sourceBinary = platform === "windows" ? `${sourceCommand}.exe` : sourceCommand
const sourceBinary = platform === "windows" ? `${command}.exe` : command
const targetBinary = path.resolve(directory, packageJson.bin[command])
const dependencies = packageJson.optionalDependencies ?? {}
const base = Object.keys(dependencies).find((name) => name.endsWith(`-${platform}-${arch}`))
+16 -14
View File
@@ -6,10 +6,15 @@ import path from "node:path"
import { fileURLToPath } from "node:url"
import { UpdateArtifact } from "../../../script/update-artifact"
if (Script.channel !== "beta") throw new Error("AUR publishing requires the beta channel")
const name = "opencode-beta"
const command = "opencode"
if (!/^\d+\.\d+\.\d+-beta[.-]\d+(?:\.\d+)?$/.test(Script.version)) throw new Error("Expected a beta release version")
if (Script.channel !== "beta" && Script.channel !== "latest") {
throw new Error("AUR publishing requires the beta or latest channel")
}
const beta = Script.channel === "beta"
const name = beta ? "opencode-beta" : "opencode-bin"
const command = beta ? "opencode2" : "opencode"
if (!(beta ? /^\d+\.\d+\.\d+-beta[.-]\d+(?:\.\d+)?$/ : /^\d+\.\d+\.\d+$/).test(Script.version)) {
throw new Error(`Expected a ${Script.channel} release version`)
}
const dir = fileURLToPath(new URL("..", import.meta.url))
const root = path.resolve(process.env.OPENCODE_CLI_DIST ?? path.join(dir, "dist"))
@@ -17,7 +22,6 @@ const outdir = path.join(root, `aur-${name}`)
const dryRun = process.argv.includes("--dry-run")
const pkgver = Script.version.replaceAll("-", ".")
const license = Bun.file(path.join(dir, "..", "..", "LICENSE"))
const shim = "#!/bin/sh\necho 'opencode2 is now just opencode. run opencode'\nexit 1\n"
await rm(outdir, { recursive: true, force: true })
await mkdir(path.dirname(outdir), { recursive: true })
@@ -44,7 +48,6 @@ const sources = await Promise.all(
)
await Bun.write(path.join(outdir, "LICENSE"), license)
await Bun.write(path.join(outdir, "opencode2"), shim)
await Bun.write(
path.join(outdir, "PKGBUILD"),
[
@@ -52,22 +55,21 @@ await Bun.write(
`pkgname=${name}`,
`pkgver=${pkgver}`,
"pkgrel=1",
"pkgdesc='OpenCode beta - the AI coding agent for the terminal'",
`pkgdesc='OpenCode${beta ? " V2 beta" : ""} - the AI coding agent for the terminal'`,
"url='https://github.com/anomalyco/opencode'",
"arch=('x86_64' 'aarch64')",
"license=('MIT')",
"depends=('glibc' 'gcc-libs' 'ripgrep')",
`provides=('${command}' 'opencode2')`,
`conflicts=('${command}' 'opencode2')`,
`provides=('${command}')`,
`conflicts=('${command}')`,
// Stripping a compiled Bun executable can damage its embedded application.
"options=('!strip' '!debug')",
"source=('LICENSE' 'opencode2')",
`sha256sums=('${new Bun.CryptoHasher("sha256").update(await license.arrayBuffer()).digest("hex")}' '${new Bun.CryptoHasher("sha256").update(shim).digest("hex")}')`,
"source=('LICENSE')",
`sha256sums=('${new Bun.CryptoHasher("sha256").update(await license.arrayBuffer()).digest("hex")}')`,
...sources,
"",
"package() {",
` install -Dm755 "$srcdir/package/bin/opencode" "$pkgdir/usr/bin/${command}"`,
' install -Dm755 "$srcdir/opencode2" "$pkgdir/usr/bin/opencode2"',
` install -Dm755 "$srcdir/package/bin/opencode2" "$pkgdir/usr/bin/${command}"`,
' install -Dm644 "$srcdir/LICENSE" "$pkgdir/usr/share/licenses/$pkgname/LICENSE"',
"}",
"",
@@ -77,7 +79,7 @@ await Bun.write(path.join(outdir, ".SRCINFO"), await $`makepkg --printsrcinfo`.c
console.log(`Prepared ${name} ${pkgver} in ${outdir}`)
if (dryRun) process.exit(0)
await $`git add PKGBUILD .SRCINFO LICENSE opencode2`.cwd(outdir)
await $`git add PKGBUILD .SRCINFO LICENSE`.cwd(outdir)
if ((await $`git diff --cached --quiet`.cwd(outdir).nothrow()).exitCode !== 0) {
await $`git commit -m ${`chore: update ${name} to ${pkgver}`}`.cwd(outdir)
await $`git push origin master`.cwd(outdir)
+4 -21
View File
@@ -29,8 +29,6 @@ async function publish(dir: string, name: string, version: string) {
async function publishDistribution(input: {
root: string
name: string
command: string
legacyCommand?: string
binary: string
packagePrefix: string
artifact: string
@@ -50,7 +48,7 @@ async function publishDistribution(input: {
await $`mkdir -p ${input.root}/${input.name}/bin`
await $`cp ./script/postinstall.mjs ${input.root}/${input.name}/postinstall.mjs`
await Bun.file(`${input.root}/${input.name}/bin/${input.command}.exe`).write(
await Bun.file(`${input.root}/${input.name}/bin/${input.binary}.exe`).write(
[
`echo "Error: ${input.name}'s postinstall script was not run." >&2`,
'echo "" >&2',
@@ -60,19 +58,11 @@ async function publishDistribution(input: {
"",
].join("\n"),
)
if (input.legacyCommand)
await Bun.file(`${input.root}/${input.name}/bin/${input.legacyCommand}.cjs`).write(
`#!/usr/bin/env node\n\nconsole.log(${JSON.stringify(`opencode2 is now just opencode. run ${input.command}`)})\nprocess.exit(1)\n`,
)
await Bun.file(`${input.root}/${input.name}/package.json`).write(
JSON.stringify(
{
name: input.name,
bin: {
[input.command]: `./bin/${input.command}.exe`,
...(input.legacyCommand ? { [input.legacyCommand]: `./bin/${input.legacyCommand}.cjs` } : {}),
},
...(input.command !== input.binary ? { opencodeSourceBinary: input.binary } : {}),
bin: { [input.binary]: `./bin/${input.binary}.exe` },
scripts: { postinstall: "node ./postinstall.mjs" },
version,
license: pkg.license,
@@ -130,9 +120,7 @@ async function publishDistribution(input: {
await publishDistribution({
root,
name: pkg.name,
command: "opencode",
legacyCommand: "opencode2",
binary: "opencode",
binary: "opencode2",
packagePrefix: "@opencode/cli-",
artifact: "cli",
})
@@ -140,18 +128,13 @@ if (existsSync(path.join(root, "node"))) {
await publishDistribution({
root: path.join(root, "node"),
name: "@opencode/cli-node",
command: "opencode2-node",
binary: "opencode2-node",
packagePrefix: "@opencode/cli-node-",
artifact: "cli-node",
})
}
if (Script.channel === "latest" && Script.release && !dryRun) {
await $`docker buildx build --platform linux/amd64,linux/arm64 --tag ghcr.io/anomalyco/opencode:${Script.version} --push .`
}
if (Script.channel === "beta" && Script.release) {
if ((Script.channel === "beta" || Script.channel === "latest") && Script.release) {
await $`bun ./script/publish-aur.ts ${dryRun ? ["--dry-run"] : []}`.env({ ...process.env, OPENCODE_CLI_DIST: root })
}
+1 -4
View File
@@ -11,10 +11,7 @@ import path from "node:path"
const nodeBuild = process.argv.includes("--node")
const target = `cli${nodeBuild ? "-node" : ""}-${process.platform === "win32" ? "windows" : process.platform}-${process.arch}`
const directory = path.join(import.meta.dir, "..", "dist", ...(nodeBuild ? ["node"] : []), target, "bin")
const binary = path.join(
directory,
`${nodeBuild ? "opencode2-node" : "opencode"}${process.platform === "win32" ? ".exe" : ""}`,
)
const binary = path.join(directory, `opencode2${nodeBuild ? "-node" : ""}${process.platform === "win32" ? ".exe" : ""}`)
if (!(await Bun.file(binary).exists())) throw new Error(`Missing compiled CLI in ${directory}`)
const root = await fs.realpath(await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-smoke-")))
+1 -1
View File
@@ -36,7 +36,7 @@ const PermissionParams = {
}
const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME : "opencode", {
description: "OpenCode command line interface",
description: "OpenCode 2.0 preview command line interface",
params: {
...ServerParams,
...PermissionParams,
+2 -5
View File
@@ -105,7 +105,7 @@ const make = Effect.gen(function* () {
global.home,
".opencode",
"bin",
process.platform === "win32" ? "opencode.exe" : "opencode",
process.platform === "win32" ? "opencode2.exe" : "opencode2",
)
if (path.resolve(process.execPath) === path.resolve(binary)) return "curl"
if (!installedPackage) return
@@ -186,10 +186,7 @@ const make = Effect.gen(function* () {
"npm",
"install",
"--global",
...((OPENCODE_ARTIFACT === "cli" && !installedPackage?.endsWith("/cli-node")) ||
(installedPackage && packageName !== installedPackage)
? ["--force"]
: []),
...(installedPackage && packageName !== installedPackage ? ["--force"] : []),
target,
],
pnpm: ["pnpm", "add", "--global", `--allow-build=${packageName}`, target],
+5 -5
View File
@@ -423,24 +423,24 @@ test("updates effective duplicate canonical keybinds", async () => {
const file = path.join(directory.path, "cli.json")
await Bun.write(
file,
`{"keybinds":{"session.delete":"first","session.delete":"last","opencode.settings":"off","opencode.settings":"on"}}`,
`{"keybinds":{"session.delete":"first","session.delete":"last","permission.mode":"off","permission.mode":"on"}}`,
)
const config = await run(
directory.path,
Effect.gen(function* () {
const service = yield* Config.Service
expect((yield* service.get()).keybinds).toEqual({ "session.delete": "last", "opencode.settings": "on" })
expect((yield* service.get()).keybinds).toEqual({ "session.delete": "last", "permission.mode": "on" })
return yield* service.update((draft) => {
draft.keybinds = { ...draft.keybinds, "session.delete": "changed", "opencode.settings": "changed" }
draft.keybinds = { ...draft.keybinds, "session.delete": "changed", "permission.mode": "changed" }
})
}),
)
expect(config.keybinds).toEqual({ "session.delete": "changed", "opencode.settings": "changed" })
expect(config.keybinds).toEqual({ "session.delete": "changed", "permission.mode": "changed" })
expect(parse(await Bun.file(file).text()).keybinds).toEqual({
"session.delete": "changed",
"opencode.settings": "changed",
"permission.mode": "changed",
})
})
+1 -1
View File
@@ -100,7 +100,7 @@ function fixture(
}
const installs = [
{ method: "npm", command: ["npm", "install", "--global", "--force", "@opencode/cli@2.3.4-beta.1"] },
{ method: "npm", command: ["npm", "install", "--global", "@opencode/cli@2.3.4-beta.1"] },
{
method: "pnpm",
command: ["pnpm", "add", "--global", "--allow-build=@opencode/cli", "@opencode/cli@2.3.4-beta.1"],
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/client",
"version": "2.0.0",
"version": "1.17.13",
"type": "module",
"license": "MIT",
"repository": {
+5 -37
View File
@@ -9,8 +9,7 @@ standard-library surface that programs can use today, plus concrete gaps that ma
- Intentional boundaries are not listed as compatibility work.
When behavior changes, update this file and the tests in the same change. The implementation and tests remain the
ultimate source of truth. Upstream test262 files run verbatim from `test/test262`; a failing file is listed in
`test/test262/skipped.txt` and its gap is an unchecked item here (see `test/test262/README.md`).
ultimate source of truth.
## Source and execution model
@@ -26,10 +25,6 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [x] The global `search(...)` built-in: synchronous tool discovery that counts as an admitted tool call and is
shadowable by program declarations like other globals.
- [x] Cooperative timeout, an optional total tool-call limit, output bounding, and unrestricted tool-call concurrency.
- [ ] Strict-mode early errors: duplicate parameter names, `yield` as an identifier, and a trailing comma after a
rest parameter are accepted unless the program itself begins with `"use strict"`.
- [ ] Valid JavaScript rejected by TypeScript transpilation before interpretation, such as `in` inside a destructuring
default in a `for...of` head and Unicode-escaped keywords.
## Values and literals
@@ -69,11 +64,6 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [x] Array binding and assignment destructuring from strings, Maps, Sets, URLSearchParams, custom synchronous
iterators, and synchronous generators, including stepwise elisions/rest and `IteratorClose` on early completion
or binding/default failure.
- [ ] Object destructuring from primitives follows ToObject (`const { length } = "abc"`, `const {} = 1`); non-object
sources are rejected.
- [ ] Destructuring a key that member access resolves through the owning built-in, such as
`const { constructor } = error`, reads `undefined`.
- [ ] Member expressions as `for...in` targets (`for (x.y in obj)`).
## Statements and control flow
@@ -121,15 +111,6 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [ ] User-defined constructor calls.
- [ ] `Function.prototype.call`, `apply`, and `bind` for CodeMode functions.
- [ ] Classes and private fields.
- [x] Functions are objects: they hold own properties (`fn.count = 1`), enumerate them, and expose read-only `name`
and `length`. Names follow JavaScript's NamedEvaluation: declarations, named expressions, bindings,
assignments, object literal keys, and destructuring or parameter defaults.
- [ ] `name` and `length` of built-in functions such as `Math.max` or `"a".includes`.
- [ ] A named function expression's name is not bound inside its own body.
- [ ] Redeclaring a function in the same scope is rejected; in JavaScript the last declaration wins.
- [ ] A line terminator between `async function` and the function name.
- [ ] Async generator functions evaluate parameter defaults and destructuring at the first `next()` rather than at the
call, so their errors are not thrown synchronously.
- [x] Synchronous and async generator declarations/expressions, `yield`, and `yield*`, including lazy bodies,
`next(value)`, `return(value)`, `throw(value)`, exhaustion, promise adoption, async request ordering,
`try`/`catch`/`finally`, and sync/async iterator symbols. Async `yield*` awaits values while adapting a sync
@@ -173,8 +154,6 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [x] Plain, arithmetic, bitwise, and logical assignment operators.
- [x] Property deletion on plain data objects and arrays, including computed and optional forms; deleting an array index
creates a hole without changing its length.
- [ ] Operators, `switch` discriminants, and coercion helpers such as `String` and `isNaN` applied to functions and
namespaces; JavaScript coerces them, the interpreter rejects non-data operands.
## Promises and tools
@@ -246,8 +225,7 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [x] Circular references are rejected when created (`o.self = o`, `array.push(array)`), not at serialization as in JS.
- [x] `Object.is` for supported data values.
- [x] `Object.groupBy` over finite collections and custom synchronous iterators/generators, with string-key coercion
and plain-object results.
- [ ] `Object.prototype` methods on values: `toString`, `toLocaleString`, `valueOf`, and `hasOwnProperty`.
and null-prototype results.
## Arrays
@@ -267,18 +245,10 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [x] `length`, numeric indexing, index assignment, spread, and `for...of`.
- [x] The `thisArg` argument of `Array.from` is accepted and ignored, like JS arrows.
- [x] `Array.prototype.toSpliced`.
- [x] Canonical array/string index parsing: keys such as `"01"` are ordinary properties rather than aliases of index
`1`.
- [x] Canonical array/string index parsing: keys such as `"01"` remain non-index properties rather than aliasing index
`1`; arbitrary array-property assignment remains unsupported.
- [x] `Array.prototype.sort` preserves trailing holes, while `toSorted` densifies holes into `undefined` elements,
like JavaScript.
- [x] Assigning `length` to truncate or extend an array; invalid lengths throw `RangeError`.
- [x] Non-index own properties on arrays (`arr.foo = 1`, `arr.constructor = null`). They are excluded from the JSON
form, like `JSON.stringify`.
- [ ] Argument coercion for `indexOf`, `lastIndexOf`, `includes`, `fill`, `flat`, `copyWithin`, and the `join`
separator: JavaScript applies ToIntegerOrInfinity/ToString (including `valueOf`, strings, and `undefined`), the
interpreter requires numbers and strings; `includes()`/`indexOf()` with no argument should search for
`undefined`.
- [ ] Iterator objects from `keys`, `values`, and `entries` with a live `next()`.
## Strings
@@ -362,7 +332,7 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [x] `test`, `exec`, and `toString`.
- [x] Readable `source`, `flags`, `lastIndex`, `hasIndices`, `global`, `ignoreCase`, `multiline`, `sticky`, `unicode`,
`unicodeSets`, and `dotAll`.
- [x] Captures, named groups, match `.index` and `.input`, and stateful global matching.
- [x] Captures, named groups, match `.index`, and stateful global matching.
- [x] Integration with supported String methods, including function replacers.
- [x] Writable `lastIndex`.
- [x] Match `indices` metadata for the `d` flag, including named groups on `exec`, `match`, and `matchAll` results.
@@ -421,5 +391,3 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [x] Caught errors do not distinguish user throws, interpreter failures, and tool failures; a program sees one
Error-shaped value with `name` and `message` in `catch`, rejection handlers, and `Promise.allSettled` reasons.
This is deliberate: the program should handle a failure the same way regardless of where it originated.
- [ ] Failures raised by the interpreter itself carry the generic `Error` name where JavaScript throws a `TypeError`,
`RangeError`, or `ReferenceError`, so `e instanceof TypeError` and `e.constructor === TypeError` are false.
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/codemode",
"version": "2.0.0",
"version": "1.18.4",
"description": "Effect-native confined code execution over schema-described tools",
"type": "module",
"license": "MIT",
-62
View File
@@ -1,62 +0,0 @@
// Copies the manifest's test262 directories from a local checkout into test/test262, verbatim.
// Files needing unsupported flags, features, or harness includes, or whose code crosses one of the
// interpreter's intentional boundaries, are not copied, so the vendored tree is exactly what
// test/test262.test.ts runs.
//
// Usage: bun run script/sync-test262.ts /path/to/test262
import path from "node:path"
import { rm } from "node:fs/promises"
type Frontmatter = { flags?: Array<string>; features?: Array<string>; includes?: Array<string> }
const root = path.resolve(import.meta.dir, "../test/test262")
const manifest = (await Bun.file(path.join(root, "manifest.json")).json()) as {
revision: string
directories: Array<string>
harness: Array<string>
flags: Array<string>
features: Array<string>
boundaries: Record<string, string>
}
const boundaries = Object.entries(manifest.boundaries).map(([name, pattern]) => [name, new RegExp(pattern)] as const)
const checkout = process.argv[2]
if (checkout === undefined) {
console.error("usage: bun run script/sync-test262.ts /path/to/test262")
process.exit(1)
}
const head = (await Bun.$`git -C ${checkout} rev-parse HEAD`.text()).trim()
if (head !== manifest.revision) {
console.error(`checkout is at ${head}; manifest pins ${manifest.revision}`)
process.exit(1)
}
const excluded = new Map<string, number>()
let copied = 0
for (const dir of manifest.directories) {
await rm(path.join(root, dir), { recursive: true, force: true })
const from = path.join(checkout, "test", dir)
for await (const file of new Bun.Glob("**/*.js").scan({ cwd: from })) {
if (file.endsWith("_FIXTURE.js")) continue
const source = await Bun.file(path.join(from, file)).text()
const start = source.indexOf("/*---")
const end = source.indexOf("---*/", start)
const meta = start === -1 ? {} : (Bun.YAML.parse(source.slice(start + 5, end)) as Frontmatter)
const code = start === -1 ? source : source.slice(end + 5)
const reason =
meta.flags?.find((flag) => manifest.flags.includes(flag)) ??
meta.features?.find((feature) => manifest.features.includes(feature)) ??
meta.includes?.find((include) => !manifest.harness.includes(include)) ??
boundaries.find(([, pattern]) => pattern.test(code))?.[0]
if (reason !== undefined) {
excluded.set(reason, (excluded.get(reason) ?? 0) + 1)
continue
}
await Bun.write(path.join(root, dir, file), Bun.file(path.join(from, file)))
copied++
}
}
console.log(`copied ${copied} files`)
for (const [reason, count] of [...excluded].sort((a, b) => b[1] - a[1])) {
console.log(` excluded ${String(count).padStart(5)} ${reason}`)
}
@@ -1,55 +0,0 @@
// Runs every vendored test262 file, including skipped ones, and groups failures by cause. Pass
// --write to regenerate test/test262/skipped.txt from the current failures.
//
// Usage: bun run script/test262-report.ts [--write] [path-prefix]
import path from "node:path"
import { root, run, skipped } from "../test/test262/run.js"
const write = process.argv.includes("--write")
const prefix = process.argv.slice(2).find((arg) => !arg.startsWith("--")) ?? ""
const files = [...new Bun.Glob("**/*.js").scanSync({ cwd: root })].filter((file) => file.startsWith(prefix)).sort()
const failures: Array<{ file: string; reason: string }> = []
const recovered: Array<string> = []
for (const file of files) {
const outcome = await run(file)
if (outcome.status === "fail") failures.push({ file, reason: outcome.reason })
if (outcome.status === "pass" && skipped.has(file)) recovered.push(file)
}
// Collapse a reason to the part that identifies the cause rather than the test.
const bucket = (reason: string) => {
const syntax = reason.match(/Syntax '([A-Za-z]+)' is not supported/)
if (syntax) return `unsupported syntax ${syntax[1]}`
if (reason.startsWith("expected ")) return reason.replace(/ but got .*/, " but the program ran")
return reason
.replace(/^(\$DONE: |ExecutionFailure: |InvalidDataValue: |ParseError: |Uncaught: |Test262Error: |Error: )+/, "")
.replace(/ \(line \d+, col \d+\)/, "")
.replace(/^[\w$]+\.(\w+) is not a function/, ".$1 is not a function")
.replace(/^[\w$]+ cannot be constructed/, "… cannot be constructed")
.replace(/'[^']*'/g, "'…'")
.slice(0, 100)
}
const buckets = new Map<string, Array<string>>()
for (const failure of failures) {
const key = bucket(failure.reason)
buckets.set(key, [...(buckets.get(key) ?? []), failure.file])
}
console.log(`${files.length - failures.length} pass, ${failures.length} fail of ${files.length}\n`)
for (const [key, list] of [...buckets].sort((a, b) => b[1].length - a[1].length)) {
console.log(`${String(list.length).padStart(5)} ${key}`)
for (const file of list.slice(0, 3)) console.log(` ${file}`)
if (list.length > 3) console.log(`${list.length - 3} more`)
}
if (recovered.length > 0) {
console.log(`\n${recovered.length} skipped files pass now; remove them from skipped.txt:`)
for (const file of recovered) console.log(` ${file}`)
}
if (write) {
const lines = failures.map((failure) => `${failure.file} # ${bucket(failure.reason)}`)
await Bun.write(path.join(root, "skipped.txt"), `${lines.join("\n")}\n`)
console.log(`\nwrote ${lines.length} entries to skipped.txt`)
}
+22 -50
View File
@@ -1,16 +1,11 @@
export * as Data from "./data.js"
import type { DiagnosticKind } from "./codemode.js"
import {
ownEntries,
parseArrayIndex,
ProgramArray,
ProgramFunction,
ProgramObject,
set,
} from "./interpreter/objects.js"
import { Values } from "./values.js"
/** A null-prototype object owned by the program. */
export type SafeObject = Record<string, unknown>
const MAX_VALUE_DEPTH = 32
export class ToolRuntimeError extends Error {
@@ -28,15 +23,15 @@ export class ToolRuntimeError extends Error {
}
/**
* Brings a host-produced value into the program: program and runtime values pass through, their
* host counterparts (Date, RegExp, Map, Set, URL, URLSearchParams) are wrapped, and host objects
* and arrays are copied.
* Brings a host-produced runtime value into the program: runtime values pass through, their host
* counterparts (Date, RegExp, Map, Set, URL, URLSearchParams) are wrapped, and objects become
* null-prototype copies. Arrays keep extra enumerable properties such as `index` and `groups`.
*/
export const toProgram = (value: unknown, label: string): unknown => copy(value, label, "program", 0, new Set())
/**
* Brings host data into the program: Date and URL become strings, other host collections become
* empty objects, and objects become program copies. Used for tool results and parsed JSON.
* empty objects, and objects become null-prototype copies. Used for tool results and parsed JSON.
*/
export const fromData = (value: unknown, label: string): unknown => copy(value, label, "data", 0, new Set())
@@ -49,7 +44,8 @@ export const fromData = (value: unknown, label: string): unknown => copy(value,
export const toData = (value: unknown, label: string, undefinedAs: "json" | "result" = "json"): unknown =>
copy(value, label, undefinedAs, 0, new Set())
// "program" and "data" build program objects; "json" and "result" build ordinary objects for the host.
// "program" and "data" build program-owned null-prototype objects; "json" and "result" build
// ordinary objects for the host.
type Mode = "program" | "data" | "json" | "result"
const copy = (value: unknown, label: string, mode: Mode, depth: number, seen: Set<object>): unknown => {
@@ -68,13 +64,9 @@ const copy = (value: unknown, label: string, mode: Mode, depth: number, seen: Se
`${label} contains an un-awaited Promise; await tool calls (e.g. \`const result = await tools.ns.tool(...)\`) before using their results.`,
)
}
if (value instanceof ProgramFunction && mode !== "program") {
throw new ToolRuntimeError("InvalidDataValue", `${label} must contain data only.`)
}
const plain = mode === "program" || mode === "data"
if (mode === "program") {
if (value instanceof ProgramObject || Values.isValue(value)) return value
if (Values.isValue(value)) return value
if (value instanceof Date) return new Values.Date(value.getTime())
if (value instanceof RegExp) return new Values.RegExp(value.source, value.flags)
if (value instanceof Map) {
@@ -93,6 +85,7 @@ const copy = (value: unknown, label: string, mode: Mode, depth: number, seen: Se
if (value instanceof URLSearchParams) return new Values.URLSearchParams(new URLSearchParams(value))
}
const plain = mode === "program" || mode === "data"
if (value instanceof Values.Date) return Number.isFinite(value.time) ? new Date(value.time).toISOString() : null
if (value instanceof Date) return Number.isFinite(value.getTime()) ? value.toISOString() : null
if (value instanceof Values.URL) return value.url.href
@@ -105,7 +98,7 @@ const copy = (value: unknown, label: string, mode: Mode, depth: number, seen: Se
value instanceof Set ||
value instanceof URLSearchParams
) {
return plain ? new ProgramObject() : {}
return plain ? (Object.create(null) as SafeObject) : {}
}
if (seen.has(value)) {
@@ -113,32 +106,17 @@ const copy = (value: unknown, label: string, mode: Mode, depth: number, seen: Se
}
seen.add(value)
if (value instanceof ProgramArray) {
const copied = Array.from(value.items, (item) => copy(item, label, mode, depth + 1, seen) ?? null)
seen.delete(value)
return copied
}
if (value instanceof ProgramObject) {
const copied: Record<string, unknown> = {}
for (const [key, item] of ownEntries(value)) {
const next = copy(item, label, mode, depth + 1, seen)
if (next === undefined && mode === "json") continue
define(copied, key, next)
}
seen.delete(value)
return copied
}
if (Array.isArray(value)) {
if (plain) {
const copied = new ProgramArray(value.map((item) => copy(item, label, mode, depth + 1, seen)))
// Host output densifies holes to null like JSON; program copies keep them.
const copied = plain
? value.map((item) => copy(item, label, mode, depth + 1, seen))
: Array.from(value, (item) => copy(item, label, mode, depth + 1, seen) ?? null)
if (mode === "program") {
for (const [key, item] of Object.entries(value)) {
if (parseArrayIndex(key) === undefined) set(copied, key, copy(item, label, mode, depth + 1, seen))
if (Object.hasOwn(copied, key)) continue
define(copied, key, copy(item, label, mode, depth + 1, seen))
}
seen.delete(value)
return copied
}
const copied = Array.from(value, (item) => copy(item, label, mode, depth + 1, seen) ?? null)
seen.delete(value)
return copied
}
@@ -148,13 +126,7 @@ const copy = (value: unknown, label: string, mode: Mode, depth: number, seen: Se
throw new ToolRuntimeError("InvalidDataValue", `${label} must contain plain objects only.`)
}
if (plain) {
const copied = new ProgramObject()
for (const [key, item] of Object.entries(value)) set(copied, key, copy(item, label, mode, depth + 1, seen))
seen.delete(value)
return copied
}
const copied: Record<string, unknown> = {}
const copied: SafeObject = plain ? (Object.create(null) as SafeObject) : {}
for (const [key, item] of Object.entries(value)) {
const next = copy(item, label, mode, depth + 1, seen)
if (next === undefined && mode === "json") continue
@@ -164,8 +136,8 @@ const copy = (value: unknown, label: string, mode: Mode, depth: number, seen: Se
return copied
}
// Own data property regardless of the target's prototype, so a "__proto__" key on a host object
// never reaches the Object.prototype setter.
// Own data property regardless of the target's prototype, so a "__proto__" key on a host object or
// array never reaches the Object.prototype setter.
const define = (target: object, key: string, value: unknown): void => {
Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true })
}
+9 -6
View File
@@ -1,11 +1,10 @@
import { Effect } from "effect"
import type { Diagnostic } from "../codemode.js"
import { ToolError } from "../tool-error.js"
import { toData, ToolRuntimeError } from "../data.js"
import { type SafeObject, toData, ToolRuntimeError } from "../data.js"
import { type AstNode, formatLocation, InterpreterRuntimeError, ProgramThrow, sourceLocation } from "./model.js"
import { containsRuntimeReference } from "./references.js"
import { type HostCall, HostFunction } from "./host.js"
import { get, ProgramError, ProgramObject } from "./objects.js"
import { type Runner } from "./runner.js"
import {
coerceToString,
@@ -45,8 +44,12 @@ export const normalizeError = (error: unknown): Diagnostic => {
message = "a non-data value"
} else if (typeof value === "string") {
message = value
} else if (value instanceof ProgramObject && typeof get(value, "message") === "string") {
message = get(value, "message") as string
} else if (
value !== null &&
typeof value === "object" &&
typeof (value as { message?: unknown }).message === "string"
) {
message = (value as { message: string }).message
} else {
try {
message = JSON.stringify(toData(value, "Thrown value")) ?? String(value)
@@ -84,14 +87,14 @@ export const caughtErrorValue = (thrown: unknown): unknown => {
return createErrorValue(name, normalizeError(thrown).message)
}
const constructErrorValue = (name: string, args: Array<unknown>): ProgramError =>
const constructErrorValue = (name: string, args: Array<unknown>): SafeObject =>
createErrorValue(name, args[0] === undefined ? "" : coerceToString(args[0]))
const constructAggregateErrorValue = <R>(
runner: Runner<R>,
args: Array<unknown>,
node: AstNode,
): Effect.Effect<ProgramError, unknown, R> =>
): Effect.Effect<SafeObject, unknown, R> =>
Effect.gen(function* () {
const cursor = yield* runner.syncIterator(args[0], node)
if (cursor === undefined) {
+1 -10
View File
@@ -7,7 +7,6 @@ import type { DataValue, Diagnostic, ResolvedExecutionLimits, Result } from "../
import { toData } from "../data.js"
import { ToolRuntime } from "../tool-runtime.js"
import { normalizeError } from "./errors.js"
import type { Host } from "./globals.js"
import { InterpreterRuntimeError } from "./model.js"
import { PromiseRuntime } from "./promises.js"
import { Runtime } from "./runtime.js"
@@ -17,7 +16,6 @@ export const executeProgram = <R>(
prepared: ToolRuntime.Prepared<R>,
limits: ResolvedExecutionLimits,
hooks: ToolRuntime.ToolCallHooks<R>,
extraGlobals?: (host: Host<R>) => ReadonlyArray<readonly [string, unknown]>,
): Effect.Effect<Result, never, R> => {
if (code.trim().length === 0) {
return Effect.succeed({
@@ -41,14 +39,7 @@ export const executeProgram = <R>(
Effect.gen(function* () {
const program = parseProgram(code)
const promises = new PromiseRuntime<R>(scope)
const value = yield* new Runtime<R>(
tools.execute,
tools.search,
tools.keys,
promises,
logs,
extraGlobals,
).run(program)
const value = yield* new Runtime<R>(tools.execute, tools.search, tools.keys, promises, logs).run(program)
const result = toData(value, "Execution result", "result") as DataValue
returned = { value: result, promises }
const warnings = yield* promises.interrupt()
+58 -61
View File
@@ -1,14 +1,13 @@
import { Effect } from "effect"
import { toProgram } from "../data.js"
import { type SafeObject, toProgram } from "../data.js"
import { dateSetterArgumentCount, invokeDateMethod } from "../stdlib/date.js"
import { invokeNumberMethod } from "../stdlib/number.js"
import { invokeRegExpMethod, matchToValue, toHostRegex } from "../stdlib/regexp.js"
import { invokeURLMethod, uriArgument } from "../stdlib/url.js"
import { coerceToNumber, coerceToString } from "../stdlib/value.js"
import { coerceToNumber, coerceToString, errorBrandName } from "../stdlib/value.js"
import { compareText } from "../tool-runtime.js"
import { Values } from "../values.js"
import { type AstNode, IntrinsicReference, InterpreterRuntimeError } from "./model.js"
import { get, ProgramArray, ProgramObject, record } from "./objects.js"
import { containsOpaqueReference, rejectCircularInsertion, typeofValue } from "./references.js"
import { applyCollectionCallback, isSupportedCallback, type Runner, toPrimitive } from "./runner.js"
@@ -17,14 +16,6 @@ export const invokeIntrinsic = <R>(
ref: IntrinsicReference,
args: Array<unknown>,
node: AstNode,
): Effect.Effect<unknown, unknown, R> =>
Effect.map(invoke(runner, ref, args, node), (result) => (Array.isArray(result) ? new ProgramArray(result) : result))
const invoke = <R>(
runner: Runner<R>,
ref: IntrinsicReference,
args: Array<unknown>,
node: AstNode,
): Effect.Effect<unknown, unknown, R> => {
if (typeof ref.receiver === "string") {
if (ref.name === "replace" || ref.name === "replaceAll") {
@@ -41,7 +32,7 @@ const invoke = <R>(
if (typeof ref.receiver === "number") {
return Effect.succeed(invokeNumberMethod(ref.receiver, ref.name, args, node))
}
if (ref.receiver instanceof ProgramArray) {
if (Array.isArray(ref.receiver)) {
return invokeArrayMethod(runner, ref.receiver, ref.name, args, node)
}
if (ref.receiver instanceof Values.Date) {
@@ -224,7 +215,7 @@ const invokeStringMethod = (value: string, name: string, args: Array<unknown>, n
node,
)
}
return new ProgramArray(Array.from(value.matchAll(pattern), matchToValue))
return Array.from(value.matchAll(pattern), matchToValue)
}
case "search": {
result = value.search(toHostRegex(args[0], name, node))
@@ -297,7 +288,13 @@ const invokeStringReplacer = <R>(
if (typeof match !== "string" || typeof offset !== "number") {
throw new InterpreterRuntimeError(`String.${name} produced an invalid replacement match.`, node)
}
if (hasGroups) callbackArgs[callbackArgs.length - 1] = record(groups as Record<string, unknown>)
if (hasGroups) {
const safeGroups: SafeObject = Object.create(null) as SafeObject
for (const [key, group] of Object.entries(groups)) {
safeGroups[key] = group
}
callbackArgs[callbackArgs.length - 1] = safeGroups
}
matches.push({ match, offset, args: callbackArgs })
return match
}
@@ -323,11 +320,14 @@ const invokeStringReplacer = <R>(
let end = 0
for (const match of matches) {
const replacement = yield* apply(match.args)
// Error values are branded plain objects; toProgram would strip the brand before coercion.
output.push(
value.slice(end, match.offset),
replacement instanceof Values.Promise
? "[object Promise]"
: coerceToString(toProgram(replacement, `String.${name} replacer result`)),
: errorBrandName(replacement)
? coerceToString(replacement)
: coerceToString(toProgram(replacement, `String.${name} replacer result`)),
)
end = match.offset + match.match.length
}
@@ -365,7 +365,7 @@ const invokeMapMethod = <R>(
case "values":
return Effect.sync(() => Array.from(target.map.values()))
case "entries":
return Effect.sync(() => Array.from(target.map.entries(), ([key, item]) => new ProgramArray([key, item])))
return Effect.sync(() => Array.from(target.map.entries(), ([key, item]): Array<unknown> => [key, item]))
case "forEach": {
const apply = applyCollectionCallback(runner, args[0], "Map.forEach", node)
return Effect.gen(function* () {
@@ -404,7 +404,7 @@ const invokeSetMethod = <R>(
case "values":
return Effect.sync(() => Array.from(target.set.values()))
case "entries":
return Effect.sync(() => Array.from(target.set.values(), (item) => new ProgramArray([item, item])))
return Effect.sync(() => Array.from(target.set.values(), (item): Array<unknown> => [item, item]))
case "forEach": {
const apply = applyCollectionCallback(runner, args[0], "Set.forEach", node)
return Effect.gen(function* () {
@@ -518,27 +518,28 @@ const loadSetRecord = <R>(runner: Runner<R>, source: unknown, name: string, node
keys: () => Effect.succeed(source.map.keys()),
})
}
if (!(source instanceof ProgramObject)) {
if (source === null || typeof source !== "object" || Values.isValue(source)) {
throw new InterpreterRuntimeError(`Set.${name} expects a Set-like object.`, node).as("TypeError")
}
const object = source as Record<string, unknown>
return Effect.gen(function* () {
const size = yield* coerceNumericArgument(runner, get(source, "size"), node)
const size = yield* coerceNumericArgument(runner, object.size, node)
if (Number.isNaN(size)) {
throw new InterpreterRuntimeError(`Set.${name} received a Set-like object with an invalid size.`, node).as(
"TypeError",
)
}
const has = get(source, "has")
const keys = get(source, "keys")
if (!isSupportedCallback(has) || !isSupportedCallback(keys)) {
if (!isSupportedCallback(object.has) || !isSupportedCallback(object.keys)) {
throw new InterpreterRuntimeError(`Set.${name} expects callable 'has' and 'keys' methods.`, node).as("TypeError")
}
const has = object.has
const keys = object.keys
return {
size: Math.max(Math.trunc(size), 0),
has: (item: unknown) => Effect.map(runner.invokeCallable(has, [item], node), Boolean),
keys: () =>
Effect.flatMap(runner.invokeCallable(keys, [], node), (result) => {
if (result instanceof ProgramArray) return Effect.succeed(result.items)
if (Array.isArray(result)) return Effect.succeed(result)
throw new InterpreterRuntimeError(`Set.${name} expected 'keys' to return an iterator.`, node).as("TypeError")
}),
}
@@ -603,7 +604,7 @@ const invokeURLSearchParamsMethod = <R>(
case "values":
return Effect.sync(() => Array.from(target.params.values()))
case "entries":
return Effect.sync(() => Array.from(target.params.entries(), ([key, value]) => new ProgramArray([key, value])))
return Effect.sync(() => Array.from(target.params.entries(), ([key, value]): Array<unknown> => [key, value]))
case "toString":
return Effect.sync(() => target.params.toString())
case "forEach": {
@@ -621,12 +622,11 @@ const invokeURLSearchParamsMethod = <R>(
const invokeArrayMethod = <R>(
runner: Runner<R>,
receiver: ProgramArray,
target: Array<unknown>,
name: string,
args: Array<unknown>,
node: AstNode,
): Effect.Effect<unknown, unknown, R> => {
const target = receiver.items
const optNumber = (value: unknown, label: string): number | undefined => {
if (value === undefined) return undefined
if (typeof value !== "number")
@@ -638,8 +638,9 @@ const invokeArrayMethod = <R>(
if (args.length > 1 || (args.length === 1 && typeof args[0] !== "string")) {
throw new InterpreterRuntimeError("Array.join expects zero arguments or one string separator.", node)
}
const input = toProgram(target, "Array.join input") as Array<unknown>
return Effect.succeed(
target.map((item) => coerceToString(item ?? "")).join(args.length === 0 ? "," : (args[0] as string)),
input.map((item) => coerceToString(item ?? "")).join(args.length === 0 ? "," : (args[0] as string)),
)
}
case "includes":
@@ -659,15 +660,11 @@ const invokeArrayMethod = <R>(
case "slice":
return Effect.succeed(target.slice(optNumber(args[0], "start"), optNumber(args[1], "end")))
case "concat":
return Effect.succeed(target.concat(...args.map((item) => (item instanceof ProgramArray ? item.items : item))))
case "flat": {
const flatten = (items: Array<unknown>, depth: number): Array<unknown> =>
items.flatMap((item) => (item instanceof ProgramArray && depth > 0 ? flatten(item.items, depth - 1) : [item]))
return Effect.succeed(flatten(target, optNumber(args[0], "depth") ?? 1))
}
return Effect.succeed(target.concat(...args))
case "flat":
return Effect.succeed(target.flat(optNumber(args[0], "depth") ?? 1))
case "reverse":
target.reverse()
return Effect.succeed(receiver)
return Effect.succeed(target.reverse())
case "sort": {
const length = target.length
const holeCount = Array.from({ length }, (_, index) => Object.hasOwn(target, index)).filter((own) => !own).length
@@ -679,7 +676,7 @@ const invokeArrayMethod = <R>(
Array.from({ length: holeCount }, (_, index) => itemCount + index).forEach((index) => {
Reflect.deleteProperty(target, index)
})
return receiver
return target
})
}
case "toSorted":
@@ -698,12 +695,12 @@ const invokeArrayMethod = <R>(
}
case "push": {
// Validate all insertions before mutating to avoid partial cyclic updates.
for (const item of args) rejectCircularInsertion(receiver, item, "Array.push result", node)
for (const item of args) rejectCircularInsertion(target, item, "Array.push result", node)
target.push(...args)
return Effect.succeed(target.length)
}
case "unshift": {
for (const item of args) rejectCircularInsertion(receiver, item, "Array.unshift result", node)
for (const item of args) rejectCircularInsertion(target, item, "Array.unshift result", node)
target.unshift(...args)
return Effect.succeed(target.length)
}
@@ -717,7 +714,7 @@ const invokeArrayMethod = <R>(
if (args.length === 1) return Effect.succeed(target.splice(start))
const deleteCount = optNumber(args[1], "delete count") ?? 0
const inserted = args.slice(2)
for (const item of inserted) rejectCircularInsertion(receiver, item, "Array.splice result", node)
for (const item of inserted) rejectCircularInsertion(target, item, "Array.splice result", node)
return Effect.succeed(target.splice(start, deleteCount, ...inserted))
}
case "toSpliced": {
@@ -734,23 +731,23 @@ const invokeArrayMethod = <R>(
return Effect.succeed(copied)
}
case "fill": {
rejectCircularInsertion(receiver, args[0], "Array.fill result", node)
target.fill(args[0], optNumber(args[1], "start"), optNumber(args[2], "end"))
return Effect.succeed(receiver)
rejectCircularInsertion(target, args[0], "Array.fill result", node)
return Effect.succeed(target.fill(args[0], optNumber(args[1], "start"), optNumber(args[2], "end")))
}
case "copyWithin":
target.copyWithin(
optNumber(args[0], "target index") ?? 0,
optNumber(args[1], "start") ?? 0,
optNumber(args[2], "end"),
return Effect.succeed(
target.copyWithin(
optNumber(args[0], "target index") ?? 0,
optNumber(args[1], "start") ?? 0,
optNumber(args[2], "end"),
),
)
return Effect.succeed(receiver)
case "keys":
return Effect.succeed(Array.from(target.keys()))
case "values":
return Effect.succeed([...target])
case "entries":
return Effect.succeed(Array.from(target.entries(), ([index, item]) => new ProgramArray([index, item])))
return Effect.succeed(Array.from(target.entries(), ([index, item]): Array<unknown> => [index, item]))
}
const apply = applyCollectionCallback(runner, args[0], `Array.${name}`, node)
@@ -763,7 +760,7 @@ const invokeArrayMethod = <R>(
values.length = length
for (let index = 0; index < length; index += 1) {
if (!(index in target)) continue
values[index] = yield* apply([target[index], index, receiver])
values[index] = yield* apply([target[index], index, target])
}
return values
}
@@ -771,8 +768,8 @@ const invokeArrayMethod = <R>(
const values: Array<unknown> = []
for (let index = 0; index < length; index += 1) {
if (!(index in target)) continue
const mapped = yield* apply([target[index], index, receiver])
if (mapped instanceof ProgramArray) values.push(...mapped.items)
const mapped = yield* apply([target[index], index, target])
if (Array.isArray(mapped)) values.push(...mapped)
else values.push(mapped)
}
return values
@@ -782,36 +779,36 @@ const invokeArrayMethod = <R>(
for (let index = 0; index < length; index += 1) {
if (!(index in target)) continue
const item = target[index]
if (yield* apply([item, index, receiver])) values.push(item)
if (yield* apply([item, index, target])) values.push(item)
}
return values
}
case "find":
for (let index = 0; index < length; index += 1) {
const item = target[index]
if (yield* apply([item, index, receiver])) return item
if (yield* apply([item, index, target])) return item
}
return undefined
case "findIndex":
for (let index = 0; index < length; index += 1) {
if (yield* apply([target[index], index, receiver])) return index
if (yield* apply([target[index], index, target])) return index
}
return -1
case "some":
for (let index = 0; index < length; index += 1) {
if (!(index in target)) continue
if (yield* apply([target[index], index, receiver])) return true
if (yield* apply([target[index], index, target])) return true
}
return false
case "every":
for (let index = 0; index < length; index += 1) {
if (!(index in target)) continue
if (!(yield* apply([target[index], index, receiver]))) return false
if (!(yield* apply([target[index], index, target]))) return false
}
return true
case "forEach":
for (let index = 0; index < length; index += 1) {
if (index in target) yield* apply([target[index], index, receiver])
if (index in target) yield* apply([target[index], index, target])
}
return undefined
case "reduce": {
@@ -828,7 +825,7 @@ const invokeArrayMethod = <R>(
}
for (let index = start; index < length; index += 1) {
if (!(index in target)) continue
accumulator = yield* apply([accumulator, target[index], index, receiver])
accumulator = yield* apply([accumulator, target[index], index, target])
}
return accumulator
}
@@ -846,19 +843,19 @@ const invokeArrayMethod = <R>(
}
for (let index = start; index >= 0; index -= 1) {
if (!(index in target)) continue
accumulator = yield* apply([accumulator, target[index], index, receiver])
accumulator = yield* apply([accumulator, target[index], index, target])
}
return accumulator
}
case "findLast":
for (let index = length - 1; index >= 0; index -= 1) {
const item = target[index]
if (yield* apply([item, index, receiver])) return item
if (yield* apply([item, index, target])) return item
}
return undefined
case "findLastIndex":
for (let index = length - 1; index >= 0; index -= 1) {
if (yield* apply([target[index], index, receiver])) return index
if (yield* apply([target[index], index, target])) return index
}
return -1
}
+13 -3
View File
@@ -1,7 +1,7 @@
import type { Node } from "acorn"
import type { BlockStatement, Expression, Node, Pattern } from "acorn"
import type { Effect } from "effect"
import type { DiagnosticKind } from "../codemode.js"
import type { ProgramObject } from "./objects.js"
import type { SafeObject } from "../data.js"
import type { Values } from "../values.js"
/** Any parsed node; the interpreter narrows on `type` and reads `loc` for diagnostics. */
@@ -20,10 +20,20 @@ export type StatementResult =
| { kind: "continue"; label?: string }
export type MemberReference = {
target: ProgramObject | Values.RegExp | Values.URL
target: SafeObject | Array<unknown> | Values.RegExp | Values.URL
key: PropertyKey
}
export class CodeModeFunction {
constructor(
readonly parameters: ReadonlyArray<Pattern>,
readonly body: BlockStatement | Expression,
readonly capturedScopes: ReadonlyArray<Map<string, Binding>>,
readonly async: boolean,
readonly generator: boolean,
) {}
}
export type GeneratorRequestKind = "next" | "return" | "throw"
export class CodeModeGenerator {
@@ -1,145 +0,0 @@
import type { BlockStatement, Expression, Pattern } from "acorn"
import { AsyncIteratorSymbol, type Binding, IteratorSymbol } from "./model.js"
/** An object owned by the program: own properties plus a prototype link. */
export class ProgramObject {
readonly props = new Map<PropertyKey, unknown>()
constructor(public proto: ProgramObject | null = null) {}
}
export class ProgramArray extends ProgramObject {
constructor(readonly items: Array<unknown> = []) {
super()
}
}
export class ProgramError extends ProgramObject {
constructor(readonly errorName: string) {
super()
}
}
export class ProgramFunction extends ProgramObject {
readonly length: number
constructor(
readonly name: string,
readonly parameters: ReadonlyArray<Pattern>,
readonly body: BlockStatement | Expression,
readonly capturedScopes: ReadonlyArray<Map<string, Binding>>,
readonly async: boolean,
readonly generator: boolean,
) {
super()
const optional = parameters.findIndex((p) => p.type === "AssignmentPattern" || p.type === "RestElement")
this.length = optional === -1 ? parameters.length : optional
}
}
const MAX_ARRAY_LENGTH = 4_294_967_295
export const parseArrayIndex = (key: string | number): number | undefined => {
const property = String(key)
if (!/^(0|[1-9]\d*)$/.test(property)) return undefined
const index = Number(property)
return index < MAX_ARRAY_LENGTH ? index : undefined
}
const canonical = (key: PropertyKey): string | symbol => (typeof key === "symbol" ? key : String(key))
const index = (target: ProgramObject, key: string | symbol): number | undefined =>
target instanceof ProgramArray && typeof key === "string" ? parseArrayIndex(key) : undefined
// Non-enumerable built-in properties: array length, function name and length.
const builtin = (target: ProgramObject, name: string | symbol): boolean =>
(name === "length" && target instanceof ProgramArray) ||
((name === "name" || name === "length") && target instanceof ProgramFunction)
export const hasOwn = (target: ProgramObject, key: PropertyKey): boolean => {
const name = canonical(key)
const at = index(target, name)
if (at !== undefined) return at in (target as ProgramArray).items
return builtin(target, name) || target.props.has(name)
}
export const getOwn = (target: ProgramObject, key: PropertyKey): unknown => {
const name = canonical(key)
const at = index(target, name)
if (at !== undefined) return (target as ProgramArray).items[at]
if (target instanceof ProgramArray && name === "length") return target.items.length
if (target instanceof ProgramFunction && name === "name") return target.name
if (target instanceof ProgramFunction && name === "length") return target.length
return target.props.get(name)
}
export const get = (target: ProgramObject, key: PropertyKey): unknown => {
for (let current: ProgramObject | null = target; current !== null; current = current.proto) {
if (hasOwn(current, key)) return getOwn(current, key)
}
return undefined
}
export const has = (target: ProgramObject, key: PropertyKey): boolean => {
for (let current: ProgramObject | null = target; current !== null; current = current.proto) {
if (hasOwn(current, key)) return true
}
return false
}
export const set = (target: ProgramObject, key: PropertyKey, value: unknown): boolean => {
const name = canonical(key)
const at = index(target, name)
if (at !== undefined) {
;(target as ProgramArray).items[at] = value
return true
}
if (name === "length" && target instanceof ProgramArray) {
const length = typeof value === "number" ? value : Number(value)
if (!Number.isInteger(length) || length < 0 || length > 4_294_967_295) return false
target.items.length = length
return true
}
if (builtin(target, name)) return false
target.props.set(name, value)
return true
}
export const remove = (target: ProgramObject, key: PropertyKey): boolean => {
const name = canonical(key)
const at = index(target, name)
if (at !== undefined) return delete (target as ProgramArray).items[at]
if (name === "length" && target instanceof ProgramArray) return false
if (builtin(target, name)) return true
target.props.delete(name)
return true
}
// JS order: array indexes, integer-like keys ascending, other strings, then symbols.
export const ownKeys = (target: ProgramObject): Array<string | symbol> => {
const strings = [...target.props.keys()].filter((key): key is string => typeof key === "string")
const symbols = [...target.props.keys()].filter((key): key is symbol => typeof key === "symbol")
return [
...(target instanceof ProgramArray ? Object.keys(target.items) : []),
...strings.filter((key) => parseArrayIndex(key) !== undefined).sort((a, b) => Number(a) - Number(b)),
...strings.filter((key) => parseArrayIndex(key) === undefined),
...symbols,
]
}
export const ownEntries = (target: ProgramObject): Array<[string, unknown]> =>
ownKeys(target)
.filter((key): key is string => typeof key === "string")
.map((key) => [key, getOwn(target, key)])
export const record = (entries: Record<string, unknown>): ProgramObject => {
const target = new ProgramObject()
for (const [key, value] of Object.entries(entries)) set(target, key, value)
return target
}
export const assign = (target: ProgramObject, source: ProgramObject, skip?: ReadonlySet<PropertyKey>): void => {
for (const key of ownKeys(source)) {
if (skip?.has(key)) continue
if (typeof key === "symbol" && key !== IteratorSymbol && key !== AsyncIteratorSymbol) continue
set(target, key, getOwn(source, key))
}
}
+23 -14
View File
@@ -1,7 +1,13 @@
import { Cause, Deferred, Effect, Exit, Fiber, Scope } from "effect"
import type { Diagnostic } from "../codemode.js"
import { type AstNode, InterpreterRuntimeError, ProgramThrow, PromiseInstanceMethodReference } from "./model.js"
import { get, ProgramArray, ProgramFunction, ProgramObject, record } from "./objects.js"
import type { SafeObject } from "../data.js"
import {
type AstNode,
CodeModeFunction,
InterpreterRuntimeError,
ProgramThrow,
PromiseInstanceMethodReference,
} from "./model.js"
import { HostFunction, requiresNew, sync } from "./host.js"
import { caughtErrorValue, normalizeError } from "./errors.js"
import { typeofValue } from "./references.js"
@@ -105,8 +111,8 @@ export const resolvePromiseValue = <R>(
): Effect.Effect<unknown, unknown, R> => {
if (own?.promise !== undefined && value === own.promise) return Effect.fail(selfResolutionError(node))
if (value instanceof Values.Promise) return runner.settlePromise(value)
if (!(value instanceof ProgramObject)) return Effect.succeed(value)
const then = get(value, "then")
if (value === null || typeof value !== "object" || !Object.hasOwn(value, "then")) return Effect.succeed(value)
const then = (value as SafeObject).then
if (typeofValue(then) !== "function") return Effect.succeed(value)
return Effect.gen(function* () {
@@ -168,12 +174,10 @@ const invokePromiseMethod = <R>(
}
if (name === "all") {
return new ProgramArray(
yield* settleAfterTurn(
Effect.all(
items.map((item) => Effect.flatten(promises.await(item))),
{ concurrency: "unbounded" },
),
return yield* settleAfterTurn(
Effect.all(
items.map((item) => Effect.flatten(promises.await(item))),
{ concurrency: "unbounded" },
),
)
}
@@ -182,14 +186,19 @@ const invokePromiseMethod = <R>(
for (const item of items) {
const exit = yield* promises.await(item)
if (Exit.isSuccess(exit)) {
outcomes.push(record({ status: "fulfilled", value: exit.value }))
outcomes.push(Object.assign(Object.create(null) as SafeObject, { status: "fulfilled", value: exit.value }))
continue
}
if (Cause.hasInterruptsOnly(exit.cause)) return yield* Effect.failCause(exit.cause)
outcomes.push(record({ status: "rejected", reason: caughtErrorValue(Cause.squash(exit.cause)) }))
outcomes.push(
Object.assign(Object.create(null) as SafeObject, {
status: "rejected",
reason: caughtErrorValue(Cause.squash(exit.cause)),
}),
)
}
yield* Effect.yieldNow
return new ProgramArray(outcomes)
return outcomes
}
if (name === "race") {
if (items.length === 0) {
@@ -244,7 +253,7 @@ const constructPromise = <R>(
executor: unknown,
node: AstNode,
): Effect.Effect<Values.Promise, unknown, R> => {
if (!(executor instanceof ProgramFunction)) {
if (!(executor instanceof CodeModeFunction)) {
throw new InterpreterRuntimeError(
"new Promise(...) expects an executor function (e.g. new Promise((resolve, reject) => { ... })).",
node,
@@ -3,18 +3,20 @@ import { Values } from "../values.js"
import { HostFunction, HostNamespace } from "./host.js"
import {
type AstNode,
AsyncIteratorSymbol,
CodeModeFunction,
CodeModeGenerator,
GeneratorMethodReference,
InterpreterRuntimeError,
IntrinsicReference,
IteratorSymbol,
PromiseInstanceMethodReference,
} from "./model.js"
import { getOwn, ownKeys, ProgramArray, ProgramFunction, ProgramObject } from "./objects.js"
export const isRuntimeReference = (value: unknown): boolean =>
value instanceof HostFunction ||
value instanceof HostNamespace ||
value instanceof ProgramFunction ||
value instanceof CodeModeFunction ||
value instanceof CodeModeGenerator ||
value instanceof GeneratorMethodReference ||
value instanceof ToolReference ||
@@ -24,8 +26,11 @@ export const isRuntimeReference = (value: unknown): boolean =>
Values.isValue(value)
function* childValues(value: object): Generator {
if (!(value instanceof ProgramObject)) return
for (const key of ownKeys(value)) yield getOwn(value, key)
for (const key of Reflect.ownKeys(value)) {
if (!Object.prototype.propertyIsEnumerable.call(value, key)) continue
if (typeof key === "symbol" && key !== AsyncIteratorSymbol && key !== IteratorSymbol) continue
yield Reflect.get(value, key)
}
}
// Depth-first search over a value tree. `match` stops the walk; `skip` prunes a subtree without matching it.
@@ -74,7 +79,7 @@ export const rejectCircularInsertion = (
export const describeValue = (value: unknown): string => {
if (value === null) return "null"
if (value instanceof ProgramArray) return "an array"
if (Array.isArray(value)) return "an array"
if (value instanceof Values.Promise) return "an un-awaited Promise"
if (value instanceof ToolReference) return "a tool reference"
if (value instanceof Values.Date) return "a Date"
@@ -92,7 +97,7 @@ export const describeValue = (value: unknown): string => {
export const typeofValue = (value: unknown): string => {
if (
value instanceof HostFunction ||
value instanceof ProgramFunction ||
value instanceof CodeModeFunction ||
value instanceof GeneratorMethodReference ||
value instanceof IntrinsicReference ||
value instanceof PromiseInstanceMethodReference
@@ -103,3 +108,12 @@ export const typeofValue = (value: unknown): string => {
if (value instanceof ToolReference) return value.path.length > 0 ? "function" : "object"
return typeof value
}
const MAX_ARRAY_LENGTH = 4_294_967_295
export const parseArrayIndex = (key: string | number): number | undefined => {
const property = String(key)
if (!/^(0|[1-9]\d*)$/.test(property)) return undefined
const index = Number(property)
return index < MAX_ARRAY_LENGTH ? index : undefined
}
+8 -10
View File
@@ -2,8 +2,7 @@ import { Effect, Exit } from "effect"
import { Values } from "../values.js"
import { coerceToString } from "../stdlib/value.js"
import { HostFunction } from "./host.js"
import { type AstNode, InterpreterRuntimeError, IntrinsicReference } from "./model.js"
import { get, has, ProgramFunction, ProgramObject } from "./objects.js"
import { type AstNode, CodeModeFunction, InterpreterRuntimeError, IntrinsicReference } from "./model.js"
import { typeofValue } from "./references.js"
export type IteratorCursor<R> = {
@@ -13,7 +12,7 @@ export type IteratorCursor<R> = {
/** Everything a host function needs to call back into the program. */
export type Runner<R> = {
readonly invokeFunction: (fn: ProgramFunction, args: Array<unknown>) => Effect.Effect<unknown, unknown, R>
readonly invokeFunction: (fn: CodeModeFunction, args: Array<unknown>) => Effect.Effect<unknown, unknown, R>
readonly invokeCallable: (
callable: unknown,
args: Array<unknown>,
@@ -43,14 +42,13 @@ export const toPrimitive = <R>(
if (Values.isValue(value)) {
return Effect.succeed(value instanceof Values.Date && hint === "number" ? value.time : coerceToString(value))
}
if (!(value instanceof ProgramObject)) return Effect.succeed(value)
const object = value as Record<string, unknown>
const order = hint === "number" ? ["valueOf", "toString"] : ["toString", "valueOf"]
return Effect.gen(function* () {
for (const method of order) {
if (method === "toString" && !has(value, "toString")) return coerceToString(value)
const callable = get(value, method)
if (typeofValue(callable) !== "function") continue
const result = yield* runner.invokeCallable(callable, [], node)
if (method === "toString" && !Object.hasOwn(object, "toString")) return coerceToString(value)
if (!Object.hasOwn(object, method) || typeofValue(object[method]) !== "function") continue
const result = yield* runner.invokeCallable(object[method], [], node)
if (result === null || (typeof result !== "object" && typeof result !== "function")) return result
}
throw new InterpreterRuntimeError("Cannot convert object to primitive value.", node).as("TypeError")
@@ -61,10 +59,10 @@ export const toPrimitive = <R>(
// Array.from mappers, and promise reactions all admit exactly these callables.
// Admission means dispatchable, not necessarily invocable: new-requiring
// constructors pass the gate and throw a TypeError on call, like JS.
export type SupportedCallback = ProgramFunction | HostFunction<unknown> | IntrinsicReference
export type SupportedCallback = CodeModeFunction | HostFunction<unknown> | IntrinsicReference
export const isSupportedCallback = (value: unknown): value is SupportedCallback =>
value instanceof ProgramFunction ||
value instanceof CodeModeFunction ||
(value instanceof HostFunction && value.callback) ||
value instanceof IntrinsicReference
+152 -159
View File
@@ -1,7 +1,6 @@
import type {
ArrayExpression,
ArrayPattern,
AssignmentPattern,
ArrowFunctionExpression,
AssignmentExpression,
AssignmentProperty,
@@ -42,12 +41,13 @@ import type {
YieldExpression,
} from "acorn"
import { Cause, Deferred, Effect, Exit } from "effect"
import { ToolRuntimeError, toProgram } from "../data.js"
import { ToolRuntimeError, type SafeObject, toProgram } from "../data.js"
import { ToolReference } from "../tool-runtime.js"
import {
type AstNode,
AsyncIteratorSymbol,
type Binding,
CodeModeFunction,
CodeModeGenerator,
ComputedValue,
GeneratorMethodReference,
@@ -55,7 +55,9 @@ import {
type GeneratorRequestKind,
IntrinsicReference,
InterpreterRuntimeError,
isRecord,
IteratorSymbol,
IteratorSymbols,
type MemberReference,
OptionalShortCircuit,
PromiseInstanceMethodReference,
@@ -64,28 +66,16 @@ import {
unsupportedSyntax,
} from "./model.js"
import { caughtErrorValue } from "./errors.js"
import { globals, type Host } from "./globals.js"
import { globals } from "./globals.js"
import { HostFunction, HostNamespace } from "./host.js"
import { invokeIntrinsic } from "./methods.js"
import {
assign,
get,
has,
ownKeys,
parseArrayIndex,
ProgramArray,
ProgramFunction,
ProgramObject,
record,
remove,
set,
} from "./objects.js"
import { preserveConsumerError, type Runner } from "./runner.js"
import { invokePromiseInstanceMethod, PromiseRuntime, resolvePromise, resolvePromiseValue } from "./promises.js"
import {
containsOpaqueReference,
describeValue,
isRuntimeReference,
parseArrayIndex,
rejectCircularInsertion,
typeofValue,
} from "./references.js"
@@ -128,11 +118,14 @@ const calleeDescription = (callee: Expression | Super | undefined): string => {
return "The called value"
}
const hasOwn = (value: unknown, key: PropertyKey): boolean =>
value !== null && typeof value === "object" && Object.hasOwn(value, key)
const constructorName = (value: unknown): string | undefined => {
if (typeof value === "string") return "String"
if (typeof value === "number") return "Number"
if (typeof value === "boolean") return "Boolean"
if (value instanceof ProgramArray) return "Array"
if (Array.isArray(value)) return "Array"
if (value instanceof Values.Date) return "Date"
if (value instanceof Values.RegExp) return "RegExp"
if (value instanceof Values.Map) return "Map"
@@ -140,7 +133,7 @@ const constructorName = (value: unknown): string | undefined => {
if (value instanceof Values.URL) return "URL"
if (value instanceof Values.URLSearchParams) return "URLSearchParams"
if (value instanceof Values.Promise) return "Promise"
if (!(value instanceof ProgramObject) || value instanceof ProgramFunction) return undefined
if (value === null || typeof value !== "object" || isRuntimeReference(value)) return undefined
return errorBrandName(value) ?? "Object"
}
@@ -237,7 +230,7 @@ const loopDeclaration = (left: VariableDeclaration | Pattern, statement: "for...
}
type CustomIterator = {
iterator: ProgramObject | CodeModeGenerator
iterator: SafeObject | CodeModeGenerator
next: unknown
asynchronous: boolean
}
@@ -254,6 +247,13 @@ const isOpaqueMemberReference = (value: unknown): value is OpaqueMemberReference
value instanceof IntrinsicReference ||
value instanceof GeneratorMethodReference
const copyIteratorSymbols = (source: object, target: object, consumed?: ReadonlySet<PropertyKey>): void => {
for (const symbol of IteratorSymbols) {
if (!consumed?.has(symbol) && Object.hasOwn(source, symbol))
Reflect.set(target, symbol, Reflect.get(source, symbol))
}
}
type GeneratorRequest = {
kind: GeneratorRequestKind
value: unknown
@@ -285,7 +285,6 @@ export class Runtime<R> {
readonly toolKeys: (path: ReadonlyArray<string>) => ReadonlyArray<string>,
readonly promises: PromiseRuntime<R>,
readonly logs: Array<string> = [],
extraGlobals: (host: Host<R>) => ReadonlyArray<readonly [string, unknown]> = () => [],
) {
const globalScope = new Map<string, Binding>()
// Calling back into the program never reads frame state, so any frame serves; the root is always alive.
@@ -296,7 +295,7 @@ export class Runtime<R> {
settlePromise: (promise) => this.root.settlePromise(promise),
syncIterator: (value, node) => this.root.syncIterator(value, node),
}
this.builtins = new Map([...globals(this), ...extraGlobals(this)])
this.builtins = new Map(globals(this))
for (const [name, value] of this.builtins) globalScope.set(name, { mutable: false, value })
}
@@ -435,19 +434,8 @@ class Frame<R> {
}).pipe(Effect.ensuring(Effect.sync(() => self.scopes.pop())))
}
private createFunction(
node: FunctionDeclaration | FunctionExpression | ArrowFunctionExpression,
name = node.type === "ArrowFunctionExpression" ? "" : (node.id?.name ?? ""),
): ProgramFunction {
return new ProgramFunction(name, node.params, node.body, this.scopes.capture(), node.async, node.generator)
}
// NamedEvaluation: an anonymous function definition takes the name of what it is assigned to.
private evaluateNamed(node: Expression, name: string): Effect.Effect<unknown, unknown, R> {
if (node.type === "ArrowFunctionExpression" || (node.type === "FunctionExpression" && !node.id)) {
return Effect.sync(() => this.createFunction(node, name))
}
return this.evaluateExpression(node)
private createFunction(node: FunctionDeclaration | FunctionExpression | ArrowFunctionExpression): CodeModeFunction {
return new CodeModeFunction(node.params, node.body, this.scopes.capture(), node.async, node.generator)
}
private hoistFunctions(statements: ReadonlyArray<Statement | ModuleDeclaration>): void {
@@ -734,26 +722,22 @@ class Frame<R> {
}
syncIterator(value: unknown, node: AstNode) {
const iterator =
value instanceof ProgramArray
? value.items[Symbol.iterator]()
: typeof value === "string"
? value[Symbol.iterator]()
: value instanceof Values.Map
? value.map.entries()
: value instanceof Values.Set
? value.set.values()
: value instanceof Values.URLSearchParams
? value.params.entries()
: undefined
const iterator = Array.isArray(value)
? value[Symbol.iterator]()
: typeof value === "string"
? value[Symbol.iterator]()
: value instanceof Values.Map
? value.map.entries()
: value instanceof Values.Set
? value.set.values()
: value instanceof Values.URLSearchParams
? value.params.entries()
: undefined
if (iterator !== undefined) {
return Effect.succeed({
next: Effect.sync(() => {
const step = iterator.next()
return {
done: Boolean(step.done),
value: Array.isArray(step.value) ? new ProgramArray(step.value) : step.value,
}
return { done: Boolean(step.done), value: step.value }
}),
close: Effect.void,
})
@@ -778,9 +762,9 @@ class Frame<R> {
asynchronous: value.asynchronous,
})
}
if (!(value instanceof ProgramObject)) return Effect.undefined
const asyncMethod = allowAsync ? get(value, AsyncIteratorSymbol) : undefined
const method = asyncMethod ?? get(value, IteratorSymbol)
if (!isRecord(value) || isRuntimeReference(value)) return Effect.undefined
const asyncMethod = allowAsync ? Reflect.get(value, AsyncIteratorSymbol) : undefined
const method = asyncMethod ?? Reflect.get(value, IteratorSymbol)
if (method === undefined || method === null) return Effect.undefined
const self = this
return Effect.map(
@@ -792,7 +776,7 @@ class Frame<R> {
next:
object instanceof CodeModeGenerator
? new GeneratorMethodReference(object, "next")
: self.requireIteratorMethod(get(object, "next"), "Iterator next", node),
: self.requireIteratorMethod(object.next, "Iterator next", node),
asynchronous: asyncMethod !== undefined && asyncMethod !== null,
}
},
@@ -808,7 +792,7 @@ class Frame<R> {
"Iterator next() result",
node,
)
return { done: Boolean(get(object, "done")), value: get(object, "value") }
return { done: Boolean(object.done), value: object.value }
}
const called = yield* Effect.exit(self.invokeCallable(iterator.next, [], node))
@@ -819,7 +803,7 @@ class Frame<R> {
const captured = yield* Effect.exit(
Effect.sync(() => {
const object = self.requireIteratorObject(called.value, "Iterator next() result", node)
return { done: Boolean(get(object, "done")), value: get(object, "value") }
return { done: Boolean(object.done), value: object.value }
}),
)
if (!Exit.isSuccess(captured)) {
@@ -839,7 +823,7 @@ class Frame<R> {
const close =
iterator.iterator instanceof CodeModeGenerator
? new GeneratorMethodReference(iterator.iterator, "return")
: get(iterator.iterator, "return")
: iterator.iterator.return
if (close === undefined || close === null) return iterator.asynchronous || !awaiting ? Effect.void : Effect.yieldNow
const self = this
return Effect.gen(function* () {
@@ -859,7 +843,7 @@ class Frame<R> {
return yield* Effect.failCause(called.cause)
}
const captured = yield* Effect.exit(
Effect.sync(() => get(self.requireIteratorObject(called.value, "Iterator return() result", node), "value")),
Effect.sync(() => self.requireIteratorObject(called.value, "Iterator return() result", node).value),
)
if (!Exit.isSuccess(captured)) {
if (awaiting) yield* Effect.yieldNow
@@ -869,12 +853,12 @@ class Frame<R> {
})
}
private requireIteratorObject(value: unknown, context: string, node: AstNode): ProgramObject {
if (value instanceof ProgramObject) return value
private requireIteratorObject(value: unknown, context: string, node: AstNode): SafeObject {
if (isRecord(value) && !isRuntimeReference(value)) return value
throw new InterpreterRuntimeError(`${context} must be an object.`, node).as("TypeError")
}
private requireIterator(value: unknown, node: AstNode): ProgramObject | CodeModeGenerator {
private requireIterator(value: unknown, node: AstNode): SafeObject | CodeModeGenerator {
return value instanceof CodeModeGenerator
? value
: this.requireIteratorObject(value, "Iterator method result", node)
@@ -889,7 +873,7 @@ class Frame<R> {
private enumerableKeys(value: unknown, node: AstNode): Array<string> {
if (value instanceof ToolReference) return [...this.runtime.toolKeys(value.path)]
if (value === null || value === undefined) return []
return ownKeys(enumerableSource("for...in", value, node)).filter((key): key is string => typeof key === "string")
return Object.keys(enumerableSource("for...in", value, node))
}
private evaluateForInStatement(
@@ -1035,14 +1019,11 @@ class Frame<R> {
const init = declaration.init
// `var x` alone is a no-op: the binding was hoisted on function entry.
const id = declaration.id
const evaluate = (init: Expression) =>
id.type === "Identifier" ? self.evaluateNamed(init, id.name) : self.evaluateExpression(init)
if (kind === "var") {
if (init) yield* self.assignPattern(id, yield* evaluate(init), declaration)
if (init) yield* self.assignPattern(declaration.id, yield* self.evaluateExpression(init), declaration)
continue
}
const value = init ? yield* evaluate(init) : undefined
const value = init ? yield* self.evaluateExpression(init) : undefined
yield* self.declarePattern(declaration.id, value, kind !== "const", declaration, true)
}
})
@@ -1065,13 +1046,13 @@ class Frame<R> {
}
if (pattern.type === "AssignmentPattern") {
const resolved = value === undefined ? yield* self.evaluateDefault(pattern) : value
const resolved = value === undefined ? yield* self.evaluateExpression(pattern.right) : value
yield* self.declarePattern(pattern.left, resolved, mutable, node, initialize)
return
}
if (pattern.type === "ObjectPattern") {
if (!(value instanceof ProgramObject)) {
if (value === null || typeof value !== "object" || isRuntimeReference(value)) {
throw new InterpreterRuntimeError(
`Object destructuring requires a data object or array value, received ${describeValue(value)}.`,
pattern,
@@ -1082,8 +1063,11 @@ class Frame<R> {
const consumed = new Set<PropertyKey>()
for (const property of pattern.properties) {
if (property.type === "RestElement") {
const rest = new ProgramObject()
assign(rest, value, consumed)
const rest: SafeObject = Object.create(null) as SafeObject
for (const [key, item] of Object.entries(value as SafeObject)) {
if (!consumed.has(key)) rest[key] = item
}
copyIteratorSymbols(value, rest, consumed)
yield* self.declarePattern(property.argument, rest, mutable, property, initialize)
continue
}
@@ -1092,7 +1076,7 @@ class Frame<R> {
consumed.add(typeof key === "symbol" ? key : String(key))
yield* self.declarePattern(
property.value,
self.destructuringPropertyValue(value, key),
self.destructuringPropertyValue(value as SafeObject | Array<unknown>, key),
mutable,
property,
initialize,
@@ -1125,13 +1109,13 @@ class Frame<R> {
}
if (pattern.type === "AssignmentPattern") {
const resolved = value === undefined ? yield* self.evaluateDefault(pattern) : value
const resolved = value === undefined ? yield* self.evaluateExpression(pattern.right) : value
yield* self.assignPattern(pattern.left, resolved, node)
return
}
if (pattern.type === "ObjectPattern") {
if (!(value instanceof ProgramObject)) {
if (value === null || typeof value !== "object" || isRuntimeReference(value)) {
throw new InterpreterRuntimeError(
`Object destructuring requires a data object or array value, received ${describeValue(value)}.`,
pattern,
@@ -1139,17 +1123,21 @@ class Frame<R> {
)
}
const source = value as SafeObject | Array<unknown>
const consumed = new Set<PropertyKey>()
for (const property of pattern.properties) {
if (property.type === "RestElement") {
const rest = new ProgramObject()
assign(rest, value, consumed)
const rest: SafeObject = Object.create(null) as SafeObject
for (const [key, item] of Object.entries(source)) {
if (!consumed.has(key)) rest[key] = item
}
copyIteratorSymbols(source, rest, consumed)
yield* self.assignPattern(property.argument, rest, property)
continue
}
const key = yield* self.destructuringPropertyKey(property)
consumed.add(typeof key === "symbol" ? key : String(key))
yield* self.assignPattern(property.value, self.destructuringPropertyValue(value, key), property)
yield* self.assignPattern(property.value, self.destructuringPropertyValue(source, key), property)
}
return
}
@@ -1164,12 +1152,6 @@ class Frame<R> {
})
}
private evaluateDefault(pattern: AssignmentPattern): Effect.Effect<unknown, unknown, R> {
return pattern.left.type === "Identifier"
? this.evaluateNamed(pattern.right, pattern.left.name)
: this.evaluateExpression(pattern.right)
}
private destructureArrayPattern(
pattern: ArrayPattern,
value: unknown,
@@ -1189,7 +1171,7 @@ class Frame<R> {
if (element === null) continue
yield* consume(
element.type === "RestElement" ? element.argument : element,
element.type === "RestElement" ? new ProgramArray() : undefined,
element.type === "RestElement" ? [] : undefined,
element,
)
if (element.type === "RestElement") return
@@ -1206,7 +1188,7 @@ class Frame<R> {
done = next.done
if (!done) rest.push(next.value)
}
yield* consume(element.argument, new ProgramArray(rest), element)
yield* consume(element.argument, rest, element)
return
}
const consumed = consume(element, step.done ? undefined : step.value, pattern)
@@ -1229,9 +1211,11 @@ class Frame<R> {
throw unsupportedSyntax(keyNode.type, keyNode)
}
private destructuringPropertyValue(source: ProgramObject, key: PropertyKey): unknown {
if (!(source instanceof ProgramArray)) return get(source, key)
if (has(source, key)) return get(source, key)
private destructuringPropertyValue(source: SafeObject | Array<unknown>, key: PropertyKey): unknown {
if (!Array.isArray(source)) return Reflect.get(source, key)
if (key === "length") return source.length
if (typeof key === "number") return source[key]
if (Object.hasOwn(source, key)) return Reflect.get(source, key)
if (typeof key === "string" && arrayMethods.has(key)) return new IntrinsicReference(source, key)
return undefined
}
@@ -1309,7 +1293,7 @@ class Frame<R> {
// otherwise; say `new` is unsupported for them and point at the plain call.
const name = calleeDescription(node.callee)
const message =
callee instanceof ProgramFunction
callee instanceof CodeModeFunction
? `${name} cannot be constructed: user-defined constructors and classes are not supported. Call it as a function that returns a plain object instead.`
: callee instanceof HostFunction
? `new ${name}(...) is not supported; call ${name}(...) without new instead.`
@@ -1337,9 +1321,6 @@ class Frame<R> {
private applyBinaryOperator(operator: string, lhs: unknown, rhs: unknown, node: AstNode): unknown {
if (operator === "===") return lhs === rhs
if (operator === "!==") return lhs !== rhs
if (operator === "in" && rhs instanceof ProgramObject && !containsOpaqueReference(lhs)) {
return has(rhs, lhs !== null && typeof lhs === "object" ? coerceToString(lhs) : (lhs as PropertyKey))
}
if (containsOpaqueReference(lhs) || containsOpaqueReference(rhs)) {
throw new InterpreterRuntimeError("Binary operators require data values.", node, "InvalidDataValue")
}
@@ -1392,10 +1373,11 @@ class Frame<R> {
case ">>>":
return (l as number) >>> (r as number)
case "in":
if (!(rhs instanceof ProgramObject)) {
if (rhs === null || typeof rhs !== "object") {
throw new InterpreterRuntimeError("The 'in' operator requires a data object on the right-hand side.", node)
}
return has(rhs, coerceOperand(lhs) as PropertyKey)
// Never expose properties inherited from host prototypes.
return Object.hasOwn(rhs as object, coerceOperand(lhs) as PropertyKey)
default:
throw new InterpreterRuntimeError(`Unsupported binary operator '${operator}'.`, node)
}
@@ -1472,7 +1454,7 @@ class Frame<R> {
const next = toProgram(self.applyCompoundAssignment(operator, current, rightValue, node), "Assignment result")
return self.scopes.set(name, next, left)
}
const rightValue = yield* self.evaluateNamed(node.right, name)
const rightValue = yield* self.evaluateExpression(node.right)
return self.scopes.set(name, rightValue, left)
}
if (left.type === "MemberExpression") {
@@ -1504,7 +1486,7 @@ class Frame<R> {
return Effect.gen(function* () {
const current = self.scopes.get(name, left)
if (!shouldAssign(current)) return current
const rightValue = yield* self.evaluateNamed(node.right, name)
const rightValue = yield* self.evaluateExpression(node.right)
return self.scopes.set(name, rightValue, left)
})
}
@@ -1593,7 +1575,7 @@ class Frame<R> {
}
return yield* self.createToolCallPromise(callable.path, args)
}
if (callable instanceof ProgramFunction) {
if (callable instanceof CodeModeFunction) {
return yield* self.invokeFunction(callable, args)
}
if (callable instanceof GeneratorMethodReference) {
@@ -1644,7 +1626,7 @@ class Frame<R> {
})
}
invokeFunction(fn: ProgramFunction, args: Array<unknown>): Effect.Effect<unknown, unknown, R> {
invokeFunction(fn: CodeModeFunction, args: Array<unknown>): Effect.Effect<unknown, unknown, R> {
const invocation = new Frame(this.runtime, new ScopeStack([...fn.capturedScopes, new Map()]))
const run = Effect.gen(function* () {
// Seed all parameters first so defaults cannot fall through to same-named outer bindings.
@@ -1656,13 +1638,7 @@ class Frame<R> {
}
for (const [index, parameter] of fn.parameters.entries()) {
if (parameter.type === "RestElement") {
yield* invocation.declarePattern(
parameter.argument,
new ProgramArray(args.slice(index)),
true,
parameter,
true,
)
yield* invocation.declarePattern(parameter.argument, args.slice(index), true, parameter, true)
break
}
yield* invocation.declarePattern(parameter, args[index], true, parameter, true)
@@ -1714,12 +1690,12 @@ class Frame<R> {
}
if (state.completed) {
if (kind === "throw") return Effect.fail(new ProgramThrow(value))
return Effect.succeed(record({ value: kind === "return" ? value : undefined, done: true }))
return Effect.succeed({ value: kind === "return" ? value : undefined, done: true })
}
if (!state.started && kind !== "next") {
state.completed = true
if (kind === "throw") return Effect.fail(new ProgramThrow(value))
return Effect.succeed(record({ value, done: true }))
return Effect.succeed({ value, done: true })
}
state.pending.push(request)
@@ -1749,7 +1725,7 @@ class Frame<R> {
if (active) {
Deferred.doneUnsafe(
active.response,
Exit.isSuccess(exit) ? Exit.succeed(record({ value: exit.value, done: true })) : exit,
Exit.isSuccess(exit) ? Exit.succeed({ value: exit.value, done: true }) : exit,
)
}
yield* invocation.completeGeneratorRequests(state, asynchronous)
@@ -1776,13 +1752,13 @@ class Frame<R> {
const resolved = yield* Effect.exit(self.awaitValue(pending.value))
Deferred.doneUnsafe(
pending.response,
Exit.isSuccess(resolved) ? Exit.succeed(record({ value: resolved.value, done: true })) : resolved,
Exit.isSuccess(resolved) ? Exit.succeed({ value: resolved.value, done: true }) : resolved,
)
continue
}
Deferred.doneUnsafe(
pending.response,
Exit.succeed(record({ value: pending.kind === "return" ? pending.value : undefined, done: true })),
Exit.succeed({ value: pending.kind === "return" ? pending.value : undefined, done: true }),
)
}
})
@@ -1827,7 +1803,7 @@ class Frame<R> {
private suspendGenerator(value: unknown, node: AstNode): Effect.Effect<unknown, unknown, R> {
const state = this.generatorState
if (!state?.active) throw new InterpreterRuntimeError("Generator has no active request.", node)
Deferred.doneUnsafe(state.active.response, Exit.succeed(record({ value, done: false })))
Deferred.doneUnsafe(state.active.response, Exit.succeed({ value, done: false }))
state.active = undefined
return Effect.flatMap(this.takeGeneratorRequest(state), (request) => {
state.active = request
@@ -1843,7 +1819,7 @@ class Frame<R> {
const self = this
return Effect.gen(function* () {
if (
value instanceof ProgramArray ||
Array.isArray(value) ||
typeof value === "string" ||
value instanceof Values.Map ||
value instanceof Values.Set ||
@@ -1884,7 +1860,7 @@ class Frame<R> {
? iterator.next
: iterator.iterator instanceof CodeModeGenerator
? new GeneratorMethodReference(iterator.iterator, kind)
: get(iterator.iterator, kind)
: iterator.iterator[kind]
if (method === undefined || method === null) {
if (kind === "return") return yield* Effect.fail(new GeneratorReturn(input))
yield* self.closeIterator(iterator, node, self.generatorAsync)
@@ -1902,11 +1878,11 @@ class Frame<R> {
`Iterator ${kind}() result`,
node,
)
const done = Boolean(get(result, "done"))
const done = Boolean(result.done)
const resultValue: unknown =
self.generatorAsync && !iterator.asynchronous
? yield* self.awaitAsyncFromSyncValue(iterator, get(result, "value"), node, kind !== "return" && !done)
: get(result, "value")
? yield* self.awaitAsyncFromSyncValue(iterator, result.value, node, kind !== "return" && !done)
: result.value
if (done) {
if (kind === "return") return yield* Effect.fail(new GeneratorReturn(resultValue))
return resultValue
@@ -1928,15 +1904,17 @@ class Frame<R> {
})
}
private evaluateObjectExpression(node: ObjectExpression): Effect.Effect<ProgramObject, unknown, R> {
const objectValue = new ProgramObject()
private evaluateObjectExpression(node: ObjectExpression): Effect.Effect<Record<string, unknown>, unknown, R> {
const objectValue: Record<string, unknown> = Object.create(null) as Record<string, unknown>
const self = this
return Effect.gen(function* () {
for (const property of node.properties) {
if (property.type === "SpreadElement") {
const spread = yield* self.evaluateExpression(property.argument)
if (spread === null || spread === undefined) continue
assign(objectValue, enumerableSource("Object spread", spread, property))
const from = enumerableSource("Object spread", spread, property)
for (const [key, value] of Object.entries(from)) objectValue[key] = value
if (typeof from === "object") copyIteratorSymbols(from, objectValue)
continue
}
@@ -1958,20 +1936,14 @@ class Frame<R> {
throw new InterpreterRuntimeError("Unsupported object property key shape.", keyNode)
}
const name =
key === IteratorSymbol
? "[Symbol.iterator]"
: key === AsyncIteratorSymbol
? "[Symbol.asyncIterator]"
: String(key)
set(objectValue, key, yield* self.evaluateNamed(property.value, name))
Reflect.set(objectValue, key, yield* self.evaluateExpression(property.value))
}
return objectValue
})
}
private evaluateArrayExpression(node: ArrayExpression): Effect.Effect<ProgramArray, unknown, R> {
private evaluateArrayExpression(node: ArrayExpression): Effect.Effect<Array<unknown>, unknown, R> {
const values: Array<unknown> = []
const self = this
@@ -1996,7 +1968,7 @@ class Frame<R> {
values.push(yield* self.evaluateExpression(element))
}
}
return new ProgramArray(values)
return values
})
}
@@ -2083,11 +2055,7 @@ class Frame<R> {
}
// Values have no prototype chain, so `.constructor` resolves to the owning built-in directly.
if (
operation === "read" &&
key === "constructor" &&
!(objectValue instanceof ProgramObject && has(objectValue, key))
) {
if (operation === "read" && key === "constructor" && !hasOwn(objectValue, key)) {
const name = constructorName(objectValue)
if (name !== undefined) return new ComputedValue(self.runtime.builtins.get(name))
}
@@ -2168,8 +2136,6 @@ class Frame<R> {
return new ComputedValue(undefined)
}
if (objectValue instanceof ProgramObject) return { target: objectValue, key }
if (isRuntimeReference(objectValue)) {
throw new InterpreterRuntimeError(
`Cannot read properties of ${describeValue(objectValue)}; only data values expose properties.`,
@@ -2177,27 +2143,42 @@ class Frame<R> {
"InvalidDataValue",
)
}
throw new InterpreterRuntimeError("Cannot access a property on a non-object value.", objectNode)
if (typeof objectValue !== "object" || objectValue === null) {
throw new InterpreterRuntimeError("Cannot access a property on a non-object value.", objectNode)
}
if (Array.isArray(objectValue)) {
if (operation === "delete") return { target: objectValue, key }
const index = typeof key === "symbol" ? undefined : parseArrayIndex(key)
if (key !== "length" && !(typeof key === "string" && arrayMethods.has(key)) && index === undefined) {
if (typeof key === "string" && Object.hasOwn(objectValue, key)) {
return new ComputedValue((objectValue as Record<string, unknown> & Array<unknown>)[key])
}
return new ComputedValue(undefined)
}
return { target: objectValue, key: index ?? key }
}
return { target: objectValue as SafeObject, key }
})
}
private readMember(node: MemberExpression): Effect.Effect<unknown, unknown, R> {
const self = this
return Effect.map(this.getMemberReference(node), (reference) => {
if (reference === OptionalShortCircuit) return OptionalShortCircuit
if (reference instanceof ComputedValue) return reference.value
if (reference === undefined || isOpaqueMemberReference(reference)) return reference
const value = self.readReferenceValue(reference, reference.key)
if (
value === undefined &&
reference.target instanceof ProgramArray &&
typeof reference.key === "string" &&
arrayMethods.has(reference.key) &&
!has(reference.target, reference.key)
) {
return new IntrinsicReference(reference.target, reference.key)
if (Array.isArray(reference.target)) {
if (reference.key === "length") return reference.target.length
if (typeof reference.key === "string") return new IntrinsicReference(reference.target, reference.key)
return Reflect.get(reference.target, reference.key)
}
return value
if (reference.target instanceof Values.RegExp) return reference.target.lastIndex
if (reference.target instanceof Values.URL) {
return Reflect.get(reference.target.url, reference.key)
}
return Reflect.get(reference.target, reference.key)
})
}
@@ -2223,7 +2204,7 @@ class Frame<R> {
if (reference.target instanceof Values.RegExp) {
return Reflect.deleteProperty(reference.target.regex, reference.key)
}
return remove(reference.target, reference.key)
return Reflect.deleteProperty(reference.target, reference.key)
})
}
@@ -2243,6 +2224,12 @@ class Frame<R> {
) {
throw new InterpreterRuntimeError("Only data fields may be assigned.", node)
}
if (Array.isArray(reference.target)) {
if (reference.key === "length") throw new InterpreterRuntimeError("Array length cannot be assigned.", node)
if (typeof reference.key === "string" && arrayMethods.has(reference.key)) {
throw new InterpreterRuntimeError("Array methods cannot be assigned.", node)
}
}
const key = reference.key
const { write, next, result } = yield* compute(self.readReferenceValue(reference, key))
if (write) self.assignToReference(reference, key, next, node)
@@ -2255,10 +2242,23 @@ class Frame<R> {
return Reflect.get(reference.target.url, key)
}
if (reference.target instanceof Values.RegExp) return reference.target.lastIndex
return get(reference.target, key)
return Reflect.get(reference.target, key)
}
private assignToReference(reference: MemberReference, key: PropertyKey, next: unknown, node: AstNode): void {
if (Array.isArray(reference.target)) {
const target = reference.target
if (typeof key !== "number" || parseArrayIndex(key) === undefined) {
throw new InterpreterRuntimeError(
"Array assignment index must be a valid array index.",
node,
"InvalidDataValue",
)
}
rejectCircularInsertion(target, next, "Array assignment result", node)
target[key] = next
return
}
if (reference.target instanceof Values.URL) {
const property = key as string
if (!urlWritableProperties.has(property)) {
@@ -2277,16 +2277,9 @@ class Frame<R> {
reference.target.lastIndex = next
return
}
const target = reference.target
rejectCircularInsertion(
target,
next,
target instanceof ProgramArray ? "Array assignment result" : "Object assignment result",
node,
)
if (set(target, key, next)) return
if (target instanceof ProgramArray) throw new InterpreterRuntimeError("Invalid array length", node).as("RangeError")
throw new InterpreterRuntimeError(`Cannot assign to read only property '${String(key)}'.`, node).as("TypeError")
const target = reference.target as SafeObject
rejectCircularInsertion(target, next, "Object assignment result", node)
Reflect.set(target, key, next)
}
private toPropertyKey(value: unknown, node: AstNode): PropertyKey {
+18 -17
View File
@@ -1,27 +1,28 @@
import { Effect } from "effect"
import { HostFunction, sync, syncCall } from "../interpreter/host.js"
import { type AstNode, CodeModeGenerator, InterpreterRuntimeError } from "../interpreter/model.js"
import { get, ProgramArray, ProgramObject } from "../interpreter/objects.js"
import { describeValue } from "../interpreter/references.js"
import { applyCollectionCallback, preserveConsumerError, type Runner } from "../interpreter/runner.js"
const constructArray = (args: Array<unknown>, node: AstNode): ProgramArray => {
if (args.length !== 1) return new ProgramArray([...args])
const constructArray = (args: Array<unknown>, node: AstNode): Array<unknown> => {
if (args.length !== 1) return [...args]
const first = args[0]
if (typeof first !== "number") return new ProgramArray([first])
if (typeof first !== "number") return [first]
if (!Number.isInteger(first) || first < 0 || first > 4294967295) {
throw new InterpreterRuntimeError("Invalid array length.", node).as("RangeError")
}
// Sparse like JS: Array(3) has holes, and combinator loops already skip them.
return new ProgramArray(new Array(first))
return new Array(first)
}
const arrayLikeSource = (
source: unknown,
node: AstNode,
): { readonly length: number; readonly source: ProgramObject } => {
if (source instanceof ProgramObject && typeof get(source, "length") === "number") {
const length = get(source, "length") as number
const arrayLikeSource = (source: unknown, node: AstNode): { readonly length: number; readonly source: object } => {
if (
source !== null &&
typeof source === "object" &&
(Object.getPrototypeOf(source) === Object.prototype || Object.getPrototypeOf(source) === null) &&
typeof (source as { length?: unknown }).length === "number"
) {
const length = (source as { length: number }).length
const normalized = Number.isNaN(length) || length <= 0 ? 0 : Math.trunc(length)
if (normalized > 4_294_967_295) throw new RangeError("Invalid array length")
return { length: normalized, source }
@@ -48,16 +49,16 @@ const arrayFrom = <R>(runner: Runner<R>, args: Array<unknown>, node: AstNode): E
const arrayLike = arrayLikeSource(source, node)
const values: Array<unknown> = []
for (let index = 0; index < arrayLike.length; index += 1) {
const item = get(arrayLike.source, index)
const item = Reflect.get(arrayLike.source, index)
values.push(apply === undefined ? item : yield* apply([item, index]))
}
return new ProgramArray(values)
return values
}
const values: Array<unknown> = []
let index = 0
while (true) {
const step = yield* cursor.next
if (step.done) return new ProgramArray(values)
if (step.done) return values
values.push(apply === undefined ? step.value : yield* preserveConsumerError(cursor, apply([step.value, index])))
index += 1
}
@@ -70,10 +71,10 @@ export const arrayGlobal = <R>(runner: Runner<R>) =>
name: "Array",
call: syncCall(constructArray),
construct: syncCall(constructArray),
instanceOf: (value) => value instanceof ProgramArray,
instanceOf: (value) => Array.isArray(value),
members: {
isArray: sync("Array.isArray", (args) => args[0] instanceof ProgramArray),
of: sync("Array.of", (args) => new ProgramArray([...args])),
isArray: sync("Array.isArray", (args) => Array.isArray(args[0])),
of: sync("Array.of", (args) => [...args]),
from: new HostFunction<R>({ name: "Array.from", call: (args, node) => arrayFrom(runner, args, node) }),
},
})
+10 -10
View File
@@ -1,7 +1,7 @@
import { Effect } from "effect"
import type { SafeObject } from "../data.js"
import { HostFunction, requiresNew } from "../interpreter/host.js"
import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js"
import { getOwn, ProgramArray, ProgramObject, set } from "../interpreter/objects.js"
import { type AstNode, InterpreterRuntimeError, isRecord } from "../interpreter/model.js"
import { describeValue, isRuntimeReference } from "../interpreter/references.js"
import { applyCollectionCallback, preserveConsumerError, type Runner, toPrimitive } from "../interpreter/runner.js"
import { Values } from "../values.js"
@@ -108,13 +108,13 @@ export const groupBy = <R>(runner: Runner<R>, namespace: "Map" | "Object") =>
const item = step.value
const key = yield* preserveConsumerError(cursor, apply([item, index]))
const group = result.map.get(key)
if (group === undefined) result.map.set(key, new ProgramArray([item]))
else (group as ProgramArray).items.push(item)
if (group === undefined) result.map.set(key, [item])
else (group as Array<unknown>).push(item)
index += 1
}
}
const result = new ProgramObject()
const result: SafeObject = Object.create(null) as SafeObject
let index = 0
while (true) {
const step = yield* cursor.next
@@ -124,9 +124,9 @@ export const groupBy = <R>(runner: Runner<R>, namespace: "Map" | "Object") =>
cursor,
Effect.flatMap(apply([item, index]), (value) => coerceGroupByPropertyKey(runner, value, node)),
)
const group = getOwn(result, key)
if (group === undefined) set(result, key, new ProgramArray([item]))
else (group as ProgramArray).items.push(item)
const group = result[key]
if (group === undefined) result[key] = [item]
else (group as Array<unknown>).push(item)
index += 1
}
})
@@ -150,12 +150,12 @@ const constructMap = <R>(runner: Runner<R>, init: unknown, node: AstNode) => {
yield* preserveConsumerError(
cursor,
Effect.sync(() => {
if (!(step.value instanceof ProgramObject)) {
if (!isRecord(step.value) || isRuntimeReference(step.value)) {
throw new InterpreterRuntimeError("new Map(...) expects [key, value] pairs as entry objects.", node).as(
"TypeError",
)
}
target.map.set(getOwn(step.value, 0), getOwn(step.value, 1))
target.map.set(step.value[0], step.value[1])
}),
)
}
+14 -15
View File
@@ -1,6 +1,5 @@
import { toData, toProgram } from "../data.js"
import { HostNamespace, sync } from "../interpreter/host.js"
import { get, ownEntries, ProgramArray, ProgramObject } from "../interpreter/objects.js"
import { containsOpaqueReference, containsRuntimeReference, isRuntimeReference } from "../interpreter/references.js"
import { Values } from "../values.js"
import { coerceToString } from "./value.js"
@@ -52,8 +51,8 @@ const formatConsoleValue = (value: unknown, seen: Set<object>, depth: number): s
if (value instanceof Values.Map) {
seen.add(value)
try {
const entries = Array.from(value.map.entries(), ([key, item]) => new ProgramArray([key, item]))
return `Map(${value.map.size}) ${formatConsoleValue(new ProgramArray(entries), seen, depth + 1)}`
const entries = Array.from(value.map.entries(), ([key, item]): Array<unknown> => [key, item])
return `Map(${value.map.size}) ${formatConsoleValue(entries, seen, depth + 1)}`
} finally {
seen.delete(value)
}
@@ -61,7 +60,7 @@ const formatConsoleValue = (value: unknown, seen: Set<object>, depth: number): s
if (value instanceof Values.Set) {
seen.add(value)
try {
return `Set(${value.set.size}) ${formatConsoleValue(new ProgramArray([...value.set.values()]), seen, depth + 1)}`
return `Set(${value.set.size}) ${formatConsoleValue(Array.from(value.set.values()), seen, depth + 1)}`
} finally {
seen.delete(value)
}
@@ -69,11 +68,10 @@ const formatConsoleValue = (value: unknown, seen: Set<object>, depth: number): s
if (isRuntimeReference(value)) return "[opaque reference]"
seen.add(value)
try {
if (value instanceof ProgramArray) {
return `[${value.items.map((item) => formatConsoleValue(item, seen, depth + 1)).join(",")}]`
if (Array.isArray(value)) {
return `[${value.map((item) => formatConsoleValue(item, seen, depth + 1)).join(",")}]`
}
if (!(value instanceof ProgramObject)) return "[object Object]"
return `{${ownEntries(value)
return `{${Object.entries(value)
.map(([key, item]) => `${JSON.stringify(key)}:${formatConsoleValue(item, seen, depth + 1)}`)
.join(",")}}`
} finally {
@@ -106,19 +104,20 @@ const consoleTableRows = (
data: unknown,
columns: ReadonlyArray<string> | undefined,
): Array<{ readonly index: string; readonly values: Record<string, unknown> }> => {
if (data instanceof ProgramArray) {
return data.items.map((item, index) => ({ index: String(index), values: consoleTableValues(item, columns) }))
if (Array.isArray(data)) {
return data.map((item, index) => ({ index: String(index), values: consoleTableValues(item, columns) }))
}
if (data instanceof ProgramObject) {
return ownEntries(data).map(([index, item]) => ({ index, values: consoleTableValues(item, columns) }))
if (data !== null && typeof data === "object" && !Values.isValue(data)) {
return Object.entries(data).map(([index, item]) => ({ index, values: consoleTableValues(item, columns) }))
}
return [{ index: "0", values: { Value: data } }]
}
const consoleTableValues = (value: unknown, columns: ReadonlyArray<string> | undefined): Record<string, unknown> => {
if (value instanceof ProgramObject && !(value instanceof ProgramArray)) {
if (columns !== undefined) return Object.fromEntries(columns.map((column) => [column, get(value, column)]))
return Object.fromEntries(ownEntries(value))
if (value !== null && typeof value === "object" && !Array.isArray(value) && !Values.isValue(value)) {
const source = value as Record<string, unknown>
if (columns !== undefined) return Object.fromEntries(columns.map((column) => [column, source[column]]))
return Object.fromEntries(Object.entries(source))
}
return { Value: value }
}
+42 -28
View File
@@ -3,8 +3,7 @@ import { HostFunction, HostNamespace } from "../interpreter/host.js"
import { applyCollectionCallback, type Runner } from "../interpreter/runner.js"
import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js"
import { typeofValue } from "../interpreter/references.js"
import { fromData, toData, toProgram } from "../data.js"
import { get, ownKeys, ProgramArray, ProgramObject, record, remove, set } from "../interpreter/objects.js"
import { fromData, type SafeObject, toData, toProgram } from "../data.js"
import { Values } from "../values.js"
export const jsonGlobal = <R>(runner: Runner<R>) =>
@@ -30,19 +29,28 @@ const parse = <R>(runner: Runner<R>, args: Array<unknown>, node: AstNode): Effec
if (typeofValue(args[1]) !== "function") return Effect.succeed(parsed)
const apply = applyCollectionCallback(runner, args[1], "JSON.parse", node)
const visit = (holder: ProgramObject, key: string): Effect.Effect<unknown, unknown, R> =>
const root: SafeObject = Object.create(null) as SafeObject
root[""] = parsed
const visit = (holder: SafeObject | Array<unknown>, key: string): Effect.Effect<unknown, unknown, R> =>
Effect.gen(function* () {
const value = get(holder, key)
if (value instanceof ProgramObject) {
for (const name of ownKeys(value)) {
const revived = yield* visit(value, name as string)
if (revived === undefined) remove(value, name)
else set(value, name, revived)
const value = holder[key as keyof typeof holder]
if (Array.isArray(value)) {
const length = value.length
for (let index = 0; index < length; index += 1) {
const revived = yield* visit(value, String(index))
if (revived === undefined) Reflect.deleteProperty(value, index)
else value[index] = revived
}
} else if (isPlainObject(value)) {
for (const name of Object.keys(value)) {
const revived = yield* visit(value, name)
if (revived === undefined) Reflect.deleteProperty(value, name)
else value[name] = revived
}
}
return yield* apply([key, value])
})
return visit(record({ "": parsed }), "")
return visit(root, "")
}
const stringify = <R>(runner: Runner<R>, args: Array<unknown>, node: AstNode): Effect.Effect<unknown, unknown, R> => {
@@ -51,41 +59,44 @@ const stringify = <R>(runner: Runner<R>, args: Array<unknown>, node: AstNode): E
const replacer = args[1]
if (typeofValue(replacer) !== "function") {
const properties =
replacer instanceof ProgramArray
? replacer.items
.filter((item): item is string | number => typeof item === "string" || typeof item === "number")
.map(String)
: null
const properties = Array.isArray(replacer)
? replacer
.filter((item): item is string | number => typeof item === "string" || typeof item === "number")
.map(String)
: null
return Effect.succeed(JSON.stringify(toData(args[0], "JSON.stringify value"), properties, indent))
}
// Validate up front; the replacer walk below reads the original value.
toProgram(args[0], "JSON.stringify value")
const apply = applyCollectionCallback(runner, replacer, "JSON.stringify", node)
const root: SafeObject = Object.create(null) as SafeObject
root[""] = args[0]
const stack = new Set<object>()
const visit = (holder: ProgramObject, key: string): Effect.Effect<unknown, unknown, R> =>
const visit = (holder: SafeObject | Array<unknown>, key: string): Effect.Effect<unknown, unknown, R> =>
Effect.gen(function* () {
const value = yield* apply([key, toJSONValue(get(holder, key))])
const value = yield* apply([key, toJSONValue(holder[key as keyof typeof holder])])
if (value === undefined || typeofValue(value) === "function") return undefined
toProgram(value, "JSON.stringify replacer result")
if (typeof value === "number") return Number.isFinite(value) ? value : null
if (value === null || typeof value === "string" || typeof value === "boolean") return value
if (!(value instanceof ProgramObject)) return {}
if (stack.has(value))
throw new InterpreterRuntimeError("Converting circular structure to JSON.", node).as("TypeError")
stack.add(value)
if (value instanceof ProgramArray) {
if (Array.isArray(value)) {
if (stack.has(value))
throw new InterpreterRuntimeError("Converting circular structure to JSON.", node).as("TypeError")
stack.add(value)
const result: Array<unknown> = []
for (let index = 0; index < value.items.length; index += 1) {
for (let index = 0; index < value.length; index += 1) {
result.push((yield* visit(value, String(index))) ?? null)
}
stack.delete(value)
return result
}
const result: Record<string, unknown> = Object.create(null)
for (const name of ownKeys(value)) {
if (typeof name !== "string") continue
if (!isPlainObject(value)) return {}
if (stack.has(value))
throw new InterpreterRuntimeError("Converting circular structure to JSON.", node).as("TypeError")
stack.add(value)
const result: SafeObject = Object.create(null) as SafeObject
for (const name of Object.keys(value)) {
const item = yield* visit(value, name)
if (item !== undefined) result[name] = item
}
@@ -93,7 +104,7 @@ const stringify = <R>(runner: Runner<R>, args: Array<unknown>, node: AstNode): E
return result
})
return Effect.map(visit(record({ "": args[0] }), ""), (value) => JSON.stringify(value, null, indent))
return Effect.map(visit(root, ""), (value) => JSON.stringify(value, null, indent))
}
const toJSONValue = (value: unknown): unknown => {
@@ -103,3 +114,6 @@ const toJSONValue = (value: unknown): unknown => {
if (value instanceof Values.URL) return value.url.href
return value
}
const isPlainObject = (value: unknown): value is SafeObject =>
value !== null && typeof value === "object" && !Values.isValue(value)
+52 -30
View File
@@ -2,10 +2,11 @@ import { Effect } from "effect"
import { toProgram } from "../data.js"
import { HostFunction, sync, syncCall } from "../interpreter/host.js"
import { type AstNode, AsyncIteratorSymbol, InterpreterRuntimeError, IteratorSymbol } from "../interpreter/model.js"
import { getOwn, hasOwn, ownEntries, ownKeys, ProgramArray, ProgramObject, set } from "../interpreter/objects.js"
import {
containsOpaqueReference,
describeValue,
isRuntimeReference,
parseArrayIndex,
rejectCircularInsertion,
typeofValue,
} from "../interpreter/references.js"
@@ -15,8 +16,9 @@ import { Values } from "../values.js"
import { groupBy } from "./collections.js"
import { coerceToString } from "./value.js"
// ToObject for enumeration.
export const enumerableSource = (label: string, value: unknown, node: AstNode): ProgramObject => {
// ToObject for enumeration. Strings return themselves: the host's Object.keys/entries/hasOwn index a
// primitive string directly. Numbers, booleans, wrappers, and functions have no own enumerable keys.
export const enumerableSource = (label: string, value: unknown, node: AstNode): Record<string, unknown> => {
if (value === null || value === undefined) {
throw new InterpreterRuntimeError(`${label} cannot convert ${describeValue(value)} to an object.`, node).as(
"TypeError",
@@ -36,41 +38,58 @@ export const enumerableSource = (label: string, value: unknown, node: AstNode):
"InvalidDataValue",
)
}
if (typeof value === "string") return new ProgramArray([...value])
if (value instanceof ProgramObject) return value
return new ProgramObject()
if (typeof value === "string") return value as unknown as Record<string, unknown>
if (typeof value !== "object" || Values.isValue(value) || isRuntimeReference(value)) return {}
return value as Record<string, unknown>
}
export const objectAssign = (args: Array<unknown>, node: AstNode): unknown => {
const target = args[0]
// JS would box a primitive target; wrappers and primitives cannot hold fields here.
if (!(target instanceof ProgramObject)) {
if (target === null || typeof target !== "object" || Values.isValue(target) || isRuntimeReference(target)) {
throw new InterpreterRuntimeError(
`Object.assign expects a data object or array target, received ${describeValue(target)}.`,
node,
).as("TypeError")
}
const out = target as Record<string, unknown>
const seen = new Set<object>()
const guardedSet = (key: PropertyKey, item: unknown): void => {
// Arrays hold only indexed elements, as with direct assignment; Reflect.set would otherwise
// reach Array's length and Object.prototype's __proto__ setter.
if (Array.isArray(out) && (typeof key === "symbol" || parseArrayIndex(key) === undefined)) {
throw new InterpreterRuntimeError(
`Object.assign cannot assign '${String(key)}' to an array: only array indexes may be assigned.`,
node,
).as("TypeError")
}
rejectCircularInsertion(out, item, "Object.assign result", node, seen)
if (!Reflect.set(out, key, item))
throw new InterpreterRuntimeError(`Object.assign could not assign property '${String(key)}'.`, node).as(
"TypeError",
)
}
for (const source of args.slice(1)) {
if (source === null || source === undefined) continue
const from = enumerableSource("Object.assign(...)", source, node)
for (const key of ownKeys(from)) {
if (typeof from !== "object") {
for (const [key, item] of Object.entries(from)) guardedSet(key, item)
continue
}
for (const key of Reflect.ownKeys(from)) {
if (typeof key === "symbol" && key !== AsyncIteratorSymbol && key !== IteratorSymbol) continue
rejectCircularInsertion(target, getOwn(from, key), "Object.assign result", node, seen)
if (!set(target, key, getOwn(from, key))) {
throw new InterpreterRuntimeError("Invalid array length", node).as("RangeError")
}
if (Object.prototype.propertyIsEnumerable.call(from, key)) guardedSet(key, Reflect.get(from, key))
}
}
return target
return out
}
const objectFromEntries = <R>(
runner: Runner<R>,
source: unknown,
node: AstNode,
): Effect.Effect<ProgramObject, unknown, R> => {
const out = new ProgramObject()
): Effect.Effect<Record<string, unknown>, unknown, R> => {
const out: Record<string, unknown> = Object.create(null)
return Effect.gen(function* () {
const cursor = yield* runner.syncIterator(source, node)
if (cursor === undefined) {
@@ -84,12 +103,21 @@ const objectFromEntries = <R>(
yield* preserveConsumerError(
cursor,
Effect.sync(() => {
if (!(step.value instanceof ProgramObject) || containsOpaqueReference(step.value)) {
if (
step.value === null ||
typeof step.value !== "object" ||
Values.isValue(step.value) ||
containsOpaqueReference(step.value)
) {
throw new InterpreterRuntimeError("Object.fromEntries expects [key, value] entry objects.", node).as(
"TypeError",
)
}
set(out, coerceToString(getOwn(step.value, 0)), getOwn(step.value, 1))
const entry = step.value as Record<string, unknown>
toProgram(entry[0], "Object.fromEntries key")
toProgram(entry[1], "Object.fromEntries value")
const key = coerceToString(entry[0])
out[key] = entry[1]
}),
)
}
@@ -98,7 +126,7 @@ const objectFromEntries = <R>(
const constructObject = (args: Array<unknown>, node: AstNode): unknown => {
const first = args[0]
if (first === null || first === undefined) return new ProgramObject()
if (first === null || first === undefined) return Object.create(null)
if (typeof first === "object") return first
throw new InterpreterRuntimeError(
`Object(${typeof first}) wrapper objects are not supported; use the primitive value directly.`,
@@ -119,24 +147,18 @@ export const objectGlobal = <R>(runner: Runner<R>, toolKeys: (path: ReadonlyArra
toProgram(
args[0] instanceof ToolReference
? [...toolKeys(args[0].path)]
: ownKeys(enumerableSource("Object.keys(...)", args[0], node)).filter((key) => typeof key === "string"),
: Object.keys(enumerableSource("Object.keys(...)", args[0], node)),
"Object.keys result",
),
),
values: sync(
"Object.values",
(args, node) =>
new ProgramArray(ownEntries(enumerableSource("Object.values(...)", args[0], node)).map((entry) => entry[1])),
values: sync("Object.values", (args, node) =>
Object.values(enumerableSource("Object.values(...)", args[0], node)),
),
entries: sync(
"Object.entries",
(args, node) =>
new ProgramArray(
ownEntries(enumerableSource("Object.entries(...)", args[0], node)).map((entry) => new ProgramArray(entry)),
),
entries: sync("Object.entries", (args, node) =>
Object.entries(enumerableSource("Object.entries(...)", args[0], node)).map(([key, item]) => [key, item]),
),
hasOwn: sync("Object.hasOwn", (args, node) =>
hasOwn(
Object.hasOwn(
enumerableSource("Object.hasOwn(...)", args[0], node),
args[1] === AsyncIteratorSymbol || args[1] === IteratorSymbol ? args[1] : String(args[1]),
),
+33 -18
View File
@@ -1,9 +1,19 @@
import { sync, syncCall } from "../interpreter/host.js"
import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js"
import { ProgramArray, record, set } from "../interpreter/objects.js"
import type { SafeObject } from "../data.js"
import { Values } from "../values.js"
import { coerceToNumber, coerceToString } from "./value.js"
type MatchValue = Array<unknown> & {
index?: number
groups?: SafeObject
indices?: IndicesValue
}
type IndicesValue = Array<unknown> & {
groups?: SafeObject
}
export const regexpMethods = new Set(["test", "exec", "toString"])
export const regexpProperties = new Set([
@@ -46,12 +56,17 @@ export const toHostRegex = (arg: unknown, method: string, node: AstNode, extraFl
)
}
export const matchToValue = (match: RegExpMatchArray): ProgramArray => {
const result = new ProgramArray(Array.from(match, (group) => group))
if (match.index !== undefined) set(result, "index", match.index)
if (match.input !== undefined) set(result, "input", match.input)
if (match.groups) set(result, "groups", record(match.groups))
if (match.indices) set(result, "indices", indicesToValue(match.indices))
export const matchToValue = (match: RegExpMatchArray): Array<unknown> => {
const result: MatchValue = Array.from(match, (group) => group)
if (match.index !== undefined) result.index = match.index
if (match.groups) {
const groups: SafeObject = Object.create(null) as SafeObject
for (const [key, group] of Object.entries(match.groups)) {
groups[key] = group
}
result.groups = groups
}
if (match.indices) result.indices = indicesToValue(match.indices)
return result
}
@@ -128,16 +143,16 @@ const toLength = (value: unknown): number => {
return Math.min(Math.floor(number), Number.MAX_SAFE_INTEGER)
}
const indicesToValue = (indices: RegExpIndicesArray): ProgramArray => {
const range = (pair: [number, number] | undefined) => (pair === undefined ? undefined : new ProgramArray([...pair]))
const result = new ProgramArray(Array.from(indices, range))
const groups = indices.groups
set(
result,
"groups",
groups === undefined
? undefined
: record(Object.fromEntries(Object.entries(groups).map(([key, pair]) => [key, range(pair)]))),
)
const indicesToValue = (indices: RegExpIndicesArray): IndicesValue => {
const result: IndicesValue = Array.from(indices, (range) => (range === undefined ? undefined : [...range]))
if (indices.groups) {
const groups: SafeObject = Object.create(null) as SafeObject
for (const [key, range] of Object.entries(indices.groups)) {
groups[key] = range === undefined ? undefined : [...range]
}
result.groups = groups
return result
}
result.groups = undefined
return result
}
+3 -3
View File
@@ -2,7 +2,6 @@ import { Effect } from "effect"
import { toProgram } from "../data.js"
import { HostFunction, requiresNew, sync, syncCall } from "../interpreter/host.js"
import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js"
import { ownEntries, ProgramObject } from "../interpreter/objects.js"
import { isRuntimeReference } from "../interpreter/references.js"
import { preserveConsumerError, type Runner } from "../interpreter/runner.js"
import { Values } from "../values.js"
@@ -184,14 +183,15 @@ const constructURLSearchParams = <R>(
).as("TypeError")
}
if (Values.isValue(init)) return new Values.URLSearchParams(new URLSearchParams())
if (!(init instanceof ProgramObject)) {
const data = toProgram(init, "new URLSearchParams input")
if (data === null || typeof data !== "object") {
throw new InterpreterRuntimeError(
"new URLSearchParams(...) expects a query string, data object, iterable pairs, or URLSearchParams.",
node,
).as("TypeError")
}
return new Values.URLSearchParams(
new URLSearchParams(Object.fromEntries(ownEntries(init).map(([key, value]) => [key, coerceToString(value)]))),
new URLSearchParams(Object.fromEntries(Object.entries(data).map(([key, value]) => [key, coerceToString(value)]))),
)
})
}
+27 -24
View File
@@ -1,7 +1,6 @@
import { type HostFunction, sync, type SyncOptions } from "../interpreter/host.js"
import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js"
import { toProgram } from "../data.js"
import { get, ProgramArray, ProgramError, set } from "../interpreter/objects.js"
import { type SafeObject, toProgram } from "../data.js"
import { Values } from "../values.js"
export const errorConstructors = new Set([
@@ -17,21 +16,21 @@ export const errorConstructors = new Set([
export const compoundOperators = new Set(["+=", "-=", "*=", "/=", "%=", "**=", "&=", "|=", "^=", "<<=", ">>=", ">>>="])
export const createErrorValue = (name: string, message: string): ProgramError => {
const value = new ProgramError(name)
set(value, "name", name)
set(value, "message", message)
const ErrorBrand: unique symbol = Symbol("codemode.error")
export const createErrorValue = (name: string, message: string): SafeObject => {
const value = Object.assign(Object.create(null) as SafeObject, { name, message })
Object.defineProperty(value, ErrorBrand, { value: name })
return value
}
export const createAggregateErrorValue = (errors: Array<unknown>, message: string): ProgramError => {
const value = createErrorValue("AggregateError", message)
set(value, "errors", new ProgramArray(errors))
return value
}
export const createAggregateErrorValue = (errors: Array<unknown>, message: string): SafeObject =>
Object.assign(createErrorValue("AggregateError", message), { errors })
export const errorBrandName = (value: unknown): string | undefined =>
value instanceof ProgramError ? value.errorName : undefined
value !== null && typeof value === "object"
? ((value as Record<PropertyKey, unknown>)[ErrorBrand] as string | undefined)
: undefined
export const coerceToString = (value: unknown): string => {
if (value === null) return "null"
@@ -43,27 +42,29 @@ export const coerceToString = (value: unknown): string => {
if (value instanceof Values.Set) return "[object Set]"
if (value instanceof Values.URL) return value.url.href
if (value instanceof Values.URLSearchParams) return value.params.toString()
if (value instanceof ProgramError) {
if (errorBrandName(value) !== undefined) {
// Match Error.prototype.toString: "name: message", or just one when the other is empty.
const name = get(value, "name")
const message = get(value, "message")
const shownName = typeof name === "string" ? name : "Error"
const shownMessage = typeof message === "string" ? message : ""
if (shownMessage === "") return shownName
if (shownName === "") return shownMessage
return `${shownName}: ${shownMessage}`
const error = value as { name?: unknown; message?: unknown }
const name = typeof error.name === "string" ? error.name : "Error"
const message = typeof error.message === "string" ? error.message : ""
if (message === "") return name
if (name === "") return message
return `${name}: ${message}`
}
if (value instanceof ProgramArray) {
return value.items.map((item) => (item === null || item === undefined ? "" : coerceToString(item))).join(",")
if (typeof value === "object") {
return Array.isArray(value)
? value.map((item) => (item === null || item === undefined ? "" : coerceToString(item))).join(",")
: "[object Object]"
}
if (typeof value === "object") return "[object Object]"
return String(value)
}
export const coerceToNumber = (value: unknown): number => {
if (value instanceof Values.Date) return value.time
if (Values.isValue(value)) return Number.NaN
if (value instanceof ProgramArray) return Number(coerceToString(value))
// Arrays coerce through our own string coercion: host Number(array) joins with host
// ToPrimitive, which throws on the null-prototype objects the interpreter produces.
if (Array.isArray(value)) return Number(coerceToString(value))
return value !== null && typeof value === "object" ? Number.NaN : Number(value)
}
@@ -77,6 +78,8 @@ const coerce = (name: Coercion, args: Array<unknown>, node: AstNode): unknown =>
if (name === "String") return ""
}
const raw = args[0]
// Error values are plain SafeObjects; the toProgram path below would strip their brand.
if (name === "String" && errorBrandName(raw) !== undefined) return coerceToString(raw)
if (Values.isValue(raw)) {
if (name === "Boolean") return true
if (name === "Number") return coerceToNumber(raw)

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