Compare commits

..

4 Commits

Author SHA1 Message Date
Aiden Cline 5076497e11 fix(core): retain raw copilot completion usage 2026-08-11 00:37:09 -05:00
Aiden Cline f31582b907 fix(core): preserve raw copilot usage 2026-08-11 00:34:05 -05:00
Aiden Cline c1abcce820 test(core): use observed copilot usage fixtures 2026-08-11 00:20:54 -05:00
Aiden Cline 24d8e41aab fix(core): normalize copilot reasoning usage 2026-08-11 00:14:08 -05:00
1242 changed files with 25122 additions and 45283 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"@opencode-ai/core": patch
---
Preserve prompt cache prefixes when sessions move between locations with unchanged instructions.
@@ -1,8 +0,0 @@
---
"@opencode-ai/core": minor
"@opencode-ai/schema": minor
"@opencode-ai/protocol": minor
"@opencode-ai/client": minor
---
Remove the unused question request API and use session forms for question tool interactions.
-9
View File
@@ -1,9 +0,0 @@
---
"@opencode-ai/core": minor
"@opencode-ai/schema": minor
"@opencode-ai/protocol": minor
"@opencode-ai/client": minor
---
Add an opt-in portable shell permission scanner. Opaque commands use normal shell authorization without inferring
external directories, while the default tree-sitter path remains unchanged.
-1
View File
@@ -1,4 +1,3 @@
packages/core/migration/**/snapshot.json linguist-generated packages/core/migration/**/snapshot.json linguist-generated
packages/core/src/database/migration.gen.ts linguist-generated packages/core/src/database/migration.gen.ts linguist-generated
packages/core/src/models-dev/snapshot.txt linguist-generated
packages/core/src/**/*.txt text eol=lf packages/core/src/**/*.txt text eol=lf
-1
View File
@@ -10,7 +10,6 @@ kitlangton
kommander kommander
ludvigrask ludvigrask
MrMushrooooom MrMushrooooom
neriousy
nexxeln nexxeln
R44VC0RP R44VC0RP
rekram1-node rekram1-node
+12 -69
View File
@@ -6,6 +6,7 @@ on:
branches: branches:
- ci - ci
- dev - dev
- v2
- beta - beta
- fix/npm-native-binary-install - fix/npm-native-binary-install
- snapshot-* - snapshot-*
@@ -74,7 +75,7 @@ jobs:
build-cli: build-cli:
needs: version needs: version
runs-on: blacksmith-4vcpu-ubuntu-2404 runs-on: blacksmith-4vcpu-ubuntu-2404
if: github.repository == 'anomalyco/opencode' && github.ref_name != 'beta' if: github.repository == 'anomalyco/opencode'
steps: steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
with: with:
@@ -90,7 +91,7 @@ jobs:
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }} opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
- name: Build legacy CLI - name: Build legacy CLI
if: github.ref_name != 'v2' && github.ref_name != 'beta' if: github.ref_name != 'v2'
run: ./packages/opencode/script/build.ts ${{ (github.ref_name == 'beta' && '--sourcemaps') || '' }} run: ./packages/opencode/script/build.ts ${{ (github.ref_name == 'beta' && '--sourcemaps') || '' }}
env: env:
OPENCODE_VERSION: ${{ needs.version.outputs.version }} OPENCODE_VERSION: ${{ needs.version.outputs.version }}
@@ -108,7 +109,7 @@ jobs:
GH_TOKEN: ${{ steps.committer.outputs.token }} GH_TOKEN: ${{ steps.committer.outputs.token }}
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: github.ref_name != 'v2' && github.ref_name != 'beta' if: github.ref_name != 'v2'
with: with:
name: opencode-cli name: opencode-cli
path: | path: |
@@ -116,76 +117,22 @@ jobs:
packages/opencode/dist/opencode-linux* packages/opencode/dist/opencode-linux*
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: github.ref_name != 'v2' && github.ref_name != 'beta' if: github.ref_name != 'v2'
with: with:
name: opencode-cli-windows name: opencode-cli-windows
path: packages/opencode/dist/opencode-windows* path: packages/opencode/dist/opencode-windows*
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: opencode-preview-cli-unsigned name: opencode-preview-cli
path: packages/cli/dist/cli-* path: packages/cli/dist/cli-*
outputs: outputs:
version: ${{ needs.version.outputs.version }} version: ${{ needs.version.outputs.version }}
sign-cli-macos:
needs: build-cli
runs-on: macos-26
if: github.repository == 'anomalyco/opencode'
steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- uses: apple-actions/import-codesign-certs@8f3fb608891dd2244cdab3d69cd68c0d37a7fe93 # v2.0.0
with:
keychain: build
p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }}
p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: opencode-preview-cli-unsigned
path: packages/cli/dist
- name: Sign macOS CLI binaries
run: |
identity=$(security find-identity -v -p codesigning build.keychain | sed -n 's/.*"\(Developer ID Application:.*\)"/\1/p' | head -n 1)
if [ -z "$identity" ]; then
echo "Developer ID Application identity not found"
exit 1
fi
found=0
for file in packages/cli/dist/cli-darwin-*/bin/opencode2; do
if [ ! -f "$file" ]; then
continue
fi
found=1
codesign \
--force \
--timestamp \
--options runtime \
--entitlements packages/cli/script/entitlements.plist \
--sign "$identity" \
"$file"
codesign --verify --deep --strict --verbose=4 "$file"
codesign --display --requirements - "$file"
done
if [ "$found" -eq 0 ]; then
echo "No macOS CLI binaries found"
exit 1
fi
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: opencode-preview-cli
path: packages/cli/dist/cli-*
if-no-files-found: error
build-node-cli: build-node-cli:
needs: version needs: version
if: github.repository == 'anomalyco/opencode' && github.ref_name != 'beta' if: github.repository == 'anomalyco/opencode'
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@@ -237,7 +184,7 @@ jobs:
- build-cli - build-cli
- version - version
runs-on: blacksmith-4vcpu-windows-2025 runs-on: blacksmith-4vcpu-windows-2025
if: github.repository == 'anomalyco/opencode' && github.ref_name != 'v2' && github.ref_name != 'beta' if: github.repository == 'anomalyco/opencode' && github.ref_name != 'v2'
env: env:
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
@@ -430,7 +377,7 @@ jobs:
env: env:
OPENCODE_VERSION: ${{ needs.version.outputs.version }} OPENCODE_VERSION: ${{ needs.version.outputs.version }}
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }} OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
OPENCODE_CLI_TARGET: ${{ matrix.settings.target }} RUST_TARGET: ${{ matrix.settings.target }}
- name: Build - name: Build
run: bun run build run: bun run build
@@ -446,7 +393,6 @@ jobs:
VITE_SENTRY_DSN: ${{ vars.WEB_SENTRY_DSN }} VITE_SENTRY_DSN: ${{ vars.WEB_SENTRY_DSN }}
VITE_SENTRY_ENVIRONMENT: ${{ (github.ref_name == 'beta' && 'beta') || 'production' }} VITE_SENTRY_ENVIRONMENT: ${{ (github.ref_name == 'beta' && 'beta') || 'production' }}
VITE_SENTRY_RELEASE: desktop@${{ needs.version.outputs.version }} VITE_SENTRY_RELEASE: desktop@${{ needs.version.outputs.version }}
OPENCODE_CLI_TARGET: ${{ matrix.settings.target }}
- name: Package - name: Package
if: needs.version.outputs.release if: needs.version.outputs.release
@@ -521,7 +467,6 @@ jobs:
needs: needs:
- version - version
- build-cli - build-cli
- sign-cli-macos
- build-node-cli - build-node-cli
- sign-cli-windows - sign-cli-windows
- build-electron - build-electron
@@ -551,31 +496,29 @@ jobs:
registry-url: "https://registry.npmjs.org" registry-url: "https://registry.npmjs.org"
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
if: github.ref_name != 'v2' && github.ref_name != 'beta' if: github.ref_name != 'v2'
with: with:
name: opencode-cli name: opencode-cli
path: packages/opencode/dist path: packages/opencode/dist
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
if: github.ref_name != 'v2' && github.ref_name != 'beta' if: github.ref_name != 'v2'
with: with:
name: opencode-cli-windows name: opencode-cli-windows
path: packages/opencode/dist path: packages/opencode/dist
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
if: github.ref_name != 'v2' && github.ref_name != 'beta' if: github.ref_name != 'v2'
with: with:
name: opencode-cli-signed-windows name: opencode-cli-signed-windows
path: packages/opencode/dist path: packages/opencode/dist
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
if: github.ref_name != 'beta'
with: with:
name: opencode-preview-cli name: opencode-preview-cli
path: packages/cli/dist path: packages/cli/dist
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
if: github.ref_name != 'beta'
with: with:
pattern: opencode-node-cli-* pattern: opencode-node-cli-*
path: packages/cli/dist/node path: packages/cli/dist/node
-6
View File
@@ -55,12 +55,6 @@ jobs:
git config --global user.email "bot@opencode.ai" git config --global user.email "bot@opencode.ai"
git config --global user.name "opencode" git config --global user.name "opencode"
- name: Install ffmpeg
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install --yes ffmpeg
- name: Cache Turbo - name: Cache Turbo
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with: with:
-48
View File
@@ -1,48 +0,0 @@
# he Glossary
## Sources
- Hebrew Academy approved IT terminology: https://terms.hebrew-academy.org.il/Millonim/ShowMillon?KodMillon=192
- Firefox Hebrew localization corpus: https://github.com/mozilla-l10n/firefox-l10n/tree/main/he
- KDE Hebrew localization team and corpus: https://l10n.kde.org/team-infos.php?teamcode=he
- Community-maintained VS Code Hebrew language pack: https://github.com/AMAARETS/vscode-language-pack-he
- Microsoft Hebrew developer documentation for Git terminology: https://learn.microsoft.com/he-il/power-platform/alm/tutorials/github-actions-deploy
- W3C guidance for bidirectional text: https://www.w3.org/International/articles/strings-and-bidi/
## Do Not Translate (Locale Additions)
- `OpenCode` (preserve casing in prose and UI copy)
- `API`, `MCP`, `LSP`, `OAuth`, `Git`, model names, and provider names
- Commands, flags, keyboard shortcuts, file paths, URLs, identifiers, hashes, and code literals
- Keep `commit` and `diff` when they name the exact Git artifact or operation
## Preferred Terms
| English / Context | Preferred | Notes |
| ----------------- | ------------- | ------------------------------------------------------------------------- |
| session | `הפעלה` | Use `שיחה` only when the source specifically means a chat or conversation |
| workspace | `סביבת עבודה` | |
| terminal | `מסוף` | Prefer the established Hebrew term over transliteration |
| command | `פקודה` | |
| provider | `ספק` | Use `ספק מודלים` where the bare noun is ambiguous |
| model | `מודל` | |
| API key | `מפתח API` | Keep the acronym in Latin letters |
| plugin | `תוסף` | |
| repository | `מאגר` | Use `מאגר Git` where context is ambiguous |
| branch | `ענף` | |
| context | `הקשר` | Use `חלון הקשר` for context window |
| tokens | `אסימונים` | |
## Guidance
- Prefer natural modern Israeli Hebrew over word-for-word translation or obscure coined terms.
- Use short action verbs for controls and translate complete phrases in context.
- Keep recognized developer acronyms and exact Git vocabulary in Latin script instead of phonetic transliteration.
- Treat embedded code, paths, commands, shortcuts, hashes, model IDs, and other Latin technical artifacts as LTR content inside the RTL interface.
- Keep recurring concepts consistent and do not collapse session, chat, run, and launch into one Hebrew term.
## Avoid
- Avoid transliterations such as `טרמינל`, `פלאגין`, and `קומנד` when `מסוף`, `תוסף`, and `פקודה` are clear.
- Avoid translating `commit` as `התחייבות`.
- Avoid inventing Hebrew expansions for `API`, `MCP`, or `LSP`.
+13
View File
@@ -0,0 +1,13 @@
import type { Context } from "../../../packages/plugin/src/tui/context"
export default {
id: "test.tui-discovery-smoke",
setup(_context: Context) {
// context.ui.toast.show({
// title: "TUI plugin discovery works",
// message: "Loaded .opencode/plugins/tui/discovery-smoke.ts",
// variant: "success",
// duration: 30_000,
// })
},
}
@@ -0,0 +1,68 @@
---
name: ideal-pseudocode
description: Function-by-function refactoring loop driven by ideal pseudocode. Use when the user says "ideal pseudocode", asks to make a function read like its pseudocode, or wants a dense module cleaned up one function at a time.
---
# Ideal Pseudocode
Clean up one function at a time by writing the pseudocode it _should_ read as, naming every delta between that and the real code, and closing only the gaps the user approves.
## Loop
One function per round. Never touch code before the user picks a direction.
1. **Pick the target** with the user — usually the next function up or down the call chain from the last round.
2. **Read the current code** fresh from disk. It may have unsaved or parallel edits; ask before overwriting anything unexpected.
3. **Distill.** Write the function's ideal pseudocode in a `ts`-fenced code block — TypeScript-flavored for syntax highlighting, but pseudocode: comments over mechanics, one line per idea, every arm of a loop visible as an arm. For a dense or unfamiliar function, first show the _current_ structure as pseudocode, then the ideal.
4. **Name the deltas.** A numbered list; each delta is one concrete gap: control flow smuggled through combinators, a flag-typed parameter, colliding names, side-effect plumbing drowning a loop body, twin functions differing only by a filter. For each, give an honest cost/benefit and a decisive recommendation — including "leave it" when the density has reasons (state the reasons).
5. **Verify before proposing.** Any API the proposal builds on (a combinator, schedule, matcher) gets checked against the installed version's source, not memory.
6. **Get buy-in, then apply.** Minimal edits. Quarantine irreducible ugliness inside a small helper whose name is the pseudocode line it implements. Collapse shallow sibling functions into one deep function only when the distinguishing argument is a domain noun, not a behavior switch.
7. **Validate.** Typecheck and the focused test suites from the package directory; rerun timing-sensitive tests repeatedly. Flag any semantic change the restructure introduced, even when tests pass.
8. **Offer the next target** and stop.
## Style
- Keep each round tight: pseudocode block, deltas, recommendation, one question.
- State mutation belongs in one visible place (usually the loop tail), never spread across error channels or callback closures.
- Prefer early return and `if` chains for control flow; matchers are for producing values.
- Compare names against the question each caller is actually asking; rename toward the domain noun.
## Example round
Every round should read like this (abridged from a real one, on an Effect step-runner):
> `runStep` it is. The ideal pseudocode:
>
> ```ts
> // One logical step: one settled model call, however many attempts it takes.
> // An attempt can end without settling in two ways:
> // transient provider failure -> retry the same call, same assistant message
> // compaction rewrote history -> rebuild the request and call again
> function runStep(session, promotable, step) {
> while (true) {
> const result = callModel(session, promotable, step)
>
> if (result.completed) return result
>
> if (result.retryable) {
> wait(retrySchedule) // exhausted -> publish Step.Failed, fail drain
> promotable = none // never re-promote on a second attempt
> continue
> }
>
> // compaction restarted the step: fresh request from rewritten history
> promotable = none
> step = result.step
> }
> }
> ```
>
> Comparing against the real thing, three deltas:
>
> 1. **The retry arm doesn't live in the loop.** In the pseudocode, retry is just another loop arm. In the real code it's smuggled through the error channel: `Effect.tapError` mutating loop variables via `Effect.sync` closures, then `Effect.retryOrElse` re-running a suspended call. Four mutable variables mutated from three different places — the state flow is invisible unless you already know the trick.
> 2. **`recoverOverflow: typeof compaction.compact | undefined`** — a function-typed parameter used as a one-shot armed/disarmed flag. The callee already closes over `compaction`; a boolean says what it is.
> 3. **`assistantMessageID`** exists solely to resume the same durable message across retries — fine, but only legible once the retry arm is visible in the loop.
>
> The unifying move for (1): a schedule driver, so a retryable failure becomes a caught value handled in the loop body exactly like the compaction arm. One honest trade: it's more manual than the blessed combinator — but the manual version is the loop the combinator was hiding, and the loop already exists for restarts. Two restart mechanisms, one control structure.
>
> Want me to apply it — unified loop, simplified schedule input, boolean `recoverOverflow`?
+253
View File
@@ -0,0 +1,253 @@
---
name: opencode-drive
description: Use when an agent needs drive OpenCode via a script or interact with an isolated instance
---
# OpenCode Drive
Use `opencode-drive` to launch an isolated OpenCode instance and control it via commands or a script.
There are two modes. Always default to using a script unless specifically directed to be interactive (connect
to an existing running instance, or start a new one, and make a few changes to the UI and read it, and iterate
on changes).
Scripts allow you to run a full walkthrough in one run. When the script is done opencode-drive exits,
stops all processes, and cleans up all artifacts.
# Prepare The Environment
Use `init` when files must be added to the isolated home or project before OpenCode starts. It prints the artifact directory without launching OpenCode. A later `start` with the same name reuses it.
```bash
artifacts=$(opencode-drive init --name demo)
cp -R ./fixtures/home/. "$artifacts/"
cp -R ./fixtures/project/. "$artifacts/files/"
opencode-drive start --name demo --dev ~/projects/opencode
```
The simulated project is under `$artifacts/files`. Running `start` without a prior `init` initializes the artifacts automatically.
# Scripted usage
You can write scripts that walk through entire flows, and gives you full access to controlling
the backend too. See examples of the script API at the bottom of this file.
After creating or editing a script, always typecheck it before running. Never skip this step:
```bash
opencode-drive check ./reproduce-stale-exploring-empty.ts
```
Run it by passing `--script` to start:
```bash
opencode-drive start --name auto-stop-reproduction --script ./reproduce-stale-exploring-empty.ts
```
It will output information about the run, including paths to log files which you can read
to inspect what happened. If you need to dig into failures that aren't clear, read those log
files. If the script is unsuccessful, automatically fix the script and run it again.
Scripts use one typed definition object. `setup` runs before OpenCode starts,
and `fs.writeFile` always writes inside the simulated project.
You can read the full typed API here: https://raw.githubusercontent.com/jlongster/opencode-drive/refs/heads/main/src/script/types.ts
```ts
import { defineScript } from "opencode-drive"
export default defineScript({
async setup({ fs, config }) {
config.autoupdate = false
await fs.writeFile("src/example.ts", "export const value = 1\n")
},
async run({ ui, llm }) {
await ui.submit("Open src/example.ts")
await llm.send(llm.text("The file exports `value`."))
await ui.waitFor("The file exports `value`.")
},
})
```
`setup` receives the current OpenCode config object, which starts from the
default drive config unless the prepared instance already has one. When a script
needs custom config, mutate this `config` parameter instead of generating and
writing a new config object from scratch, so the script keeps the default
provider/model settings unless it intentionally changes them.
Note that the simulated model is a GPT model type, and opencode uses the `patch` tool for working with files Do not use a `edit` or `write` tool to edit files.
Use `launch: "manual"` when the script needs to launch the server and every TUI
itself (this is extremely rare, do not use this unless explicitly asked). In this
mode `ui` is typed as `null`; call `server.launch()` exactly
once before launching clients. Each `clients.launch(name)` result provides the
same UI methods as the automatic client. You can see an example of this API
here: https://raw.githubusercontent.com/jlongster/opencode-drive/refs/heads/main/examples/multiple-clients.ts
Use the exported `wait(milliseconds)` utility for an unconditional delay.
`await llm.send(...)` waits for the next request and resolves after OpenCode
acknowledges its complete response. `llm.queue(...)` declares responses in
advance. Chunks may be built with `text`, `reasoning`, `toolCall`, `raw`,
`finish`, and `disconnect`. A normal response receives `finish("stop")`
automatically unless it yields or queues an explicit terminal event.
`llm.text(text, { delay, chunkSize })` defaults to a 2 ms delay and a
15-character target varied by plus or minus 5 per chunk.
`llm.reasoning` accepts the same options, and `llm.pause(milliseconds)` adds a
delay between any two outputs.
Use `llm.serve` for an ongoing typed response generator:
```ts
llm.serve(async function* (request, index) {
yield llm.reasoning(`Handling request ${index + 1}`)
yield llm.text(`Received ${request.id}`)
yield llm.finish("stop")
})
```
The backend connection, response cleanup, cancellation, and recording
completion are automatic.
You can see some example scripts here:
- https://raw.githubusercontent.com/jlongster/opencode-drive/refs/heads/main/examples/simple.ts
- https://raw.githubusercontent.com/jlongster/opencode-drive/refs/heads/main/examples/serve.ts
## Prune
- `prune` removes artifact directories. These are always cleaned up after running a script
successfully, but leftover on failed runs. Always call this if a script fails.
```bash
opencode-drive prune --name demo
// --force cleans up all artifcat directories
opencode-dirve prune --force
```
# Live interaction usage
- Always give headless instances a unique `--name`. Visible instances may omit it.
- A normal headless `start` detaches automatically and returns after the instance is ready.
- Do not add `&`; the long-running owner already runs in the background.
- Configure simulated model responses after startup when needed.
- Send ordered UI commands with `send`.
- Always stop the instance when finished.
```bash
opencode-drive start --name demo
opencode-drive send --name demo \
--command.ui.type '{"text":"Explain this project"}' \
--command.ui.enter
opencode-drive stop --name demo
```
## Send UI Commands
- Every `send` opens a connection to the named instance, runs its commands in order, and exits.
- Combine typing and Enter in one command when submitting a prompt.
- JSON-valued commands require one JSON argument.
- Multiple command flags execute from left to right.
Commands:
- `--command.ui.type <json>` types into the focused editor. Arguments: `text` string.
- `--command.ui.press <json>` presses a key. Arguments: `key` string; optional `modifiers` object with boolean `ctrl`, `shift`, `meta`, `super`, or `hyper`.
- `--command.ui.enter` presses Enter. Arguments: none.
- `--command.ui.arrow <json>` presses an arrow key. Arguments: `direction` is `up`, `down`, `left`, or `right`.
- `--command.ui.focus <json>` focuses an element. Arguments: `target` is the numeric element `num` returned by `ui.state`.
- `--command.ui.click <json>` clicks an element. Arguments: numeric `target`, `x`, and `y`; use the element `num` returned by `ui.state` as `target`.
- `--command.ui.state` prints focus and interactive element metadata as JSON. Arguments: none.
- `--command.ui.matches <json>` prints whether literal, case-sensitive text appears on screen. Arguments: `text` string.
```bash
opencode-drive send --name demo \
--command.ui.type '{"text":"Find the relevant code and explain it"}' \
--command.ui.enter
opencode-drive send --name demo \
--command.ui.press '{"key":"p","modifiers":{"ctrl":true}}'
opencode-drive send --name demo \
--command.ui.arrow '{"direction":"down"}'
opencode-drive send --name demo \
--command.ui.focus '{"target":12}'
opencode-drive send --name demo \
--command.ui.click '{"target":12,"x":4,"y":1}'
opencode-drive send --name demo \
--command.ui.matches '{"text":"OpenCode"}'
```
To read the UI state and see information about interactable elements, use the `ui.state` command:
```bash
opencode-drive send --name demo --command.ui.state
```
## Configure LLM Responses
- `responses` controls what the LLM responds with
- Only use this if you are wanting to reproduce an exact type of response
- Defaults are `text,reasoning,diff,tool` with `write,apply_patch`.
- Supported types are `text`, `reasoning`, `diff`, and `tool`.
- `--tools` limits generated tool calls to names offered by OpenCode.
```bash
opencode-drive responses --name demo \
--types text,reasoning,diff,tool \
--tools write,apply_patch
opencode-drive responses --name demo \
--types tool \
--tools read,glob,grep
```
## Inspect The UI
- `ui.state` prints focus and interactive element metadata as JSON.
- `ui.matches` checks for literal, case-sensitive screen text.
- `screenshot` prints the generated image path.
```bash
opencode-drive screenshot --name demo
```
## Lifecycle
- `stop` waits for recording export and owner cleanup before returning.
```bash
opencode-drive stop --name demo
```
# Record The UI
- Start with `--record` to capture a headless instance from its first rendered frame.
- `stop` finishes the recording, exports an MP4, and prints its path.
```bash
opencode-drive start --name demo --record
opencode-drive send --name demo \
--command.ui.type '{"text":"Show me the current architecture"}' \
--command.ui.enter
opencode-drive stop --name demo
```
# Artifacts dir
- `dir` prints the artifact directory for the instance.
```bash
opencode-drive dir --name demo
```
+1 -1
View File
@@ -4,7 +4,7 @@ import { tool } from "@opencode-ai/plugin"
const TEAM = { const TEAM = {
tui: ["kommander", "simonklee"], tui: ["kommander", "simonklee"],
desktop_web: ["Hona", "Brendonovich"], desktop_web: ["Hona", "Brendonovich"],
core: ["jlongster", "rekram1-node", "neriousy", "nexxeln", "kitlangton"], core: ["jlongster", "rekram1-node", "nexxeln", "kitlangton"],
inference: ["fwang", "MrMushrooooom", "starptech"], inference: ["fwang", "MrMushrooooom", "starptech"],
windows: ["Hona"], windows: ["Hona"],
} as const } as const
+3 -18
View File
@@ -1,9 +1,8 @@
- After changing the public Protocol or Server `HttpApi`, run `bun run generate` from `packages/client`. Do not edit `src/generated` or `src/generated-effect` directly. - After changing the public Protocol or Server `HttpApi`, run `bun run generate` from `packages/client`. Do not edit `src/generated` or `src/generated-effect` directly.
- Keep runtime dependencies directed from Schema to Core and Protocol, then from Core and Protocol to Server. Client runtime code may depend on Schema and Protocol but never Core or Server; `sdk-next` composes Client, Core, and Server. - Keep runtime dependencies directed from Schema to Core and Protocol, then from Core and Protocol to Server. Client runtime code may depend on Schema and Protocol but never Core or Server; `sdk-next` composes Client, Core, and Server.
- Do not modify `packages/opencode` unless the user explicitly asks for V1 work. `packages/opencode` is the V1 implementation and is present for reference only. New implementation changes should land in the V2 package set: `packages/core`, `packages/cli`, `packages/server`, `packages/protocol`, `packages/schema`, and related generated client surfaces when required. - Do not modify `packages/opencode` unless the user explicitly asks for V1 work. `packages/opencode` is the V1 implementation and is present for reference only. New implementation changes should land in the V2 package set: `packages/core`, `packages/cli`, `packages/server`, `packages/protocol`, `packages/schema`, and related generated client surfaces when required.
- The default branch in this repo is `v2`. - The default branch in this repo is `dev`.
- Base all new branches and worktrees on `v2`, or `origin/v2` when the local `v2` ref is unavailable. Do not base them on `dev`. - Local `main` ref may not exist; use `dev` or `origin/dev` for diffs.
- Local `main` ref may not exist; use `v2` or `origin/v2` for diffs.
## Live V2 TUI Testing ## Live V2 TUI Testing
@@ -12,20 +11,6 @@
- The script discovers the server with `opencode2 service status`, injects its private local credential from `opencode2 service get password`, and uses the `next` 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 `next` 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. - 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
- When a user asks for a TUI story, add a fixture-driven story under `packages/tui/src/feature-plugins/system/storybook` and register it in `index.tsx`.
- Render the real production component rather than a visual copy. Keep submissions and other side effects local to the story so it is safe to explore repeatedly.
- Expose the meaningful state dimensions through story keybindings and list them in `StoryFooter`; include a reset command when combinations can leave the fixture in a confusing state.
- Run a specific story with `OPENCODE_STORY=<story-id> bun run dev:live` from the development worktree, and exercise narrow and wide terminal sizes when layout is relevant.
## TUI Theme Tokens
- Choose theme tokens by semantic role, not by their current color. Do not use raw `theme.hue` values or borrow an unrelated semantic token to achieve a preferred appearance.
- Use `text.feedback` and `background.feedback` only for outcome or status feedback such as errors, warnings, success messages, and informational messages. Use `formfield` states for form-control text, ordinals, and selection markers, and `action` states for actions.
- If the theme does not expose a token for the required semantic role, extend the theme schema, defaults, resolution, and types with that role before using it in a component. Do not repurpose the nearest-looking existing token.
- When changing the public theme token surface, verify the built-in light and dark defaults and the custom-theme fallback path in addition to the affected TUI component.
## Branch Names ## Branch Names
Use a short branch name of at most three words, separated by hyphens. Do not use slashes or type prefixes such as `feat/` or `fix/`. Use a short branch name of at most three words, separated by hyphens. Do not use slashes or type prefixes such as `feat/` or `fix/`.
@@ -185,4 +170,4 @@ const table = sqliteTable("session", {
- One step is one logical LLM call; its durable record covers only the model-visible span. Do not write "provider turn", and do not use bare "turn" for a single call: "turn" is reserved for the future assistant-turn unit containing all steps from prompt promotion until the session would go idle. - One step is one logical LLM call; its durable record covers only the model-visible span. Do not write "provider turn", and do not use bare "turn" for a single call: "turn" is reserved for the future assistant-turn unit containing all steps from prompt promotion until the session would go idle.
- Keep EventV2 replay owner claims separate from clustered Session execution ownership. - Keep EventV2 replay owner claims separate from clustered Session execution ownership.
- Keep the Instructions algebra and built-ins in `src/instructions`; keep instruction producers with their observed domains, and keep Session History selection plus `InstructionState` and `InstructionEntry` persistence Session-owned. `InstructionDiscovery` observes ambient global and upward-project instructions. The runner composes built-ins, discovery, guidance, and entries explicitly in `loadInstructions`; there is no instruction registry. - Keep the Instructions algebra and built-ins in `src/instructions`; keep instruction producers with their observed domains, and keep Session History selection plus `InstructionState` and `InstructionEntry` persistence Session-owned. `InstructionDiscovery` observes ambient global and upward-project instructions. The runner composes built-ins, discovery, guidance, and entries explicitly in `loadInstructions`; there is no instruction registry.
- `session.instructions.updated` stores only changed source keys and content hashes. Blob values live once in `instruction_blob`; `instruction_state` is a rebuildable fold cache, never primary state. Render initial instructions and chronological updates from values during request assembly. Completed compaction moves the instruction epoch; Session movement retains it so destination instruction changes are chronological, while committed revert clears it. Unavailable sources retain the last value and block only the initial complete delta. - `session.instructions.updated` stores only changed source keys and content hashes. Blob values live once in `instruction_blob`; `instruction_state` is a rebuildable fold cache, never primary state. Render initial instructions and chronological updates from values during request assembly. Completed compaction moves the instruction epoch; Session movement and committed revert clear it. Unavailable sources retain the last value and block only the initial complete delta.
+632 -496
View File
File diff suppressed because it is too large Load Diff
+46 -38
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
APP=opencode2 APP=opencode
MUTED='\033[0;2m' MUTED='\033[0;2m'
RED='\033[0;31m' RED='\033[0;31m'
@@ -15,14 +15,14 @@ Usage: install.sh [options]
Options: Options:
-h, --help Display this help message -h, --help Display this help message
-v, --version <version> Install a specific version (e.g., 0.0.0-next-17236) -v, --version <version> Install a specific version (e.g., 1.0.180)
-b, --binary <path> Install from a local binary instead of downloading -b, --binary <path> Install from a local binary instead of downloading
--no-modify-path Don't modify shell config files (.zshrc, .bashrc, etc.) --no-modify-path Don't modify shell config files (.zshrc, .bashrc, etc.)
Examples: Examples:
curl -fsSL https://raw.githubusercontent.com/anomalyco/opencode/v2/install | bash curl -fsSL https://opencode.ai/install | bash
curl -fsSL https://raw.githubusercontent.com/anomalyco/opencode/v2/install | bash -s -- --version 0.0.0-next-17236 curl -fsSL https://opencode.ai/install | bash -s -- --version 1.0.180
./install --binary /path/to/opencode2 ./install --binary /path/to/opencode
EOF EOF
} }
@@ -109,6 +109,11 @@ else
;; ;;
esac esac
archive_ext=".zip"
if [ "$os" = "linux" ]; then
archive_ext=".tar.gz"
fi
is_musl=false is_musl=false
if [ "$os" = "linux" ]; then if [ "$os" = "linux" ]; then
if [ -f /etc/alpine-release ]; then if [ -f /etc/alpine-release ]; then
@@ -160,42 +165,42 @@ else
target="$target-musl" target="$target-musl"
fi fi
filename="$APP-$target$archive_ext"
if [ "$os" = "linux" ]; then
if ! command -v tar >/dev/null 2>&1; then if ! command -v tar >/dev/null 2>&1; then
echo -e "${RED}Error: 'tar' is required but not installed.${NC}" echo -e "${RED}Error: 'tar' is required but not installed.${NC}"
exit 1 exit 1
fi fi
else
if ! command -v unzip >/dev/null 2>&1; then
echo -e "${RED}Error: 'unzip' is required but not installed.${NC}"
exit 1
fi
fi
if [ -z "$requested_version" ]; then if [ -z "$requested_version" ]; then
metadata=$(curl -fsSL https://registry.npmjs.org/@opencode-ai%2fcli/next || true) url="https://github.com/anomalyco/opencode/releases/latest/download/$filename"
specific_version=$(echo "$metadata" | sed -n 's/.*"version":"\([^"]*\)".*/\1/p') specific_version=$(curl -s https://api.github.com/repos/anomalyco/opencode/releases/latest | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p')
if [ -z "$specific_version" ]; then if [[ $? -ne 0 || -z "$specific_version" ]]; then
echo -e "${RED}Failed to fetch version information${NC}" echo -e "${RED}Failed to fetch version information${NC}"
exit 1 exit 1
fi fi
else else
# Strip leading 'v' if present # Strip leading 'v' if present
requested_version="${requested_version#v}" requested_version="${requested_version#v}"
url="https://github.com/anomalyco/opencode/releases/download/v${requested_version}/$filename"
specific_version=$requested_version specific_version=$requested_version
fi
package_name="@opencode-ai/cli-$target" # Verify the release exists before downloading
http_status=$(curl -s -o /dev/null -w "%{http_code}" "https://registry.npmjs.org/@opencode-ai%2fcli-$target/$specific_version" || true) http_status=$(curl -sI -o /dev/null -w "%{http_code}" "https://github.com/anomalyco/opencode/releases/tag/v${requested_version}")
if [ "$http_status" = "404" ]; then if [ "$http_status" = "404" ]; then
echo -e "${RED}Error: Version ${specific_version} is not available for $target${NC}" echo -e "${RED}Error: Release v${requested_version} not found${NC}"
echo -e "${MUTED}Available versions: https://www.npmjs.com/package/$package_name?activeTab=versions${NC}" echo -e "${MUTED}Available releases: https://github.com/anomalyco/opencode/releases${NC}"
exit 1 exit 1
fi fi
if [ "$http_status" != "200" ]; then
echo -e "${RED}Failed to fetch package information${NC}"
exit 1
fi
filename="cli-$target-$specific_version.tgz"
url="https://registry.npmjs.org/$package_name/-/$filename"
binary_name="$APP"
if [ "$os" = "windows" ]; then
binary_name="$APP.exe"
fi fi
fi fi
@@ -214,13 +219,11 @@ print_message() {
} }
check_version() { check_version() {
if command -v "$APP" >/dev/null 2>&1; then if command -v opencode >/dev/null 2>&1; then
opencode_path=$(which "$APP") opencode_path=$(which opencode)
## Check the installed version ## Check the installed version
installed_version=$("$APP" --version 2>/dev/null || echo "") installed_version=$(opencode --version 2>/dev/null || echo "")
installed_version="${installed_version##* }"
installed_version="${installed_version#v}"
if [[ "$installed_version" != "$specific_version" ]]; then if [[ "$installed_version" != "$specific_version" ]]; then
print_message info "${MUTED}Installed version: ${NC}$installed_version." print_message info "${MUTED}Installed version: ${NC}$installed_version."
@@ -284,7 +287,7 @@ download_with_progress() {
trap "trap - RETURN; rm -f \"$tracefile\"; printf '\033[?25h' >&4; exec 4>&-" RETURN trap "trap - RETURN; rm -f \"$tracefile\"; printf '\033[?25h' >&4; exec 4>&-" RETURN
( (
curl --trace-ascii "$tracefile" -fsL -o "$output" "$url" curl --trace-ascii "$tracefile" -s -L -o "$output" "$url"
) & ) &
local curl_pid=$! local curl_pid=$!
@@ -322,25 +325,30 @@ download_with_progress() {
} }
download_and_install() { download_and_install() {
print_message info "\n${MUTED}Installing ${NC}$APP ${MUTED}version: ${NC}$specific_version" print_message info "\n${MUTED}Installing ${NC}opencode ${MUTED}version: ${NC}$specific_version"
local tmp_dir="${TMPDIR:-/tmp}/opencode_install_$$" local tmp_dir="${TMPDIR:-/tmp}/opencode_install_$$"
mkdir -p "$tmp_dir" mkdir -p "$tmp_dir"
if [[ "$os" == "windows" ]] || ! [ -t 2 ] || ! download_with_progress "$url" "$tmp_dir/$filename"; then if [[ "$os" == "windows" ]] || ! [ -t 2 ] || ! download_with_progress "$url" "$tmp_dir/$filename"; then
# Fallback to standard curl on Windows, non-TTY environments, or if custom progress fails # Fallback to standard curl on Windows, non-TTY environments, or if custom progress fails
curl -f -# -L -o "$tmp_dir/$filename" "$url" curl -# -L -o "$tmp_dir/$filename" "$url"
fi fi
if [ "$os" = "linux" ]; then
tar -xzf "$tmp_dir/$filename" -C "$tmp_dir" tar -xzf "$tmp_dir/$filename" -C "$tmp_dir"
mv "$tmp_dir/package/bin/$binary_name" "$INSTALL_DIR" else
chmod 755 "${INSTALL_DIR}/$binary_name" unzip -q "$tmp_dir/$filename" -d "$tmp_dir"
fi
mv "$tmp_dir/opencode" "$INSTALL_DIR"
chmod 755 "${INSTALL_DIR}/opencode"
rm -rf "$tmp_dir" rm -rf "$tmp_dir"
} }
install_from_binary() { install_from_binary() {
print_message info "\n${MUTED}Installing ${NC}$APP ${MUTED}from: ${NC}$binary_path" print_message info "\n${MUTED}Installing ${NC}opencode ${MUTED}from: ${NC}$binary_path"
cp "$binary_path" "${INSTALL_DIR}/$APP" cp "$binary_path" "${INSTALL_DIR}/opencode"
chmod 755 "${INSTALL_DIR}/$APP" chmod 755 "${INSTALL_DIR}/opencode"
} }
if [ -n "$binary_path" ]; then if [ -n "$binary_path" ]; then
@@ -445,8 +453,8 @@ echo -e ""
echo -e "${MUTED}OpenCode includes free models, to start:${NC}" echo -e "${MUTED}OpenCode includes free models, to start:${NC}"
echo -e "" echo -e ""
echo -e "cd <project> ${MUTED}# Open directory${NC}" echo -e "cd <project> ${MUTED}# Open directory${NC}"
echo -e "opencode2 ${MUTED}# Run command${NC}" echo -e "opencode ${MUTED}# Run command${NC}"
echo -e "" echo -e ""
echo -e "${MUTED}For more information visit ${NC}https://opencode.ai/v2/docs" echo -e "${MUTED}For more information visit ${NC}https://opencode.ai/docs"
echo -e "" echo -e ""
echo -e "" echo -e ""
+8 -10
View File
@@ -46,9 +46,9 @@
"@octokit/rest": "22.0.0", "@octokit/rest": "22.0.0",
"@hono/standard-validator": "0.2.0", "@hono/standard-validator": "0.2.0",
"@hono/zod-validator": "0.4.2", "@hono/zod-validator": "0.4.2",
"@opentui/core": "0.5.3", "@opentui/core": "0.0.0-20260808-9ecf7c0a",
"@opentui/keymap": "0.5.3", "@opentui/keymap": "0.0.0-20260808-9ecf7c0a",
"@opentui/solid": "0.5.3", "@opentui/solid": "0.0.0-20260808-9ecf7c0a",
"@tanstack/solid-virtual": "3.13.32", "@tanstack/solid-virtual": "3.13.32",
"@shikijs/stream": "4.2.0", "@shikijs/stream": "4.2.0",
"@standard-schema/spec": "1.1.0", "@standard-schema/spec": "1.1.0",
@@ -120,8 +120,7 @@
"prettier": "3.6.2", "prettier": "3.6.2",
"semver": "^7.6.0", "semver": "^7.6.0",
"sst": "catalog:", "sst": "catalog:",
"turbo": "2.10.2", "turbo": "2.10.2"
"vitest": "4.1.10"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "3.933.0", "@aws-sdk/client-s3": "3.933.0",
@@ -151,9 +150,9 @@
"electron" "electron"
], ],
"overrides": { "overrides": {
"@opentui/core": "catalog:", "@opentui/core": "0.0.0-20260808-9ecf7c0a",
"@opentui/keymap": "catalog:", "@opentui/keymap": "0.0.0-20260808-9ecf7c0a",
"@opentui/solid": "catalog:", "@opentui/solid": "0.0.0-20260808-9ecf7c0a",
"@types/bun": "catalog:", "@types/bun": "catalog:",
"@types/node": "catalog:", "@types/node": "catalog:",
"effect": "catalog:" "effect": "catalog:"
@@ -172,7 +171,6 @@
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch", "@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch", "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
"effect@4.0.0-beta.101": "patches/effect@4.0.0-beta.101.patch", "effect@4.0.0-beta.101": "patches/effect@4.0.0-beta.101.patch",
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch", "@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch"
"@ff-labs/fff-bun@0.10.1": "patches/@ff-labs%2Ffff-bun@0.10.1.patch"
} }
} }
+7 -8
View File
@@ -80,7 +80,7 @@ Route defaults are request-shaping defaults such as `headers`, `limits`, `genera
The four-axis decomposition is the reason DeepSeek, TogetherAI, Cerebras, Baseten, Fireworks, and DeepInfra all reuse `OpenAIChat.protocol` verbatim — each provider deployment is a 5-15 line `Route.make(...)` call instead of a 300-400 line route clone. Bug fixes in one protocol propagate to every consumer of that protocol in a single commit. The four-axis decomposition is the reason DeepSeek, TogetherAI, Cerebras, Baseten, Fireworks, and DeepInfra all reuse `OpenAIChat.protocol` verbatim — each provider deployment is a 5-15 line `Route.make(...)` call instead of a 300-400 line route clone. Bug fixes in one protocol propagate to every consumer of that protocol in a single commit.
When a provider supports multiple physical transports, selection remains execution policy below its semantic route. `OpenResponsesChannel.transport(...)` owns the provider-neutral Responses WebSocket concept: it prepares one final request, executes HTTP by default, strips WebSocket-disallowed fields, and passes a generic channel exchange to a per-call `WebSocketChannelExecutor` when supplied. Provider-specific Responses routes opt in with handshake and connection-age policy. `Route.streamPrepared` owns decoding and acknowledges channel completion only after successful full consumption. When a provider ships a non-HTTP transport (OpenAI's WebSocket Responses backend, hypothetical bidirectional streaming APIs), the seam is `Transport``WebSocketTransport.jsonTransport.with(...)` constructs an IO template whose `prepare` receives the route endpoint/auth at compile time, builds a WebSocket URL and message, and whose `frames` yields decoded text from the socket. Same protocol and endpoint source, different transport.
### URL Construction ### URL Construction
@@ -106,7 +106,7 @@ const proxied = gateway.model("openai/gpt-4o-mini")
Keep provider facades small and explicit: Keep provider facades small and explicit:
- Use branded `ProviderID.make(...)` and `ModelID.make(...)` where ids are constructed directly. - Use branded `ProviderID.make(...)` and `ModelID.make(...)` where ids are constructed directly.
- Use `model` for the default API path and named methods for provider-native alternatives such as OpenAI `responses` and `chat`. - Use `model` for the default API path and named methods for provider-native alternatives such as OpenAI `responses`, `responsesWebSocket`, and `chat`.
- Put provider-specific setup on `.configure(...)`; do not add `model(id, overrides)` as a duplicate construction path. - Put provider-specific setup on `.configure(...)`; do not add `model(id, overrides)` as a duplicate construction path.
- Export lower-level `routes` arrays separately only when advanced internal wiring needs them. - Export lower-level `routes` arrays separately only when advanced internal wiring needs them.
- Prefer `apiKey` as provider-specific sugar and `auth` as the explicit override; keep them mutually exclusive in provider option types with `ProviderAuthOption`. - Prefer `apiKey` as provider-specific sugar and `auth` as the explicit override; keep them mutually exclusive in provider option types with `ProviderAuthOption`.
@@ -124,10 +124,11 @@ import { model } from "@opencode-ai/ai/providers/openai/responses"
const selected = model("gpt-5", { const selected = model("gpt-5", {
apiKey, apiKey,
transport: "websocket",
}) })
``` ```
Keep semantic APIs as separate entrypoints, such as OpenAI `chat` and `responses`. Transport is execution policy: OpenAI Responses uses HTTP by default and may receive a per-call WebSocket channel executor through `StreamOptions` without changing model or route identity. Keep semantic APIs as separate entrypoints, such as OpenAI `chat` and `responses`. Keep transport choices inside the semantic entrypoint settings, so OpenAI Responses HTTP and WebSocket share one entrypoint. Provider facades may still expose named selectors such as `responsesWebSocket` for direct typed call sites; the package-like contract maps its settings to those selectors before returning an executable `LanguageModel`.
Do not expose `Route` in provider package settings. Route composition stays an implementation detail behind `model(...)`. Do not expose `Route` in provider package settings. Route composition stays an implementation detail behind `model(...)`.
@@ -153,16 +154,14 @@ packages/ai/src/
auth-options.ts ProviderAuthOption shape, AuthOptions.bearer, AtLeastOne helper auth-options.ts ProviderAuthOption shape, AuthOptions.bearer, AtLeastOne helper
framing.ts Framing type + Framing.sse framing.ts Framing type + Framing.sse
transport/ transport implementations transport/ transport implementations
index.ts Transport execution types + HttpTransport / WebSocketTransport namespaces index.ts Transport type + HttpTransport / WebSocketTransport namespaces
websocket-channel.ts generic sequential channel executor/driver contract
http.ts HttpTransport.httpJson — POST + framing http.ts HttpTransport.httpJson — POST + framing
websocket.ts direct one-request channel executor + raw socket adapter websocket.ts WebSocketTransport.json + WebSocketExecutor service
protocols/ protocols/
shared.ts ProviderShared toolkit used inside protocol impls shared.ts ProviderShared toolkit used inside protocol impls
openai-chat.ts protocol + route (compose OpenAIChat.protocol) openai-chat.ts protocol + route (compose OpenAIChat.protocol)
open-responses.ts provider-neutral Responses protocol baseline open-responses.ts provider-neutral Responses protocol baseline
open-responses-channel.ts provider-neutral Responses WebSocket transport factory openai-responses.ts OpenAI tools/events/transports composed over OpenResponses
openai-responses.ts OpenAI tools/events and channel policy composed over OpenResponses
anthropic-messages.ts anthropic-messages.ts
gemini.ts gemini.ts
bedrock-converse.ts bedrock-converse.ts
+2 -1
View File
@@ -315,6 +315,7 @@ import { model } from "@opencode-ai/ai/providers/openai/responses"
const selected = model("gpt-5", { const selected = model("gpt-5", {
apiKey: process.env.OPENAI_API_KEY, apiKey: process.env.OPENAI_API_KEY,
transport: "websocket",
headers: { "x-application": "opencode" }, headers: { "x-application": "opencode" },
limits: { context: 200_000, output: 64_000 }, limits: { context: 200_000, output: 64_000 },
}) })
@@ -331,7 +332,7 @@ OpenAI Chat and OpenAI Responses are separate semantic entrypoints:
- `@opencode-ai/ai/providers/google-vertex/responses` - `@opencode-ai/ai/providers/google-vertex/responses`
- `@opencode-ai/ai/providers/google-vertex/messages` - `@opencode-ai/ai/providers/google-vertex/messages`
OpenAI Responses has one semantic route and uses HTTP by default. Advanced callers may supply a per-call WebSocket channel executor through `StreamOptions`; transport policy does not change provider settings, model identity, or route identity. The provider-neutral Open Responses implementation owns the reusable WebSocket request and event contract, while each provider opts in with its own handshake and connection policy. Azure follows the same Chat/Responses split at `providers/azure/chat` and `providers/azure/responses`. Generic OpenAI-compatible Chat remains at `providers/openai-compatible`; the Responses adapter at `providers/openai-compatible/responses` uses the provider-neutral Open Responses protocol. OpenAI Responses extends that baseline with OpenAI tools, event variants, metadata, and defaults. Generic Anthropic Messages-compatible providers use `providers/anthropic-compatible`, which the named Anthropic provider composes. Google Gemini and Amazon Bedrock expose their single native API through their existing provider paths. Responses HTTP versus WebSocket is a scoped `transport` setting on the OpenAI Responses entrypoint, not another entrypoint. Azure follows the same Chat/Responses split at `providers/azure/chat` and `providers/azure/responses`. Generic OpenAI-compatible Chat remains at `providers/openai-compatible`; the Responses adapter at `providers/openai-compatible/responses` uses the provider-neutral Open Responses protocol. OpenAI Responses extends that baseline with OpenAI tools, event variants, metadata, defaults, and transports. Generic Anthropic Messages-compatible providers use `providers/anthropic-compatible`, which the named Anthropic provider composes. Google Gemini and Amazon Bedrock expose their single native API through their existing provider paths.
Vertex Gemini, Vertex Chat, Vertex Responses, and Vertex Messages are separate API entrypoints. All accept `project`, `location`, and an optional `accessToken`; when no explicit token or auth override is supplied they lazily use Google Application Default Credentials. Vertex Gemini instead selects express mode when `apiKey` or `GOOGLE_VERTEX_API_KEY` is present. Vertex Chat targets MaaS models through the OpenAI-compatible Chat Completions endpoint, while Vertex Responses targets Grok models and defaults `store` to `false` as required by Vertex. `providers/google-vertex` remains the default alias for `providers/google-vertex/gemini`. Vertex Gemini, Vertex Chat, Vertex Responses, and Vertex Messages are separate API entrypoints. All accept `project`, `location`, and an optional `accessToken`; when no explicit token or auth override is supplied they lazily use Google Application Default Credentials. Vertex Gemini instead selects express mode when `apiKey` or `GOOGLE_VERTEX_API_KEY` is present. Vertex Chat targets MaaS models through the OpenAI-compatible Chat Completions endpoint, while Vertex Responses targets Grok models and defaults `store` to `false` as required by Vertex. `providers/google-vertex` remains the default alias for `providers/google-vertex/gemini`.
+7 -6
View File
@@ -1,6 +1,6 @@
# LLM Provider Parity Status # LLM Provider Parity Status
Last reviewed: 2026-08-07 Last reviewed: 2026-07-24
This file tracks the gap between the native `@opencode-ai/ai` package and the AI SDK provider packages that opencode still depends on for many catalog/runtime paths. This file tracks the gap between the native `@opencode-ai/ai` package and the AI SDK provider packages that opencode still depends on for many catalog/runtime paths.
@@ -16,7 +16,8 @@ This file tracks the gap between the native `@opencode-ai/ai` package and the AI
| Native slice | Source | Current state | Main gaps | | Native slice | Source | Current state | Main gaps |
| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| OpenAI Chat | `src/protocols/openai-chat.ts`, `src/providers/openai.ts` | Usable. Streams text, reasoning deltas, tool calls, usage, images, and common generation controls. | No typed structured-output / `response_format` path. Limited typed OpenAI option surface compared with SDK escape hatches. | | OpenAI Chat | `src/protocols/openai-chat.ts`, `src/providers/openai.ts` | Usable. Streams text, reasoning deltas, tool calls, usage, images, and common generation controls. | No typed structured-output / `response_format` path. Limited typed OpenAI option surface compared with SDK escape hatches. |
| OpenAI Responses | `src/protocols/open-responses.ts`, `src/protocols/openai-responses.ts`, `src/providers/openai.ts` | Usable over HTTP by default, with optional per-call WebSocket channel execution on the same model and route identity. | No incremental `previous_response_id` path or persistent Session channel manager yet. Typed options cover only a subset of Responses fields. Structured output is still mostly synthetic-tool based. | | OpenAI Responses HTTP | `src/protocols/open-responses.ts`, `src/protocols/openai-responses.ts`, `src/providers/openai.ts` | Usable. Extends the Open Responses baseline with hosted-tool event surfacing, reasoning replay metadata, GPT-5 defaults, and cache usage. | No explicit `previous_response_id` path. Typed options cover only a subset of Responses fields. Structured output is still mostly synthetic-tool based. |
| OpenAI Responses WebSocket | `src/protocols/openai-responses.ts`, `src/route/transport/websocket.ts` | Present as `OpenAI.responsesWebSocket(...)`. | Runner/catalog support explicitly must not downgrade WebSocket routes; broader runtime selection is not complete. |
| OpenAI-compatible Chat | `src/protocols/openai-compatible-chat.ts`, `src/providers/openai-compatible.ts` | Usable for generic Chat and several profiles: Baseten, Cerebras, DeepInfra, DeepSeek, Fireworks, Groq, TogetherAI. | Family quirks are mostly endpoint defaults, not full typed behavior. | | OpenAI-compatible Chat | `src/protocols/openai-compatible-chat.ts`, `src/providers/openai-compatible.ts` | Usable for generic Chat and several profiles: Baseten, Cerebras, DeepInfra, DeepSeek, Fireworks, Groq, TogetherAI. | Family quirks are mostly endpoint defaults, not full typed behavior. |
| Open Responses-compatible | `src/protocols/open-responses.ts`, `src/protocols/openai-compatible-responses.ts`, `src/providers/openai-compatible-responses.ts` | Usable for deployments that implement the provider-neutral Open Responses protocol. The deployment adapter does not inherit OpenAI tools, events, metadata, or defaults. | No named family profiles or recorded deployment coverage yet. | | Open Responses-compatible | `src/protocols/open-responses.ts`, `src/protocols/openai-compatible-responses.ts`, `src/providers/openai-compatible-responses.ts` | Usable for deployments that implement the provider-neutral Open Responses protocol. The deployment adapter does not inherit OpenAI tools, events, metadata, or defaults. | No named family profiles or recorded deployment coverage yet. |
| Anthropic-compatible Messages | `src/protocols/anthropic-messages.ts`, `src/providers/anthropic-compatible.ts` | Usable for deployments that implement the Anthropic Messages wire protocol. Named Anthropic composes this base; MiniMax M3 has recorded text and tool-loop coverage. | No named compatible family profiles yet. | | Anthropic-compatible Messages | `src/protocols/anthropic-messages.ts`, `src/providers/anthropic-compatible.ts` | Usable for deployments that implement the Anthropic Messages wire protocol. Named Anthropic composes this base; MiniMax M3 has recorded text and tool-loop coverage. | No named compatible family profiles yet. |
@@ -48,8 +49,8 @@ Other `aisdk:` packages, including Google Vertex, Azure, and Bedrock, currently
## AI SDK Package Parity Matrix ## AI SDK Package Parity Matrix
| AI SDK package | Intended native target | Status | Biggest gaps | | AI SDK package | Intended native target | Status | Biggest gaps |
| --------------------------------- | --------------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | --------------------------------- | -------------------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `@ai-sdk/openai` | `OpenAI.chat`, `OpenAI.responses` | Partial / usable | Add complete typed option coverage, structured output strategy, explicit Responses continuation support, and runner execution policy for optional WebSocket channels. | | `@ai-sdk/openai` | `OpenAI.chat`, `OpenAI.responses`, `OpenAI.responsesWebSocket` | Partial / usable | Add complete typed option coverage, structured output strategy, explicit Responses continuation support, and runner route selection between Chat/Responses/WebSocket. |
| `@ai-sdk/openai-compatible` | Generic OpenAI-compatible Chat and Responses | Partial / usable | Decide per-family namespace/profile behavior and runner API selection for providers that support Responses versus Chat only. | | `@ai-sdk/openai-compatible` | Generic OpenAI-compatible Chat and Responses | Partial / usable | Decide per-family namespace/profile behavior and runner API selection for providers that support Responses versus Chat only. |
| `@ai-sdk/anthropic` | `AnthropicMessages` | Partial / usable | Finish Messages API parity for headers/betas/metadata/newer fields and document hosted-tool continuation expectations. | | `@ai-sdk/anthropic` | `AnthropicMessages` | Partial / usable | Finish Messages API parity for headers/betas/metadata/newer fields and document hosted-tool continuation expectations. |
| `@ai-sdk/google` | Gemini Developer API | Partial / usable | Add typed options for safety, response schema/modalities, cached content, grounding/search/code execution, and non-text output modes where supported. | | `@ai-sdk/google` | Gemini Developer API | Partial / usable | Add typed options for safety, response schema/modalities, cached content, grounding/search/code execution, and non-text output modes where supported. |
@@ -78,9 +79,9 @@ Other `aisdk:` packages, including Google Vertex, Azure, and Bedrock, currently
These are implementation/API slices, not separate npm packages. These are implementation/API slices, not separate npm packages.
| API slice | Package-like entrypoint | Purpose | | API slice | Package-like entrypoint | Purpose |
| ----------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------ | | ----------------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------- |
| OpenAI Chat | `@opencode-ai/ai/providers/openai/chat` | OpenAI `/chat/completions` semantics. | | OpenAI Chat | `@opencode-ai/ai/providers/openai/chat` | OpenAI `/chat/completions` semantics. |
| OpenAI Responses | `@opencode-ai/ai/providers/openai/responses` | OpenAI `/responses` semantics with HTTP default and optional per-call WebSocket execution. | | OpenAI Responses | `@opencode-ai/ai/providers/openai/responses` | OpenAI `/responses` semantics with HTTP/WebSocket selected through settings. |
| OpenAI-compatible Chat | `@opencode-ai/ai/providers/openai-compatible` | Generic OpenAI-compatible `/chat/completions`. | | OpenAI-compatible Chat | `@opencode-ai/ai/providers/openai-compatible` | Generic OpenAI-compatible `/chat/completions`. |
| Open Responses-compatible | `@opencode-ai/ai/providers/openai-compatible/responses` | Generic provider-neutral `/responses`. | | Open Responses-compatible | `@opencode-ai/ai/providers/openai-compatible/responses` | Generic provider-neutral `/responses`. |
| Anthropic-compatible Messages | `@opencode-ai/ai/providers/anthropic-compatible` | Generic Anthropic-compatible `/messages`. | | Anthropic-compatible Messages | `@opencode-ai/ai/providers/anthropic-compatible` | Generic Anthropic-compatible `/messages`. |
+41 -18
View File
@@ -67,6 +67,7 @@ Examples:
```ts ```ts
OpenAI.responses("gpt-4o") OpenAI.responses("gpt-4o")
OpenAI.chat("gpt-4o") OpenAI.chat("gpt-4o")
OpenAI.responsesWebSocket("gpt-4o")
Azure.configure({ resourceName, apiKey }).responses("my-deployment") Azure.configure({ resourceName, apiKey }).responses("my-deployment")
AmazonBedrock.configure({ region, credentials }).model("anthropic.claude-3-5-sonnet-20241022-v2:0") AmazonBedrock.configure({ region, credentials }).model("anthropic.claude-3-5-sonnet-20241022-v2:0")
@@ -249,6 +250,11 @@ const openAIChat = Route.make({
auth: Auth.envBearer("OPENAI_API_KEY"), auth: Auth.envBearer("OPENAI_API_KEY"),
}) })
const openAIResponsesWebSocket = openAIResponses.with({
id: "openai-responses-websocket",
transport: WebSocketTransport.json,
})
const openAIConfig = (input: OpenAIConfig) => ({ const openAIConfig = (input: OpenAIConfig) => ({
endpoint: input.endpoint, endpoint: input.endpoint,
auth: input.auth ?? (input.apiKey ? Auth.bearer(input.apiKey) : undefined), auth: input.auth ?? (input.apiKey ? Auth.bearer(input.apiKey) : undefined),
@@ -260,11 +266,13 @@ const openAIConfig = (input: OpenAIConfig) => ({
const configureOpenAI = (input: OpenAIConfig = {}) => { const configureOpenAI = (input: OpenAIConfig = {}) => {
const responses = openAIResponses.with(openAIConfig(input)) const responses = openAIResponses.with(openAIConfig(input))
const responsesWebSocket = openAIResponsesWebSocket.with(openAIConfig(input))
const chat = openAIChat.with(openAIConfig(input)) const chat = openAIChat.with(openAIConfig(input))
return { return {
id: openAIProvider, id: openAIProvider,
responses: responses.model, responses: responses.model,
responsesWebSocket: responsesWebSocket.model,
chat: chat.model, chat: chat.model,
model: responses.model, model: responses.model,
configure: configureOpenAI, configure: configureOpenAI,
@@ -334,19 +342,22 @@ const response =
) )
``` ```
For direct provider-facade calls, Responses has one semantic model and route: For direct provider-facade calls, HTTP versus WebSocket is represented as named
route selectors, not as model or request overrides. Same protocol, different
transport, different route:
```ts ```ts
OpenAI.responses("gpt-4o") OpenAI.responses("gpt-4o")
OpenAI.responsesWebSocket("gpt-4o")
``` ```
The package-like OpenAI Responses entrypoint has the same transport-neutral The package-like OpenAI Responses entrypoint instead keeps transport scoped to
`model(...)` contract: Responses settings while preserving the same `model(...)` contract:
```ts ```ts
import { model } from "@opencode-ai/ai/providers/openai/responses" import { model } from "@opencode-ai/ai/providers/openai/responses"
model("gpt-4o", { apiKey }) model("gpt-4o", { apiKey, transport: "websocket" })
``` ```
Vertex keeps Gemini, Chat, Responses, and Messages as separate package-like entrypoints, Vertex keeps Gemini, Chat, Responses, and Messages as separate package-like entrypoints,
@@ -376,9 +387,11 @@ import { model } from "@opencode-ai/ai/providers/google-vertex/messages"
model("claude-sonnet-4-6", { project, location: "global" }) model("claude-sonnet-4-6", { project, location: "global" })
``` ```
The client does not require a different public layer for WebSocket execution. The client should not require a different public layer just because a selected
Responses routes use HTTP by default, and callers may pass a channel executor per route uses WebSocket. Use one `LLMClient.layer` with HTTP and WebSocket runtime
call. Routes without channel support simply ignore that execution capability. capabilities available; routes that do not need WebSocket simply never touch it.
If a WebSocket route is selected in an environment without WebSocket support,
fail with a typed transport configuration error.
Azure is a route specialization with auth/path/default changes plus input Azure is a route specialization with auth/path/default changes plus input
mapping. The public API configures the Azure resource once, then selects mapping. The public API configures the Azure resource once, then selects
@@ -484,13 +497,18 @@ generic dynamic resolver:
```ts ```ts
const model = const model =
providerID === "azure" ? Azure.configure(resolvedAzureConfig).responses(apiModelID) : OpenAI.responses(apiModelID) providerID === "azure"
? Azure.configure(resolvedAzureConfig).responses(apiModelID)
: endpoint.websocket
? OpenAI.responsesWebSocket(apiModelID)
: OpenAI.responses(apiModelID)
``` ```
That boundary can branch on durable config/catalog metadata and call typed That boundary can branch on durable config/catalog metadata and call typed
provider APIs directly. Transport selection remains execution policy: a Session provider APIs directly. A direct provider-facade boundary maps metadata like
or other caller may pass a WebSocket channel executor per call without changing `endpoint.websocket` to `OpenAI.responsesWebSocket(apiModelID)`. A package-loading
the model constructed by this boundary. boundary passes `transport: "websocket"` to the OpenAI Responses entrypoint.
The client runtime only executes the route carried by the resulting model.
## Competitive Shape ## Competitive Shape
@@ -526,8 +544,9 @@ App boundary = explicit durable-config -> typed-provider call
id. id.
- No `model(id, overrides)` escape hatch. Model selection takes the model id; - No `model(id, overrides)` escape hatch. Model selection takes the model id;
endpoint/auth/deployment customization happens by configuring the route first. endpoint/auth/deployment customization happens by configuring the route first.
- No transport setting on a provider or executable model. OpenAI Responses uses - No transport override on an executable model or request. Direct provider
HTTP by default and accepts an optional per-call channel executor as execution policy. facades use `responses` versus `responsesWebSocket`; the package-like Responses
entrypoint maps its scoped `transport` setting before constructing the model.
- No separate public `LLMClient.layerWithWebSocket`. The runtime should expose one - No separate public `LLMClient.layerWithWebSocket`. The runtime should expose one
client layer with the available transport capabilities. client layer with the available transport capabilities.
- No executable `ModelRef`. The executable handle is `LanguageModel`; durable model - No executable `ModelRef`. The executable handle is `LanguageModel`; durable model
@@ -561,10 +580,12 @@ App boundary = explicit durable-config -> typed-provider call
- [x] Make unconfigured transports reusable constants such as - [x] Make unconfigured transports reusable constants such as
`HttpTransport.sseJson`; keep transport functions only for configured/fresh `HttpTransport.sseJson`; keep transport functions only for configured/fresh
state construction. state construction.
- [x] Collapse the public WebSocket runtime split so one `LLMClient.layer` accepts - [x] Collapse the public WebSocket runtime split so one `LLMClient.layer`
optional per-call channel execution without changing route identity. exposes available transport capabilities and selected routes fail with typed
transport config errors when a required capability is missing.
- [x] Convert OpenAI provider APIs to provider-facade shape: - [x] Convert OpenAI provider APIs to provider-facade shape:
`OpenAI.configure(config).responses(id)` and `.chat(id)`. `OpenAI.configure(config).responses(id)`, `.chat(id)`, and
`.responsesWebSocket(id)`.
- [x] Convert Azure to a configured facade where resource/base URL/api version - [x] Convert Azure to a configured facade where resource/base URL/api version
setup happens before selecting deployment ids. setup happens before selecting deployment ids.
- [x] Split Cloudflare products into separate facades such as - [x] Split Cloudflare products into separate facades such as
@@ -578,8 +599,10 @@ App boundary = explicit durable-config -> typed-provider call
- [ ] Decide whether a tiny `Provider.define(...)` helper is warranted after two - [ ] Decide whether a tiny `Provider.define(...)` helper is warranted after two
or three provider conversions; start with plain objects if duplication is not or three provider conversions; start with plain objects if duplication is not
yet painful. yet painful.
- [x] Keep executable model construction transport-neutral at the Session boundary; - [x] Update `packages/opencode/src/session/llm/native-request.ts` to construct
Session-scoped execution policy supplies channel capability separately. executable models at the session boundary with explicit provider facade
calls, mapping catalog metadata such as `endpoint.websocket` to the correct
named route selector.
- [ ] Update tests so direct route/provider tests assert route values are carried - [ ] Update tests so direct route/provider tests assert route values are carried
by executable models, and opencode/native tests assert boundary-based route by executable models, and opencode/native tests assert boundary-based route
selection. selection.
+4 -3
View File
@@ -1,6 +1,6 @@
import { Config, Effect, Formatter, Layer, Schema, Stream } from "effect" import { Config, Effect, Formatter, Layer, Schema, Stream } from "effect"
import { LLM, LLMClient, LLMRequest, Message, ProviderID, Tool, ToolRuntime } from "@opencode-ai/ai" import { LLM, LLMClient, LLMRequest, Message, ProviderID, Tool, ToolRuntime } from "@opencode-ai/ai"
import { Route, Auth, Endpoint, Framing, Protocol, RequestExecutor } from "@opencode-ai/ai/route" import { Route, Auth, Endpoint, Framing, Protocol, RequestExecutor, WebSocketExecutor } from "@opencode-ai/ai/route"
import { OpenAI } from "@opencode-ai/ai/providers" import { OpenAI } from "@opencode-ai/ai/providers"
/** /**
@@ -213,7 +213,8 @@ const FakeEcho = {
// enabled at a time so the tutorial can demonstrate generate, stream, or // enabled at a time so the tutorial can demonstrate generate, stream, or
// tool-loop behavior without spending tokens on every example. // tool-loop behavior without spending tokens on every example.
const requestExecutorLayer = RequestExecutor.fetchLayer const requestExecutorLayer = RequestExecutor.fetchLayer
const llmClientLayer = LLMClient.layer.pipe(Layer.provide(requestExecutorLayer)) const llmDeps = Layer.mergeAll(requestExecutorLayer, WebSocketExecutor.layer)
const llmClientLayer = LLMClient.layer.pipe(Layer.provide(llmDeps))
const program = Effect.gen(function* () { const program = Effect.gen(function* () {
// yield* generateOnce // yield* generateOnce
@@ -221,6 +222,6 @@ const program = Effect.gen(function* () {
// yield* generateStructuredObject // yield* generateStructuredObject
// yield* generateDynamicObject.pipe(Effect.andThen((response) => Effect.sync(() => console.log(response.object)))) // yield* generateDynamicObject.pipe(Effect.andThen((response) => Effect.sync(() => console.log(response.object))))
yield* streamWithTools yield* streamWithTools
}).pipe(Effect.provide(Layer.mergeAll(requestExecutorLayer, llmClientLayer))) }).pipe(Effect.provide(Layer.mergeAll(llmDeps, llmClientLayer)))
Effect.runPromise(program) Effect.runPromise(program)
+2 -2
View File
@@ -10,8 +10,8 @@
// //
// Manual `cache: CacheHint` placements on individual parts are preserved and // Manual `cache: CacheHint` placements on individual parts are preserved and
// count against the four-breakpoint budget; auto only fills remaining slots. // count against the four-breakpoint budget; auto only fills remaining slots.
import { CacheHint, type CachePolicy, type CachePolicyObject } from "./schema/options.js" import { CacheHint, type CachePolicy, type CachePolicyObject } from "./schema/options"
import { LLMRequest, Message, ToolDefinition, type ContentPart } from "./schema/messages.js" import { LLMRequest, Message, ToolDefinition, type ContentPart } from "./schema/messages"
const AUTO: CachePolicyObject = { const AUTO: CachePolicyObject = {
tools: true, tools: true,
+3 -3
View File
@@ -1,7 +1,7 @@
import { Context, Effect, Layer } from "effect" import { Context, Effect, Layer } from "effect"
import { RequestExecutor } from "./route/executor.js" import { RequestExecutor } from "./route/executor"
import type { ImageOptions, ImageRequest, ImageRequestFor, ImageResponse } from "./image.js" import type { ImageOptions, ImageRequest, ImageRequestFor, ImageResponse } from "./image"
import type { AIError } from "./schema/index.js" import type { AIError } from "./schema"
export type Execute = RequestExecutor.Interface["execute"] export type Execute = RequestExecutor.Interface["execute"]
+2 -10
View File
@@ -1,14 +1,6 @@
import { Effect, Schema } from "effect" import { Effect, Schema } from "effect"
import { import { HttpOptions, InvalidRequestReason, AIError, ModelID, ProviderID, ProviderMetadata, Usage } from "./schema"
HttpOptions, import { ImageClient, Service, type Execute as ImageExecute } from "./image-client"
InvalidRequestReason,
AIError,
ModelID,
ProviderID,
ProviderMetadata,
Usage,
} from "./schema/index.js"
import { ImageClient, Service, type Execute as ImageExecute } from "./image-client.js"
export interface ImageRoute<Options extends ImageOptions = ImageOptions> { export interface ImageRoute<Options extends ImageOptions = ImageOptions> {
readonly id: string readonly id: string
+18 -21
View File
@@ -1,22 +1,22 @@
export { LLMClient } from "./route/client.js" export { LLMClient } from "./route/client"
export { ImageClient } from "./image-client.js" export { ImageClient } from "./image-client"
export { Auth } from "./route/auth.js" export { Auth } from "./route/auth"
export { Provider } from "./provider.js" export { Provider } from "./provider"
export { ProviderPackage } from "./provider-package.js" export { ProviderPackage } from "./provider-package"
export { isContextOverflow, isContextOverflowFailure } from "./provider-error.js" export { isContextOverflow, isContextOverflowFailure } from "./provider-error"
export type { export type {
RouteLanguageModelInput, RouteLanguageModelInput,
RouteRoutedLanguageModelInput, RouteRoutedLanguageModelInput,
Interface as LLMClientShape, Interface as LLMClientShape,
Service as LLMClientService, Service as LLMClientService,
} from "./route/client.js" } from "./route/client"
export * from "./schema/index.js" export * from "./schema"
export { GeneratedImage, ImageInput, ImageInputSchema, ImageModel, ImageRequest, ImageResponse } from "./image.js" export { GeneratedImage, ImageInput, ImageInputSchema, ImageModel, ImageRequest, ImageResponse } from "./image"
export type { ImageModelOptions, ImageOptions, ImageRequestFor, ImageRequestInput, ImageRoute } from "./image.js" export type { ImageModelOptions, ImageOptions, ImageRequestFor, ImageRequestInput, ImageRoute } from "./image"
export { Image } from "./image.js" export { Image } from "./image"
export { Tool, ToolFailure, toDefinitions } from "./tool.js" export { Tool, ToolFailure, toDefinitions } from "./tool"
export { ToolRuntime } from "./tool-runtime.js" export { ToolRuntime } from "./tool-runtime"
export type { DispatchResult as ToolDispatchResult, ToolSettlement } from "./tool-runtime.js" export type { DispatchResult as ToolDispatchResult, ToolSettlement } from "./tool-runtime"
export type { export type {
AnyExecutableTool, AnyExecutableTool,
AnyTool, AnyTool,
@@ -29,14 +29,11 @@ export type {
Tools, Tools,
ToolSchema, ToolSchema,
ToolToModelOutput, ToolToModelOutput,
} from "./tool.js" } from "./tool"
export * as LLM from "./llm.js" export * as LLM from "./llm"
export type { export type {
Definition as ProviderDefinition, Definition as ProviderDefinition,
LanguageModelFactory as ProviderLanguageModelFactory, LanguageModelFactory as ProviderLanguageModelFactory,
LanguageModelOptions as ProviderLanguageModelOptions, LanguageModelOptions as ProviderLanguageModelOptions,
} from "./provider.js" } from "./provider"
export type { export type { Definition as ProviderPackageDefinition, Settings as ProviderPackageSettings } from "./provider-package"
Definition as ProviderPackageDefinition,
Settings as ProviderPackageSettings,
} from "./provider-package.js"
+3 -3
View File
@@ -1,5 +1,5 @@
import { Effect, JsonSchema, Schema } from "effect" import { Effect, JsonSchema, Schema } from "effect"
import { LLMClient, Service } from "./route/client.js" import { LLMClient, Service } from "./route/client"
import { import {
GenerationOptions, GenerationOptions,
HttpOptions, HttpOptions,
@@ -15,8 +15,8 @@ import {
ToolDefinition, ToolDefinition,
type ContentPart, type ContentPart,
type LanguageModelProviderOptions, type LanguageModelProviderOptions,
} from "./schema/index.js" } from "./schema"
import { make as makeTool, toDefinitions, type ToolSchema } from "./tool.js" import { make as makeTool, toDefinitions, type ToolSchema } from "./tool"
/** Input accepted by `LLM.request`, normalized into the canonical `LLMRequest` class. */ /** Input accepted by `LLM.request`, normalized into the canonical `LLMRequest` class. */
export type RequestInput<SelectedLanguageModel extends LanguageModel = LanguageModel> = Omit< export type RequestInput<SelectedLanguageModel extends LanguageModel = LanguageModel> = Omit<
+1 -1
View File
@@ -1 +1 @@
export * from "./protocols/index.js" export * from "./protocols/index"
+14 -17
View File
@@ -1,10 +1,10 @@
import { Effect, Schema } from "effect" import { Effect, Schema } from "effect"
import { Tool } from "@opencode-ai/schema/tool" import { Tool } from "@opencode-ai/schema/tool"
import { Route } from "../route/client.js" import { Route } from "../route/client"
import { Auth } from "../route/auth.js" import { Auth } from "../route/auth"
import { Endpoint } from "../route/endpoint.js" import { Endpoint } from "../route/endpoint"
import { Framing } from "../route/framing.js" import { Framing } from "../route/framing"
import { Protocol } from "../route/protocol.js" import { Protocol } from "../route/protocol"
import { import {
AIError, AIError,
LLMEvent, LLMEvent,
@@ -21,13 +21,13 @@ import {
type ToolCallPart, type ToolCallPart,
type ToolDefinition, type ToolDefinition,
type ToolResultPart, type ToolResultPart,
} from "../schema/index.js" } from "../schema"
import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js" import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared"
import { classifyProviderFailure } from "../provider-error.js" import { classifyProviderFailure } from "../provider-error"
import * as Cache from "./utils/cache.js" import * as Cache from "./utils/cache"
import { Lifecycle } from "./utils/lifecycle.js" import { Lifecycle } from "./utils/lifecycle"
import { ToolSchemaProjection } from "./utils/tool-schema.js" import { ToolSchemaProjection } from "./utils/tool-schema"
import { ToolStream } from "./utils/tool-stream.js" import { ToolStream } from "./utils/tool-stream"
const ADAPTER = "anthropic-messages" const ADAPTER = "anthropic-messages"
const MEDIA_MIMES = new Set<string>([...ProviderShared.IMAGE_MIMES, ...ProviderShared.PDF_MIMES]) const MEDIA_MIMES = new Set<string>([...ProviderShared.IMAGE_MIMES, ...ProviderShared.PDF_MIMES])
@@ -573,10 +573,7 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (
cache_control: cacheControl(breakpoints, part.cache), cache_control: cacheControl(breakpoints, part.cache),
}) })
} }
const previous = messages.at(-1) messages.push({ role: "user", content })
if (previous?.role === "user" && previous.content.every((block) => block.type === "tool_result"))
messages[messages.length - 1] = { role: "user", content: [...previous.content, ...content] }
else messages.push({ role: "user", content })
} }
return messages return messages
@@ -1032,4 +1029,4 @@ export const route = Route.make({
headers: () => ({ "anthropic-version": "2023-06-01" }), headers: () => ({ "anthropic-version": "2023-06-01" }),
}) })
export * as AnthropicMessages from "./anthropic-messages.js" export * as AnthropicMessages from "./anthropic-messages"
+17 -23
View File
@@ -1,7 +1,7 @@
import { Effect, Schema } from "effect" import { Effect, Schema } from "effect"
import { Route } from "../route/client.js" import { Route } from "../route/client"
import { Endpoint } from "../route/endpoint.js" import { Endpoint } from "../route/endpoint"
import { Protocol } from "../route/protocol.js" import { Protocol } from "../route/protocol"
import { import {
AIError, AIError,
LLMEvent, LLMEvent,
@@ -17,20 +17,20 @@ import {
type ToolCallPart, type ToolCallPart,
type ToolDefinition, type ToolDefinition,
type ToolResultPart, type ToolResultPart,
} from "../schema/index.js" } from "../schema"
import { BedrockEventStream } from "./bedrock-event-stream.js" import { BedrockEventStream } from "./bedrock-event-stream"
import { classifyProviderFailure } from "../provider-error.js" import { classifyProviderFailure } from "../provider-error"
import { JsonObject, optionalArray, ProviderShared } from "./shared.js" import { JsonObject, optionalArray, ProviderShared } from "./shared"
import { BedrockAuth } from "./utils/bedrock-auth.js" import { BedrockAuth } from "./utils/bedrock-auth"
import { BedrockCache } from "./utils/bedrock-cache.js" import { BedrockCache } from "./utils/bedrock-cache"
import { BedrockMedia } from "./utils/bedrock-media.js" import { BedrockMedia } from "./utils/bedrock-media"
import { Lifecycle } from "./utils/lifecycle.js" import { Lifecycle } from "./utils/lifecycle"
import { ToolSchemaProjection } from "./utils/tool-schema.js" import { ToolSchemaProjection } from "./utils/tool-schema"
import { ToolStream } from "./utils/tool-stream.js" import { ToolStream } from "./utils/tool-stream"
const ADAPTER = "bedrock-converse" const ADAPTER = "bedrock-converse"
export type { Credentials as BedrockCredentials } from "./utils/bedrock-auth.js" export type { Credentials as BedrockCredentials } from "./utils/bedrock-auth"
// ============================================================================= // =============================================================================
// Request Body Schema // Request Body Schema
@@ -348,10 +348,7 @@ const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* (
continue continue
} }
} }
const previous = messages.at(-1) messages.push({ role: "user", content })
if (previous?.role === "user")
messages[messages.length - 1] = { role: "user", content: [...previous.content, ...content] }
else messages.push({ role: "user", content })
continue continue
} }
@@ -395,10 +392,7 @@ const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* (
const cachePoint = BedrockCache.block(breakpoints, part.cache) const cachePoint = BedrockCache.block(breakpoints, part.cache)
if (cachePoint) content.push(cachePoint) if (cachePoint) content.push(cachePoint)
} }
const previous = messages.at(-1) messages.push({ role: "user", content })
if (previous?.role === "user")
messages[messages.length - 1] = { role: "user", content: [...previous.content, ...content] }
else messages.push({ role: "user", content })
} }
return messages return messages
@@ -737,4 +731,4 @@ export const route = Route.make({
export const sigV4Auth = BedrockAuth.sigV4 export const sigV4Auth = BedrockAuth.sigV4
export * as BedrockConverse from "./bedrock-converse.js" export * as BedrockConverse from "./bedrock-converse"
@@ -1,8 +1,8 @@
import { EventStreamCodec } from "@smithy/eventstream-codec" import { EventStreamCodec } from "@smithy/eventstream-codec"
import { fromUtf8, toUtf8 } from "@smithy/util-utf8" import { fromUtf8, toUtf8 } from "@smithy/util-utf8"
import { Effect, Stream } from "effect" import { Effect, Stream } from "effect"
import { Framing } from "../route/framing.js" import { Framing } from "../route/framing"
import { ProviderShared } from "./shared.js" import { ProviderShared } from "./shared"
// Bedrock streams responses using the AWS event stream binary protocol — each // Bedrock streams responses using the AWS event stream binary protocol — each
// frame is `[length:4][headers-length:4][prelude-crc:4][headers][payload][crc:4]`. // frame is `[length:4][headers-length:4][prelude-crc:4][headers][payload][crc:4]`.
@@ -98,4 +98,4 @@ export const framing = (route: string): Framing.Definition<object> => ({
frame: (bytes) => bytes.pipe(Stream.mapAccumEffect(() => initialFrameBuffer, consumeFrames(route))), frame: (bytes) => bytes.pipe(Stream.mapAccumEffect(() => initialFrameBuffer, consumeFrames(route))),
}) })
export * as BedrockEventStream from "./bedrock-event-stream.js" export * as BedrockEventStream from "./bedrock-event-stream"
+11 -11
View File
@@ -1,10 +1,10 @@
import { Effect, Schema } from "effect" import { Effect, Schema } from "effect"
import { Tool } from "@opencode-ai/schema/tool" import { Tool } from "@opencode-ai/schema/tool"
import { Route } from "../route/client.js" import { Route } from "../route/client"
import { Auth } from "../route/auth.js" import { Auth } from "../route/auth"
import { Endpoint } from "../route/endpoint.js" import { Endpoint } from "../route/endpoint"
import { Framing } from "../route/framing.js" import { Framing } from "../route/framing"
import { Protocol } from "../route/protocol.js" import { Protocol } from "../route/protocol"
import { import {
LLMEvent, LLMEvent,
Usage, Usage,
@@ -17,11 +17,11 @@ import {
type TextPart, type TextPart,
type ToolCallPart, type ToolCallPart,
type ToolDefinition, type ToolDefinition,
} from "../schema/index.js" } from "../schema"
import { JsonObject, optionalArray, ProviderShared } from "./shared.js" import { JsonObject, optionalArray, ProviderShared } from "./shared"
import { GeminiToolSchema } from "./utils/gemini-tool-schema.js" import { GeminiToolSchema } from "./utils/gemini-tool-schema"
import { Lifecycle } from "./utils/lifecycle.js" import { Lifecycle } from "./utils/lifecycle"
import { ToolSchemaProjection } from "./utils/tool-schema.js" import { ToolSchemaProjection } from "./utils/tool-schema"
const ADAPTER = "gemini" const ADAPTER = "gemini"
const MEDIA_MIMES = new Set<string>(ProviderShared.MEDIA_MIMES) const MEDIA_MIMES = new Set<string>(ProviderShared.MEDIA_MIMES)
@@ -643,4 +643,4 @@ export const route = Route.make({
framing: Framing.sse, framing: Framing.sse,
}) })
export * as Gemini from "./gemini.js" export * as Gemini from "./gemini"
+5 -5
View File
@@ -7,8 +7,8 @@ import {
type ImageInput, type ImageInput,
type ImageRequestFor, type ImageRequestFor,
type ImageRoute, type ImageRoute,
} from "../image.js" } from "../image"
import { Auth, type Definition as AuthDefinition } from "../route/auth.js" import { Auth, type Definition as AuthDefinition } from "../route/auth"
import { import {
InvalidProviderOutputReason, InvalidProviderOutputReason,
AIError, AIError,
@@ -17,9 +17,9 @@ import {
mergeJsonRecords, mergeJsonRecords,
type HttpOptions, type HttpOptions,
type ProviderMetadata, type ProviderMetadata,
} from "../schema/index.js" } from "../schema"
import { ProviderShared } from "./shared.js" import { ProviderShared } from "./shared"
import { ImageInputs } from "./utils/image-input.js" import { ImageInputs } from "./utils/image-input"
const ADAPTER = "google-images" const ADAPTER = "google-images"
export const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta" export const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta"
+9 -10
View File
@@ -1,10 +1,9 @@
export * as AnthropicMessages from "./anthropic-messages.js" export * as AnthropicMessages from "./anthropic-messages"
export * as BedrockConverse from "./bedrock-converse.js" export * as BedrockConverse from "./bedrock-converse"
export * as Gemini from "./gemini.js" export * as Gemini from "./gemini"
export * as OpenAIChat from "./openai-chat.js" export * as OpenAIChat from "./openai-chat"
export * as OpenAIImages from "./openai-images.js" export * as OpenAIImages from "./openai-images"
export * as OpenAICompatibleChat from "./openai-compatible-chat.js" export * as OpenAICompatibleChat from "./openai-compatible-chat"
export * as OpenAICompatibleResponses from "./openai-compatible-responses.js" export * as OpenAICompatibleResponses from "./openai-compatible-responses"
export * as OpenAIResponses from "./openai-responses.js" export * as OpenAIResponses from "./openai-responses"
export * as OpenResponses from "./open-responses.js" export * as OpenResponses from "./open-responses"
export * as OpenResponsesChannel from "./open-responses-channel.js"
@@ -1,191 +0,0 @@
import { Effect, Schema, Stream } from "effect"
import { Headers } from "effect/unstable/http"
import { Framing } from "../route/framing.js"
import {
HttpTransport,
WebSocketTransport,
type Transport,
type WebSocketChannelDriver,
type WebSocketChannelExchange,
} from "../route/transport/index.js"
import * as ProviderShared from "./shared.js"
import { OpenResponses } from "./open-responses.js"
const WebSocketResponseCreate = Schema.StructWithRest(Schema.Struct({ type: Schema.tag("response.create") }), [
Schema.Record(Schema.String, Schema.Unknown),
])
const decodeMessage = ProviderShared.validateWith(Schema.decodeUnknownEffect(WebSocketResponseCreate))
const encodeMessage = Schema.encodeSync(Schema.fromJsonString(WebSocketResponseCreate))
const decodeEvent = Schema.decodeUnknownEffect(OpenResponses.protocol.stream.event)
export interface Options {
readonly id: string
readonly name: string
readonly rotateAfterMs?: number
readonly headers?: (headers: Headers.Headers) => Headers.Headers
readonly driver?: (input: {
readonly request: Readonly<Record<string, unknown>>
readonly message: string
readonly base: WebSocketChannelDriver
}) => WebSocketChannelDriver
}
export interface Prepared {
readonly http: HttpTransport.HttpPrepared<string>
readonly channel?: {
readonly url: string
readonly headers: Headers.Headers
readonly rotateAfterMs?: number
readonly driver: WebSocketChannelDriver
}
}
const message = (body: unknown) =>
Effect.gen(function* () {
if (!ProviderShared.isRecord(body))
return yield* ProviderShared.invalidRequest("Open Responses WebSocket body must be a JSON object")
const { stream: _stream, stream_options: _streamOptions, background: _background, ...request } = body
const decoded = yield* decodeMessage({ ...request, type: "response.create" })
return { request: decoded, message: encodeMessage(decoded) }
})
const driver = (options: Options, body: string): WebSocketChannelDriver => {
let responseID: string | undefined
let terminal = false
return {
create: () =>
Effect.sync(() => {
responseID = undefined
terminal = false
return { message: body, mode: "full" }
}),
observe: (_create, frame) =>
Effect.gen(function* () {
const event = yield* decodeEvent(frame).pipe(
Effect.mapError(() =>
ProviderShared.eventError(options.id, `Invalid ${options.name} WebSocket event`, frame),
),
)
if (terminal)
return yield* ProviderShared.eventError(
options.id,
`${options.name} emitted ${event.type} after a terminal event`,
frame,
)
if (event.type === "error") {
terminal = true
yield* OpenResponses.decodeKnownErrorEvent(event).pipe(
Effect.mapError(() =>
ProviderShared.eventError(options.id, `${options.name} returned a malformed error event`, frame),
),
)
return {
type: "provider-failure",
error: OpenResponses.providerFailure(options.id, event, `${options.name} stream error`),
}
}
if (event.type === "response.failed") {
terminal = true
if (responseID && event.response?.id && event.response.id !== responseID)
return yield* ProviderShared.eventError(
options.id,
`${options.name} response ID changed during execution`,
frame,
)
return {
type: "provider-failure",
error: OpenResponses.providerFailure(options.id, event, `${options.name} response failed`),
}
}
if (event.type === "response.created") {
const created = event.response?.id
if (responseID)
return yield* ProviderShared.eventError(
options.id,
`${options.name} emitted duplicate response.created`,
frame,
)
if (!created)
return yield* ProviderShared.eventError(
options.id,
`${options.name} response.created is missing response.id`,
frame,
)
responseID = created
return { type: "frame", frame }
}
if (!responseID)
return yield* ProviderShared.eventError(
options.id,
`${options.name} emitted ${event.type} before response.created`,
frame,
)
if (event.response?.id && event.response.id !== responseID)
return yield* ProviderShared.eventError(
options.id,
`${options.name} response ID changed during execution`,
frame,
)
if (event.type === "response.completed") {
terminal = true
return { type: "completed", frame }
}
if (event.type === "response.incomplete") {
terminal = true
return { type: "incomplete", frame }
}
return { type: "frame", frame }
}),
}
}
export const transport = <Body>(options: Options): Transport<Body, Prepared, string> => {
const http = HttpTransport.sseJson.with<Body>()
return {
id: http.id,
prepare: (input) =>
Effect.gen(function* () {
const parts = yield* HttpTransport.jsonRequestParts(input)
const headers = Headers.remove(options.headers?.(parts.headers) ?? parts.headers, "content-length")
const channel = input.webSocket
? yield* Effect.gen(function* () {
const create = yield* message(parts.jsonBody)
const base = driver(options, create.message)
return {
url: yield* WebSocketTransport.toWebSocketUrl(parts.url),
headers,
rotateAfterMs: options.rotateAfterMs,
driver: options.driver?.({ request: create.request, message: create.message, base }) ?? base,
}
})
: undefined
return {
http: {
request: ProviderShared.jsonPost({ url: parts.url, body: parts.bodyText, headers: parts.headers }),
framing: Framing.sse,
middleware: input.middleware,
},
channel,
}
}),
execute: (prepared, request, runtime, executeOptions) => {
if (!executeOptions?.webSocket || !prepared.channel) return http.execute(prepared.http, request, runtime)
const exchange: WebSocketChannelExchange = {
id: request.id ?? "request",
connect: {
url: prepared.channel.url,
headers: prepared.channel.headers,
rotateAfterMs: prepared.channel.rotateAfterMs,
},
fallback: () =>
Stream.unwrap(
http.execute(prepared.http, request, runtime).pipe(Effect.map((execution) => execution.frames)),
),
driver: prepared.channel.driver,
}
return executeOptions.webSocket.execute(exchange)
},
}
}
export const OpenResponsesChannel = { transport } as const
+17 -64
View File
@@ -1,7 +1,7 @@
import { Effect, Schema } from "effect" import { Effect, Schema } from "effect"
import type { Content } from "@opencode-ai/schema/tool" import type { Content } from "@opencode-ai/schema/tool"
import { HttpTransport } from "../route/transport/index.js" import { HttpTransport } from "../route/transport"
import { Protocol } from "../route/protocol.js" import { Protocol } from "../route/protocol"
import { import {
AIError, AIError,
LLMEvent, LLMEvent,
@@ -16,13 +16,13 @@ import {
type ToolCallPart, type ToolCallPart,
type ToolDefinition, type ToolDefinition,
type ToolResultPart, type ToolResultPart,
} from "../schema/index.js" } from "../schema"
import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js" import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared"
import { classifyProviderFailure } from "../provider-error.js" import { classifyProviderFailure } from "../provider-error"
import { OpenResponsesOptions } from "./utils/open-responses-options.js" import { OpenResponsesOptions } from "./utils/open-responses-options"
import { Lifecycle } from "./utils/lifecycle.js" import { Lifecycle } from "./utils/lifecycle"
import { ToolSchemaProjection } from "./utils/tool-schema.js" import { ToolSchemaProjection } from "./utils/tool-schema"
import { ToolStream } from "./utils/tool-stream.js" import { ToolStream } from "./utils/tool-stream"
const ADAPTER = "open-responses" const ADAPTER = "open-responses"
const NAME = "Open Responses" const NAME = "Open Responses"
@@ -211,43 +211,11 @@ export type StreamItem = Schema.Schema.Type<typeof StreamItem>
// event-level `error` envelope, so accept all three shapes here. // event-level `error` envelope, so accept all three shapes here.
// https://www.openresponses.org/specification // https://www.openresponses.org/specification
const OpenResponsesErrorPayload = Schema.Struct({ const OpenResponsesErrorPayload = Schema.Struct({
type: optionalNull(Schema.String),
code: optionalNull(Schema.String), code: optionalNull(Schema.String),
message: optionalNull(Schema.String), message: optionalNull(Schema.String),
param: optionalNull(Schema.String), param: optionalNull(Schema.String),
}) })
const WebSocketErrorHeader = Schema.Union([Schema.String, Schema.Number, Schema.Boolean])
export const WebSocketErrorEvent = Schema.StructWithRest(
Schema.Struct({
type: Schema.tag("error"),
status: Schema.optional(Schema.Number),
status_code: Schema.optional(Schema.Number),
code: optionalNull(Schema.String),
message: Schema.optional(Schema.String),
param: optionalNull(Schema.String),
error: optionalNull(OpenResponsesErrorPayload),
headers: Schema.optional(Schema.Record(Schema.String, WebSocketErrorHeader)),
}),
[Schema.Record(Schema.String, Schema.Unknown)],
)
const decodeWebSocketErrorEvent = Schema.decodeUnknownEffect(WebSocketErrorEvent)
export const decodeKnownErrorEvent = (event: Event) =>
decodeWebSocketErrorEvent({
...event,
status: typeof event.status === "number" ? event.status : undefined,
status_code: typeof event.status_code === "number" ? event.status_code : undefined,
headers: ProviderShared.isRecord(event.headers)
? Object.fromEntries(
Object.entries(event.headers).filter(
(entry): entry is [string, string | number | boolean] =>
typeof entry[1] === "string" || typeof entry[1] === "number" || typeof entry[1] === "boolean",
),
)
: undefined,
})
export const Event = Schema.StructWithRest( export const Event = Schema.StructWithRest(
Schema.Struct({ Schema.Struct({
type: Schema.String, type: Schema.String,
@@ -272,9 +240,6 @@ export const Event = Schema.StructWithRest(
message: Schema.optional(Schema.String), message: Schema.optional(Schema.String),
param: optionalNull(Schema.String), param: optionalNull(Schema.String),
error: optionalNull(OpenResponsesErrorPayload), error: optionalNull(OpenResponsesErrorPayload),
status: Schema.optional(Schema.Unknown),
status_code: Schema.optional(Schema.Unknown),
headers: Schema.optional(Schema.Unknown),
}), }),
[Schema.Record(Schema.String, Schema.Unknown)], [Schema.Record(Schema.String, Schema.Unknown)],
) )
@@ -667,9 +632,9 @@ export type StepResult = readonly [ParserState, ReadonlyArray<LLMEvent>]
const NO_EVENTS: StepResult["1"] = [] const NO_EVENTS: StepResult["1"] = []
// `response.completed` / `response.incomplete` are clean finishes that emit a // `response.completed` / `response.incomplete` are clean finishes that emit a
// `finish` event; `response.failed` and `error` are hard failures. All four end // `finish` event; `response.failed` is a hard failure. All three end the stream,
// the stream, so keep this set aligned with `step` and the protocol's terminal predicate. // so keep this set aligned with `step` and the protocol's terminal predicate.
const TERMINAL_TYPES = new Set(["error", "response.completed", "response.incomplete", "response.failed"]) const TERMINAL_TYPES = new Set(["response.completed", "response.incomplete", "response.failed"])
export const terminal = (event: Event) => TERMINAL_TYPES.has(event.type) export const terminal = (event: Event) => TERMINAL_TYPES.has(event.type)
const onOutputTextDelta = (state: ParserState, event: Event, id: string): StepResult => { const onOutputTextDelta = (state: ParserState, event: Event, id: string): StepResult => {
@@ -1001,24 +966,16 @@ const providerErrorMessage = (event: Event, fallback: string): string => {
return message || code || fallback return message || code || fallback
} }
export const providerFailure = (id: string, event: Event, fallback: string) => { const providerError = (state: ParserState, event: Event, fallback: string) => {
const code = event.code || event.error?.code || event.response?.error?.code || undefined const code = event.code || event.error?.code || event.response?.error?.code || undefined
const message = providerErrorMessage(event, fallback) const message = providerErrorMessage(event, fallback)
const status =
typeof event.status === "number"
? event.status
: typeof event.status_code === "number"
? event.status_code
: undefined
return new AIError({ return new AIError({
module: id, module: state.id,
method: "stream", method: "stream",
reason: classifyProviderFailure({ message, code, status }), reason: classifyProviderFailure({ message, code }),
}) })
} }
const providerError = (state: ParserState, event: Event, fallback: string) => providerFailure(state.id, event, fallback)
export const step = (state: ParserState, event: Event) => { export const step = (state: ParserState, event: Event) => {
if (event.type === "response.output_text.delta" || event.type === "response.output_text.done") { if (event.type === "response.output_text.delta" || event.type === "response.output_text.done") {
if (!event.item_id) return ProviderShared.eventError(state.id, `${event.type} is missing item_id`) if (!event.item_id) return ProviderShared.eventError(state.id, `${event.type} is missing item_id`)
@@ -1058,11 +1015,7 @@ export const step = (state: ParserState, event: Event) => {
if (event.type === "response.completed" || event.type === "response.incomplete") if (event.type === "response.completed" || event.type === "response.incomplete")
return Effect.succeed(onResponseFinish(state, event)) return Effect.succeed(onResponseFinish(state, event))
if (event.type === "response.failed") return providerError(state, event, `${state.name} response failed`) if (event.type === "response.failed") return providerError(state, event, `${state.name} response failed`)
if (event.type === "error") if (event.type === "error") return providerError(state, event, `${state.name} stream error`)
return decodeKnownErrorEvent(event).pipe(
Effect.mapError(() => ProviderShared.eventError(state.id, `${state.name} returned a malformed error event`)),
Effect.flatMap(() => providerError(state, event, `${state.name} stream error`)),
)
return Effect.succeed<StepResult>([state, NO_EVENTS]) return Effect.succeed<StepResult>([state, NO_EVENTS])
} }
@@ -1108,4 +1061,4 @@ export const protocol = Protocol.make({
export const httpTransport = HttpTransport.sseJson.with<OpenResponsesBody>() export const httpTransport = HttpTransport.sseJson.with<OpenResponsesBody>()
export * as OpenResponses from "./open-responses.js" export * as OpenResponses from "./open-responses"
+13 -13
View File
@@ -1,10 +1,10 @@
import { Effect, Schema } from "effect" import { Effect, Schema } from "effect"
import { Tool } from "@opencode-ai/schema/tool" import { Tool } from "@opencode-ai/schema/tool"
import { Route } from "../route/client.js" import { Route } from "../route/client"
import { Auth } from "../route/auth.js" import { Auth } from "../route/auth"
import { Endpoint } from "../route/endpoint.js" import { Endpoint } from "../route/endpoint"
import { HttpTransport } from "../route/transport/index.js" import { HttpTransport } from "../route/transport"
import { Protocol } from "../route/protocol.js" import { Protocol } from "../route/protocol"
import { import {
AIError, AIError,
LLMEvent, LLMEvent,
@@ -19,13 +19,13 @@ import {
type TextPart, type TextPart,
type ToolCallPart, type ToolCallPart,
type ToolDefinition, type ToolDefinition,
} from "../schema/index.js" } from "../schema"
import { classifyProviderFailure } from "../provider-error.js" import { classifyProviderFailure } from "../provider-error"
import { isRecord, JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js" import { isRecord, JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared"
import { OpenAIOptions } from "./utils/openai-options.js" import { OpenAIOptions } from "./utils/openai-options"
import { Lifecycle } from "./utils/lifecycle.js" import { Lifecycle } from "./utils/lifecycle"
import { ToolSchemaProjection } from "./utils/tool-schema.js" import { ToolSchemaProjection } from "./utils/tool-schema"
import { ToolStream } from "./utils/tool-stream.js" import { ToolStream } from "./utils/tool-stream"
const ADAPTER = "openai-chat" const ADAPTER = "openai-chat"
const IMAGE_MIMES = new Set<string>(ProviderShared.IMAGE_MIMES) const IMAGE_MIMES = new Set<string>(ProviderShared.IMAGE_MIMES)
@@ -884,4 +884,4 @@ export const route = Route.make({
transport: httpTransport, transport: httpTransport,
}) })
export * as OpenAIChat from "./openai-chat.js" export * as OpenAIChat from "./openai-chat"
@@ -1,7 +1,7 @@
import { Route, type RouteRoutedLanguageModelInput } from "../route/client.js" import { Route, type RouteRoutedLanguageModelInput } from "../route/client"
import { Endpoint } from "../route/endpoint.js" import { Endpoint } from "../route/endpoint"
import { Framing } from "../route/framing.js" import { Framing } from "../route/framing"
import * as OpenAIChat from "./openai-chat.js" import * as OpenAIChat from "./openai-chat"
const ADAPTER = "openai-compatible-chat" const ADAPTER = "openai-compatible-chat"
@@ -22,4 +22,4 @@ export const route = Route.make({
framing: Framing.sse, framing: Framing.sse,
}) })
export * as OpenAICompatibleChat from "./openai-compatible-chat.js" export * as OpenAICompatibleChat from "./openai-compatible-chat"
@@ -1,6 +1,6 @@
import { Route, type RouteRoutedLanguageModelInput } from "../route/client.js" import { Route, type RouteRoutedLanguageModelInput } from "../route/client"
import { Endpoint } from "../route/endpoint.js" import { Endpoint } from "../route/endpoint"
import { OpenResponses } from "./open-responses.js" import { OpenResponses } from "./open-responses"
const ADAPTER = "openai-compatible-responses" const ADAPTER = "openai-compatible-responses"
@@ -19,4 +19,4 @@ export const route = Route.make({
transport: OpenResponses.httpTransport, transport: OpenResponses.httpTransport,
}) })
export * as OpenAICompatibleResponses from "./openai-compatible-responses.js" export * as OpenAICompatibleResponses from "./openai-compatible-responses"
+6 -6
View File
@@ -7,8 +7,8 @@ import {
type ImageInput, type ImageInput,
type ImageRequestFor, type ImageRequestFor,
type ImageRoute, type ImageRoute,
} from "../image.js" } from "../image"
import { Auth, type Definition as AuthDefinition } from "../route/auth.js" import { Auth, type Definition as AuthDefinition } from "../route/auth"
import { import {
InvalidProviderOutputReason, InvalidProviderOutputReason,
AIError, AIError,
@@ -16,10 +16,10 @@ import {
mergeHttpOptions, mergeHttpOptions,
mergeJsonRecords, mergeJsonRecords,
type HttpOptions, type HttpOptions,
} from "../schema/index.js" } from "../schema"
import { ProviderShared } from "./shared.js" import { ProviderShared } from "./shared"
import { ImageInputs } from "./utils/image-input.js" import { ImageInputs } from "./utils/image-input"
import { OpenAIImage } from "./utils/openai-image.js" import { OpenAIImage } from "./utils/openai-image"
const ADAPTER = "openai-images" const ADAPTER = "openai-images"
export const DEFAULT_BASE_URL = "https://api.openai.com/v1" export const DEFAULT_BASE_URL = "https://api.openai.com/v1"
@@ -1,164 +0,0 @@
import { AIError, TransportReason } from "../schema/index.js"
import type { ChannelCheckpoint, ChannelObservation, WebSocketChannelDriver } from "../route/transport/index.js"
import { Effect, Option, Schema } from "effect"
import * as ProviderShared from "./shared.js"
import { OpenResponses } from "./open-responses.js"
const PROTOCOL = "openai-responses.websocket.v1"
const VERSION = 1
const decodeEvent = Schema.decodeUnknownEffect(OpenResponses.protocol.stream.event)
interface CheckpointValue {
readonly version: typeof VERSION
readonly responseID: string
readonly request: Readonly<Record<string, unknown>>
readonly output: ReadonlyArray<unknown>
}
export interface DriverInput {
readonly id: string
readonly name: string
readonly request: Readonly<Record<string, unknown>>
readonly message: string
readonly base: WebSocketChannelDriver
}
const checkpointValue = (checkpoint: ChannelCheckpoint | undefined): CheckpointValue | undefined => {
if (checkpoint?.protocol !== PROTOCOL || !ProviderShared.isRecord(checkpoint.value)) return undefined
if (checkpoint.value.version !== VERSION) return undefined
if (typeof checkpoint.value.responseID !== "string" || checkpoint.value.responseID.trim().length === 0)
return undefined
if (!ProviderShared.isRecord(checkpoint.value.request) || !Array.isArray(checkpoint.value.output)) return undefined
return {
version: VERSION,
responseID: checkpoint.value.responseID,
request: checkpoint.value.request,
output: checkpoint.value.output,
}
}
const canonical = (value: unknown): string => {
if (value === undefined) return "undefined"
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`
if (!ProviderShared.isRecord(value)) return ProviderShared.encodeJson(value)
return `{${Object.keys(value)
.sort()
.map((key) => `${ProviderShared.encodeJson(key)}:${canonical(value[key])}`)
.join(",")}}`
}
const json = (value: unknown) => {
if (typeof value !== "string") return value
return Option.getOrElse(Schema.decodeUnknownOption(ProviderShared.Json)(value), () => value)
}
const comparable = (value: unknown) => {
if (!ProviderShared.isRecord(value)) return value
if (value.type === "message" && value.role === "assistant")
return {
role: "assistant",
content: value.content,
...(value.phase === undefined ? {} : { phase: value.phase }),
}
if (value.type === "function_call")
return {
type: value.type,
call_id: value.call_id,
name: value.name,
arguments: json(value.arguments),
}
if (value.type === "reasoning")
return {
type: value.type,
summary: value.summary,
encrypted_content: value.encrypted_content,
}
return value
}
const invariant = (request: Readonly<Record<string, unknown>>) => {
const { type: _type, input: _input, previous_response_id: _previousResponseID, ...rest } = request
return rest
}
const incremental = (
request: Readonly<Record<string, unknown>>,
checkpoint: CheckpointValue,
): ReadonlyArray<unknown> | undefined => {
const input = request.input
const previousInput = checkpoint.request.input
if (!Array.isArray(input) || !Array.isArray(previousInput)) return undefined
if (canonical(invariant(request)) !== canonical(invariant(checkpoint.request))) return undefined
const baseline = [...previousInput, ...checkpoint.output]
if (input.length <= baseline.length) return undefined
if (!baseline.every((item, index) => canonical(comparable(item)) === canonical(comparable(input[index]))))
return undefined
return input.slice(baseline.length)
}
const code = (event: OpenResponses.Event) => event.code || event.error?.code || event.response?.error?.code || undefined
const rejected = (
input: DriverInput,
observation: Extract<ChannelObservation, { readonly type: "provider-failure" }>,
recovery: "retry-full" | "rotate-and-retry-full",
): ChannelObservation => ({
type: "rejected",
recovery,
error: new AIError({
module: input.id,
method: "stream",
reason: new TransportReason({
message: observation.error.message,
transport: "websocket",
operation: "read",
phase: "receive",
delivery: "rejected",
recovery,
}),
}),
})
export const driver = (input: DriverInput): WebSocketChannelDriver => {
const { previous_response_id: _previousResponseID, ...request } = input.request
let output: unknown[] = []
return {
create: (checkpoint) =>
Effect.sync(() => {
output = []
const previous = checkpointValue(checkpoint)
const delta = previous ? incremental(request, previous) : undefined
if (!previous || !delta) return { message: ProviderShared.encodeJson(request), mode: "full" as const }
return {
message: ProviderShared.encodeJson({ ...request, input: delta, previous_response_id: previous.responseID }),
mode: "incremental" as const,
}
}),
observe: (create, frame) =>
Effect.gen(function* () {
const event = yield* decodeEvent(frame).pipe(
Effect.mapError(() => ProviderShared.eventError(input.id, `Invalid ${input.name} WebSocket event`, frame)),
)
const observation = yield* input.base.observe(create, frame)
if (event.type === "response.output_item.done" && event.item) output.push(event.item)
if (observation.type === "provider-failure") {
const rejection = code(event)
if (rejection === "previous_response_not_found") return rejected(input, observation, "retry-full")
if (rejection === "websocket_connection_limit_reached")
return rejected(input, observation, "rotate-and-retry-full")
}
if (observation.type !== "completed") return observation
const responseID = event.response?.id
if (!responseID || responseID.trim().length === 0) return observation
return {
...observation,
checkpoint: {
protocol: PROTOCOL,
value: { version: VERSION, responseID, request, output: output.slice() } satisfies CheckpointValue,
},
}
}),
}
}
export const OpenAIResponsesChannel = { driver } as const
+52 -25
View File
@@ -1,23 +1,18 @@
import { Effect, Encoding, Schema } from "effect" import { Effect, Encoding, Schema } from "effect"
import { Headers } from "effect/unstable/http" import { Route } from "../route/client"
import { Route } from "../route/client.js" import { Auth } from "../route/auth"
import { Auth } from "../route/auth.js" import { Endpoint } from "../route/endpoint"
import { Endpoint } from "../route/endpoint.js" import { Protocol } from "../route/protocol"
import { Protocol } from "../route/protocol.js" import { HttpTransport, WebSocketTransport } from "../route/transport"
import { HttpTransport } from "../route/transport/index.js" import { LLMEvent, LLMRequest, type JsonSchema, type ToolDefinition } from "../schema"
import { LLMEvent, LLMRequest, type JsonSchema, type ToolDefinition } from "../schema/index.js" import { OpenResponses } from "./open-responses"
import { OpenResponses } from "./open-responses.js" import { optionalArray, ProviderShared } from "./shared"
import { optionalArray, ProviderShared } from "./shared.js" import { Lifecycle } from "./utils/lifecycle"
import { Lifecycle } from "./utils/lifecycle.js" import { OpenAIImage } from "./utils/openai-image"
import { OpenAIImage } from "./utils/openai-image.js" import { ToolSchemaProjection } from "./utils/tool-schema"
import { ToolSchemaProjection } from "./utils/tool-schema.js"
import { OpenResponsesChannel } from "./open-responses-channel.js"
import { OpenAIResponsesChannel } from "./openai-responses-channel.js"
const ADAPTER = "openai-responses" const ADAPTER = "openai-responses"
const NAME = "OpenAI Responses" const NAME = "OpenAI Responses"
const WEBSOCKET_PROTOCOL_HEADER = "responses_websockets=2026-02-06"
const WEBSOCKET_ROTATE_AFTER_MS = 55 * 60 * 1000
export const DEFAULT_BASE_URL = "https://api.openai.com/v1" export const DEFAULT_BASE_URL = "https://api.openai.com/v1"
export const PATH = OpenResponses.PATH export const PATH = OpenResponses.PATH
@@ -62,6 +57,16 @@ const OpenAIResponsesBody = Schema.Struct({
}) })
export type OpenAIResponsesBody = Schema.Schema.Type<typeof OpenAIResponsesBody> export type OpenAIResponsesBody = Schema.Schema.Type<typeof OpenAIResponsesBody>
const OpenAIResponsesWebSocketMessage = Schema.StructWithRest(
Schema.Struct({
type: Schema.tag("response.create"),
...OpenAIResponsesCoreFields,
}),
[Schema.Record(Schema.String, Schema.Unknown)],
)
type OpenAIResponsesWebSocketMessage = Schema.Schema.Type<typeof OpenAIResponsesWebSocketMessage>
const encodeWebSocketMessage = Schema.encodeSync(Schema.fromJsonString(OpenAIResponsesWebSocketMessage))
const extension = { const extension = {
id: ADAPTER, id: ADAPTER,
name: NAME, name: NAME,
@@ -244,13 +249,6 @@ const endpoint = Endpoint.path<OpenAIResponsesBody>(PATH, { baseURL: DEFAULT_BAS
const auth = Auth.none const auth = Auth.none
export const httpTransport = HttpTransport.sseJson.with<OpenAIResponsesBody>() export const httpTransport = HttpTransport.sseJson.with<OpenAIResponsesBody>()
export const transport = OpenResponsesChannel.transport<OpenAIResponsesBody>({
id: ADAPTER,
name: NAME,
rotateAfterMs: WEBSOCKET_ROTATE_AFTER_MS,
headers: (headers) => Headers.set(headers, "openai-beta", headers["openai-beta"] ?? WEBSOCKET_PROTOCOL_HEADER),
driver: (input) => OpenAIResponsesChannel.driver({ id: ADAPTER, name: NAME, ...input }),
})
export const route = Route.make({ export const route = Route.make({
id: ADAPTER, id: ADAPTER,
@@ -259,8 +257,37 @@ export const route = Route.make({
protocol, protocol,
endpoint, endpoint,
auth, auth,
transport, transport: httpTransport,
defaults: { providerOptions: { openai: { store: false } } }, defaults: { providerOptions: { openai: { store: false } } },
}) })
export * as OpenAIResponses from "./openai-responses.js" const decodeWebSocketMessage = ProviderShared.validateWith(Schema.decodeUnknownEffect(OpenAIResponsesWebSocketMessage))
const webSocketMessage = (body: OpenAIResponsesBody | Record<string, unknown>) =>
Effect.gen(function* () {
if (!ProviderShared.isRecord(body))
return yield* ProviderShared.invalidRequest("OpenAI Responses WebSocket body must be a JSON object")
const { stream: _stream, ...message } = body
return yield* decodeWebSocketMessage({ ...message, type: "response.create" })
})
export const webSocketTransport = WebSocketTransport.jsonTransport.with<
OpenAIResponsesBody,
OpenAIResponsesWebSocketMessage
>({
toMessage: webSocketMessage,
encodeMessage: encodeWebSocketMessage,
})
export const webSocketRoute = Route.make({
id: `${ADAPTER}-websocket`,
provider: "openai",
providerMetadataKey: "openai",
protocol,
endpoint,
auth,
transport: webSocketTransport,
defaults: { providerOptions: { openai: { store: false } } },
})
export * as OpenAIResponses from "./openai-responses"
+3 -3
View File
@@ -12,8 +12,8 @@ import {
type MediaPart, type MediaPart,
type TextPart, type TextPart,
type ToolResultPart, type ToolResultPart,
} from "../schema/index.js" } from "../schema"
import { isRecord } from "../utils/record.js" import { isRecord } from "../utils/record"
export { isRecord } export { isRecord }
export const Json = Schema.fromJsonString(Schema.Unknown) export const Json = Schema.fromJsonString(Schema.Unknown)
@@ -324,4 +324,4 @@ export const jsonPost = (input: { readonly url: string; readonly body: string; r
HttpClientRequest.bodyText(input.body, "application/json"), HttpClientRequest.bodyText(input.body, "application/json"),
) )
export * as ProviderShared from "./shared.js" export * as ProviderShared from "./shared"
@@ -1,8 +1,8 @@
import { AwsV4Signer } from "aws4fetch" import { AwsV4Signer } from "aws4fetch"
import { Effect } from "effect" import { Effect } from "effect"
import { Headers } from "effect/unstable/http" import { Headers } from "effect/unstable/http"
import { Auth, type AuthInput } from "../../route/auth.js" import { Auth, type AuthInput } from "../../route/auth"
import { ProviderShared } from "../shared.js" import { ProviderShared } from "../shared"
/** /**
* AWS credentials for SigV4 signing. Bedrock also supports Bearer API key auth, * AWS credentials for SigV4 signing. Bedrock also supports Bearer API key auth,
@@ -74,4 +74,4 @@ export const sigV4 = (
/** Bedrock route auth defaults to SigV4 and expects credentials from route configuration. */ /** Bedrock route auth defaults to SigV4 and expects credentials from route configuration. */
export const auth = sigV4(undefined) export const auth = sigV4(undefined)
export * as BedrockAuth from "./bedrock-auth.js" export * as BedrockAuth from "./bedrock-auth"
@@ -1,6 +1,6 @@
import { Schema } from "effect" import { Schema } from "effect"
import type { CacheHint } from "../../schema/index.js" import type { CacheHint } from "../../schema"
import { newBreakpoints, ttlBucket, type Breakpoints } from "./cache.js" import { newBreakpoints, ttlBucket, type Breakpoints } from "./cache"
// Bedrock cache markers are positional: emit a `cachePoint` block immediately // Bedrock cache markers are positional: emit a `cachePoint` block immediately
// after the content the caller wants treated as a cacheable prefix. Bedrock // after the content the caller wants treated as a cacheable prefix. Bedrock
@@ -18,7 +18,7 @@ export type CachePointBlock = Schema.Schema.Type<typeof CachePointBlock>
// budget is respected across `system`, `messages`, and `tools`. // budget is respected across `system`, `messages`, and `tools`.
export const BEDROCK_BREAKPOINT_CAP = 4 export const BEDROCK_BREAKPOINT_CAP = 4
export type { Breakpoints } from "./cache.js" export type { Breakpoints } from "./cache"
export const breakpoints = () => newBreakpoints(BEDROCK_BREAKPOINT_CAP) export const breakpoints = () => newBreakpoints(BEDROCK_BREAKPOINT_CAP)
const DEFAULT_5M: CachePointBlock = { cachePoint: { type: "default" } } const DEFAULT_5M: CachePointBlock = { cachePoint: { type: "default" } }
@@ -34,4 +34,4 @@ export const block = (breakpoints: Breakpoints, cache: CacheHint | undefined): C
return ttlBucket(cache.ttlSeconds) === "1h" ? DEFAULT_1H : DEFAULT_5M return ttlBucket(cache.ttlSeconds) === "1h" ? DEFAULT_1H : DEFAULT_5M
} }
export * as BedrockCache from "./bedrock-cache.js" export * as BedrockCache from "./bedrock-cache"
@@ -1,6 +1,6 @@
import { Effect, Schema } from "effect" import { Effect, Schema } from "effect"
import type { MediaPart } from "../../schema/index.js" import type { MediaPart } from "../../schema"
import { ProviderShared } from "../shared.js" import { ProviderShared } from "../shared"
// Bedrock Converse accepts image `format` as the file extension and // Bedrock Converse accepts image `format` as the file extension and
// `source.bytes` as base64 in the JSON wire format. // `source.bytes` as base64 in the JSON wire format.
@@ -89,4 +89,4 @@ export const lower = Effect.fn("BedrockMedia.lower")(function* (part: MediaPart)
return yield* ProviderShared.invalidRequest(`Bedrock Converse does not support media type ${part.mediaType}`) return yield* ProviderShared.invalidRequest(`Bedrock Converse does not support media type ${part.mediaType}`)
}) })
export * as BedrockMedia from "./bedrock-media.js" export * as BedrockMedia from "./bedrock-media"
@@ -1,4 +1,4 @@
import { isRecord } from "../../utils/record.js" import { isRecord } from "../../utils/record"
// Gemini accepts a JSON Schema-like dialect for tool parameters, but rejects a // Gemini accepts a JSON Schema-like dialect for tool parameters, but rejects a
// handful of common JSON Schema shapes. Keep this projection isolated so the // handful of common JSON Schema shapes. Keep this projection isolated so the
@@ -116,4 +116,4 @@ const projectNode = (schema: unknown, nested = false): Record<string, unknown> |
export const convert = (schema: unknown) => projectNode(sanitizeNode(schema)) export const convert = (schema: unknown) => projectNode(sanitizeNode(schema))
export * as GeminiToolSchema from "./gemini-tool-schema.js" export * as GeminiToolSchema from "./gemini-tool-schema"
@@ -1,6 +1,6 @@
import { Effect, Encoding } from "effect" import { Effect, Encoding } from "effect"
import type { ImageInput } from "../../image.js" import type { ImageInput } from "../../image"
import { InvalidRequestReason, AIError } from "../../schema/index.js" import { InvalidRequestReason, AIError } from "../../schema"
const invalid = (module: string, message: string) => const invalid = (module: string, message: string) =>
new AIError({ new AIError({
+2 -2
View File
@@ -1,4 +1,4 @@
import { LLMEvent, type FinishReasonDetails, type ProviderMetadata, type Usage } from "../../schema/index.js" import { LLMEvent, type FinishReasonDetails, type ProviderMetadata, type Usage } from "../../schema"
export interface State { export interface State {
readonly stepStarted: boolean readonly stepStarted: boolean
@@ -102,4 +102,4 @@ export const finish = (
return { ...stepped, stepStarted: false } return { ...stepped, stepStarted: false }
} }
export * as Lifecycle from "./lifecycle.js" export * as Lifecycle from "./lifecycle"
@@ -1,5 +1,5 @@
import { Schema } from "effect" import { Schema } from "effect"
import { TextVerbosity, type LLMRequest } from "../../schema/index.js" import { TextVerbosity, type LLMRequest } from "../../schema"
export const ResponseIncludables = [ export const ResponseIncludables = [
"file_search_call.results", "file_search_call.results",
@@ -60,4 +60,4 @@ export const resolve = (request: LLMRequest): Resolved => {
} }
} }
export * as OpenResponsesOptions from "./open-responses-options.js" export * as OpenResponsesOptions from "./open-responses-options"
@@ -1,5 +1,5 @@
import { ReasoningEfforts } from "../../schema/index.js" import { ReasoningEfforts } from "../../schema"
import { OpenResponsesOptions } from "./open-responses-options.js" import { OpenResponsesOptions } from "./open-responses-options"
export const OpenAIReasoningEfforts = ReasoningEfforts export const OpenAIReasoningEfforts = ReasoningEfforts
export type OpenAIReasoningEffort = string export type OpenAIReasoningEffort = string
@@ -20,4 +20,4 @@ export const isReasoningEffort = (effort: unknown): effort is OpenAIReasoningEff
export const resolve = OpenResponsesOptions.resolve export const resolve = OpenResponsesOptions.resolve
export * as OpenAIOptions from "./openai-options.js" export * as OpenAIOptions from "./openai-options"
@@ -1,6 +1,6 @@
import type { JsonSchema, LanguageModelToolSchemaCompatibility } from "../../schema/index.js" import type { JsonSchema, LanguageModelToolSchemaCompatibility } from "../../schema"
import { isRecord } from "../../utils/record.js" import { isRecord } from "../../utils/record"
import { GeminiToolSchema } from "./gemini-tool-schema.js" import { GeminiToolSchema } from "./gemini-tool-schema"
const removeNullSchemas = (value: unknown): unknown => { const removeNullSchemas = (value: unknown): unknown => {
if (Array.isArray(value)) return value.map(removeNullSchemas) if (Array.isArray(value)) return value.map(removeNullSchemas)
@@ -1,6 +1,6 @@
import { Effect } from "effect" import { Effect } from "effect"
import { AIError, LLMEvent, type ProviderMetadata, type ToolCall, type ToolInputError } from "../../schema/index.js" import { AIError, LLMEvent, type ProviderMetadata, type ToolCall, type ToolInputError } from "../../schema"
import { eventError, parseToolInput, type ToolAccumulator } from "../shared.js" import { eventError, parseToolInput, type ToolAccumulator } from "../shared"
type StreamKey = string | number type StreamKey = string | number
@@ -223,4 +223,4 @@ export const finishAll = <K extends StreamKey>(route: string, tools: State<K>) =
} }
}) })
export * as ToolStream from "./tool-stream.js" export * as ToolStream from "./tool-stream"
+5 -5
View File
@@ -1,7 +1,7 @@
import { Effect, Encoding, Schema } from "effect" import { Effect, Encoding, Schema } from "effect"
import { Headers, HttpClientRequest } from "effect/unstable/http" import { Headers, HttpClientRequest } from "effect/unstable/http"
import { GeneratedImage, ImageModel, ImageResponse, type ImageRequestFor, type ImageRoute } from "../image.js" import { GeneratedImage, ImageModel, ImageResponse, type ImageRequestFor, type ImageRoute } from "../image"
import { Auth, type Definition as AuthDefinition } from "../route/auth.js" import { Auth, type Definition as AuthDefinition } from "../route/auth"
import { import {
InvalidProviderOutputReason, InvalidProviderOutputReason,
AIError, AIError,
@@ -9,9 +9,9 @@ import {
mergeHttpOptions, mergeHttpOptions,
mergeJsonRecords, mergeJsonRecords,
type HttpOptions, type HttpOptions,
} from "../schema/index.js" } from "../schema"
import { ProviderShared, optionalNull } from "./shared.js" import { ProviderShared, optionalNull } from "./shared"
import { ImageInputs } from "./utils/image-input.js" import { ImageInputs } from "./utils/image-input"
const ADAPTER = "xai-images" const ADAPTER = "xai-images"
export const DEFAULT_BASE_URL = "https://api.x.ai/v1" export const DEFAULT_BASE_URL = "https://api.x.ai/v1"
+5 -11
View File
@@ -1,16 +1,10 @@
import { Effect, Schema } from "effect" import { Effect, Schema } from "effect"
import { Headers, HttpClientRequest } from "effect/unstable/http" import { Headers, HttpClientRequest } from "effect/unstable/http"
import { GeneratedImage, ImageModel, ImageResponse, type ImageRequestFor, type ImageRoute } from "../image.js" import { GeneratedImage, ImageModel, ImageResponse, type ImageRequestFor, type ImageRoute } from "../image"
import { Auth, type Definition as AuthDefinition } from "../route/auth.js" import { Auth, type Definition as AuthDefinition } from "../route/auth"
import { import { InvalidProviderOutputReason, AIError, mergeHttpOptions, mergeJsonRecords, type HttpOptions } from "../schema"
InvalidProviderOutputReason, import { ProviderShared } from "./shared"
AIError, import { ImageInputs } from "./utils/image-input"
mergeHttpOptions,
mergeJsonRecords,
type HttpOptions,
} from "../schema/index.js"
import { ProviderShared } from "./shared.js"
import { ImageInputs } from "./utils/image-input.js"
const ADAPTER = "zai-images" const ADAPTER = "zai-images"
export const DEFAULT_BASE_URL = "https://api.z.ai/api/paas/v4" export const DEFAULT_BASE_URL = "https://api.z.ai/api/paas/v4"
+1 -2
View File
@@ -12,7 +12,7 @@ import {
type HttpContext, type HttpContext,
type HttpRateLimitDetails, type HttpRateLimitDetails,
type ProviderMetadata, type ProviderMetadata,
} from "./schema/index.js" } from "./schema"
const patterns = [ const patterns = [
/prompt is too long/i, /prompt is too long/i,
@@ -67,7 +67,6 @@ const SERVER_CODES = new Set([
"overloaded_error", "overloaded_error",
"server_error", "server_error",
"server_is_overloaded", "server_is_overloaded",
"slow_down",
"serviceunavailableexception", "serviceunavailableexception",
]) ])
const INVALID_REQUEST_CODES = new Set(["invalid_prompt", "invalid_request_error", "validationexception"]) const INVALID_REQUEST_CODES = new Set(["invalid_prompt", "invalid_request_error", "validationexception"])
+2 -2
View File
@@ -1,4 +1,4 @@
import type { LanguageModel, ProviderOptions } from "./schema/index.js" import type { LanguageModel, ProviderOptions } from "./schema"
export interface Settings extends Readonly<Record<string, unknown>> { export interface Settings extends Readonly<Record<string, unknown>> {
readonly baseURL?: string readonly baseURL?: string
@@ -18,4 +18,4 @@ export interface Definition<
readonly model: (modelID: string, settings: ProviderSettings) => LanguageModel<Options> readonly model: (modelID: string, settings: ProviderSettings) => LanguageModel<Options>
} }
export * as ProviderPackage from "./provider-package.js" export * as ProviderPackage from "./provider-package"
+2 -2
View File
@@ -1,4 +1,4 @@
import type { LanguageModel, ModelID, ProviderID } from "./schema/index.js" import type { LanguageModel, ModelID, ProviderID } from "./schema"
export type LanguageModelOptions = Pick<LanguageModel.Input, "defaults" | "compatibility"> export type LanguageModelOptions = Pick<LanguageModel.Input, "defaults" | "compatibility">
@@ -33,4 +33,4 @@ export const make = <DefinitionType extends DefinitionShape>(
definition: NoExtraFields<DefinitionType, DefinitionShape>, definition: NoExtraFields<DefinitionType, DefinitionShape>,
) => definition ) => definition
export * as Provider from "./provider.js" export * as Provider from "./provider"
+1 -1
View File
@@ -1 +1 @@
export * from "./providers/index.js" export * from "./providers/index"
@@ -1,11 +1,11 @@
import { Auth } from "../route/auth.js" import { Auth } from "../route/auth"
import type { Route as RouteDef, RouteDefaultsInput } from "../route/client.js" import type { Route as RouteDef, RouteDefaultsInput } from "../route/client"
import type { ProviderPackage } from "../provider-package.js" import type { ProviderPackage } from "../provider-package"
import { OpenAIChat } from "../protocols/openai-chat.js" import { OpenAIChat } from "../protocols/openai-chat"
import { OpenAIResponses } from "../protocols/openai-responses.js" import { OpenAIResponses } from "../protocols/openai-responses"
import { BedrockAuth, type Credentials } from "../protocols/utils/bedrock-auth.js" import { BedrockAuth, type Credentials } from "../protocols/utils/bedrock-auth"
import { ProviderID, type ModelID } from "../schema/index.js" import { ProviderID, type ModelID } from "../schema"
import { withOpenAIOptions, type OpenAIProviderOptionsInput } from "./openai-options.js" import { withOpenAIOptions, type OpenAIProviderOptionsInput } from "./openai-options"
export const id = ProviderID.make("amazon-bedrock") export const id = ProviderID.make("amazon-bedrock")
+6 -6
View File
@@ -1,9 +1,9 @@
import type { RouteDefaultsInput } from "../route/client.js" import type { RouteDefaultsInput } from "../route/client"
import { Auth } from "../route/auth.js" import { Auth } from "../route/auth"
import type { ProviderPackage } from "../provider-package.js" import type { ProviderPackage } from "../provider-package"
import { ProviderID, type ModelID } from "../schema/index.js" import { ProviderID, type ModelID } from "../schema"
import * as BedrockConverse from "../protocols/bedrock-converse.js" import * as BedrockConverse from "../protocols/bedrock-converse"
import type { BedrockCredentials } from "../protocols/bedrock-converse.js" import type { BedrockCredentials } from "../protocols/bedrock-converse"
export const id = ProviderID.make("amazon-bedrock") export const id = ProviderID.make("amazon-bedrock")
@@ -1,2 +1,2 @@
export { chatModel as model } from "../amazon-bedrock-mantle.js" export { chatModel as model } from "../amazon-bedrock-mantle"
export type { Settings } from "../amazon-bedrock-mantle.js" export type { Settings } from "../amazon-bedrock-mantle"
@@ -1,2 +1,2 @@
export { chatModel as model } from "../../amazon-bedrock-mantle.js" export { chatModel as model } from "../../amazon-bedrock-mantle"
export type { Settings } from "../../amazon-bedrock-mantle.js" export type { Settings } from "../../amazon-bedrock-mantle"
@@ -1,2 +1,2 @@
export { responsesModel as model } from "../../amazon-bedrock-mantle.js" export { responsesModel as model } from "../../amazon-bedrock-mantle"
export type { Settings } from "../../amazon-bedrock-mantle.js" export type { Settings } from "../../amazon-bedrock-mantle"
@@ -1,9 +1,9 @@
import type { ProviderPackage } from "../provider-package.js" import type { ProviderPackage } from "../provider-package"
import { AnthropicMessages } from "../protocols/anthropic-messages.js" import { AnthropicMessages } from "../protocols/anthropic-messages"
import { Auth } from "../route/auth.js" import { Auth } from "../route/auth"
import type { ProviderAuthOption } from "../route/auth-options.js" import type { ProviderAuthOption } from "../route/auth-options"
import type { RouteDefaultsInput } from "../route/client.js" import type { RouteDefaultsInput } from "../route/client"
import { ProviderID, type ModelID } from "../schema/index.js" import { ProviderID, type ModelID } from "../schema"
export type AnthropicOptionsInput = AnthropicMessages.OptionsInput export type AnthropicOptionsInput = AnthropicMessages.OptionsInput
export type AnthropicProviderOptionsInput = AnthropicMessages.ProviderOptionsInput export type AnthropicProviderOptionsInput = AnthropicMessages.ProviderOptionsInput
@@ -74,4 +74,4 @@ export const model: ProviderPackage.Definition<Settings, AnthropicMessages.Provi
}).model(modelID) }).model(modelID)
} }
export * as AnthropicCompatible from "./anthropic-compatible.js" export * as AnthropicCompatible from "./anthropic-compatible"
+7 -7
View File
@@ -1,10 +1,10 @@
import type { RouteDefaultsInput } from "../route/client.js" import type { RouteDefaultsInput } from "../route/client"
import { Auth } from "../route/auth.js" import { Auth } from "../route/auth"
import type { ProviderAuthOption } from "../route/auth-options.js" import type { ProviderAuthOption } from "../route/auth-options"
import type { ProviderPackage } from "../provider-package.js" import type { ProviderPackage } from "../provider-package"
import { ProviderID, type ModelID } from "../schema/index.js" import { ProviderID, type ModelID } from "../schema"
import { AnthropicMessages } from "../protocols/anthropic-messages.js" import { AnthropicMessages } from "../protocols/anthropic-messages"
import { AnthropicCompatible } from "./anthropic-compatible.js" import { AnthropicCompatible } from "./anthropic-compatible"
export type AnthropicOptionsInput = AnthropicMessages.OptionsInput export type AnthropicOptionsInput = AnthropicMessages.OptionsInput
export type AnthropicProviderOptionsInput = AnthropicMessages.ProviderOptionsInput export type AnthropicProviderOptionsInput = AnthropicMessages.ProviderOptionsInput
+9 -9
View File
@@ -1,12 +1,12 @@
import { Auth } from "../route/auth.js" import { Auth } from "../route/auth"
import { type AtLeastOne, type ProviderAuthOption } from "../route/auth-options.js" import { type AtLeastOne, type ProviderAuthOption } from "../route/auth-options"
import type { Route as RouteDef, RouteDefaultsInput } from "../route/client.js" import type { Route as RouteDef, RouteDefaultsInput } from "../route/client"
import type { ProviderPackage } from "../provider-package.js" import type { ProviderPackage } from "../provider-package"
import { ProviderID, type ModelID } from "../schema/index.js" import { ProviderID, type ModelID } from "../schema"
import * as OpenAIChat from "../protocols/openai-chat.js" import * as OpenAIChat from "../protocols/openai-chat"
import * as OpenAIResponses from "../protocols/openai-responses.js" import * as OpenAIResponses from "../protocols/openai-responses"
import { ProviderShared } from "../protocols/shared.js" import { ProviderShared } from "../protocols/shared"
import { withOpenAIOptions, type OpenAIProviderOptionsInput } from "./openai-options.js" import { withOpenAIOptions, type OpenAIProviderOptionsInput } from "./openai-options"
export const id = ProviderID.make("azure") export const id = ProviderID.make("azure")
const routeAuth = Auth.remove("authorization") const routeAuth = Auth.remove("authorization")
+2 -2
View File
@@ -1,2 +1,2 @@
export { chatModel as model } from "../azure.js" export { chatModel as model } from "../azure"
export type { Settings } from "../azure.js" export type { Settings } from "../azure"
+2 -2
View File
@@ -1,2 +1,2 @@
export { responsesModel as model } from "../azure.js" export { responsesModel as model } from "../azure"
export type { Settings } from "../azure.js" export type { Settings } from "../azure"
+6 -6
View File
@@ -1,10 +1,10 @@
import type { Config, Redacted } from "effect" import type { Config, Redacted } from "effect"
import * as OpenAICompatibleChat from "../protocols/openai-compatible-chat.js" import * as OpenAICompatibleChat from "../protocols/openai-compatible-chat"
import { Auth } from "../route/auth.js" import { Auth } from "../route/auth"
import { AuthOptions, type AtLeastOne, type ProviderAuthOption } from "../route/auth-options.js" import { AuthOptions, type AtLeastOne, type ProviderAuthOption } from "../route/auth-options"
import type { RouteDefaultsInput } from "../route/client.js" import type { RouteDefaultsInput } from "../route/client"
import { ProviderID, type ModelID } from "../schema/index.js" import { ProviderID, type ModelID } from "../schema"
import type { OpenAIProviderOptionsInput } from "./openai-options.js" import type { OpenAIProviderOptionsInput } from "./openai-options"
export const aiGatewayID = ProviderID.make("cloudflare-ai-gateway") export const aiGatewayID = ProviderID.make("cloudflare-ai-gateway")
export const workersAIID = ProviderID.make("cloudflare-workers-ai") export const workersAIID = ProviderID.make("cloudflare-workers-ai")
@@ -1,9 +1,9 @@
import type { ProviderPackage } from "../provider-package.js" import type { ProviderPackage } from "../provider-package"
import { OpenAICompatibleChat } from "../protocols/openai-compatible-chat.js" import { OpenAICompatibleChat } from "../protocols/openai-compatible-chat"
import type { RouteDefaultsInput } from "../route/client.js" import type { RouteDefaultsInput } from "../route/client"
import { ProviderID, type ModelID } from "../schema/index.js" import { ProviderID, type ModelID } from "../schema"
import { GoogleVertexShared } from "./google-vertex-shared.js" import { GoogleVertexShared } from "./google-vertex-shared"
import type { OpenAIProviderOptionsInput } from "./openai-options.js" import type { OpenAIProviderOptionsInput } from "./openai-options"
export const id = ProviderID.make("google-vertex") export const id = ProviderID.make("google-vertex")
@@ -1,13 +1,13 @@
import { Effect, Schema, Struct } from "effect" import { Effect, Schema, Struct } from "effect"
import type { ProviderPackage } from "../provider-package.js" import type { ProviderPackage } from "../provider-package"
import { AnthropicMessages } from "../protocols/anthropic-messages.js" import { AnthropicMessages } from "../protocols/anthropic-messages"
import { Auth } from "../route/auth.js" import { Auth } from "../route/auth"
import { Route, type RouteDefaultsInput } from "../route/client.js" import { Route, type RouteDefaultsInput } from "../route/client"
import { Endpoint } from "../route/endpoint.js" import { Endpoint } from "../route/endpoint"
import { Framing } from "../route/framing.js" import { Framing } from "../route/framing"
import { Protocol } from "../route/protocol.js" import { Protocol } from "../route/protocol"
import { ProviderID, type ModelID } from "../schema/index.js" import { ProviderID, type ModelID } from "../schema"
import { GoogleVertexShared } from "./google-vertex-shared.js" import { GoogleVertexShared } from "./google-vertex-shared"
export type AnthropicOptionsInput = AnthropicMessages.OptionsInput export type AnthropicOptionsInput = AnthropicMessages.OptionsInput
export type AnthropicProviderOptionsInput = AnthropicMessages.ProviderOptionsInput export type AnthropicProviderOptionsInput = AnthropicMessages.ProviderOptionsInput
@@ -1,9 +1,9 @@
import type { ProviderPackage } from "../provider-package.js" import type { ProviderPackage } from "../provider-package"
import { OpenAICompatibleResponses } from "../protocols/openai-compatible-responses.js" import { OpenAICompatibleResponses } from "../protocols/openai-compatible-responses"
import type { RouteDefaultsInput } from "../route/client.js" import type { RouteDefaultsInput } from "../route/client"
import { ProviderID, type ModelID } from "../schema/index.js" import { ProviderID, type ModelID } from "../schema"
import { GoogleVertexShared } from "./google-vertex-shared.js" import { GoogleVertexShared } from "./google-vertex-shared"
import type { OpenResponsesProviderOptionsInput } from "./open-responses-options.js" import type { OpenResponsesProviderOptionsInput } from "./open-responses-options"
export const id = ProviderID.make("google-vertex") export const id = ProviderID.make("google-vertex")
@@ -1,6 +1,6 @@
import type { AnyAuthClient } from "google-auth-library" import type { AnyAuthClient } from "google-auth-library"
import { Effect, Redacted } from "effect" import { Effect, Redacted } from "effect"
import { Auth, MissingCredentialError } from "../route/auth.js" import { Auth, MissingCredentialError } from "../route/auth"
const SCOPE = "https://www.googleapis.com/auth/cloud-platform" const SCOPE = "https://www.googleapis.com/auth/cloud-platform"
@@ -74,4 +74,4 @@ export const oauth = (input: OAuthOptions, project?: string) => {
return adc(project) return adc(project)
} }
export * as GoogleVertexShared from "./google-vertex-shared.js" export * as GoogleVertexShared from "./google-vertex-shared"
+8 -8
View File
@@ -1,11 +1,11 @@
import type { ProviderPackage } from "../provider-package.js" import type { ProviderPackage } from "../provider-package"
import { Gemini } from "../protocols/gemini.js" import { Gemini } from "../protocols/gemini"
import { Auth } from "../route/auth.js" import { Auth } from "../route/auth"
import { Route, type RouteDefaultsInput } from "../route/client.js" import { Route, type RouteDefaultsInput } from "../route/client"
import { Endpoint } from "../route/endpoint.js" import { Endpoint } from "../route/endpoint"
import { Framing } from "../route/framing.js" import { Framing } from "../route/framing"
import { ProviderID, type ModelID } from "../schema/index.js" import { ProviderID, type ModelID } from "../schema"
import { GoogleVertexShared } from "./google-vertex-shared.js" import { GoogleVertexShared } from "./google-vertex-shared"
export type GeminiOptionsInput = Gemini.OptionsInput export type GeminiOptionsInput = Gemini.OptionsInput
export type GeminiProviderOptionsInput = Gemini.ProviderOptionsInput export type GeminiProviderOptionsInput = Gemini.ProviderOptionsInput
@@ -1,2 +1,2 @@
export { model } from "../google-vertex-chat.js" export { model } from "../google-vertex-chat"
export type { Settings } from "../google-vertex-chat.js" export type { Settings } from "../google-vertex-chat"
@@ -1,2 +1,2 @@
export { model } from "../google-vertex.js" export { model } from "../google-vertex"
export type { Settings } from "../google-vertex.js" export type { Settings } from "../google-vertex"
@@ -1,2 +1,2 @@
export { model } from "../google-vertex-messages.js" export { model } from "../google-vertex-messages"
export type { Settings } from "../google-vertex-messages.js" export type { Settings } from "../google-vertex-messages"
@@ -1,2 +1,2 @@
export { model } from "../google-vertex-responses.js" export { model } from "../google-vertex-responses"
export type { Settings } from "../google-vertex-responses.js" export type { Settings } from "../google-vertex-responses"
+8 -8
View File
@@ -1,12 +1,12 @@
import type { RouteDefaultsInput } from "../route/client.js" import type { RouteDefaultsInput } from "../route/client"
import { Auth } from "../route/auth.js" import { Auth } from "../route/auth"
import type { ProviderAuthOption } from "../route/auth-options.js" import type { ProviderAuthOption } from "../route/auth-options"
import type { ProviderPackage } from "../provider-package.js" import type { ProviderPackage } from "../provider-package"
import { HttpOptions, ProviderID, mergeHttpOptions, type ModelID } from "../schema/index.js" import { HttpOptions, ProviderID, mergeHttpOptions, type ModelID } from "../schema"
import { Gemini } from "../protocols/gemini.js" import { Gemini } from "../protocols/gemini"
import { GoogleImages } from "../protocols/google-images.js" import { GoogleImages } from "../protocols/google-images"
export type { GoogleImageOptions } from "../protocols/google-images.js" export type { GoogleImageOptions } from "../protocols/google-images"
export type GeminiOptionsInput = Gemini.OptionsInput export type GeminiOptionsInput = Gemini.OptionsInput
export type GeminiProviderOptionsInput = Gemini.ProviderOptionsInput export type GeminiProviderOptionsInput = Gemini.ProviderOptionsInput
+18 -18
View File
@@ -1,18 +1,18 @@
export * as Anthropic from "./anthropic.js" export * as Anthropic from "./anthropic"
export * as AnthropicCompatible from "./anthropic-compatible.js" export * as AnthropicCompatible from "./anthropic-compatible"
export * as AmazonBedrock from "./amazon-bedrock.js" export * as AmazonBedrock from "./amazon-bedrock"
export * as AmazonBedrockMantle from "./amazon-bedrock-mantle.js" export * as AmazonBedrockMantle from "./amazon-bedrock-mantle"
export * as Azure from "./azure.js" export * as Azure from "./azure"
export * as Cloudflare from "./cloudflare.js" export * as Cloudflare from "./cloudflare"
export { CloudflareAIGateway, CloudflareWorkersAI } from "./cloudflare.js" export { CloudflareAIGateway, CloudflareWorkersAI } from "./cloudflare"
export * as Google from "./google.js" export * as Google from "./google"
export * as GoogleVertex from "./google-vertex.js" export * as GoogleVertex from "./google-vertex"
export * as GoogleVertexChat from "./google-vertex-chat.js" export * as GoogleVertexChat from "./google-vertex-chat"
export * as GoogleVertexMessages from "./google-vertex-messages.js" export * as GoogleVertexMessages from "./google-vertex-messages"
export * as GoogleVertexResponses from "./google-vertex-responses.js" export * as GoogleVertexResponses from "./google-vertex-responses"
export * as OpenAI from "./openai.js" export * as OpenAI from "./openai"
export * as OpenAICompatible from "./openai-compatible.js" export * as OpenAICompatible from "./openai-compatible"
export * as OpenAICompatibleResponses from "./openai-compatible-responses.js" export * as OpenAICompatibleResponses from "./openai-compatible-responses"
export * as OpenRouter from "./openrouter.js" export * as OpenRouter from "./openrouter"
export * as XAI from "./xai.js" export * as XAI from "./xai"
export * as ZAI from "./zai.js" export * as ZAI from "./zai"
@@ -1,5 +1,5 @@
import type { ResponseIncludable, ServiceTier } from "../protocols/utils/open-responses-options.js" import type { ResponseIncludable, ServiceTier } from "../protocols/utils/open-responses-options"
import type { ProviderOptions, ReasoningEffort, TextVerbosity } from "../schema/index.js" import type { ProviderOptions, ReasoningEffort, TextVerbosity } from "../schema"
export interface OpenResponsesOptionsInput { export interface OpenResponsesOptionsInput {
readonly [key: string]: unknown readonly [key: string]: unknown
@@ -16,4 +16,4 @@ export type OpenResponsesProviderOptionsInput = ProviderOptions & {
readonly openresponses?: OpenResponsesOptionsInput readonly openresponses?: OpenResponsesOptionsInput
} }
export * as OpenResponsesProviderOptions from "./open-responses-options.js" export * as OpenResponsesProviderOptions from "./open-responses-options"
@@ -1,11 +1,11 @@
import type { ProviderPackage } from "../provider-package.js" import type { ProviderPackage } from "../provider-package"
import { OpenAICompatibleResponses } from "../protocols/openai-compatible-responses.js" import { OpenAICompatibleResponses } from "../protocols/openai-compatible-responses"
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options.js" import { AuthOptions, type ProviderAuthOption } from "../route/auth-options"
import type { RouteDefaultsInput } from "../route/client.js" import type { RouteDefaultsInput } from "../route/client"
import { ProviderID, type ModelID } from "../schema/index.js" import { ProviderID, type ModelID } from "../schema"
import type { OpenResponsesProviderOptionsInput } from "./open-responses-options.js" import type { OpenResponsesProviderOptionsInput } from "./open-responses-options"
export type { OpenResponsesOptionsInput, OpenResponsesProviderOptionsInput } from "./open-responses-options.js" export type { OpenResponsesOptionsInput, OpenResponsesProviderOptionsInput } from "./open-responses-options"
export const id = ProviderID.make("openai-compatible") export const id = ProviderID.make("openai-compatible")
@@ -1,10 +1,10 @@
import { ProviderID, type ModelID } from "../schema/index.js" import { ProviderID, type ModelID } from "../schema"
import * as OpenAICompatibleChat from "../protocols/openai-compatible-chat.js" import * as OpenAICompatibleChat from "../protocols/openai-compatible-chat"
import type { RouteDefaultsInput } from "../route/client.js" import type { RouteDefaultsInput } from "../route/client"
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options.js" import { AuthOptions, type ProviderAuthOption } from "../route/auth-options"
import type { ProviderPackage } from "../provider-package.js" import type { ProviderPackage } from "../provider-package"
import { profiles, type OpenAICompatibleProfile } from "./openai-compatible-profile.js" import { profiles, type OpenAICompatibleProfile } from "./openai-compatible-profile"
import type { OpenAIProviderOptionsInput } from "./openai-options.js" import type { OpenAIProviderOptionsInput } from "./openai-options"
export const id = ProviderID.make("openai-compatible") export const id = ProviderID.make("openai-compatible")
@@ -1 +1 @@
export * from "../openai-compatible-responses.js" export * from "../openai-compatible-responses"
+5 -5
View File
@@ -1,8 +1,8 @@
import type { ProviderOptions } from "../schema/index.js" import type { ProviderOptions } from "../schema"
import { mergeProviderOptions } from "../schema/index.js" import { mergeProviderOptions } from "../schema"
import type { OpenResponsesOptionsInput } from "./open-responses-options.js" import type { OpenResponsesOptionsInput } from "./open-responses-options"
export type { OpenAIResponseIncludable, OpenAIServiceTier } from "../protocols/utils/openai-options.js" export type { OpenAIResponseIncludable, OpenAIServiceTier } from "../protocols/utils/openai-options"
export type OpenAIOptionsInput = OpenResponsesOptionsInput export type OpenAIOptionsInput = OpenResponsesOptionsInput
@@ -67,4 +67,4 @@ export const withOpenAIOptions = <Options extends { readonly providerOptions?: O
} }
} }
export * as OpenAIProviderOptions from "./openai-options.js" export * as OpenAIProviderOptions from "./openai-options"
+23 -12
View File
@@ -1,18 +1,18 @@
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options.js" import { AuthOptions, type ProviderAuthOption } from "../route/auth-options"
import type { Route, RouteDefaultsInput } from "../route/client.js" import type { Route, RouteDefaultsInput } from "../route/client"
import type { ProviderPackage } from "../provider-package.js" import type { ProviderPackage } from "../provider-package"
import { HttpOptions, ProviderID, ToolDefinition, mergeHttpOptions, type ModelID } from "../schema/index.js" import { HttpOptions, ProviderID, ToolDefinition, mergeHttpOptions, type ModelID } from "../schema"
import * as OpenAIChat from "../protocols/openai-chat.js" import * as OpenAIChat from "../protocols/openai-chat"
import * as OpenAIResponses from "../protocols/openai-responses.js" import * as OpenAIResponses from "../protocols/openai-responses"
import { withOpenAIOptions, type OpenAIProviderOptionsInput } from "./openai-options.js" import { withOpenAIOptions, type OpenAIProviderOptionsInput } from "./openai-options"
import { OpenAIImages, type OpenAIImageString } from "../protocols/openai-images.js" import { OpenAIImages, type OpenAIImageString } from "../protocols/openai-images"
export type { OpenAIOptionsInput, OpenAIResponseIncludable } from "./openai-options.js" export type { OpenAIOptionsInput, OpenAIResponseIncludable } from "./openai-options"
export type { OpenAIImageOptions } from "../protocols/openai-images.js" export type { OpenAIImageOptions } from "../protocols/openai-images"
export const id = ProviderID.make("openai") export const id = ProviderID.make("openai")
export const routes = [OpenAIResponses.route, OpenAIChat.route] export const routes = [OpenAIResponses.route, OpenAIResponses.webSocketRoute, OpenAIChat.route]
// This provider facade wraps the lower-level Responses and Chat model factories // This provider facade wraps the lower-level Responses and Chat model factories
// with OpenAI-specific conveniences: typed options, API-key sugar, env fallback, // with OpenAI-specific conveniences: typed options, API-key sugar, env fallback,
@@ -63,6 +63,7 @@ export interface Settings extends ProviderPackage.Settings {
readonly organization?: string readonly organization?: string
readonly project?: string readonly project?: string
readonly queryParams?: Readonly<Record<string, string>> readonly queryParams?: Readonly<Record<string, string>>
readonly transport?: "http" | "websocket"
readonly providerOptions?: OpenAIProviderOptionsInput readonly providerOptions?: OpenAIProviderOptionsInput
} }
@@ -81,12 +82,17 @@ const configuredRoute = <Body, Prepared>(route: Route<Body, Prepared>, input: Co
export const configure = (input: Config = {}) => { export const configure = (input: Config = {}) => {
const responsesRoute = configuredRoute(OpenAIResponses.route, input) const responsesRoute = configuredRoute(OpenAIResponses.route, input)
const responsesWebSocketRoute = configuredRoute(OpenAIResponses.webSocketRoute, input)
const chatRoute = configuredRoute(OpenAIChat.route, input) const chatRoute = configuredRoute(OpenAIChat.route, input)
const modelDefaults = defaults(input) const modelDefaults = defaults(input)
const responses = (id: string | ModelID) => const responses = (id: string | ModelID) =>
responsesRoute responsesRoute
.with(withOpenAIOptions(id, modelDefaults, { textVerbosity: true })) .with(withOpenAIOptions(id, modelDefaults, { textVerbosity: true }))
.model<OpenAIProviderOptionsInput>({ id }) .model<OpenAIProviderOptionsInput>({ id })
const responsesWebSocket = (id: string | ModelID) =>
responsesWebSocketRoute
.with(withOpenAIOptions(id, modelDefaults, { textVerbosity: true }))
.model<OpenAIProviderOptionsInput>({ id })
const chat = (id: string | ModelID) => const chat = (id: string | ModelID) =>
chatRoute.with(withOpenAIOptions(id, modelDefaults)).model<OpenAIProviderOptionsInput>({ id }) chatRoute.with(withOpenAIOptions(id, modelDefaults)).model<OpenAIProviderOptionsInput>({ id })
const image = (modelID: string | ModelID) => const image = (modelID: string | ModelID) =>
@@ -105,6 +111,7 @@ export const configure = (input: Config = {}) => {
id, id,
model: responses, model: responses,
responses, responses,
responsesWebSocket,
chat, chat,
image, image,
configure, configure,
@@ -131,7 +138,10 @@ const config = (settings: Settings): Config => {
} }
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (modelID, settings) => { export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (modelID, settings) => {
return configure(config(settings)).responses(modelID) const configured = configure(config(settings))
if (settings.transport === undefined || settings.transport === "http") return configured.responses(modelID)
if (settings.transport === "websocket") return configured.responsesWebSocket(modelID)
throw new Error(`Unsupported OpenAI Responses transport: ${String(settings.transport)}`)
} }
export const chatModel: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = ( export const chatModel: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (
@@ -139,5 +149,6 @@ export const chatModel: ProviderPackage.Definition<Settings, OpenAIProviderOptio
settings, settings,
) => configure(config(settings)).chat(modelID) ) => configure(config(settings)).chat(modelID)
export const responses = provider.responses export const responses = provider.responses
export const responsesWebSocket = provider.responsesWebSocket
export const chat = provider.chat export const chat = provider.chat
export const image = provider.image export const image = provider.image
+2 -2
View File
@@ -1,2 +1,2 @@
export { chatModel as model } from "../openai.js" export { chatModel as model } from "../openai"
export type { Settings } from "../openai.js" export type { Settings } from "../openai"
@@ -1,2 +1,2 @@
export { model } from "../openai.js" export { model } from "../openai"
export type { Settings } from "../openai.js" export type { Settings } from "../openai"
+11 -11
View File
@@ -1,15 +1,15 @@
import { Effect, Schema } from "effect" import { Effect, Schema } from "effect"
import { Route, type RouteDefaultsInput } from "../route/client.js" import { Route, type RouteDefaultsInput } from "../route/client"
import { Endpoint } from "../route/endpoint.js" import { Endpoint } from "../route/endpoint"
import { Framing } from "../route/framing.js" import { Framing } from "../route/framing"
import { Protocol } from "../route/protocol.js" import { Protocol } from "../route/protocol"
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options.js" import { AuthOptions, type ProviderAuthOption } from "../route/auth-options"
import { ProviderID, type CacheHint, type ModelID, type ProviderOptions } from "../schema/index.js" import { ProviderID, type CacheHint, type ModelID, type ProviderOptions } from "../schema"
import type { ProviderPackage } from "../provider-package.js" import type { ProviderPackage } from "../provider-package"
import * as OpenAICompatibleProfiles from "./openai-compatible-profile.js" import * as OpenAICompatibleProfiles from "./openai-compatible-profile"
import * as OpenAIChat from "../protocols/openai-chat.js" import * as OpenAIChat from "../protocols/openai-chat"
import { newBreakpoints, ttlBucket } from "../protocols/utils/cache.js" import { newBreakpoints, ttlBucket } from "../protocols/utils/cache"
import { isRecord } from "../protocols/shared.js" import { isRecord } from "../protocols/shared"
export const profile = OpenAICompatibleProfiles.profiles.openrouter export const profile = OpenAICompatibleProfiles.profiles.openrouter
export const id = ProviderID.make(profile.provider) export const id = ProviderID.make(profile.provider)
+12 -12
View File
@@ -1,14 +1,14 @@
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options.js" import { AuthOptions, type ProviderAuthOption } from "../route/auth-options"
import { Route, type RouteDefaultsInput } from "../route/client.js" import { Route, type RouteDefaultsInput } from "../route/client"
import { Endpoint } from "../route/endpoint.js" import { Endpoint } from "../route/endpoint"
import { HttpOptions, ProviderID, type ModelID, type ProviderOptions } from "../schema/index.js" import { HttpOptions, ProviderID, type ModelID, type ProviderOptions } from "../schema"
import * as OpenAICompatibleProfiles from "./openai-compatible-profile.js" import * as OpenAICompatibleProfiles from "./openai-compatible-profile"
import * as OpenAICompatibleChat from "../protocols/openai-compatible-chat.js" import * as OpenAICompatibleChat from "../protocols/openai-compatible-chat"
import * as OpenAIChat from "../protocols/openai-chat.js" import * as OpenAIChat from "../protocols/openai-chat"
import * as OpenAIResponses from "../protocols/openai-responses.js" import * as OpenAIResponses from "../protocols/openai-responses"
import { XAIImages } from "../protocols/xai-images.js" import { XAIImages } from "../protocols/xai-images"
import type { OpenAIOptionsInput } from "./openai-options.js" import type { OpenAIOptionsInput } from "./openai-options"
import type { ProviderPackage } from "../provider-package.js" import type { ProviderPackage } from "../provider-package"
export const id = ProviderID.make("xai") export const id = ProviderID.make("xai")
@@ -28,7 +28,7 @@ export interface Settings extends ProviderPackage.Settings {
readonly providerOptions?: XAIProviderOptionsInput readonly providerOptions?: XAIProviderOptionsInput
} }
export type { XAIImageOptions } from "../protocols/xai-images.js" export type { XAIImageOptions } from "../protocols/xai-images"
const responsesRoute = Route.make({ const responsesRoute = Route.make({
id: "openai-responses", id: "openai-responses",
+4 -4
View File
@@ -1,6 +1,6 @@
import { ZAIImages } from "../protocols/zai-images.js" import { ZAIImages } from "../protocols/zai-images"
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options.js" import { AuthOptions, type ProviderAuthOption } from "../route/auth-options"
import { HttpOptions, ProviderID, type ModelID } from "../schema/index.js" import { HttpOptions, ProviderID, type ModelID } from "../schema"
export const id = ProviderID.make("zai") export const id = ProviderID.make("zai")
@@ -10,7 +10,7 @@ export type Config = ProviderAuthOption<"optional"> & {
readonly http?: HttpOptions.Input readonly http?: HttpOptions.Input
} }
export type { ZAIImageOptions } from "../protocols/zai-images.js" export type { ZAIImageOptions } from "../protocols/zai-images"
const auth = (options: ProviderAuthOption<"optional">) => AuthOptions.bearer(options, "ZAI_API_KEY") const auth = (options: ProviderAuthOption<"optional">) => AuthOptions.bearer(options, "ZAI_API_KEY")
+1 -1
View File
@@ -1 +1 @@
export * from "./route/index.js" export * from "./route/index"
+2 -2
View File
@@ -1,5 +1,5 @@
import type { Config, Redacted } from "effect" import type { Config, Redacted } from "effect"
import { Auth } from "./auth.js" import { Auth } from "./auth"
export type ApiKeyMode = "optional" | "required" export type ApiKeyMode = "optional" | "required"
@@ -61,4 +61,4 @@ export const bearer = (
.bearer() .bearer()
} }
export * as AuthOptions from "./auth-options.js" export * as AuthOptions from "./auth-options"
+2 -2
View File
@@ -1,6 +1,6 @@
import { Config, Effect, Redacted } from "effect" import { Config, Effect, Redacted } from "effect"
import { Headers } from "effect/unstable/http" import { Headers } from "effect/unstable/http"
import { AuthenticationReason, InvalidRequestReason, AIError, type HttpOptions } from "../schema/index.js" import { AuthenticationReason, InvalidRequestReason, AIError, type HttpOptions } from "../schema"
export class MissingCredentialError extends Error { export class MissingCredentialError extends Error {
readonly _tag = "MissingCredentialError" readonly _tag = "MissingCredentialError"
@@ -153,4 +153,4 @@ export const toEffect =
(authInput: AuthInput): Effect.Effect<Headers.Headers, AIError> => (authInput: AuthInput): Effect.Effect<Headers.Headers, AIError> =>
input.apply(authInput).pipe(Effect.mapError(toAIError)) input.apply(authInput).pipe(Effect.mapError(toAIError))
export * as Auth from "./auth.js" export * as Auth from "./auth"
+20 -25
View File
@@ -1,14 +1,16 @@
import { Cause, Context, Effect, Layer, Schema, Stream } from "effect" import { Cause, Context, Effect, Layer, Schema, Stream } from "effect"
import { Auth } from "./auth.js" import * as Option from "effect/Option"
import { Endpoint, type EndpointPatch } from "./endpoint.js" import { Auth } from "./auth"
import { RequestExecutor } from "./executor.js" import { Endpoint, type EndpointPatch } from "./endpoint"
import { Framing } from "./framing.js" import { RequestExecutor } from "./executor"
import { HttpTransport } from "./transport/index.js" import { Framing } from "./framing"
import type { HttpMiddleware, Transport, TransportRuntime, WebSocketChannelExecutor } from "./transport/index.js" import { HttpTransport } from "./transport"
import type { Protocol } from "./protocol.js" import type { HttpMiddleware, Transport, TransportRuntime } from "./transport"
import { applyCachePolicy } from "../cache-policy.js" import { WebSocketExecutor } from "./transport"
import * as ProviderShared from "../protocols/shared.js" import type { Protocol } from "./protocol"
import type { ProtocolID, ProviderOptions } from "../schema/index.js" import { applyCachePolicy } from "../cache-policy"
import * as ProviderShared from "../protocols/shared"
import type { ProtocolID, ProviderOptions } from "../schema"
import { import {
AIError, AIError,
GenerationOptions, GenerationOptions,
@@ -23,7 +25,7 @@ import {
mergeGenerationOptions, mergeGenerationOptions,
mergeHttpOptions, mergeHttpOptions,
mergeProviderOptions, mergeProviderOptions,
} from "../schema/index.js" } from "../schema"
export interface RouteBody<Body> { export interface RouteBody<Body> {
/** Schema for the validated provider-native body sent as the JSON request. */ /** Schema for the validated provider-native body sent as the JSON request. */
@@ -56,7 +58,6 @@ export interface Route<Body, Prepared = unknown> {
prepared: Prepared, prepared: Prepared,
request: LLMRequest, request: LLMRequest,
runtime: TransportRuntime, runtime: TransportRuntime,
options?: StreamOptions,
) => Stream.Stream<LLMEvent, AIError> ) => Stream.Stream<LLMEvent, AIError>
} }
@@ -156,7 +157,6 @@ export interface Interface {
export interface StreamOptions { export interface StreamOptions {
readonly http?: HttpMiddleware readonly http?: HttpMiddleware
readonly webSocket?: WebSocketChannelExecutor
} }
export interface StreamMethod { export interface StreamMethod {
@@ -314,18 +314,16 @@ function makeFromTransport<Body, Prepared, Frame, Event, State>(
encodeBody, encodeBody,
headers: routeInput.headers, headers: routeInput.headers,
middleware: options?.http, middleware: options?.http,
webSocket: options?.webSocket,
}), }),
streamPrepared: (prepared: Prepared, request: LLMRequest, runtime: TransportRuntime, options?: StreamOptions) => { streamPrepared: (prepared: Prepared, request: LLMRequest, runtime: TransportRuntime) => {
const route = `${request.model.provider}/${request.model.route.id}` const route = `${request.model.provider}/${request.model.route.id}`
return Stream.unwrap( const events = routeInput.transport
routeInput.transport.execute(prepared, request, runtime, options).pipe( .frames(prepared, request, runtime)
Effect.map((execution) => { .pipe(
const events = execution.frames.pipe(
Stream.mapEffect(decodeEvent(route)), Stream.mapEffect(decodeEvent(route)),
protocol.stream.terminal ? Stream.takeUntil(protocol.stream.terminal) : (stream) => stream, protocol.stream.terminal ? Stream.takeUntil(protocol.stream.terminal) : (stream) => stream,
) )
const stream = events.pipe( return events.pipe(
Stream.mapAccumEffect( Stream.mapAccumEffect(
() => protocol.stream.initial(request), () => protocol.stream.initial(request),
protocol.stream.step, protocol.stream.step,
@@ -334,10 +332,6 @@ function makeFromTransport<Body, Prepared, Frame, Event, State>(
Stream.catchCause((cause) => Stream.fail(streamError(route, `Failed to read ${route} stream`, cause))), Stream.catchCause((cause) => Stream.fail(streamError(route, `Failed to read ${route} stream`, cause))),
requireTerminalEvent(route), requireTerminalEvent(route),
) )
return execution.complete ? stream.pipe(Stream.onEnd(execution.complete)) : stream
}),
),
)
}, },
} satisfies Route<Body, Prepared> } satisfies Route<Body, Prepared>
return route return route
@@ -419,7 +413,7 @@ const streamRequestWith = (runtime: TransportRuntime) => (request: LLMRequest, o
Stream.unwrap( Stream.unwrap(
Effect.gen(function* () { Effect.gen(function* () {
const compiled = yield* compile(request, options) const compiled = yield* compile(request, options)
return compiled.route.streamPrepared(compiled.prepared, compiled.request, runtime, options) return compiled.route.streamPrepared(compiled.prepared, compiled.request, runtime)
}), }),
) )
@@ -457,6 +451,7 @@ export const layer: Layer.Layer<Service, never, RequestExecutor.Service> = Layer
Effect.gen(function* () { Effect.gen(function* () {
const stream = streamRequestWith({ const stream = streamRequestWith({
http: yield* RequestExecutor.Service, http: yield* RequestExecutor.Service,
webSocket: Option.getOrUndefined(yield* Effect.serviceOption(WebSocketExecutor.Service)),
}) })
return Service.of({ stream, generate: generateWith(stream) }) return Service.of({ stream, generate: generateWith(stream) })
}), }),
+3 -3
View File
@@ -1,5 +1,5 @@
import type { LLMRequest } from "../schema/index.js" import type { LLMRequest } from "../schema"
import * as ProviderShared from "../protocols/shared.js" import * as ProviderShared from "../protocols/shared"
export interface EndpointInput<Body> { export interface EndpointInput<Body> {
readonly request: LLMRequest readonly request: LLMRequest
@@ -53,4 +53,4 @@ export const render = <Body>(endpoint: Definition<Body>, input: EndpointInput<Bo
return url return url
} }
export * as Endpoint from "./endpoint.js" export * as Endpoint from "./endpoint"
+135 -100
View File
@@ -1,4 +1,4 @@
import { Cause, Context, Effect, Layer, Option, Schema, Stream } from "effect" import { Cause, Context, Effect, Layer, Option, Schema } from "effect"
import { import {
FetchHttpClient, FetchHttpClient,
Headers, Headers,
@@ -14,8 +14,8 @@ import {
HttpResponseDetails, HttpResponseDetails,
AIError, AIError,
TransportReason, TransportReason,
} from "../schema/index.js" } from "../schema"
import { classifyProviderFailure } from "../provider-error.js" import { classifyProviderFailure } from "../provider-error"
export interface Interface { export interface Interface {
readonly execute: ( readonly execute: (
@@ -34,8 +34,44 @@ export type HttpMiddleware = (
export class Service extends Context.Service<Service, Interface>()("@opencode/AI/RequestExecutor") {} export class Service extends Context.Service<Service, Interface>()("@opencode/AI/RequestExecutor") {}
const headerDetails = (headers: Headers.Headers) => const BODY_LIMIT = 16_384
Object.fromEntries(Object.entries(headers).map(([name, value]) => [name, String(value)])) const REDACTED = "<redacted>"
// One source of truth for what counts as a sensitive name across headers,
// URL query keys, and field names embedded inside request/response bodies.
//
// `SENSITIVE_NAME` is used as both a substring matcher (for free-form header
// names like `Authorization` / `X-API-Key`) and as the body-field alternation
// list. `SHORT_QUERY_NAME` covers anchored short keys like `?key=…` / `?sig=…`
// that are too generic to redact substring-style without false positives.
const SENSITIVE_NAME_SOURCE =
"authorization|api[-_]?key|access[-_]?token|refresh[-_]?token|id[-_]?token|token|secret|credential|signature|x-amz-signature"
const SENSITIVE_NAME = new RegExp(SENSITIVE_NAME_SOURCE, "i")
const SHORT_QUERY_NAME = /^(key|sig)$/i
const SENSITIVE_BODY_FIELD = new RegExp(`(?:${SENSITIVE_NAME_SOURCE}|key)`, "i")
const REDACT_JSON_FIELD = new RegExp(`("(?:${SENSITIVE_BODY_FIELD.source})"\\s*:\\s*)"[^"]*"`, "gi")
const REDACT_QUERY_FIELD = new RegExp(`((?:${SENSITIVE_BODY_FIELD.source})=)[^&\\s"]+`, "gi")
const isSensitiveHeaderName = (name: string) => SENSITIVE_NAME.test(name)
const isSensitiveQueryName = (name: string) => isSensitiveHeaderName(name) || SHORT_QUERY_NAME.test(name)
const redactHeaders = (headers: Headers.Headers, redactedNames: ReadonlyArray<string | RegExp>) =>
Object.fromEntries(
Object.entries(Headers.redact(headers, [...redactedNames, SENSITIVE_NAME])).map(([name, value]) => [
name,
String(value),
]),
)
const redactUrl = (value: string) => {
if (!URL.canParse(value)) return REDACTED
const url = new URL(value)
url.searchParams.forEach((_, key) => {
if (isSensitiveQueryName(key)) url.searchParams.set(key, REDACTED)
})
return url.toString()
}
const normalizedHeaders = (headers: Headers.Headers) => const normalizedHeaders = (headers: Headers.Headers) =>
Object.fromEntries(Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value])) Object.fromEntries(Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value]))
@@ -108,22 +144,58 @@ const rateLimitDetails = (headers: Record<string, string>, retryAfter: number |
}) })
} }
const requestDetails = (request: HttpClientRequest.HttpClientRequest) => const requestDetails = (request: HttpClientRequest.HttpClientRequest, redactedNames: ReadonlyArray<string | RegExp>) =>
new HttpRequestDetails({ new HttpRequestDetails({
method: request.method, method: request.method,
url: request.url, url: redactUrl(request.url),
headers: headerDetails(request.headers), headers: redactHeaders(request.headers, redactedNames),
}) })
const responseDetails = (response: HttpClientResponse.HttpClientResponse) => const responseDetails = (
response: HttpClientResponse.HttpClientResponse,
redactedNames: ReadonlyArray<string | RegExp>,
) =>
new HttpResponseDetails({ new HttpResponseDetails({
status: response.status, status: response.status,
headers: headerDetails(response.headers), headers: redactHeaders(response.headers, redactedNames),
}) })
const responseBody = (body: string | void) => { const secretValues = (request: HttpClientRequest.HttpClientRequest) => {
const values = new Set<string>()
const add = (value: string) => {
if (value.length < 4) return
values.add(value)
values.add(encodeURIComponent(value))
}
Object.entries(request.headers).forEach(([name, value]) => {
if (!isSensitiveHeaderName(name)) return
add(value)
const bearer = /^Bearer\s+(.+)$/i.exec(value)?.[1]
if (bearer) add(bearer)
})
if (!URL.canParse(request.url)) return values
new URL(request.url).searchParams.forEach((value, key) => {
if (isSensitiveQueryName(key)) add(value)
})
return values
}
// Two passes: structural (redact `"name": "value"` and `name=value` patterns
// for any field name that looks sensitive) plus literal (replace any actual
// secret values we sent in the request, in case the response echoes one back).
const redactBody = (body: string, secrets: ReadonlySet<string>) =>
Array.from(secrets).reduce(
(text, secret) => text.split(secret).join(REDACTED),
body.replace(REDACT_JSON_FIELD, `$1"${REDACTED}"`).replace(REDACT_QUERY_FIELD, `$1${REDACTED}`),
)
const responseBody = (body: string | void, secrets: ReadonlySet<string>) => {
if (body === undefined) return {} if (body === undefined) return {}
return { body } const redacted = redactBody(body, secrets)
if (redacted.length <= BODY_LIMIT) return { body: redacted }
return { body: redacted.slice(0, BODY_LIMIT), bodyTruncated: true }
} }
const decodeProviderBody = Schema.decodeUnknownOption( const decodeProviderBody = Schema.decodeUnknownOption(
@@ -135,49 +207,52 @@ const decodeProviderBody = Schema.decodeUnknownOption(
), ),
) )
const providerMessage = (status: number, body: string | void) => { const providerMessage = (status: number, body: { readonly body?: string }) => {
const decoded = body === undefined ? undefined : Option.getOrUndefined(decodeProviderBody(body)) if (body.body && body.body.length <= 500) {
return ( const decoded = Option.getOrUndefined(decodeProviderBody(body.body))
[decoded?.error?.message, decoded?.message].find((message) => message?.trim()) ?? return `Provider request failed with HTTP ${status}: ${decoded?.error?.message ?? decoded?.message ?? body.body}`
`Provider request failed with HTTP ${status}` }
) return `Provider request failed with HTTP ${status}`
} }
const responseHttp = (input: { const responseHttp = (input: {
readonly request: HttpClientRequest.HttpClientRequest readonly request: HttpClientRequest.HttpClientRequest
readonly response: HttpClientResponse.HttpClientResponse readonly response: HttpClientResponse.HttpClientResponse
readonly redactedNames: ReadonlyArray<string | RegExp>
readonly body: ReturnType<typeof responseBody> readonly body: ReturnType<typeof responseBody>
readonly requestId?: string | undefined readonly requestId?: string | undefined
readonly rateLimit?: HttpRateLimitDetails | undefined readonly rateLimit?: HttpRateLimitDetails | undefined
}) => }) =>
new HttpContext({ new HttpContext({
request: requestDetails(input.request), request: requestDetails(input.request, input.redactedNames),
response: responseDetails(input.response), response: responseDetails(input.response, input.redactedNames),
...input.body, ...input.body,
requestId: input.requestId, requestId: input.requestId,
rateLimit: input.rateLimit, rateLimit: input.rateLimit,
}) })
const statusError = const statusError =
(request: HttpClientRequest.HttpClientRequest) => (response: HttpClientResponse.HttpClientResponse) => (request: HttpClientRequest.HttpClientRequest, redactedNames: ReadonlyArray<string | RegExp>) =>
(response: HttpClientResponse.HttpClientResponse) =>
Effect.gen(function* () { Effect.gen(function* () {
if (response.status < 400) return response if (response.status < 400) return response
const body = yield* response.text.pipe(Effect.catch(() => Effect.void)) const body = yield* response.text.pipe(Effect.catch(() => Effect.void))
const headers = normalizedHeaders(response.headers) const headers = normalizedHeaders(response.headers)
const retryAfter = retryAfterMs(headers) const retryAfter = retryAfterMs(headers)
const rateLimit = rateLimitDetails(headers, retryAfter) const rateLimit = rateLimitDetails(headers, retryAfter)
const details = responseBody(body) const details = responseBody(body, secretValues(request))
return yield* new AIError({ return yield* new AIError({
module: "RequestExecutor", module: "RequestExecutor",
method: "execute", method: "execute",
reason: classifyProviderFailure({ reason: classifyProviderFailure({
status: response.status, status: response.status,
message: providerMessage(response.status, body), message: providerMessage(response.status, details),
retryAfterMs: retryAfter, retryAfterMs: retryAfter,
rateLimit, rateLimit,
http: responseHttp({ http: responseHttp({
request, request,
response, response,
redactedNames,
body: details, body: details,
requestId: requestId(headers), requestId: requestId(headers),
rateLimit, rateLimit,
@@ -187,10 +262,10 @@ const statusError =
}) })
// Classifies an HTTP failure captured outside the executor (for example by the // Classifies an HTTP failure captured outside the executor (for example by the
// AI SDK's own fetch) onto the same reason types and HttpContext that // AI SDK's own fetch) onto the same reason types and redacted HttpContext that
// executor-driven requests produce. The originating request is not available on // executor-driven requests produce. The originating request is not available on
// that path, so the method is assumed (language model calls are always POST), // that path, so the method is assumed (language model calls are always POST),
// request headers are empty. // request headers are empty, and only structural body redaction applies.
export const classifyHttpFailure = (input: { export const classifyHttpFailure = (input: {
readonly message: string readonly message: string
readonly url: string readonly url: string
@@ -202,7 +277,7 @@ export const classifyHttpFailure = (input: {
const headers = normalizedHeaders(Headers.fromInput(input.responseHeaders)) const headers = normalizedHeaders(Headers.fromInput(input.responseHeaders))
const retryAfter = retryAfterMs(headers) const retryAfter = retryAfterMs(headers)
const rateLimit = rateLimitDetails(headers, retryAfter) const rateLimit = rateLimitDetails(headers, retryAfter)
const details = responseBody(input.responseBody) const details = responseBody(input.responseBody ?? undefined, new Set<string>())
return classifyProviderFailure({ return classifyProviderFailure({
message: input.message, message: input.message,
status: input.status, status: input.status,
@@ -210,11 +285,11 @@ export const classifyHttpFailure = (input: {
retryAfterMs: retryAfter, retryAfterMs: retryAfter,
rateLimit, rateLimit,
http: new HttpContext({ http: new HttpContext({
request: new HttpRequestDetails({ method: "POST", url: input.url, headers: {} }), request: new HttpRequestDetails({ method: "POST", url: redactUrl(input.url), headers: {} }),
response: response:
input.status === undefined input.status === undefined
? undefined ? undefined
: new HttpResponseDetails({ status: input.status, headers: headerDetails(Headers.fromInput(headers)) }), : new HttpResponseDetails({ status: input.status, headers: redactHeaders(Headers.fromInput(headers), []) }),
...details, ...details,
requestId: requestId(headers), requestId: requestId(headers),
rateLimit, rateLimit,
@@ -222,102 +297,62 @@ export const classifyHttpFailure = (input: {
}) })
} }
type HttpOperation = "request" | "read" const toHttpError = (redactedNames: ReadonlyArray<string | RegExp>) => (error: unknown) => {
const transportError = (input: {
const NativeTransportFailure = Schema.Struct({ readonly message: string
message: Schema.String, readonly kind?: string | undefined
code: Schema.optionalKey(Schema.String), readonly request?: HttpClientRequest.HttpClientRequest | undefined
cause: Schema.optionalKey(Schema.Unknown), }) =>
})
const decodeNativeTransportFailure = Schema.decodeUnknownOption(NativeTransportFailure)
const nativeTransportFailure = (error: unknown) => {
const failure = Option.getOrUndefined(decodeNativeTransportFailure(error))
if (!failure) return undefined
if (failure.code !== undefined) return failure
const cause = Option.getOrUndefined(decodeNativeTransportFailure(failure.cause))
if (cause?.code !== undefined) return cause
return failure
}
const httpError = (input: {
readonly error: unknown
readonly request: HttpClientRequest.HttpClientRequest
readonly operation: HttpOperation
}) => {
const request = HttpClientError.isHttpClientError(input.error) ? input.error.request : input.request
const transportError = (failure: { readonly message: string; readonly code?: string | undefined }) =>
new AIError({ new AIError({
module: "RequestExecutor", module: "RequestExecutor",
method: input.operation, method: "execute",
reason: new TransportReason({ reason: new TransportReason({
message: failure.message, message: input.message,
transport: "http", kind: input.kind,
operation: input.operation, url: input.request ? redactUrl(input.request.url) : undefined,
code: failure.code, http: input.request ? new HttpContext({ request: requestDetails(input.request, redactedNames) }) : undefined,
url: request.url,
http: new HttpContext({ request: requestDetails(request) }),
}), }),
}) })
const source = if (Cause.isTimeoutError(error)) {
HttpClientError.isHttpClientError(input.error) && "cause" in input.error.reason return transportError({ message: error.message, kind: "Timeout" })
? input.error.reason.cause }
: input.error if (!HttpClientError.isHttpClientError(error)) {
const native = nativeTransportFailure(source) return transportError({ message: error instanceof Error ? error.message : "HTTP transport failed" })
const code = native?.code }
const raw = native?.message ?? (input.error instanceof Error ? input.error.message : undefined) const request = "request" in error ? error.request : undefined
const detail = raw if (error.reason._tag === "TransportError") {
const message = code && detail && !detail.includes(code) ? `${code}: ${detail}` : detail
if (Cause.isTimeoutError(input.error) || Cause.isTimeoutError(source))
return transportError({ message: message ?? "HTTP transport timed out", code: code ?? "Timeout" })
if (!HttpClientError.isHttpClientError(input.error))
return transportError({ message: message ?? "HTTP transport failed", code })
if (input.error.reason._tag === "TransportError") {
return transportError({ return transportError({
message: message ?? input.error.reason.description ?? "HTTP transport failed", message: error.reason.description ?? "HTTP transport failed",
code: code ?? input.error.reason._tag, kind: error.reason._tag,
request,
}) })
} }
return transportError({ return transportError({
message: message ?? `HTTP transport failed: ${input.error.reason._tag}`, message: `HTTP transport failed: ${error.reason._tag}`,
code: code ?? input.error.reason._tag, kind: error.reason._tag,
request,
}) })
} }
export const stream = (
executor: Interface,
request: HttpClientRequest.HttpClientRequest,
middleware?: HttpMiddleware,
): Stream.Stream<Uint8Array, AIError> =>
Stream.unwrap(
Effect.gen(function* () {
const response = yield* executor.execute(request, middleware)
return response.stream.pipe(
Stream.mapError((error) => httpError({ error, request: response.request, operation: "read" })),
)
}),
)
export const layer: Layer.Layer<Service, never, HttpClient.HttpClient> = Layer.effect( export const layer: Layer.Layer<Service, never, HttpClient.HttpClient> = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const http = yield* HttpClient.HttpClient const http = yield* HttpClient.HttpClient
const executeOnce = (request: HttpClientRequest.HttpClientRequest, middleware?: HttpMiddleware) => const executeOnce = (request: HttpClientRequest.HttpClientRequest, middleware?: HttpMiddleware) =>
Effect.gen(function* () { Effect.gen(function* () {
const redactedNames = yield* Headers.CurrentRedactedNames
if (!middleware) if (!middleware)
return yield* http.execute(request).pipe( return yield* http
Effect.mapError((error) => httpError({ error, request, operation: "request" })), .execute(request)
Effect.flatMap(statusError(request)), .pipe(Effect.mapError(toHttpError(redactedNames)), Effect.flatMap(statusError(request, redactedNames)))
)
const response = yield* middleware(request, (input) => const response = yield* middleware(request, (input) =>
http http
.execute(input) .execute(input)
.pipe(Effect.mapError((cause) => (cause instanceof Error ? cause : new Error(String(cause))))), .pipe(Effect.mapError((cause) => (cause instanceof Error ? cause : new Error(String(cause))))),
).pipe(Effect.mapError((error) => httpError({ error, request, operation: "request" }))) ).pipe(Effect.mapError(toHttpError(redactedNames)))
return yield* statusError(response.request)(response) return yield* statusError(response.request, redactedNames)(response)
}) })
return Service.of({ return Service.of({
execute: executeOnce, execute: executeOnce,
@@ -327,4 +362,4 @@ export const layer: Layer.Layer<Service, never, HttpClient.HttpClient> = Layer.e
export const fetchLayer = layer.pipe(Layer.provide(FetchHttpClient.layer)) export const fetchLayer = layer.pipe(Layer.provide(FetchHttpClient.layer))
export * as RequestExecutor from "./executor.js" export * as RequestExecutor from "./executor"
+3 -3
View File
@@ -1,6 +1,6 @@
import type { Stream } from "effect" import type { Stream } from "effect"
import * as ProviderShared from "../protocols/shared.js" import * as ProviderShared from "../protocols/shared"
import type { AIError } from "../schema/index.js" import type { AIError } from "../schema"
/** /**
* Decode a streaming HTTP response body into provider-protocol frames. * Decode a streaming HTTP response body into provider-protocol frames.
@@ -24,4 +24,4 @@ export interface Definition<Frame> {
/** Server-Sent Events framing. Used by every JSON-streaming HTTP provider. */ /** Server-Sent Events framing. Used by every JSON-streaming HTTP provider. */
export const sse: Definition<string> = { id: "sse", frame: ProviderShared.sseFraming } export const sse: Definition<string> = { id: "sse", frame: ProviderShared.sseFraming }
export * as Framing from "./framing.js" export * as Framing from "./framing"
+16 -33
View File
@@ -1,4 +1,4 @@
export { Route, LLMClient } from "./client.js" export { Route, LLMClient } from "./client"
export type { export type {
Route as RouteShape, Route as RouteShape,
RouteLanguageModelInput, RouteLanguageModelInput,
@@ -9,35 +9,18 @@ export type {
Interface as LLMClientShape, Interface as LLMClientShape,
Service as LLMClientService, Service as LLMClientService,
StreamOptions, StreamOptions,
} from "./client.js" } from "./client"
export * from "./executor.js" export * from "./executor"
export { Auth } from "./auth.js" export { Auth } from "./auth"
export { AuthOptions } from "./auth-options.js" export { AuthOptions } from "./auth-options"
export { Endpoint } from "./endpoint.js" export { Endpoint } from "./endpoint"
export { Framing } from "./framing.js" export { Framing } from "./framing"
export { Protocol } from "./protocol.js" export { Protocol } from "./protocol"
export { HttpTransport, WebSocketTransport } from "./transport/index.js" export { HttpTransport, WebSocketExecutor, WebSocketTransport } from "./transport"
export * as Transport from "./transport/index.js" export * as Transport from "./transport"
export type { Definition as AuthShape, AuthInput, Credential, CredentialError } from "./auth.js" export type { Definition as AuthShape, AuthInput, Credential, CredentialError } from "./auth"
export type { ApiKeyMode, AuthOverride, ProviderAuthOption } from "./auth-options.js" export type { ApiKeyMode, AuthOverride, ProviderAuthOption } from "./auth-options"
export type { Definition as EndpointFn, EndpointInput } from "./endpoint.js" export type { Definition as EndpointFn, EndpointInput } from "./endpoint"
export type { Definition as FramingDef } from "./framing.js" export type { Definition as FramingDef } from "./framing"
export type { Protocol as ProtocolDef } from "./protocol.js" export type { Protocol as ProtocolDef } from "./protocol"
export type { export type { HttpHandler, HttpMiddleware, Transport as TransportDef, TransportRuntime } from "./transport"
ChannelCheckpoint,
ChannelCreate,
ChannelObservation,
HttpHandler,
HttpMiddleware,
Transport as TransportDef,
TransportExecuteOptions,
TransportExecution,
TransportRuntime,
WebSocketConnection,
WebSocketChannelDriver,
WebSocketChannelExchange,
WebSocketChannelExecution,
WebSocketChannelExecutor,
WebSocketConnector,
WebSocketRequest,
} from "./transport/index.js"
+2 -2
View File
@@ -1,5 +1,5 @@
import { Schema, type Effect } from "effect" import { Schema, type Effect } from "effect"
import type { AIError, LLMEvent, LLMRequest, ProtocolID } from "../schema/index.js" import type { AIError, LLMEvent, LLMRequest, ProtocolID } from "../schema"
/** /**
* The semantic API contract of one model server family. * The semantic API contract of one model server family.
@@ -82,4 +82,4 @@ export const make = <Body, Frame, Event, State>(
export const jsonEvent = <const S extends Schema.Top>(schema: S) => Schema.fromJsonString(schema) export const jsonEvent = <const S extends Schema.Top>(schema: S) => Schema.fromJsonString(schema)
export * as Protocol from "./protocol.js" export * as Protocol from "./protocol"

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