mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-29 14:41:48 -04:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 182494f885 | |||
| 2b767d8cb1 |
@@ -1,13 +0,0 @@
|
||||
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,
|
||||
// })
|
||||
},
|
||||
}
|
||||
@@ -147,11 +147,6 @@
|
||||
"ws": "8.21.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ff-labs/fff-bin-darwin-arm64": "0.10.1",
|
||||
"@ff-labs/fff-bin-linux-arm64-gnu": "0.10.1",
|
||||
"@ff-labs/fff-bin-linux-x64-gnu": "0.10.1",
|
||||
"@ff-labs/fff-bin-win32-arm64": "0.10.1",
|
||||
"@ff-labs/fff-bin-win32-x64": "0.10.1",
|
||||
"@lydell/node-pty-darwin-arm64": "1.2.0-beta.12",
|
||||
"@lydell/node-pty-darwin-x64": "1.2.0-beta.12",
|
||||
"@lydell/node-pty-linux-arm64": "1.2.0-beta.12",
|
||||
@@ -169,11 +164,6 @@
|
||||
"@types/bun": "catalog:",
|
||||
"@types/semver": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"@yuuang/ffi-rs-darwin-arm64": "1.3.2",
|
||||
"@yuuang/ffi-rs-linux-arm64-gnu": "1.3.2",
|
||||
"@yuuang/ffi-rs-linux-x64-gnu": "1.3.2",
|
||||
"@yuuang/ffi-rs-win32-arm64-msvc": "1.3.2",
|
||||
"@yuuang/ffi-rs-win32-x64-msvc": "1.3.2",
|
||||
"vite": "catalog:",
|
||||
"vite-plugin-solid": "catalog:",
|
||||
},
|
||||
@@ -374,7 +364,6 @@
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@effect/sql-sqlite-bun": "catalog:",
|
||||
"@ff-labs/fff-bun": "0.10.1",
|
||||
"@ff-labs/fff-node": "0.10.1",
|
||||
"@lydell/node-pty": "catalog:",
|
||||
"@modelcontextprotocol/sdk": "1.29.0",
|
||||
"@opencode-ai/ai": "workspace:*",
|
||||
@@ -600,7 +589,6 @@
|
||||
"zod": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@opencode-ai/theme": "workspace:*",
|
||||
"@opentui/core": "catalog:",
|
||||
"@opentui/keymap": "catalog:",
|
||||
"@opentui/solid": "catalog:",
|
||||
@@ -608,22 +596,17 @@
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"solid-js": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opencode-ai/theme": "workspace:*",
|
||||
"@opentui/core": ">=0.4.5",
|
||||
"@opentui/keymap": ">=0.4.5",
|
||||
"@opentui/solid": ">=0.4.5",
|
||||
"solid-js": ">=1.9.0",
|
||||
},
|
||||
"optionalPeers": [
|
||||
"@opencode-ai/theme",
|
||||
"@opentui/core",
|
||||
"@opentui/keymap",
|
||||
"@opentui/solid",
|
||||
"solid-js",
|
||||
],
|
||||
},
|
||||
"packages/protocol": {
|
||||
@@ -876,20 +859,6 @@
|
||||
"vite": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/theme": {
|
||||
"name": "@opencode-ai/theme",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@opentui/core": "catalog:",
|
||||
"effect": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/tui": {
|
||||
"name": "@opencode-ai/tui",
|
||||
"version": "1.18.4",
|
||||
@@ -899,7 +868,6 @@
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@opencode-ai/simulation": "workspace:*",
|
||||
"@opencode-ai/theme": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@opentui/core": "catalog:",
|
||||
@@ -1062,7 +1030,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/cloudflare": "14.1.4",
|
||||
"@opencode-ai/theme": "workspace:*",
|
||||
"@types/bun": "catalog:",
|
||||
"astro": "7.1.3",
|
||||
"effect": "catalog:",
|
||||
@@ -1710,8 +1677,6 @@
|
||||
|
||||
"@ff-labs/fff-bun": ["@ff-labs/fff-bun@0.10.1", "", { "optionalDependencies": { "@ff-labs/fff-bin-android-arm64": "0.10.1", "@ff-labs/fff-bin-darwin-arm64": "0.10.1", "@ff-labs/fff-bin-darwin-x64": "0.10.1", "@ff-labs/fff-bin-linux-arm64-gnu": "0.10.1", "@ff-labs/fff-bin-linux-arm64-musl": "0.10.1", "@ff-labs/fff-bin-linux-x64-gnu": "0.10.1", "@ff-labs/fff-bin-linux-x64-musl": "0.10.1", "@ff-labs/fff-bin-win32-arm64": "0.10.1", "@ff-labs/fff-bin-win32-x64": "0.10.1" }, "os": [ "!aix", "!sunos", "!freebsd", "!openbsd", ], "cpu": [ "x64", "arm64", ] }, "sha512-9oUCxypGbf2q3vNfKZ31wdzt5KqjhA9S6TwQaFol/j1lkSHVGvtIa3RvdGHPs1UUuvR/MO7b6pHj054BR9bXPQ=="],
|
||||
|
||||
"@ff-labs/fff-node": ["@ff-labs/fff-node@0.10.1", "", { "dependencies": { "ffi-rs": "^1.0.0" }, "optionalDependencies": { "@ff-labs/fff-bin-android-arm64": "0.10.1", "@ff-labs/fff-bin-darwin-arm64": "0.10.1", "@ff-labs/fff-bin-darwin-x64": "0.10.1", "@ff-labs/fff-bin-linux-arm64-gnu": "0.10.1", "@ff-labs/fff-bin-linux-arm64-musl": "0.10.1", "@ff-labs/fff-bin-linux-x64-gnu": "0.10.1", "@ff-labs/fff-bin-linux-x64-musl": "0.10.1", "@ff-labs/fff-bin-win32-arm64": "0.10.1", "@ff-labs/fff-bin-win32-x64": "0.10.1" }, "os": [ "!aix", "!sunos", "!freebsd", "!openbsd", ], "cpu": [ "x64", "arm64", ] }, "sha512-I2TIWHkey4wpLCncUQymBz/yWrjp/TE91yjrPvSLaPMUMRf3wXwEmelGNnZ5ynxy3cUauYg5b5hsUwhOK4skfA=="],
|
||||
|
||||
"@floating-ui/core": ["@floating-ui/core@1.8.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.12" } }, "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ=="],
|
||||
|
||||
"@floating-ui/dom": ["@floating-ui/dom@1.8.0", "", { "dependencies": { "@floating-ui/core": "^1.8.0", "@floating-ui/utils": "^0.2.12" } }, "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg=="],
|
||||
@@ -2110,8 +2075,6 @@
|
||||
|
||||
"@opencode-ai/storybook": ["@opencode-ai/storybook@workspace:packages/storybook"],
|
||||
|
||||
"@opencode-ai/theme": ["@opencode-ai/theme@workspace:packages/theme"],
|
||||
|
||||
"@opencode-ai/tui": ["@opencode-ai/tui@workspace:packages/tui"],
|
||||
|
||||
"@opencode-ai/ui": ["@opencode-ai/ui@workspace:packages/ui"],
|
||||
@@ -3288,28 +3251,6 @@
|
||||
|
||||
"@xmldom/xmldom": ["@xmldom/xmldom@0.8.13", "", {}, "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw=="],
|
||||
|
||||
"@yuuang/ffi-rs-android-arm64": ["@yuuang/ffi-rs-android-arm64@1.3.2", "", { "os": "android", "cpu": "arm64" }, "sha512-eDYLT0kVBkp7e2BwdRDmt6N1rkeDPUHDefk3ZX0/nok+GLsqfy1WBoSL3Yg7HVXN1EyW8OBVc2uK8Zq8HbmaSA=="],
|
||||
|
||||
"@yuuang/ffi-rs-darwin-arm64": ["@yuuang/ffi-rs-darwin-arm64@1.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kRdgPaOM6TfuC5wHUwstlatk4HNie2lwSLJWQL2LiAUIJ7+96CoiWUNVhwBcFrhdfxhnWenYS6F668CV0vit8Q=="],
|
||||
|
||||
"@yuuang/ffi-rs-darwin-x64": ["@yuuang/ffi-rs-darwin-x64@1.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-O3AlVgre8FQcZRJe44Xs7A6iDLumoPXqbw40+eJCa2gyXaXyLPdHoWrS1W9rBCa1QZRRnG7zRulPVFw8C5uo8g=="],
|
||||
|
||||
"@yuuang/ffi-rs-linux-arm-gnueabihf": ["@yuuang/ffi-rs-linux-arm-gnueabihf@1.3.2", "", { "os": "linux", "cpu": "arm" }, "sha512-IXiNdTbIcTCPny5eeElijFWYeKSJjQWSjt9ZyJNdLHYiB1Np+XD6K7wNZS6EOMgMelhW1kQE62T654skGkVDIA=="],
|
||||
|
||||
"@yuuang/ffi-rs-linux-arm64-gnu": ["@yuuang/ffi-rs-linux-arm64-gnu@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-gWFO6xufUK9lPYUqDvKa6IR243dPqdetgl9Q7HrZWaDu7wLo06QQrosw8QTzndafQnOcBKm6LoLujmGCfTgJOA=="],
|
||||
|
||||
"@yuuang/ffi-rs-linux-arm64-musl": ["@yuuang/ffi-rs-linux-arm64-musl@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-lejvOSqypPziQH5rzfkDlJ6e92qhWbDutE9ttOO6z5I2k83zoh9iZhZWhaXSU5VqgQpcshRkrbtXb9gy1ft5dA=="],
|
||||
|
||||
"@yuuang/ffi-rs-linux-x64-gnu": ["@yuuang/ffi-rs-linux-x64-gnu@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-s8VCFazaJKmgY2hgMTpWk4TtBY/zy5ovbaGgwyY0FvBD0YvyhcET4IrMsDJpHhFVTPCYfKZ1dN45clD/YiFp6g=="],
|
||||
|
||||
"@yuuang/ffi-rs-linux-x64-musl": ["@yuuang/ffi-rs-linux-x64-musl@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Ahr5chfKZKWUik20bEZRug+be57LZ2yYrtolyjSRoo7A4ZniBUHBZUNWm6TD6i0CJayqyxWeVk/XiaABD8bY0w=="],
|
||||
|
||||
"@yuuang/ffi-rs-win32-arm64-msvc": ["@yuuang/ffi-rs-win32-arm64-msvc@1.3.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-yhpLcj0qel5VNlpzxPZfNmi7+rEX8444QHjUP6WWLxdRfqPllROu/Cp3OpkBpw3BLdxfcDhWkjWMD5QsJN0Pvg=="],
|
||||
|
||||
"@yuuang/ffi-rs-win32-ia32-msvc": ["@yuuang/ffi-rs-win32-ia32-msvc@1.3.2", "", { "os": "win32", "cpu": [ "x64", "ia32", ] }, "sha512-BFVSbdtg/7mJBw5kQFOPKFiA+SF7z3240HpzHN81Umm4Bp4dWkyx0msYn8+Q7/BBJiLQ4F6bi3Nftk58YA9r9w=="],
|
||||
|
||||
"@yuuang/ffi-rs-win32-x64-msvc": ["@yuuang/ffi-rs-win32-x64-msvc@1.3.2", "", { "os": "win32", "cpu": "x64" }, "sha512-ZL5MJ76n2rjwGo26kCWW7wK6QT/cee00Rx8pfW79pz6vM6jqfhoE7zTnwFiw4aOQUes9+HUc5DeeJ3z+Vb9oLg=="],
|
||||
|
||||
"@zip.js/zip.js": ["@zip.js/zip.js@2.7.62", "", {}, "sha512-OaLvZ8j4gCkLn048ypkZu29KX30r8/OfFF2w4Jo5WXFr+J04J+lzJ5TKZBVgFXhlvSkqNFQdfnY1Q8TMTCyBVA=="],
|
||||
|
||||
"abbrev": ["abbrev@4.0.0", "", {}, "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA=="],
|
||||
@@ -4074,8 +4015,6 @@
|
||||
|
||||
"file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="],
|
||||
|
||||
"ffi-rs": ["ffi-rs@1.3.2", "", { "optionalDependencies": { "@yuuang/ffi-rs-android-arm64": "1.3.2", "@yuuang/ffi-rs-darwin-arm64": "1.3.2", "@yuuang/ffi-rs-darwin-x64": "1.3.2", "@yuuang/ffi-rs-linux-arm-gnueabihf": "1.3.2", "@yuuang/ffi-rs-linux-arm64-gnu": "1.3.2", "@yuuang/ffi-rs-linux-arm64-musl": "1.3.2", "@yuuang/ffi-rs-linux-x64-gnu": "1.3.2", "@yuuang/ffi-rs-linux-x64-musl": "1.3.2", "@yuuang/ffi-rs-win32-arm64-msvc": "1.3.2", "@yuuang/ffi-rs-win32-ia32-msvc": "1.3.2", "@yuuang/ffi-rs-win32-x64-msvc": "1.3.2" } }, "sha512-4s8dX9VbBw/jd5NOuE3EJRqXaIVdjMyiumeeDzrOhtjQRwp6Bz2za7iksWXTnvTQKV/tTdm1s1w7mObe92zPjQ=="],
|
||||
|
||||
"filelist": ["filelist@1.0.6", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA=="],
|
||||
|
||||
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
# V1 to V2 Database Migration
|
||||
|
||||
## Approach
|
||||
|
||||
- Use the `dev` branch database schema and migration registry as the V1 baseline.
|
||||
- Remove migrations that exist only on the V2 branch.
|
||||
- Generate one canonical migration from the `dev` schema to the final V2 schema.
|
||||
- Add explicit data operations to that migration where generated DDL is insufficient.
|
||||
- Test the migration against a populated database at the exact `dev` schema.
|
||||
|
||||
## Preserve
|
||||
|
||||
The canonical V1 data remains in its existing tables. In particular, preserve `session`, `message`, and `part` rows.
|
||||
|
||||
Preserve `workspace` rows and existing `session.workspace_id` values unchanged. The migration must not clear or rebuild
|
||||
workspace relationships.
|
||||
|
||||
Keep the `todo` table and its data unchanged. V2 does not currently migrate todos into another representation, and the
|
||||
generated migration must not drop the table.
|
||||
|
||||
## Truncate
|
||||
|
||||
Truncate these pre-launch V2 tables before applying schema changes:
|
||||
|
||||
- `event`
|
||||
- `event_sequence`
|
||||
- `session_message`
|
||||
|
||||
These rows are not canonical V1 data. Truncating `event` before adding the required `event.created` column means the
|
||||
column needs neither a backfill nor a default. After truncation, rebuild `session_message` from canonical V1 `message`
|
||||
and `part` rows rather than retaining its pre-launch V2 contents.
|
||||
|
||||
## Message Backfill
|
||||
|
||||
Backfill canonical V1 history from `message` and `part` into `session_message`. This is the main data transformation in
|
||||
the migration. Preserving the V1 tables alone keeps the data safe but does not make existing history visible through the
|
||||
V2 session APIs, which read `session_message`.
|
||||
|
||||
Reuse each V1 `message.id` as the corresponding `session_message.id`. Stable IDs keep the migration deterministic and
|
||||
avoid rewriting other persisted state that may refer to a message.
|
||||
|
||||
Within each session, order V1 messages by `time_created` and then `id`, matching the existing V1 message index. Assign
|
||||
contiguous `session_message.seq` values starting at `0`.
|
||||
|
||||
Map ordinary V1 messages one-to-one by role. Each ordinary V1 user message becomes one V2 `user` row, and each ordinary
|
||||
V1 assistant message becomes one V2 `assistant` row. Fold the source message's ordered V1 parts into that row's V2
|
||||
payload.
|
||||
|
||||
Handle semantic marker parts before applying the ordinary mapping. In particular, a V1 user message containing a
|
||||
`compaction` part and its paired assistant summary represent one compaction operation, not two ordinary messages. Special
|
||||
part mappings must be decided explicitly before implementing the backfill.
|
||||
|
||||
V1 synthetic content is represented by user text parts with `synthetic: true`, not by a separate message role. A V1 user
|
||||
message whose visible text parts are all synthetic should become a V2 `synthetic` message. If a V1 user message mixes
|
||||
ordinary and synthetic content, preserve the ordinary content in the V2 `user` row and emit the synthetic content as an
|
||||
adjacent V2 `synthetic` row. Ignore text parts marked `ignored`, matching V1 model-history behavior.
|
||||
|
||||
Use the V1 compaction user message ID as the ID of the collapsed V2 compaction message. This matches V2's use of the
|
||||
admitted compaction input ID and preserves references to the initiating message.
|
||||
|
||||
For a completed compaction, create one V2 `compaction` row with `status: "completed"`. Set `reason` from the V1
|
||||
compaction part's `auto` flag, join the paired summary assistant's nonempty text parts with blank lines for `summary`, and
|
||||
serialize the retained V1 tail beginning at `tail_start_id` for `recent`. Use an empty `recent` value when no tail was
|
||||
retained, and use the compaction user message creation time. Do not emit the paired summary assistant as a separate V2
|
||||
assistant row.
|
||||
|
||||
After rebuilding `session_message`, seed `event_sequence` with one row per migrated session. Set its watermark to that
|
||||
session's maximum backfilled `session_message.seq`. This prevents new V2 events from reusing sequence numbers or sorting
|
||||
before migrated history. The `event` table remains empty.
|
||||
|
||||
## Drop
|
||||
|
||||
Drop these pre-launch V2 tables without preserving or transforming their rows:
|
||||
|
||||
- `session_input`
|
||||
- `session_context_epoch`
|
||||
|
||||
Do not transfer `session_input` rows into `session_pending`.
|
||||
|
||||
## Create Empty
|
||||
|
||||
Let the generated migration create these tables empty:
|
||||
|
||||
- `instruction_blob`
|
||||
- `instruction_entry`
|
||||
- `instruction_state`
|
||||
- `session_pending`
|
||||
- `kv`
|
||||
|
||||
V1 has no canonical data to backfill into these tables. V2 initializes their state as it runs.
|
||||
|
||||
## Fork Storage
|
||||
|
||||
V1 has no fork-boundary state to backfill. New V2 forks use a required message boundary and persist it in
|
||||
`session.fork_boundary`. The durable fork event contains no parent sequence. Its resolved boundary is one of:
|
||||
|
||||
- `before`: copy messages before the identified message.
|
||||
- `through`: copy messages through the identified message.
|
||||
|
||||
Forking an empty session is not supported. `session.fork_seq` and `session.fork_message_id` are not part of the final V2
|
||||
schema.
|
||||
|
||||
New nullable session columns, including `fork_session_id`, `fork_boundary`, and `time_suspended`, require no explicit
|
||||
backfill. Existing rows naturally receive `NULL` when the generated migration adds the columns.
|
||||
|
||||
## Verification
|
||||
|
||||
The canonical migration test should seed representative V1 sessions, messages, parts, todos, projects, accounts,
|
||||
credentials, permissions, shares, and workspaces. After migration, it should verify:
|
||||
|
||||
- Preserved rows and encoded values remain unchanged.
|
||||
- Todo rows remain available in the unchanged `todo` table.
|
||||
- `event` is empty, and stale pre-launch rows are absent from the rebuilt projections.
|
||||
- Backfilled `session_message` rows represent the canonical V1 `message` and `part` history.
|
||||
- Each migrated session's `event_sequence` watermark matches its maximum backfilled message sequence.
|
||||
- Dropped tables no longer exist.
|
||||
- New tables exist and are empty.
|
||||
- The final schema has no ungenerated changes.
|
||||
@@ -46,7 +46,7 @@ const response = yield * LLMClient.generate(request)
|
||||
|
||||
`LLM.request(...)` builds an `LLMRequest`. `LLMClient.generate(...)` reads the executable route carried by `request.model.route`, builds the provider-native body, asks the route's transport for a real `HttpClientRequest.HttpClientRequest`, sends it through `RequestExecutor.Service`, parses the provider stream into common `LLMEvent`s, and finally returns an `LLMResponse`.
|
||||
|
||||
Use `LLMClient.stream(request)` when callers want incremental `LLMEvent`s. Use `LLMClient.generate(request)` when callers want those same events collected into an `LLMResponse`.
|
||||
Use `LLMClient.stream(request)` when callers want incremental `LLMEvent`s. Use `LLMClient.generate(request)` when callers want those same events collected into an `LLMResponse`. Use `LLMClient.prepare<Body>(request)` to compile a request through the route pipeline without sending it — the optional `Body` type argument narrows `.body` to the route's native shape (e.g. `prepare<OpenAIChatBody>(...)` returns a `PreparedRequestOf<OpenAIChatBody>`). The runtime body is identical; the generic is a type-level assertion.
|
||||
|
||||
Filter or narrow `LLMEvent` streams with `LLMEvent.is.*` (camelCase guards, e.g. `events.filter(LLMEvent.is.toolCall)`). The kebab-case `LLMEvent.guards["tool-call"]` form also works but prefer `is.*` in new code.
|
||||
|
||||
@@ -138,13 +138,13 @@ packages/ai/src/
|
||||
ids.ts branded IDs, literal types, ProviderMetadata
|
||||
options.ts Generation/Provider/Http options, Limits, Model, cache policy
|
||||
messages.ts content parts, Message, ToolDefinition, LLMRequest
|
||||
events.ts Usage, individual events, LLMEvent, LLMResponse
|
||||
events.ts Usage, individual events, LLMEvent, PreparedRequest, LLMResponse
|
||||
errors.ts error reasons, LLMError, ToolFailure
|
||||
index.ts barrel
|
||||
llm.ts request constructors and convenience helpers
|
||||
route/
|
||||
index.ts @opencode-ai/ai/route advanced barrel
|
||||
client.ts Route.make + LLMClient.stream/generate
|
||||
client.ts Route.make + LLMClient.prepare/stream/generate
|
||||
executor.ts RequestExecutor service + transport error mapping
|
||||
protocol.ts Protocol type + Protocol.make
|
||||
endpoint.ts Endpoint type + Endpoint.path
|
||||
|
||||
@@ -196,6 +196,7 @@ The hosted result is represented as a provider-executed tool call and tool resul
|
||||
- **`LLM.generate` / `LLM.stream`** — re-exported from `LLMClient` for one-import use.
|
||||
- **`Message.user(...)` / `Message.assistant(...)` / `Message.tool(...)`** — message constructors from the canonical schema model.
|
||||
- **`Model.make(...)` / `ToolCallPart.make(...)` / `ToolResultPart.make(...)` / `ToolDefinition.make(...)`** — model and tool-related constructors from the canonical schema model.
|
||||
- **`LLMClient.prepare(request)`** — compile a request through protocol body construction, validation, and HTTP preparation without sending. Useful for inspection and testing.
|
||||
- **`LLMEvent.is.*`** — typed guards (`is.textDelta`, `is.toolCall`, `is.finish`, …) for filtering streams.
|
||||
- **`Image.generate({...})`** — generate images through a provider-neutral image request and response model.
|
||||
- **`ImageClient`** — Effect service and layer for image execution, parallel to `LLMClient`.
|
||||
|
||||
@@ -568,7 +568,7 @@ App boundary = explicit durable-config -> typed-provider call
|
||||
calling `.model(...)`.
|
||||
- [x] Remove request-shaping defaults from `Model`; selected models now carry only
|
||||
id, provider, and configured route while defaults live on routes or requests.
|
||||
- [x] Rework `LLMClient.stream` / `generate` to read
|
||||
- [x] Rework `LLMClient.prepare` / `stream` / `generate` to read
|
||||
`request.model.route` directly instead of calling `registeredRoute(...)`.
|
||||
- [x] Remove `Route.make(...)` global registration from the normal execution
|
||||
path; keep route ids only as diagnostics/provider API labels.
|
||||
|
||||
@@ -50,6 +50,18 @@ const request = LLM.request({
|
||||
},
|
||||
})
|
||||
|
||||
// `http` is intentionally not needed for normal calls. This shows the shape for
|
||||
// newly released provider fields before they deserve a typed provider option.
|
||||
const rawOverlayExample = LLM.request({
|
||||
model,
|
||||
prompt: "Show the final HTTP overlay shape.",
|
||||
http: {
|
||||
body: { metadata: { example: "tutorial" } },
|
||||
headers: { "x-opencode-tutorial": "1" },
|
||||
query: { debug: "1" },
|
||||
},
|
||||
})
|
||||
|
||||
// 3. `generate` sends the request and collects the event stream into one
|
||||
// response object. `response.text` is the collected text output.
|
||||
const generateOnce = Effect.gen(function* () {
|
||||
@@ -210,15 +222,33 @@ const FakeEcho = {
|
||||
}),
|
||||
}
|
||||
|
||||
// `LLMClient.prepare` is the lower-level inspection hook: it compiles through
|
||||
// body conversion, validation, endpoint, auth, and HTTP construction without
|
||||
// sending anything over the network.
|
||||
const inspectFakeProvider = Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
model: FakeEcho.configure().model("tiny-echo"),
|
||||
prompt: "Show me the provider pipeline.",
|
||||
}),
|
||||
)
|
||||
|
||||
console.log("\n== fake provider prepare ==")
|
||||
console.log("route:", prepared.route)
|
||||
console.log("body:", Formatter.formatJson(prepared.body, { space: 2 }))
|
||||
})
|
||||
|
||||
// Provide the LLM runtime and the HTTP request executor once. Keep one path
|
||||
// enabled at a time so the tutorial can demonstrate generate, stream, or
|
||||
// tool-loop behavior without spending tokens on every example.
|
||||
// enabled at a time so the tutorial can demonstrate generate, prepare, stream,
|
||||
// or tool-loop behavior without spending tokens on every example.
|
||||
const requestExecutorLayer = RequestExecutor.fetchLayer
|
||||
const llmDeps = Layer.mergeAll(requestExecutorLayer, WebSocketExecutor.layer)
|
||||
const llmClientLayer = LLMClient.layer.pipe(Layer.provide(llmDeps))
|
||||
|
||||
const program = Effect.gen(function* () {
|
||||
// yield* generateOnce
|
||||
// yield* inspectFakeProvider
|
||||
// yield* LLMClient.prepare(rawOverlayExample).pipe(Effect.andThen((prepared) => Effect.sync(() => console.log(prepared.body))))
|
||||
// yield* streamText
|
||||
// yield* generateStructuredObject
|
||||
// yield* generateDynamicObject.pipe(Effect.andThen((response) => Effect.sync(() => console.log(response.object))))
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
],
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./testing": "./src/testing.ts",
|
||||
"./*": "./src/*.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
+11
-16
@@ -9,28 +9,25 @@ import {
|
||||
LLMRequest,
|
||||
LLMResponse,
|
||||
Message,
|
||||
Model,
|
||||
SystemPart,
|
||||
ToolChoice,
|
||||
ToolDefinition,
|
||||
type ContentPart,
|
||||
type ModelProviderOptions,
|
||||
} from "./schema"
|
||||
import { make as makeTool, toDefinitions, type ToolSchema } from "./tool"
|
||||
|
||||
/** Input accepted by `LLM.request`, normalized into the canonical `LLMRequest` class. */
|
||||
export type RequestInput<SelectedModel extends Model = Model> = Omit<
|
||||
export type RequestInput = Omit<
|
||||
ConstructorParameters<typeof LLMRequest>[0],
|
||||
"model" | "system" | "messages" | "tools" | "toolChoice" | "generation" | "http" | "providerOptions"
|
||||
"system" | "messages" | "tools" | "toolChoice" | "generation" | "http" | "providerOptions"
|
||||
> & {
|
||||
readonly model: SelectedModel
|
||||
readonly system?: string | SystemPart | ReadonlyArray<SystemPart>
|
||||
readonly prompt?: string | ContentPart | ReadonlyArray<ContentPart>
|
||||
readonly messages?: ReadonlyArray<Message | Message.Input>
|
||||
readonly tools?: ReadonlyArray<ToolDefinition.Input>
|
||||
readonly toolChoice?: ToolChoice.Input
|
||||
readonly generation?: GenerationOptions.Input
|
||||
readonly providerOptions?: NoInfer<ModelProviderOptions<SelectedModel>>
|
||||
readonly providerOptions?: ConstructorParameters<typeof LLMRequest>[0]["providerOptions"]
|
||||
readonly http?: HttpOptions.Input
|
||||
}
|
||||
|
||||
@@ -38,7 +35,7 @@ export const generate = LLMClient.generate
|
||||
|
||||
export const stream = LLMClient.stream
|
||||
|
||||
export const request = <const SelectedModel extends Model>(input: RequestInput<SelectedModel>) => {
|
||||
export const request = (input: RequestInput) => {
|
||||
const {
|
||||
system: requestSystem,
|
||||
prompt,
|
||||
@@ -66,7 +63,7 @@ const GENERATE_OBJECT_TOOL_NAME = "generate_object"
|
||||
|
||||
const GENERATE_OBJECT_TOOL_DESCRIPTION = "Return the structured result by calling this tool."
|
||||
|
||||
type GenerateObjectBase<SelectedModel extends Model = Model> = Omit<RequestInput<SelectedModel>, "tools" | "toolChoice">
|
||||
type GenerateObjectBase = Omit<RequestInput, "tools" | "toolChoice">
|
||||
|
||||
export class GenerateObjectResponse<T> {
|
||||
constructor(
|
||||
@@ -83,13 +80,11 @@ export class GenerateObjectResponse<T> {
|
||||
}
|
||||
}
|
||||
|
||||
export interface GenerateObjectOptions<S extends ToolSchema<any>, SelectedModel extends Model = Model>
|
||||
extends GenerateObjectBase<SelectedModel> {
|
||||
export interface GenerateObjectOptions<S extends ToolSchema<any>> extends GenerateObjectBase {
|
||||
readonly schema: S
|
||||
}
|
||||
|
||||
export interface GenerateObjectDynamicOptions<SelectedModel extends Model = Model>
|
||||
extends GenerateObjectBase<SelectedModel> {
|
||||
export interface GenerateObjectDynamicOptions extends GenerateObjectBase {
|
||||
/** Raw JSON Schema object describing the expected output shape. */
|
||||
readonly jsonSchema: JsonSchema.JsonSchema
|
||||
}
|
||||
@@ -142,11 +137,11 @@ const runGenerateObject = Effect.fn("LLM.generateObject")(function* (
|
||||
* 2. `jsonSchema: JsonSchema.JsonSchema` — `.object` is `unknown`. Use when
|
||||
* the schema is only available at runtime (MCP, plugin manifests). Caller validates.
|
||||
*/
|
||||
export function generateObject<const SelectedModel extends Model, S extends ToolSchema<any>>(
|
||||
options: GenerateObjectOptions<S, SelectedModel>,
|
||||
export function generateObject<S extends ToolSchema<any>>(
|
||||
options: GenerateObjectOptions<S>,
|
||||
): Effect.Effect<GenerateObjectResponse<Schema.Schema.Type<S>>, LLMError>
|
||||
export function generateObject<const SelectedModel extends Model>(
|
||||
options: GenerateObjectDynamicOptions<SelectedModel>,
|
||||
export function generateObject(
|
||||
options: GenerateObjectDynamicOptions,
|
||||
): Effect.Effect<GenerateObjectResponse<unknown>, LLMError>
|
||||
export function generateObject(options: GenerateObjectOptions<ToolSchema<any>> | GenerateObjectDynamicOptions) {
|
||||
if ("schema" in options) {
|
||||
|
||||
@@ -135,7 +135,7 @@ export function classifyProviderFailure(input: ProviderFailure): LLMError["reaso
|
||||
rateLimit: input.rateLimit,
|
||||
})
|
||||
}
|
||||
if (input.status === 408 || input.status === 409 || (input.status !== undefined && input.status >= 500))
|
||||
if (input.status !== undefined && input.status >= 500)
|
||||
return new ProviderInternalReason({
|
||||
...common,
|
||||
status: input.status,
|
||||
@@ -145,6 +145,7 @@ export function classifyProviderFailure(input: ProviderFailure): LLMError["reaso
|
||||
if (
|
||||
input.status === 400 ||
|
||||
input.status === 404 ||
|
||||
input.status === 409 ||
|
||||
input.status === 413 ||
|
||||
input.status === 422
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Model, ProviderOptions } from "./schema"
|
||||
import type { Model } from "./schema"
|
||||
|
||||
export interface Settings extends Readonly<Record<string, unknown>> {
|
||||
readonly headers?: Readonly<Record<string, string>>
|
||||
@@ -9,11 +9,8 @@ export interface Settings extends Readonly<Record<string, unknown>> {
|
||||
}
|
||||
}
|
||||
|
||||
export interface Definition<
|
||||
ProviderSettings extends Settings = Settings,
|
||||
Options extends ProviderOptions = ProviderOptions,
|
||||
> {
|
||||
readonly model: (modelID: string, settings: ProviderSettings) => Model<Options>
|
||||
export interface Definition<ProviderSettings extends Settings = Settings> {
|
||||
readonly model: (modelID: string, settings: ProviderSettings) => Model
|
||||
}
|
||||
|
||||
export * as ProviderPackage from "./provider-package"
|
||||
|
||||
@@ -47,7 +47,7 @@ export const configure = (input: Config) => {
|
||||
})
|
||||
return {
|
||||
id: ProviderID.make(provider),
|
||||
model: (modelID: string | ModelID) => route.model<AnthropicMessages.ProviderOptionsInput>({ id: modelID }),
|
||||
model: (modelID: string | ModelID) => route.model({ id: modelID }),
|
||||
configure,
|
||||
}
|
||||
}
|
||||
@@ -57,10 +57,7 @@ export const provider = {
|
||||
configure,
|
||||
}
|
||||
|
||||
export const model: ProviderPackage.Definition<Settings, AnthropicMessages.ProviderOptionsInput>["model"] = (
|
||||
modelID,
|
||||
settings,
|
||||
) => {
|
||||
export const model: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) => {
|
||||
if (settings.apiKey !== undefined && settings.authToken !== undefined)
|
||||
throw new Error("Anthropic-compatible apiKey cannot be combined with authToken")
|
||||
return configure({
|
||||
|
||||
@@ -52,10 +52,7 @@ export const configure = (input: Config = {}) => {
|
||||
}
|
||||
|
||||
export const provider = configure()
|
||||
export const model: ProviderPackage.Definition<Settings, AnthropicMessages.ProviderOptionsInput>["model"] = (
|
||||
modelID,
|
||||
settings,
|
||||
) => {
|
||||
export const model: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) => {
|
||||
if (settings.apiKey !== undefined && settings.authToken !== undefined)
|
||||
throw new Error("Anthropic apiKey cannot be combined with authToken")
|
||||
return configure({
|
||||
|
||||
@@ -99,14 +99,10 @@ export const configure = (input: Config) => {
|
||||
const modelDefaults = defaults(input)
|
||||
|
||||
const responses = (modelID: string | ModelID) =>
|
||||
configuredResponsesRoute
|
||||
.with(withOpenAIOptions(modelID, modelDefaults))
|
||||
.model<OpenAIProviderOptionsInput>({ id: modelID })
|
||||
configuredResponsesRoute.with(withOpenAIOptions(modelID, modelDefaults)).model({ id: modelID })
|
||||
|
||||
const chat = (modelID: string | ModelID) =>
|
||||
configuredChatRoute
|
||||
.with(withOpenAIOptions(modelID, modelDefaults))
|
||||
.model<OpenAIProviderOptionsInput>({ id: modelID })
|
||||
configuredChatRoute.with(withOpenAIOptions(modelID, modelDefaults)).model({ id: modelID })
|
||||
|
||||
return {
|
||||
id,
|
||||
@@ -137,12 +133,8 @@ const config = (settings: Settings): Config => {
|
||||
throw new Error("Azure requires resourceName or baseURL")
|
||||
}
|
||||
|
||||
export const responsesModel: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (
|
||||
modelID,
|
||||
settings,
|
||||
) => configure(config(settings)).responses(modelID)
|
||||
export const chatModel: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (
|
||||
modelID,
|
||||
settings,
|
||||
) => configure(config(settings)).chat(modelID)
|
||||
export const responsesModel: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) =>
|
||||
configure(config(settings)).responses(modelID)
|
||||
export const chatModel: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) =>
|
||||
configure(config(settings)).chat(modelID)
|
||||
export const model = responsesModel
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Auth } from "../route/auth"
|
||||
import { AuthOptions, type AtLeastOne, type ProviderAuthOption } from "../route/auth-options"
|
||||
import type { RouteDefaultsInput } from "../route/client"
|
||||
import { ProviderID, type ModelID } from "../schema"
|
||||
import type { OpenAIProviderOptionsInput } from "./openai-options"
|
||||
|
||||
export const aiGatewayID = ProviderID.make("cloudflare-ai-gateway")
|
||||
export const workersAIID = ProviderID.make("cloudflare-workers-ai")
|
||||
@@ -21,11 +20,10 @@ type GatewayURL = AtLeastOne<{
|
||||
}
|
||||
|
||||
export type AIGatewayOptions = GatewayURL &
|
||||
Omit<RouteDefaultsInput, "providerOptions"> &
|
||||
RouteDefaultsInput &
|
||||
ProviderAuthOption<"optional"> & {
|
||||
/** Cloudflare AI Gateway authentication token. Sent as `cf-aig-authorization`. */
|
||||
readonly gatewayApiKey?: CloudflareSecret
|
||||
readonly providerOptions?: OpenAIProviderOptionsInput
|
||||
}
|
||||
|
||||
type WorkersAIURL = AtLeastOne<{
|
||||
@@ -33,11 +31,7 @@ type WorkersAIURL = AtLeastOne<{
|
||||
readonly baseURL: string
|
||||
}>
|
||||
|
||||
export type WorkersAIOptions = WorkersAIURL &
|
||||
Omit<RouteDefaultsInput, "providerOptions"> &
|
||||
ProviderAuthOption<"optional"> & {
|
||||
readonly providerOptions?: OpenAIProviderOptionsInput
|
||||
}
|
||||
export type WorkersAIOptions = WorkersAIURL & RouteDefaultsInput & ProviderAuthOption<"optional">
|
||||
|
||||
export const aiGatewayBaseURL = (input: GatewayURL) => {
|
||||
if (input.baseURL) return input.baseURL
|
||||
@@ -104,7 +98,7 @@ const configureAIGateway = (options: AIGatewayOptions) => {
|
||||
})
|
||||
return {
|
||||
id: aiGatewayID,
|
||||
model: (modelID: string | ModelID) => route.model<OpenAIProviderOptionsInput>({ id: modelID }),
|
||||
model: (modelID: string | ModelID) => route.model({ id: modelID }),
|
||||
configure: configureAIGateway,
|
||||
}
|
||||
}
|
||||
@@ -117,7 +111,7 @@ const configureWorkersAI = (options: WorkersAIOptions) => {
|
||||
})
|
||||
return {
|
||||
id: workersAIID,
|
||||
model: (modelID: string | ModelID) => route.model<OpenAIProviderOptionsInput>({ id: modelID }),
|
||||
model: (modelID: string | ModelID) => route.model({ id: modelID }),
|
||||
configure: configureWorkersAI,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,11 +50,9 @@ export const configure = (options: ModelOptions) => {
|
||||
const responsesRoute = configuredResponsesRoute(options)
|
||||
const chatRoute = configuredChatRoute(options)
|
||||
const responses = (modelID: string | ModelID) =>
|
||||
responsesRoute
|
||||
.with(withOpenAIOptions(modelID, defaults(options)))
|
||||
.model<OpenAIProviderOptionsInput>({ id: modelID })
|
||||
responsesRoute.with(withOpenAIOptions(modelID, defaults(options))).model({ id: modelID })
|
||||
const chat = (modelID: string | ModelID) =>
|
||||
chatRoute.with(withOpenAIOptions(modelID, defaults(options))).model<OpenAIProviderOptionsInput>({ id: modelID })
|
||||
chatRoute.with(withOpenAIOptions(modelID, defaults(options))).model({ id: modelID })
|
||||
return {
|
||||
id,
|
||||
model: (modelID: string | ModelID) =>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { ProviderPackage } from "../provider-package"
|
||||
import { OpenAICompatibleChat } from "../protocols/openai-compatible-chat"
|
||||
import type { RouteDefaultsInput } from "../route/client"
|
||||
import { ProviderID, type ModelID } from "../schema"
|
||||
import { ProviderID, type ModelID, type ProviderOptions } from "../schema"
|
||||
import { GoogleVertexShared } from "./google-vertex-shared"
|
||||
import type { OpenAIProviderOptionsInput } from "./openai-options"
|
||||
|
||||
export const id = ProviderID.make("google-vertex")
|
||||
|
||||
@@ -12,7 +11,6 @@ export type Config = RouteDefaultsInput &
|
||||
readonly baseURL?: string
|
||||
readonly location?: string
|
||||
readonly project?: string
|
||||
readonly providerOptions?: OpenAIProviderOptionsInput
|
||||
}
|
||||
|
||||
export interface Settings extends ProviderPackage.Settings {
|
||||
@@ -21,7 +19,7 @@ export interface Settings extends ProviderPackage.Settings {
|
||||
readonly baseURL?: string
|
||||
readonly location?: string
|
||||
readonly project?: string
|
||||
readonly providerOptions?: OpenAIProviderOptionsInput
|
||||
readonly providerOptions?: ProviderOptions
|
||||
}
|
||||
|
||||
const route = OpenAICompatibleChat.route.with({
|
||||
@@ -58,7 +56,7 @@ export const configure = (input: Config = {}) => {
|
||||
const route = configuredRoute(input)
|
||||
return {
|
||||
id,
|
||||
model: (modelID: string | ModelID) => route.model<OpenAIProviderOptionsInput>({ id: modelID }),
|
||||
model: (modelID: string | ModelID) => route.model({ id: modelID }),
|
||||
configure,
|
||||
}
|
||||
}
|
||||
@@ -68,7 +66,7 @@ export const provider = {
|
||||
configure,
|
||||
}
|
||||
|
||||
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (modelID, settings) => {
|
||||
export const model: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) => {
|
||||
if (settings.apiKey !== undefined) throw new Error("Google Vertex Chat does not support API keys")
|
||||
return configure({
|
||||
accessToken: settings.accessToken,
|
||||
|
||||
@@ -91,7 +91,7 @@ export const configure = (input: Config = {}) => {
|
||||
const route = configuredRoute(input)
|
||||
return {
|
||||
id,
|
||||
model: (modelID: string | ModelID) => route.model<AnthropicMessages.ProviderOptionsInput>({ id: modelID }),
|
||||
model: (modelID: string | ModelID) => route.model({ id: modelID }),
|
||||
configure,
|
||||
}
|
||||
}
|
||||
@@ -101,10 +101,7 @@ export const provider = {
|
||||
configure,
|
||||
}
|
||||
|
||||
export const model: ProviderPackage.Definition<Settings, AnthropicMessages.ProviderOptionsInput>["model"] = (
|
||||
modelID,
|
||||
settings,
|
||||
) => {
|
||||
export const model: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) => {
|
||||
if (settings.apiKey !== undefined) throw new Error("Google Vertex Messages does not support API keys")
|
||||
return configure({
|
||||
accessToken: settings.accessToken,
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { ProviderPackage } from "../provider-package"
|
||||
import { OpenAICompatibleResponses } from "../protocols/openai-compatible-responses"
|
||||
import type { RouteDefaultsInput } from "../route/client"
|
||||
import { ProviderID, type ModelID } from "../schema"
|
||||
import { ProviderID, type ModelID, type ProviderOptions } from "../schema"
|
||||
import { GoogleVertexShared } from "./google-vertex-shared"
|
||||
import type { OpenResponsesProviderOptionsInput } from "./open-responses-options"
|
||||
|
||||
export const id = ProviderID.make("google-vertex")
|
||||
|
||||
@@ -12,7 +11,6 @@ export type Config = RouteDefaultsInput &
|
||||
readonly baseURL?: string
|
||||
readonly location?: string
|
||||
readonly project?: string
|
||||
readonly providerOptions?: OpenResponsesProviderOptionsInput
|
||||
}
|
||||
|
||||
export interface Settings extends ProviderPackage.Settings {
|
||||
@@ -21,7 +19,7 @@ export interface Settings extends ProviderPackage.Settings {
|
||||
readonly baseURL?: string
|
||||
readonly location?: string
|
||||
readonly project?: string
|
||||
readonly providerOptions?: OpenResponsesProviderOptionsInput
|
||||
readonly providerOptions?: ProviderOptions
|
||||
}
|
||||
|
||||
const route = OpenAICompatibleResponses.route.with({
|
||||
@@ -60,7 +58,7 @@ export const configure = (input: Config = {}) => {
|
||||
const route = configuredRoute(input)
|
||||
return {
|
||||
id,
|
||||
model: (modelID: string | ModelID) => route.model<OpenResponsesProviderOptionsInput>({ id: modelID }),
|
||||
model: (modelID: string | ModelID) => route.model({ id: modelID }),
|
||||
configure,
|
||||
}
|
||||
}
|
||||
@@ -70,10 +68,7 @@ export const provider = {
|
||||
configure,
|
||||
}
|
||||
|
||||
export const model: ProviderPackage.Definition<Settings, OpenResponsesProviderOptionsInput>["model"] = (
|
||||
modelID,
|
||||
settings,
|
||||
) => {
|
||||
export const model: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) => {
|
||||
if (settings.apiKey !== undefined) throw new Error("Google Vertex Responses does not support API keys")
|
||||
return configure({
|
||||
accessToken: settings.accessToken,
|
||||
|
||||
@@ -77,8 +77,7 @@ const configuredRoute = (input: Config, modelID: string | ModelID) => {
|
||||
export const configure = (input: Config = {}) => {
|
||||
return {
|
||||
id,
|
||||
model: (modelID: string | ModelID) =>
|
||||
configuredRoute(input, modelID).model<Gemini.ProviderOptionsInput>({ id: modelID }),
|
||||
model: (modelID: string | ModelID) => configuredRoute(input, modelID).model({ id: modelID }),
|
||||
configure,
|
||||
}
|
||||
}
|
||||
@@ -87,10 +86,7 @@ export const provider = {
|
||||
id,
|
||||
configure,
|
||||
}
|
||||
export const model: ProviderPackage.Definition<Settings, Gemini.ProviderOptionsInput>["model"] = (
|
||||
modelID,
|
||||
settings,
|
||||
) => {
|
||||
export const model: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) => {
|
||||
if (settings.apiKey !== undefined && settings.accessToken !== undefined)
|
||||
throw new Error("Google Vertex apiKey cannot be combined with accessToken or auth")
|
||||
return configure({
|
||||
|
||||
@@ -50,14 +50,14 @@ export const configure = (input: Config = {}) => {
|
||||
})
|
||||
return {
|
||||
id,
|
||||
model: (modelID: string | ModelID) => route.model<Gemini.ProviderOptionsInput>({ id: modelID }),
|
||||
model: (modelID: string | ModelID) => route.model({ id: modelID }),
|
||||
image,
|
||||
configure,
|
||||
}
|
||||
}
|
||||
|
||||
export const provider = configure()
|
||||
export const model: ProviderPackage.Definition<Settings, Gemini.ProviderOptionsInput>["model"] = (modelID, settings) =>
|
||||
export const model: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) =>
|
||||
configure({
|
||||
apiKey: settings.apiKey,
|
||||
baseURL: settings.baseURL,
|
||||
|
||||
@@ -36,7 +36,7 @@ export const configure = (input: Config) => {
|
||||
})
|
||||
return {
|
||||
id: ProviderID.make(provider),
|
||||
model: (modelID: string | ModelID) => route.model<OpenResponsesProviderOptionsInput>({ id: modelID }),
|
||||
model: (modelID: string | ModelID) => route.model({ id: modelID }),
|
||||
configure,
|
||||
}
|
||||
}
|
||||
@@ -46,10 +46,7 @@ export const provider = {
|
||||
configure,
|
||||
}
|
||||
|
||||
export const model: ProviderPackage.Definition<Settings, OpenResponsesProviderOptionsInput>["model"] = (
|
||||
modelID,
|
||||
settings,
|
||||
) =>
|
||||
export const model: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) =>
|
||||
configure({
|
||||
apiKey: settings.apiKey,
|
||||
baseURL: settings.baseURL,
|
||||
|
||||
@@ -4,15 +4,13 @@ import type { RouteDefaultsInput } from "../route/client"
|
||||
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options"
|
||||
import type { ProviderPackage } from "../provider-package"
|
||||
import { profiles, type OpenAICompatibleProfile } from "./openai-compatible-profile"
|
||||
import type { OpenAIProviderOptionsInput } from "./openai-options"
|
||||
|
||||
export const id = ProviderID.make("openai-compatible")
|
||||
|
||||
type GenericModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
|
||||
type GenericModelOptions = RouteDefaultsInput &
|
||||
ProviderAuthOption<"optional"> & {
|
||||
readonly provider?: string
|
||||
readonly baseURL: string
|
||||
readonly providerOptions?: OpenAIProviderOptionsInput
|
||||
}
|
||||
|
||||
export interface Settings extends ProviderPackage.Settings {
|
||||
@@ -21,10 +19,9 @@ export interface Settings extends ProviderPackage.Settings {
|
||||
readonly provider?: string
|
||||
}
|
||||
|
||||
export type FamilyModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
|
||||
export type FamilyModelOptions = RouteDefaultsInput &
|
||||
ProviderAuthOption<"optional"> & {
|
||||
readonly baseURL?: string
|
||||
readonly providerOptions?: OpenAIProviderOptionsInput
|
||||
}
|
||||
|
||||
export const routes = [OpenAICompatibleChat.route]
|
||||
@@ -40,8 +37,7 @@ export const configure = (input: GenericModelOptions) => {
|
||||
})
|
||||
return {
|
||||
id: ProviderID.make(provider),
|
||||
model: (modelID: string | ModelID) =>
|
||||
route.model<OpenAIProviderOptionsInput>({ id: modelID, provider: ProviderID.make(provider) }),
|
||||
model: (modelID: string | ModelID) => route.model({ id: modelID, provider: ProviderID.make(provider) }),
|
||||
configure,
|
||||
}
|
||||
}
|
||||
@@ -67,7 +63,7 @@ export const provider = {
|
||||
configure,
|
||||
}
|
||||
|
||||
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (modelID, settings) =>
|
||||
export const model: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) =>
|
||||
configure({
|
||||
apiKey: settings.apiKey,
|
||||
baseURL: settings.baseURL,
|
||||
|
||||
@@ -86,15 +86,10 @@ export const configure = (input: Config = {}) => {
|
||||
const chatRoute = configuredRoute(OpenAIChat.route, input)
|
||||
const modelDefaults = defaults(input)
|
||||
const responses = (id: string | ModelID) =>
|
||||
responsesRoute
|
||||
.with(withOpenAIOptions(id, modelDefaults, { textVerbosity: true }))
|
||||
.model<OpenAIProviderOptionsInput>({ id })
|
||||
responsesRoute.with(withOpenAIOptions(id, modelDefaults, { textVerbosity: true })).model({ id })
|
||||
const responsesWebSocket = (id: string | ModelID) =>
|
||||
responsesWebSocketRoute
|
||||
.with(withOpenAIOptions(id, modelDefaults, { textVerbosity: true }))
|
||||
.model<OpenAIProviderOptionsInput>({ id })
|
||||
const chat = (id: string | ModelID) =>
|
||||
chatRoute.with(withOpenAIOptions(id, modelDefaults)).model<OpenAIProviderOptionsInput>({ id })
|
||||
responsesWebSocketRoute.with(withOpenAIOptions(id, modelDefaults, { textVerbosity: true })).model({ id })
|
||||
const chat = (id: string | ModelID) => chatRoute.with(withOpenAIOptions(id, modelDefaults)).model({ id })
|
||||
const image = (modelID: string | ModelID) =>
|
||||
OpenAIImages.model({
|
||||
id: modelID,
|
||||
@@ -137,17 +132,15 @@ const config = (settings: Settings): Config => {
|
||||
}
|
||||
}
|
||||
|
||||
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (modelID, settings) => {
|
||||
export const model: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) => {
|
||||
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"] = (
|
||||
modelID,
|
||||
settings,
|
||||
) => configure(config(settings)).chat(modelID)
|
||||
export const chatModel: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) =>
|
||||
configure(config(settings)).chat(modelID)
|
||||
export const responses = provider.responses
|
||||
export const responsesWebSocket = provider.responsesWebSocket
|
||||
export const chat = provider.chat
|
||||
|
||||
@@ -107,7 +107,7 @@ export const configure = (input: ModelOptions = {}) => {
|
||||
const route = configuredRoute(input)
|
||||
return {
|
||||
id,
|
||||
model: (modelID: string | ModelID) => route.model<OpenRouterProviderOptionsInput>({ id: modelID }),
|
||||
model: (modelID: string | ModelID) => route.model({ id: modelID }),
|
||||
configure,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,12 @@ import * as OpenAICompatibleProfiles from "./openai-compatible-profile"
|
||||
import * as OpenAICompatibleChat from "../protocols/openai-compatible-chat"
|
||||
import * as OpenAIResponses from "../protocols/openai-responses"
|
||||
import { XAIImages } from "../protocols/xai-images"
|
||||
import type { OpenAIProviderOptionsInput } from "./openai-options"
|
||||
|
||||
export const id = ProviderID.make("xai")
|
||||
|
||||
export type ModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
|
||||
export type ModelOptions = RouteDefaultsInput &
|
||||
ProviderAuthOption<"optional"> & {
|
||||
readonly baseURL?: string
|
||||
readonly providerOptions?: OpenAIProviderOptionsInput
|
||||
}
|
||||
|
||||
export type { XAIImageOptions } from "../protocols/xai-images"
|
||||
@@ -44,8 +42,8 @@ const configuredChatRoute = (input: ModelOptions) => {
|
||||
export const configure = (input: ModelOptions = {}) => {
|
||||
const responsesRoute = configuredResponsesRoute(input)
|
||||
const chatRoute = configuredChatRoute(input)
|
||||
const responses = (modelID: string | ModelID) => responsesRoute.model<OpenAIProviderOptionsInput>({ id: modelID })
|
||||
const chat = (modelID: string | ModelID) => chatRoute.model<OpenAIProviderOptionsInput>({ id: modelID })
|
||||
const responses = (modelID: string | ModelID) => responsesRoute.model({ id: modelID })
|
||||
const chat = (modelID: string | ModelID) => chatRoute.model({ id: modelID })
|
||||
const image = (modelID: string | ModelID) =>
|
||||
XAIImages.model({
|
||||
id: modelID,
|
||||
|
||||
@@ -10,7 +10,7 @@ import { WebSocketExecutor } from "./transport"
|
||||
import type { Protocol } from "./protocol"
|
||||
import { applyCachePolicy } from "../cache-policy"
|
||||
import * as ProviderShared from "../protocols/shared"
|
||||
import type { LLMError, ProtocolID, ProviderOptions } from "../schema"
|
||||
import type { LLMError, PreparedRequestOf, ProtocolID, ProviderOptions } from "../schema"
|
||||
import {
|
||||
GenerationOptions,
|
||||
HttpOptions,
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
ModelLimits,
|
||||
LLMError as LLMErrorClass,
|
||||
LLMEvent,
|
||||
PreparedRequest,
|
||||
ProviderID,
|
||||
mergeGenerationOptions,
|
||||
mergeHttpOptions,
|
||||
@@ -45,7 +46,7 @@ export interface Route<Body, Prepared = unknown> {
|
||||
readonly defaults: RouteDefaults
|
||||
readonly body: RouteBody<Body>
|
||||
readonly with: (patch: RoutePatch<Body, Prepared>) => Route<Body, Prepared>
|
||||
readonly model: <Options extends ProviderOptions = ProviderOptions>(input: RouteMappedModelInput) => Model<Options>
|
||||
readonly model: (input: RouteMappedModelInput) => Model
|
||||
readonly prepareTransport: (body: Body, request: LLMRequest) => Effect.Effect<Prepared, LLMError>
|
||||
readonly streamPrepared: (
|
||||
prepared: Prepared,
|
||||
@@ -92,12 +93,12 @@ export interface RoutePatch<Body, Prepared> extends RouteDefaultsInput {
|
||||
|
||||
type RouteMappedModelInput = RouteModelInput | RouteRoutedModelInput
|
||||
|
||||
const makeRouteModel = <Options extends ProviderOptions = ProviderOptions>(route: AnyRoute, mapped: RouteMappedModelInput) => {
|
||||
const makeRouteModel = (route: AnyRoute, mapped: RouteMappedModelInput) => {
|
||||
const provider = route.provider ?? ("provider" in mapped ? mapped.provider : undefined)
|
||||
if (!provider) throw new Error(`Route.model(${route.id}) requires a provider`)
|
||||
if (!endpointBaseURL(route.endpoint))
|
||||
throw new Error(`Route.model(${route.id}) requires an endpoint baseURL — configure it on the route first`)
|
||||
return Model.make<Options>({
|
||||
return Model.make({
|
||||
...mapped,
|
||||
provider,
|
||||
route,
|
||||
@@ -141,6 +142,17 @@ export const httpOptions = (input: HttpOptionsInput | undefined) => {
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
/**
|
||||
* Compile a request through protocol body construction, validation, and HTTP
|
||||
* preparation without sending it. Returns the prepared request including the
|
||||
* provider-native body.
|
||||
*
|
||||
* Pass a `Body` type argument to statically expose the route's body
|
||||
* shape (e.g. `prepare<OpenAIChatBody>(...)`) — the runtime body is
|
||||
* identical, so this is a type-level assertion the caller makes about which
|
||||
* route the request will resolve to.
|
||||
*/
|
||||
readonly prepare: <Body = unknown>(request: LLMRequest) => Effect.Effect<PreparedRequestOf<Body>, LLMError>
|
||||
readonly stream: StreamMethod
|
||||
readonly generate: GenerateMethod
|
||||
}
|
||||
@@ -284,8 +296,7 @@ function makeFromTransport<Body, Prepared, Frame, Event, State>(
|
||||
defaults: mergeRouteDefaults(route.defaults, defaults),
|
||||
})
|
||||
},
|
||||
model: <Options extends ProviderOptions = ProviderOptions>(input: RouteMappedModelInput) =>
|
||||
makeRouteModel<Options>(route, input),
|
||||
model: (input) => makeRouteModel(route, input),
|
||||
prepareTransport: (body, request) =>
|
||||
routeInput.transport.prepare({
|
||||
body,
|
||||
@@ -359,6 +370,9 @@ export function make<Body, Prepared, Frame, Event, State>(
|
||||
})
|
||||
}
|
||||
|
||||
// `compile` is the important boundary: it turns a common `LLMRequest` into a
|
||||
// validated provider body plus transport-private prepared data, but does not
|
||||
// execute transport.
|
||||
const compile = Effect.fn("LLM.compile")(function* (request: LLMRequest) {
|
||||
const resolved = applyCachePolicy(resolveRequestOptions(request))
|
||||
const route = resolved.model.route
|
||||
@@ -376,17 +390,17 @@ const compile = Effect.fn("LLM.compile")(function* (request: LLMRequest) {
|
||||
}
|
||||
})
|
||||
|
||||
/** @internal Test-only projection of the execution compiler; not exported from package barrels. */
|
||||
export const compileRequest = Effect.fn("LLM.compileRequest")(function* (request: LLMRequest) {
|
||||
const prepareWith = Effect.fn("LLMClient.prepare")(function* (request: LLMRequest) {
|
||||
const compiled = yield* compile(request)
|
||||
return {
|
||||
|
||||
return new PreparedRequest({
|
||||
id: compiled.request.id ?? "request",
|
||||
route: compiled.route.id,
|
||||
protocol: compiled.route.protocol,
|
||||
model: compiled.request.model,
|
||||
body: compiled.body,
|
||||
metadata: { transport: compiled.route.transport.id },
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const streamRequestWith = (runtime: TransportRuntime) => (request: LLMRequest) =>
|
||||
@@ -408,6 +422,9 @@ const generateWith = (stream: Interface["stream"]) =>
|
||||
)
|
||||
})
|
||||
|
||||
export const prepare = <Body = unknown>(request: LLMRequest) =>
|
||||
prepareWith(request) as Effect.Effect<PreparedRequestOf<Body>, LLMError>
|
||||
|
||||
export function stream(request: LLMRequest): Stream.Stream<LLMEvent, LLMError> {
|
||||
return Stream.unwrap(
|
||||
Effect.gen(function* () {
|
||||
@@ -436,7 +453,7 @@ export const layer: Layer.Layer<Service, never, RequestExecutor.Service> = Layer
|
||||
http: yield* RequestExecutor.Service,
|
||||
webSocket: Option.getOrUndefined(yield* Effect.serviceOption(WebSocketExecutor.Service)),
|
||||
})
|
||||
return Service.of({ stream, generate: generateWith(stream) })
|
||||
return Service.of({ prepare: prepareWith as Interface["prepare"], stream, generate: generateWith(stream) })
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -445,6 +462,7 @@ export const Route = { make } as const
|
||||
export const LLMClient = {
|
||||
Service,
|
||||
layer,
|
||||
prepare,
|
||||
stream,
|
||||
generate,
|
||||
} as const
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Schema } from "effect"
|
||||
import { ContentBlockID, FinishReason, ProviderMetadata, ToolCallID } from "./ids"
|
||||
import { ContentBlockID, FinishReason, ProtocolID, ProviderMetadata, RouteID, ToolCallID } from "./ids"
|
||||
import { ModelSchema } from "./options"
|
||||
import { Message, ToolCallPart, ToolOutput, ToolResultPart, ToolResultValue, type ContentPart } from "./messages"
|
||||
import { ProviderFailureClassification } from "./errors"
|
||||
|
||||
@@ -313,6 +314,29 @@ export const LLMEvent = Object.assign(llmEventTagged, {
|
||||
})
|
||||
export type LLMEvent = Schema.Schema.Type<typeof llmEventTagged>
|
||||
|
||||
export class PreparedRequest extends Schema.Class<PreparedRequest>("LLM.PreparedRequest")({
|
||||
id: Schema.String,
|
||||
route: RouteID,
|
||||
protocol: ProtocolID,
|
||||
model: ModelSchema,
|
||||
body: Schema.Unknown,
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
||||
}) {}
|
||||
|
||||
/**
|
||||
* A `PreparedRequest` whose `body` is typed as `Body`. Use with the generic
|
||||
* on `LLMClient.prepare<Body>(...)` when the caller knows which route their
|
||||
* request will resolve to and wants its native shape statically exposed
|
||||
* (debug UIs, request previews, plan rendering).
|
||||
*
|
||||
* The runtime body is identical — the route still emits `body: unknown` — so
|
||||
* this is a type-level assertion the caller makes about what they expect to
|
||||
* find. The prepare runtime does not validate the assertion.
|
||||
*/
|
||||
export type PreparedRequestOf<Body> = Omit<PreparedRequest, "body"> & {
|
||||
readonly body: Body
|
||||
}
|
||||
|
||||
const responseText = (events: ReadonlyArray<LLMEvent>) =>
|
||||
events
|
||||
.filter(LLMEvent.is.textDelta)
|
||||
|
||||
@@ -178,8 +178,7 @@ export namespace ModelCompatibility {
|
||||
export const make = (input: Input) => (input instanceof ModelCompatibility ? input : new ModelCompatibility(input))
|
||||
}
|
||||
|
||||
export class Model<Options extends ProviderOptions = ProviderOptions> {
|
||||
declare protected readonly _ProviderOptions: Options
|
||||
export class Model {
|
||||
readonly id: ModelID
|
||||
readonly provider: ProviderID
|
||||
readonly route: AnyRoute
|
||||
@@ -194,8 +193,8 @@ export class Model<Options extends ProviderOptions = ProviderOptions> {
|
||||
this.compatibility = input.compatibility
|
||||
}
|
||||
|
||||
static make<Options extends ProviderOptions = ProviderOptions>(input: Model.Input) {
|
||||
return new Model<Options>({
|
||||
static make(input: Model.Input) {
|
||||
return new Model({
|
||||
id: ModelID.make(input.id),
|
||||
provider: ProviderID.make(input.provider),
|
||||
route: input.route,
|
||||
@@ -204,7 +203,7 @@ export class Model<Options extends ProviderOptions = ProviderOptions> {
|
||||
})
|
||||
}
|
||||
|
||||
static input<Options extends ProviderOptions>(model: Model<Options>): Model.ConstructorInput {
|
||||
static input(model: Model): Model.ConstructorInput {
|
||||
return {
|
||||
id: model.id,
|
||||
provider: model.provider,
|
||||
@@ -214,9 +213,9 @@ export class Model<Options extends ProviderOptions = ProviderOptions> {
|
||||
}
|
||||
}
|
||||
|
||||
static update<Options extends ProviderOptions>(model: Model<Options>, patch: Partial<Model.Input>) {
|
||||
static update(model: Model, patch: Partial<Model.Input>) {
|
||||
if (Object.keys(patch).length === 0) return model
|
||||
return Model.make<Options>({
|
||||
return Model.make({
|
||||
...Model.input(model),
|
||||
...patch,
|
||||
})
|
||||
@@ -242,8 +241,6 @@ export namespace Model {
|
||||
|
||||
export type ModelInput = Model.Input
|
||||
|
||||
export type ModelProviderOptions<SelectedModel> = SelectedModel extends Model<infer Options> ? Options : never
|
||||
|
||||
export const ModelSchema = Schema.declare((value): value is Model => value instanceof Model, { expected: "LLM.Model" })
|
||||
|
||||
export class CacheHint extends Schema.Class<CacheHint>("LLM.CacheHint")({
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
export * as TestLLM from "./testing"
|
||||
|
||||
import { LLMClient, type Interface as LLMClientShape } from "./route/client"
|
||||
import {
|
||||
LLMEvent,
|
||||
LLMResponse,
|
||||
type FinishReasonDetails,
|
||||
type LLMError,
|
||||
type LLMRequest,
|
||||
type UsageInput,
|
||||
} from "./schema"
|
||||
import { Context, Deferred, Effect, Latch, Layer, Queue, Scope, Stream } from "effect"
|
||||
|
||||
export type Response = readonly LLMEvent[] | Stream.Stream<LLMEvent, LLMError>
|
||||
|
||||
export type Gate = Readonly<{ started: Effect.Effect<void>; release: Effect.Effect<void> }>
|
||||
|
||||
export interface Interface {
|
||||
readonly requests: LLMRequest[]
|
||||
readonly push: (...responses: readonly Response[]) => Effect.Effect<void>
|
||||
readonly always: (response: Response) => Effect.Effect<void>
|
||||
readonly wait: (count: number) => Effect.Effect<void>
|
||||
readonly gate: Effect.Effect<Gate, never, Scope.Scope>
|
||||
readonly client: LLMClientShape
|
||||
}
|
||||
|
||||
export interface LayerOptions {
|
||||
readonly transformRequest?: (request: LLMRequest) => LLMRequest
|
||||
/** Used after the one-shot response queue is exhausted. Omit to defect on unexpected requests. */
|
||||
readonly fallback?: Response
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/ai/TestLLM") {}
|
||||
|
||||
export const complete = (
|
||||
options: { readonly reason: FinishReasonDetails; readonly usage?: UsageInput },
|
||||
...events: readonly LLMEvent[]
|
||||
) => [
|
||||
LLMEvent.stepStart({ index: 0 }),
|
||||
...events,
|
||||
LLMEvent.stepFinish({ index: 0, reason: options.reason, usage: options.usage }),
|
||||
LLMEvent.finish({ reason: options.reason }),
|
||||
]
|
||||
|
||||
export const stop = (...events: readonly LLMEvent[]) => complete({ reason: { normalized: "stop" } }, ...events)
|
||||
|
||||
export const toolCalls = (...events: readonly LLMEvent[]) =>
|
||||
complete({ reason: { normalized: "tool-calls" } }, ...events)
|
||||
|
||||
const textEvents = (value: string, id: string) => [
|
||||
LLMEvent.textStart({ id }),
|
||||
LLMEvent.textDelta({ id, text: value }),
|
||||
LLMEvent.textEnd({ id }),
|
||||
]
|
||||
|
||||
export const text = (value: string, id: string) => stop(...textEvents(value, id))
|
||||
|
||||
export const textWithUsage = (value: string, id: string, inputTokens: number) =>
|
||||
complete(
|
||||
{ reason: { normalized: "stop" }, usage: { inputTokens, nonCachedInputTokens: inputTokens } },
|
||||
...textEvents(value, id),
|
||||
)
|
||||
|
||||
export const tool = (id: string, name: string, input: unknown) => toolCalls(LLMEvent.toolCall({ id, name, input }))
|
||||
|
||||
export const failAfter = (error: LLMError, ...events: readonly LLMEvent[]) =>
|
||||
Stream.fromIterable(events).pipe(Stream.concat(Stream.fail(error)))
|
||||
|
||||
export const hangAfter = (...events: readonly LLMEvent[]) => Stream.concat(Stream.fromIterable(events), Stream.never)
|
||||
|
||||
const toStream = (response: Response) => (Stream.isStream(response) ? response : Stream.fromIterable(response))
|
||||
|
||||
export const layer = (options: LayerOptions = {}) =>
|
||||
Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const requests: LLMRequest[] = []
|
||||
const responses: Response[] = []
|
||||
let started = Deferred.makeUnsafe<void>()
|
||||
let fallback = options.fallback
|
||||
let activeGate: { readonly started: Queue.Queue<void>; readonly release: Latch.Latch } | undefined
|
||||
const wait = (count: number): Effect.Effect<void> =>
|
||||
Effect.suspend(() =>
|
||||
requests.length >= count ? Effect.void : Deferred.await(started).pipe(Effect.andThen(wait(count))),
|
||||
)
|
||||
|
||||
const stream = ((request: LLMRequest) => {
|
||||
requests.push(options.transformRequest?.(request) ?? request)
|
||||
const waiting = started
|
||||
started = Deferred.makeUnsafe()
|
||||
Deferred.doneUnsafe(waiting, Effect.void)
|
||||
const response = responses.shift() ?? fallback
|
||||
if (!response) return Stream.die(new Error(`TestLLM has no response for request ${requests.length}`))
|
||||
const streamed = toStream(response)
|
||||
const gate = activeGate
|
||||
if (!gate) return streamed
|
||||
return Stream.unwrap(
|
||||
Queue.offer(gate.started, undefined).pipe(Effect.andThen(gate.release.await), Effect.as(streamed)),
|
||||
)
|
||||
}) as LLMClientShape["stream"]
|
||||
const client = LLMClient.Service.of({
|
||||
stream,
|
||||
generate: (request) =>
|
||||
stream(request).pipe(
|
||||
Stream.runFold(LLMResponse.empty, LLMResponse.reduce),
|
||||
Effect.flatMap((state) => {
|
||||
const response = LLMResponse.complete(state)
|
||||
if (response) return Effect.succeed(response)
|
||||
return Effect.die("TestLLM response ended without a terminal finish event")
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
||||
return Service.of({
|
||||
requests,
|
||||
push: (...input) =>
|
||||
Effect.sync(() => {
|
||||
responses.push(...input)
|
||||
}),
|
||||
always: (response) =>
|
||||
Effect.sync(() => {
|
||||
fallback = response
|
||||
}),
|
||||
wait,
|
||||
gate: Effect.gen(function* () {
|
||||
const gate = {
|
||||
started: yield* Effect.acquireRelease(Queue.unbounded<void>(), Queue.shutdown),
|
||||
release: yield* Latch.make(),
|
||||
}
|
||||
activeGate = gate
|
||||
const release = Effect.sync(() => {
|
||||
if (activeGate === gate) activeGate = undefined
|
||||
}).pipe(Effect.andThen(gate.release.open), Effect.asVoid)
|
||||
yield* Effect.addFinalizer(() => release)
|
||||
return {
|
||||
started: Queue.take(gate.started),
|
||||
release,
|
||||
}
|
||||
}),
|
||||
client,
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
export const clientLayer = Layer.effect(
|
||||
LLMClient.Service,
|
||||
Effect.map(Service, (service) => service.client),
|
||||
)
|
||||
|
||||
export const push = (...responses: readonly Response[]) => Service.use((service) => service.push(...responses))
|
||||
|
||||
export const always = (response: Response) => Service.use((service) => service.always(response))
|
||||
|
||||
export const wait = (count: number) => Service.use((service) => service.wait(count))
|
||||
|
||||
export const gate = Service.use((service) => service.gate)
|
||||
@@ -2,7 +2,6 @@ import { describe, expect } from "bun:test"
|
||||
import { Effect, Schema, Stream } from "effect"
|
||||
import { LLM, LLMRequest, LLMResponse } from "../src"
|
||||
import { Route, Endpoint, LLMClient, Protocol, type FramingDef } from "../src/route"
|
||||
import { compileRequest } from "../src/route/client"
|
||||
import { Model } from "../src/schema"
|
||||
import { testEffect } from "./lib/effect"
|
||||
import { dynamicResponse } from "./lib/http"
|
||||
@@ -140,7 +139,8 @@ describe("llm route", () => {
|
||||
|
||||
it.effect("selects routes by model route value", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const llm = yield* LLMClient.Service
|
||||
const prepared = yield* llm.prepare(
|
||||
LLMRequest.update(request, { model: updateModel(request.model, { route: configuredGemini }) }),
|
||||
)
|
||||
|
||||
@@ -173,7 +173,7 @@ describe("llm route", () => {
|
||||
framing: fakeFraming,
|
||||
})
|
||||
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* (yield* LLMClient.Service).prepare(
|
||||
LLMRequest.update(request, { model: updateModel(request.model, { route: duplicate }) }),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { CacheHint, LLM, Message } from "../src"
|
||||
import { Auth } from "../src/route"
|
||||
import { compileRequest } from "../src/route/client"
|
||||
import { Auth, LLMClient } from "../src/route"
|
||||
import { AmazonBedrock } from "../src/providers"
|
||||
import * as AnthropicMessages from "../src/protocols/anthropic-messages"
|
||||
import * as Gemini from "../src/protocols/gemini"
|
||||
@@ -32,7 +31,7 @@ const geminiModel = Gemini.route
|
||||
describe("applyCachePolicy", () => {
|
||||
it.effect("undefined cache resolves to 'auto' (the recommended default)", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
model: anthropicModel,
|
||||
system: "You are concise.",
|
||||
@@ -51,7 +50,7 @@ describe("applyCachePolicy", () => {
|
||||
|
||||
it.effect("'auto' marks the last tool, first and last system parts, and final message boundary on Anthropic", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
model: anthropicModel,
|
||||
system: [
|
||||
@@ -88,7 +87,7 @@ describe("applyCachePolicy", () => {
|
||||
|
||||
it.effect("'auto' is a no-op on OpenAI (implicit caching protocol)", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
model: openaiModel,
|
||||
system: "Sys",
|
||||
@@ -107,7 +106,7 @@ describe("applyCachePolicy", () => {
|
||||
|
||||
it.effect("'auto' is a no-op on Gemini (out-of-band caching protocol)", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
model: geminiModel,
|
||||
system: "Sys",
|
||||
@@ -124,7 +123,7 @@ describe("applyCachePolicy", () => {
|
||||
|
||||
it.effect("'auto' on Bedrock emits cachePoint markers in the right places", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
model: bedrockModel,
|
||||
system: [
|
||||
@@ -158,7 +157,7 @@ describe("applyCachePolicy", () => {
|
||||
|
||||
it.effect("'none' disables auto placement even when manual hints exist", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
model: anthropicModel,
|
||||
system: "Sys",
|
||||
@@ -177,7 +176,7 @@ describe("applyCachePolicy", () => {
|
||||
|
||||
it.effect("granular object form: tools-only marks just tools", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
model: anthropicModel,
|
||||
system: "Sys",
|
||||
@@ -196,7 +195,7 @@ describe("applyCachePolicy", () => {
|
||||
|
||||
it.effect("auto policy preserves manual CacheHints on other parts", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
model: anthropicModel,
|
||||
system: [
|
||||
@@ -242,7 +241,7 @@ describe("applyCachePolicy", () => {
|
||||
expect("cache" in tail ? tail.cache : undefined).toBeUndefined()
|
||||
expect(applyCachePolicy(applied)).toBe(applied)
|
||||
|
||||
const prepared = yield* compileRequest(request)
|
||||
const prepared = yield* LLMClient.prepare(request)
|
||||
|
||||
const body = prepared.body as {
|
||||
tools: Array<{ cache_control?: unknown }>
|
||||
@@ -262,7 +261,7 @@ describe("applyCachePolicy", () => {
|
||||
|
||||
it.effect("ttlSeconds in the policy flows through to wire markers", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
model: anthropicModel,
|
||||
system: "Sys",
|
||||
@@ -279,7 +278,7 @@ describe("applyCachePolicy", () => {
|
||||
|
||||
it.effect("messages: { tail: 2 } marks the last 2 message boundaries", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
model: anthropicModel,
|
||||
messages: [Message.user("u1"), Message.assistant("a1"), Message.user("u2"), Message.assistant("a2")],
|
||||
@@ -297,7 +296,7 @@ describe("applyCachePolicy", () => {
|
||||
|
||||
it.effect("'latest-assistant' marks the last assistant message", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
model: anthropicModel,
|
||||
messages: [Message.user("u1"), Message.assistant("a1"), Message.user("u2")],
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
OpenResponses,
|
||||
} from "@opencode-ai/ai/protocols"
|
||||
import * as AnthropicMessages from "@opencode-ai/ai/protocols/anthropic-messages"
|
||||
import { TestLLM } from "@opencode-ai/ai/testing"
|
||||
|
||||
describe("public exports", () => {
|
||||
test("root exposes app-facing runtime APIs", () => {
|
||||
@@ -29,7 +28,6 @@ describe("public exports", () => {
|
||||
expect(ImageInput.bytes).toBeFunction()
|
||||
expect(Provider.make).toBeFunction()
|
||||
expect(ProviderSubpath.make).toBe(Provider.make)
|
||||
expect(TestLLM.layer).toBeFunction()
|
||||
})
|
||||
|
||||
test("route barrel exposes route-authoring APIs", () => {
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import { Schema } from "effect"
|
||||
import { LLM, type Model, type ModelProviderOptions, type ProviderOptions } from "../src"
|
||||
import { OpenAIChat } from "../src/protocols"
|
||||
|
||||
interface ExampleOptions {
|
||||
readonly [key: string]: unknown
|
||||
readonly mode?: "fast" | "thorough"
|
||||
}
|
||||
|
||||
type ExampleProviderOptions = ProviderOptions & {
|
||||
readonly example?: ExampleOptions
|
||||
}
|
||||
|
||||
const model = OpenAIChat.route
|
||||
.with({ endpoint: { baseURL: "https://example.com/v1" } })
|
||||
.model<ExampleProviderOptions>({ id: "example" })
|
||||
|
||||
LLM.request({ model, prompt: "Hello", providerOptions: { example: { mode: "fast" } } })
|
||||
LLM.request({ model, prompt: "Hello", providerOptions: { future: { option: true } } })
|
||||
|
||||
LLM.request({
|
||||
model,
|
||||
prompt: "Hello",
|
||||
// @ts-expect-error Known provider options preserve their value types.
|
||||
providerOptions: { example: { mode: "slow" } },
|
||||
})
|
||||
|
||||
LLM.generateObject({
|
||||
model,
|
||||
prompt: "Hello",
|
||||
schema: Schema.Struct({ answer: Schema.String }),
|
||||
providerOptions: { example: { mode: "thorough" } },
|
||||
})
|
||||
|
||||
LLM.generateObject({
|
||||
model,
|
||||
prompt: "Hello",
|
||||
jsonSchema: { type: "object" },
|
||||
// @ts-expect-error Dynamic object generation uses the selected model's provider options.
|
||||
providerOptions: { example: { mode: false } },
|
||||
})
|
||||
|
||||
declare const generic: Model
|
||||
LLM.request({ model: generic, prompt: "Hello", providerOptions: { arbitrary: { option: true } } })
|
||||
|
||||
const options: ModelProviderOptions<typeof model> = { example: { mode: "fast" } }
|
||||
void options
|
||||
@@ -4,7 +4,6 @@ import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { LLM, mergeProviderOptions } from "../src"
|
||||
import { AnthropicMessages, OpenAIChat } from "../src/protocols"
|
||||
import { Auth, LLMClient } from "../src/route"
|
||||
import { compileRequest } from "../src/route/client"
|
||||
import { it } from "./lib/effect"
|
||||
import { dynamicResponse } from "./lib/http"
|
||||
import { deltaChunk } from "./lib/openai-chunks"
|
||||
@@ -45,7 +44,7 @@ describe("request option precedence", () => {
|
||||
})
|
||||
})
|
||||
|
||||
it.effect("compiles bodies with route defaults, model defaults, and call options in order", () =>
|
||||
it.effect("prepares bodies with route defaults, model defaults, and call options in order", () =>
|
||||
Effect.gen(function* () {
|
||||
const route = OpenAIChat.route.with({
|
||||
endpoint: { baseURL: "https://api.openai.test/v1/" },
|
||||
@@ -60,7 +59,7 @@ describe("request option precedence", () => {
|
||||
providerOptions: { openai: { reasoningEffort: "medium" } },
|
||||
},
|
||||
})
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
prompt: "Say hello.",
|
||||
@@ -142,7 +141,7 @@ describe("request option precedence", () => {
|
||||
const model = OpenAIChat.route
|
||||
.with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") })
|
||||
.model({ id: "gpt-4o-mini" })
|
||||
const error = yield* compileRequest(
|
||||
const error = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
model,
|
||||
prompt: "Say hello.",
|
||||
@@ -165,8 +164,10 @@ describe("request option precedence", () => {
|
||||
limits: { output: 128 },
|
||||
})
|
||||
const model = route.model({ id: "claude-sonnet-4-5", defaults: { limits: { output: 64 } } })
|
||||
const withoutMaxTokens = yield* compileRequest(LLM.request({ model, prompt: "Say hello.", cache: "none" }))
|
||||
const withMaxTokens = yield* compileRequest(
|
||||
const withoutMaxTokens = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
|
||||
LLM.request({ model, prompt: "Say hello.", cache: "none" }),
|
||||
)
|
||||
const withMaxTokens = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
|
||||
LLM.request({ model, prompt: "Say hello.", cache: "none", generation: { maxTokens: 32 } }),
|
||||
)
|
||||
|
||||
@@ -58,12 +58,6 @@ describe("provider error classification", () => {
|
||||
).toEqual(["ProviderInternal", "ProviderInternal"])
|
||||
})
|
||||
|
||||
test("classifies transient client statuses as provider internal", () => {
|
||||
expect(
|
||||
[408, 409].map((status) => classifyProviderFailure({ message: `HTTP ${status}`, status })._tag),
|
||||
).toEqual(["ProviderInternal", "ProviderInternal"])
|
||||
})
|
||||
|
||||
test("classifies nested provider codes when a top-level code is also present", () => {
|
||||
expect(
|
||||
[
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { LLM } from "../../src"
|
||||
import { AnthropicCompatible } from "../../src/providers"
|
||||
|
||||
const model = AnthropicCompatible.configure({ baseURL: "https://example.com" }).model("claude")
|
||||
|
||||
LLM.request({ model, prompt: "Hello", providerOptions: { anthropic: { effort: "high" } } })
|
||||
|
||||
LLM.request({
|
||||
model,
|
||||
prompt: "Hello",
|
||||
// @ts-expect-error Anthropic effort must be a string.
|
||||
providerOptions: { anthropic: { effort: 1 } },
|
||||
})
|
||||
@@ -1,13 +0,0 @@
|
||||
import { LLM } from "../../src"
|
||||
import { Anthropic } from "../../src/providers"
|
||||
|
||||
const model = Anthropic.provider.model("claude-sonnet-4-5")
|
||||
|
||||
LLM.request({ model, prompt: "Hello", providerOptions: { anthropic: { thinking: { type: "adaptive" } } } })
|
||||
|
||||
LLM.request({
|
||||
model,
|
||||
prompt: "Hello",
|
||||
// @ts-expect-error Anthropic thinking modes are a fixed union.
|
||||
providerOptions: { anthropic: { thinking: { type: "automatic" } } },
|
||||
})
|
||||
@@ -1,13 +0,0 @@
|
||||
import { LLM } from "../../src"
|
||||
import { Azure } from "../../src/providers"
|
||||
|
||||
const model = Azure.configure({ resourceName: "example" }).responses("deployment")
|
||||
|
||||
LLM.request({ model, prompt: "Hello", providerOptions: { openai: { store: false } } })
|
||||
|
||||
LLM.request({
|
||||
model,
|
||||
prompt: "Hello",
|
||||
// @ts-expect-error Azure OpenAI store must be boolean.
|
||||
providerOptions: { openai: { store: "false" } },
|
||||
})
|
||||
@@ -1,13 +0,0 @@
|
||||
import { LLM } from "../../src"
|
||||
import { CloudflareWorkersAI } from "../../src/providers"
|
||||
|
||||
const model = CloudflareWorkersAI.configure({ accountId: "account", apiKey: "test" }).model("model")
|
||||
|
||||
LLM.request({ model, prompt: "Hello", providerOptions: { openai: { promptCacheKey: "cache" } } })
|
||||
|
||||
LLM.request({
|
||||
model,
|
||||
prompt: "Hello",
|
||||
// @ts-expect-error Cloudflare's OpenAI-compatible prompt cache key must be a string.
|
||||
providerOptions: { openai: { promptCacheKey: 1 } },
|
||||
})
|
||||
@@ -1,13 +0,0 @@
|
||||
import { LLM } from "../../src"
|
||||
import { GitHubCopilot } from "../../src/providers"
|
||||
|
||||
const model = GitHubCopilot.configure({ baseURL: "https://example.com" }).model("gpt-5")
|
||||
|
||||
LLM.request({ model, prompt: "Hello", providerOptions: { openai: { reasoningSummary: "auto" } } })
|
||||
|
||||
LLM.request({
|
||||
model,
|
||||
prompt: "Hello",
|
||||
// @ts-expect-error Copilot reasoning summaries use the OpenAI union.
|
||||
providerOptions: { openai: { reasoningSummary: "full" } },
|
||||
})
|
||||
@@ -1,13 +0,0 @@
|
||||
import { LLM } from "../../src"
|
||||
import { GoogleVertexChat } from "../../src/providers"
|
||||
|
||||
const model = GoogleVertexChat.configure({ accessToken: "test", project: "project" }).model("gemini")
|
||||
|
||||
LLM.request({ model, prompt: "Hello", providerOptions: { openai: { serviceTier: "priority" } } })
|
||||
|
||||
LLM.request({
|
||||
model,
|
||||
prompt: "Hello",
|
||||
// @ts-expect-error Vertex OpenAI-compatible service tiers use the OpenAI union.
|
||||
providerOptions: { openai: { serviceTier: "premium" } },
|
||||
})
|
||||
@@ -1,13 +0,0 @@
|
||||
import { LLM } from "../../src"
|
||||
import { GoogleVertexMessages } from "../../src/providers"
|
||||
|
||||
const model = GoogleVertexMessages.configure({ accessToken: "test", project: "project" }).model("claude")
|
||||
|
||||
LLM.request({ model, prompt: "Hello", providerOptions: { anthropic: { effort: "medium" } } })
|
||||
|
||||
LLM.request({
|
||||
model,
|
||||
prompt: "Hello",
|
||||
// @ts-expect-error Vertex Anthropic effort must be a string.
|
||||
providerOptions: { anthropic: { effort: false } },
|
||||
})
|
||||
@@ -1,13 +0,0 @@
|
||||
import { LLM } from "../../src"
|
||||
import { GoogleVertexResponses } from "../../src/providers"
|
||||
|
||||
const model = GoogleVertexResponses.configure({ accessToken: "test", project: "project" }).model("gemini")
|
||||
|
||||
LLM.request({ model, prompt: "Hello", providerOptions: { openresponses: { textVerbosity: "high" } } })
|
||||
|
||||
LLM.request({
|
||||
model,
|
||||
prompt: "Hello",
|
||||
// @ts-expect-error Vertex Responses verbosity uses the Open Responses union.
|
||||
providerOptions: { openresponses: { textVerbosity: "verbose" } },
|
||||
})
|
||||
@@ -1,17 +0,0 @@
|
||||
import { LLM } from "../../src"
|
||||
import { GoogleVertex } from "../../src/providers"
|
||||
|
||||
const model = GoogleVertex.provider.configure({ apiKey: "test" }).model("gemini-2.5-pro")
|
||||
|
||||
LLM.request({
|
||||
model,
|
||||
prompt: "Hello",
|
||||
providerOptions: { gemini: { thinkingConfig: { includeThoughts: true } } },
|
||||
})
|
||||
|
||||
LLM.request({
|
||||
model,
|
||||
prompt: "Hello",
|
||||
// @ts-expect-error Vertex Gemini includeThoughts must be boolean.
|
||||
providerOptions: { gemini: { thinkingConfig: { includeThoughts: "yes" } } },
|
||||
})
|
||||
@@ -1,17 +0,0 @@
|
||||
import { LLM } from "../../src"
|
||||
import { Google } from "../../src/providers"
|
||||
|
||||
const model = Google.provider.model("gemini-2.5-pro")
|
||||
|
||||
LLM.request({
|
||||
model,
|
||||
prompt: "Hello",
|
||||
providerOptions: { gemini: { thinkingConfig: { thinkingBudget: 1024 } } },
|
||||
})
|
||||
|
||||
LLM.request({
|
||||
model,
|
||||
prompt: "Hello",
|
||||
// @ts-expect-error Gemini thinking budgets must be numeric.
|
||||
providerOptions: { gemini: { thinkingConfig: { thinkingBudget: "large" } } },
|
||||
})
|
||||
@@ -1,13 +0,0 @@
|
||||
import { LLM } from "../../src"
|
||||
import { OpenAICompatibleResponses } from "../../src/providers"
|
||||
|
||||
const model = OpenAICompatibleResponses.configure({ baseURL: "https://example.com" }).model("model")
|
||||
|
||||
LLM.request({ model, prompt: "Hello", providerOptions: { openresponses: { reasoningSummary: "detailed" } } })
|
||||
|
||||
LLM.request({
|
||||
model,
|
||||
prompt: "Hello",
|
||||
// @ts-expect-error Open Responses reasoning summaries use a fixed union.
|
||||
providerOptions: { openresponses: { reasoningSummary: "full" } },
|
||||
})
|
||||
@@ -1,13 +0,0 @@
|
||||
import { LLM } from "../../src"
|
||||
import { OpenAICompatible } from "../../src/providers"
|
||||
|
||||
const model = OpenAICompatible.deepseek.model("deepseek-chat")
|
||||
|
||||
LLM.request({ model, prompt: "Hello", providerOptions: { openai: { store: false } } })
|
||||
|
||||
LLM.request({
|
||||
model,
|
||||
prompt: "Hello",
|
||||
// @ts-expect-error OpenAI-compatible store must be boolean.
|
||||
providerOptions: { openai: { store: "false" } },
|
||||
})
|
||||
@@ -1,13 +0,0 @@
|
||||
import { LLM } from "../../src"
|
||||
import { OpenAI } from "../../src/providers"
|
||||
|
||||
const model = OpenAI.responses("gpt-5")
|
||||
|
||||
LLM.request({ model, prompt: "Hello", providerOptions: { openai: { reasoningEffort: "high" } } })
|
||||
|
||||
LLM.request({
|
||||
model,
|
||||
prompt: "Hello",
|
||||
// @ts-expect-error OpenAI reasoning effort must be a string.
|
||||
providerOptions: { openai: { reasoningEffort: 1 } },
|
||||
})
|
||||
@@ -1,13 +0,0 @@
|
||||
import { LLM } from "../../src"
|
||||
import { OpenRouter } from "../../src/providers"
|
||||
|
||||
const model = OpenRouter.provider.model("anthropic/claude-sonnet-4.5")
|
||||
|
||||
LLM.request({ model, prompt: "Hello", providerOptions: { openrouter: { usage: true } } })
|
||||
|
||||
LLM.request({
|
||||
model,
|
||||
prompt: "Hello",
|
||||
// @ts-expect-error OpenRouter usage must be boolean or an option record.
|
||||
providerOptions: { openrouter: { usage: "yes" } },
|
||||
})
|
||||
@@ -1,13 +0,0 @@
|
||||
import { LLM } from "../../src"
|
||||
import { XAI } from "../../src/providers"
|
||||
|
||||
const model = XAI.provider.model("grok-4")
|
||||
|
||||
LLM.request({ model, prompt: "Hello", providerOptions: { openai: { reasoningEffort: "high" } } })
|
||||
|
||||
LLM.request({
|
||||
model,
|
||||
prompt: "Hello",
|
||||
// @ts-expect-error xAI's OpenAI-compatible reasoning effort must be a string.
|
||||
providerOptions: { openai: { reasoningEffort: true } },
|
||||
})
|
||||
@@ -3,7 +3,6 @@ import { Effect } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { CacheHint, LLM, LLMError, LLMRequest, Message, ToolCallPart, ToolDefinition, Usage } from "../../src"
|
||||
import { Auth, LLMClient } from "../../src/route"
|
||||
import { compileRequest } from "../../src/route/client"
|
||||
import * as AnthropicMessages from "../../src/protocols/anthropic-messages"
|
||||
import { continuationRequest, nativeAnthropicMessagesContinuation } from "../continuation-scenarios"
|
||||
import { it } from "../lib/effect"
|
||||
@@ -45,7 +44,7 @@ const expectToolResult = (body: AnthropicMessages.AnthropicMessagesBody): Anthro
|
||||
describe("Anthropic Messages route", () => {
|
||||
it.effect("prepares Anthropic Messages target", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(request)
|
||||
const prepared = yield* LLMClient.prepare(request)
|
||||
|
||||
expect(prepared.body).toEqual({
|
||||
model: "claude-sonnet-4-5",
|
||||
@@ -60,7 +59,7 @@ describe("Anthropic Messages route", () => {
|
||||
|
||||
it.effect("lowers adaptive thinking settings with effort", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
|
||||
LLMRequest.update(request, {
|
||||
providerOptions: {
|
||||
anthropic: { thinking: { type: "adaptive", display: "summarized" }, effort: "low" },
|
||||
@@ -77,17 +76,17 @@ describe("Anthropic Messages route", () => {
|
||||
|
||||
it.effect("normalizes enabled and disabled thinking settings", () =>
|
||||
Effect.gen(function* () {
|
||||
const enabled = yield* compileRequest(
|
||||
const enabled = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
|
||||
LLMRequest.update(request, {
|
||||
providerOptions: { anthropic: { thinking: { type: "enabled", budgetTokens: 1_024 } } },
|
||||
}),
|
||||
)
|
||||
const legacy = yield* compileRequest(
|
||||
const legacy = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
|
||||
LLMRequest.update(request, {
|
||||
providerOptions: { anthropic: { thinking: { type: "enabled", budget_tokens: 2_048 } } },
|
||||
}),
|
||||
)
|
||||
const disabled = yield* compileRequest(
|
||||
const disabled = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
|
||||
LLMRequest.update(request, {
|
||||
providerOptions: { anthropic: { thinking: { type: "disabled" } } },
|
||||
}),
|
||||
@@ -101,7 +100,7 @@ describe("Anthropic Messages route", () => {
|
||||
|
||||
it.effect("rejects enabled thinking without a budget", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* compileRequest(
|
||||
const error = yield* LLMClient.prepare(
|
||||
LLMRequest.update(request, {
|
||||
providerOptions: { anthropic: { thinking: { type: "enabled" } } },
|
||||
}),
|
||||
@@ -113,7 +112,7 @@ describe("Anthropic Messages route", () => {
|
||||
|
||||
it.effect("lowers chronological system updates natively for Claude Opus 4.8 with cache hints", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
|
||||
LLM.request({
|
||||
model: opus48,
|
||||
messages: [
|
||||
@@ -138,7 +137,7 @@ describe("Anthropic Messages route", () => {
|
||||
|
||||
it.effect("lowers chronological system updates to wrapped user text for unsupported Anthropic models", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [
|
||||
@@ -165,7 +164,7 @@ describe("Anthropic Messages route", () => {
|
||||
|
||||
it.effect("rejects non-text chronological system update content before send", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* compileRequest(
|
||||
const error = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
model: opus48,
|
||||
messages: [
|
||||
@@ -182,7 +181,7 @@ describe("Anthropic Messages route", () => {
|
||||
it.effect("falls back for unsupported native chronological system update placement", () =>
|
||||
Effect.gen(function* () {
|
||||
expect(
|
||||
(yield* compileRequest(
|
||||
(yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
|
||||
LLM.request({
|
||||
model: opus48,
|
||||
messages: [Message.assistant("Plain."), Message.system("After plain assistant.")],
|
||||
@@ -197,11 +196,12 @@ describe("Anthropic Messages route", () => {
|
||||
},
|
||||
])
|
||||
expect(
|
||||
(yield* compileRequest(LLM.request({ model: opus48, messages: [Message.system("First.")], cache: "none" })))
|
||||
.body.messages,
|
||||
(yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
|
||||
LLM.request({ model: opus48, messages: [Message.system("First.")], cache: "none" }),
|
||||
)).body.messages,
|
||||
).toEqual([{ role: "user", content: [{ type: "text", text: "<system-update>\nFirst.\n</system-update>" }] }])
|
||||
expect(
|
||||
(yield* compileRequest(
|
||||
(yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
|
||||
LLM.request({
|
||||
model: opus48,
|
||||
messages: [Message.user("Before."), Message.system("One."), Message.system("Two.")],
|
||||
@@ -223,7 +223,7 @@ describe("Anthropic Messages route", () => {
|
||||
|
||||
it.effect("rejects a system update between a local tool call and its result", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* compileRequest(
|
||||
const error = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
model: opus48,
|
||||
messages: [
|
||||
@@ -242,7 +242,7 @@ describe("Anthropic Messages route", () => {
|
||||
|
||||
it.effect("prepares tool call and tool result messages", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
|
||||
LLM.request({
|
||||
id: "req_tool_result",
|
||||
model,
|
||||
@@ -273,7 +273,7 @@ describe("Anthropic Messages route", () => {
|
||||
|
||||
it.effect("keeps tools and sends tool_choice none", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
|
||||
LLM.request({
|
||||
id: "req_tool_choice_none",
|
||||
model,
|
||||
@@ -303,7 +303,7 @@ describe("Anthropic Messages route", () => {
|
||||
// not JSON-stringified into `tool_result.content`.
|
||||
it.effect("lowers media tool-result content as structured blocks", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
|
||||
LLM.request({
|
||||
id: "req_tool_result_image",
|
||||
model,
|
||||
@@ -335,7 +335,7 @@ describe("Anthropic Messages route", () => {
|
||||
|
||||
it.effect("lowers single-image tool-result content as a structured image block", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
|
||||
LLM.request({
|
||||
id: "req_tool_result_image_only",
|
||||
model,
|
||||
@@ -360,7 +360,7 @@ describe("Anthropic Messages route", () => {
|
||||
|
||||
it.effect("rejects unsupported media in tool-result content with a clear error", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* compileRequest(
|
||||
const error = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
id: "req_tool_result_unsupported_media",
|
||||
model,
|
||||
@@ -384,7 +384,7 @@ describe("Anthropic Messages route", () => {
|
||||
|
||||
it.effect("prepares the composed native continuation request", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
|
||||
continuationRequest({
|
||||
id: "req_native_continuation_anthropic",
|
||||
model,
|
||||
@@ -428,7 +428,7 @@ describe("Anthropic Messages route", () => {
|
||||
|
||||
it.effect("lowers preserved Anthropic reasoning signature metadata", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [
|
||||
@@ -447,7 +447,7 @@ describe("Anthropic Messages route", () => {
|
||||
|
||||
it.effect("round-trips redacted thinking as redacted_thinking blocks", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [
|
||||
@@ -628,7 +628,9 @@ describe("Anthropic Messages route", () => {
|
||||
{ type: "reasoning", text: "", providerMetadata: { anthropic: { signature: "sig_1" } } },
|
||||
])
|
||||
|
||||
const prepared = yield* compileRequest(LLM.request({ model, messages: [response.message], cache: "none" }))
|
||||
const prepared = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
|
||||
LLM.request({ model, messages: [response.message], cache: "none" }),
|
||||
)
|
||||
expect(prepared.body.messages).toEqual([
|
||||
{ role: "assistant", content: [{ type: "thinking", thinking: "", signature: "sig_1" }] },
|
||||
])
|
||||
@@ -771,7 +773,7 @@ describe("Anthropic Messages route", () => {
|
||||
),
|
||||
),
|
||||
)
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [
|
||||
@@ -1131,7 +1133,7 @@ describe("Anthropic Messages route", () => {
|
||||
|
||||
it.effect("round-trips provider-executed assistant content into server tool blocks", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
id: "req_round_trip",
|
||||
model,
|
||||
@@ -1182,7 +1184,7 @@ describe("Anthropic Messages route", () => {
|
||||
|
||||
it.effect("rejects round-trip for unknown server tool names", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* compileRequest(
|
||||
const error = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
id: "req_unknown_server_tool",
|
||||
model,
|
||||
@@ -1259,7 +1261,7 @@ describe("Anthropic Messages route", () => {
|
||||
|
||||
it.effect("maps ttlSeconds >= 3600 to cache_control ttl: '1h'", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
model,
|
||||
system: { type: "text", text: "system", cache: new CacheHint({ type: "ephemeral", ttlSeconds: 3600 }) },
|
||||
@@ -1275,7 +1277,7 @@ describe("Anthropic Messages route", () => {
|
||||
|
||||
it.effect("emits cache_control on tool definitions and tool-result blocks", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
model,
|
||||
tools: [
|
||||
@@ -1316,7 +1318,7 @@ describe("Anthropic Messages route", () => {
|
||||
it.effect("drops cache_control breakpoints past the 4-per-request cap", () =>
|
||||
Effect.gen(function* () {
|
||||
const hint = new CacheHint({ type: "ephemeral" })
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
model,
|
||||
system: [
|
||||
@@ -1342,7 +1344,7 @@ describe("Anthropic Messages route", () => {
|
||||
it.effect("spends breakpoint budget on tools before system before messages", () =>
|
||||
Effect.gen(function* () {
|
||||
const hint = new CacheHint({ type: "ephemeral" })
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
model,
|
||||
tools: [
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
ToolDefinition,
|
||||
} from "../../src"
|
||||
import { LLMClient } from "../../src/route"
|
||||
import { compileRequest } from "../../src/route/client"
|
||||
import { AmazonBedrock } from "../../src/providers"
|
||||
import * as BedrockConverse from "../../src/protocols/bedrock-converse"
|
||||
import { it } from "../lib/effect"
|
||||
@@ -102,7 +101,7 @@ const baseRequest = LLM.request({
|
||||
describe("Bedrock Converse route", () => {
|
||||
it.effect("prepares Converse target with system, inference config, and messages", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(baseRequest)
|
||||
const prepared = yield* LLMClient.prepare(baseRequest)
|
||||
|
||||
expect(prepared.body).toEqual({
|
||||
modelId: "anthropic.claude-3-5-sonnet-20240620-v1:0",
|
||||
@@ -115,7 +114,7 @@ describe("Bedrock Converse route", () => {
|
||||
|
||||
it.effect("passes topK through additionalModelRequestFields as top_k", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<BedrockConverse.BedrockConverseBody>(
|
||||
LLMRequest.update(baseRequest, {
|
||||
generation: GenerationOptions.make({ maxTokens: 64, temperature: 0, topK: 40 }),
|
||||
}),
|
||||
@@ -130,14 +129,14 @@ describe("Bedrock Converse route", () => {
|
||||
|
||||
it.effect("omits additionalModelRequestFields when topK is unset", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(baseRequest)
|
||||
const prepared = yield* LLMClient.prepare<BedrockConverse.BedrockConverseBody>(baseRequest)
|
||||
expect(prepared.body.additionalModelRequestFields).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("lowers chronological system updates to wrapped user text in order", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<BedrockConverse.BedrockConverseBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [Message.user("Before."), Message.system("Update."), Message.assistant("After.")],
|
||||
@@ -154,7 +153,7 @@ describe("Bedrock Converse route", () => {
|
||||
|
||||
it.effect("prepares tool config with toolSpec and toolChoice", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLMRequest.update(baseRequest, {
|
||||
tools: [
|
||||
ToolDefinition.make({
|
||||
@@ -188,7 +187,7 @@ describe("Bedrock Converse route", () => {
|
||||
|
||||
it.effect("keeps tools and omits the unsupported choice when tool choice is none", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLMRequest.update(baseRequest, {
|
||||
tools: [
|
||||
ToolDefinition.make({
|
||||
@@ -218,7 +217,7 @@ describe("Bedrock Converse route", () => {
|
||||
|
||||
it.effect("lowers assistant tool-call + tool-result message history", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
id: "req_history",
|
||||
model,
|
||||
@@ -257,7 +256,7 @@ describe("Bedrock Converse route", () => {
|
||||
|
||||
it.effect("lowers image content in tool-result messages", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
id: "req_tool_image",
|
||||
model,
|
||||
@@ -492,7 +491,7 @@ describe("Bedrock Converse route", () => {
|
||||
providerMetadata: { bedrock: { signature: "sig_1" } },
|
||||
})
|
||||
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<BedrockConverse.BedrockConverseBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [
|
||||
@@ -547,7 +546,9 @@ describe("Bedrock Converse route", () => {
|
||||
},
|
||||
])
|
||||
|
||||
const prepared = yield* compileRequest(LLM.request({ model, messages: [response.message], cache: "none" }))
|
||||
const prepared = yield* LLMClient.prepare<BedrockConverse.BedrockConverseBody>(
|
||||
LLM.request({ model, messages: [response.message], cache: "none" }),
|
||||
)
|
||||
expect(prepared.body.messages).toEqual([
|
||||
{
|
||||
role: "assistant",
|
||||
@@ -638,7 +639,7 @@ describe("Bedrock Converse route", () => {
|
||||
text: "",
|
||||
providerMetadata: { bedrock: { redactedData } },
|
||||
})
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<BedrockConverse.BedrockConverseBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [
|
||||
@@ -753,7 +754,7 @@ describe("Bedrock Converse route", () => {
|
||||
secretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
||||
},
|
||||
}).model("anthropic.claude-3-5-sonnet-20240620-v1:0")
|
||||
const prepared = yield* compileRequest(LLMRequest.update(baseRequest, { model: signed }))
|
||||
const prepared = yield* LLMClient.prepare(LLMRequest.update(baseRequest, { model: signed }))
|
||||
|
||||
expect(prepared.route).toBe("bedrock-converse")
|
||||
expect(prepared.model).toBe(signed)
|
||||
@@ -763,7 +764,7 @@ describe("Bedrock Converse route", () => {
|
||||
it.effect("emits cachePoint markers after system, user-text, and assistant-text with cache hints", () =>
|
||||
Effect.gen(function* () {
|
||||
const cache = new CacheHint({ type: "ephemeral" })
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
id: "req_cache",
|
||||
model,
|
||||
@@ -795,7 +796,7 @@ describe("Bedrock Converse route", () => {
|
||||
|
||||
it.effect("does not emit cachePoint when no cache hint is set", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(baseRequest)
|
||||
const prepared = yield* LLMClient.prepare(baseRequest)
|
||||
expect(prepared.body).toMatchObject({
|
||||
system: [{ text: "You are concise." }],
|
||||
messages: [{ role: "user", content: [{ text: "Say hello." }] }],
|
||||
@@ -805,7 +806,7 @@ describe("Bedrock Converse route", () => {
|
||||
|
||||
it.effect("lowers image media into Bedrock image blocks", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
id: "req_image",
|
||||
model,
|
||||
@@ -842,7 +843,7 @@ describe("Bedrock Converse route", () => {
|
||||
|
||||
it.effect("base64-encodes Uint8Array image bytes", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
id: "req_image_bytes",
|
||||
model,
|
||||
@@ -864,7 +865,7 @@ describe("Bedrock Converse route", () => {
|
||||
|
||||
it.effect("lowers document media into Bedrock document blocks with format and name", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
id: "req_doc",
|
||||
model,
|
||||
@@ -896,7 +897,7 @@ describe("Bedrock Converse route", () => {
|
||||
|
||||
it.effect("requires names for document media", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* compileRequest(
|
||||
const error = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [Message.user({ type: "media", mediaType: "application/pdf", data: "UERGREFUQQ==" })],
|
||||
@@ -909,7 +910,7 @@ describe("Bedrock Converse route", () => {
|
||||
|
||||
it.effect("passes named document-only messages through for provider validation", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<BedrockConverse.BedrockConverseBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
cache: "none",
|
||||
@@ -935,7 +936,7 @@ describe("Bedrock Converse route", () => {
|
||||
|
||||
it.effect("lowers document media in tool results", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<BedrockConverse.BedrockConverseBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
cache: "none",
|
||||
@@ -987,7 +988,7 @@ describe("Bedrock Converse route", () => {
|
||||
|
||||
it.effect("rejects unsupported image media types", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* compileRequest(
|
||||
const error = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
id: "req_bad_image",
|
||||
model,
|
||||
@@ -1001,7 +1002,7 @@ describe("Bedrock Converse route", () => {
|
||||
|
||||
it.effect("rejects unsupported document media types", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* compileRequest(
|
||||
const error = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
id: "req_bad_doc",
|
||||
model,
|
||||
@@ -1016,7 +1017,7 @@ describe("Bedrock Converse route", () => {
|
||||
it.effect("maps ttlSeconds >= 3600 to cachePoint ttl: '1h'", () =>
|
||||
Effect.gen(function* () {
|
||||
const cache = new CacheHint({ type: "ephemeral", ttlSeconds: 3600 })
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
model,
|
||||
system: [{ type: "text", text: "system", cache }],
|
||||
@@ -1033,7 +1034,7 @@ describe("Bedrock Converse route", () => {
|
||||
it.effect("appends cachePoint after marked tool definitions and tool-result blocks", () =>
|
||||
Effect.gen(function* () {
|
||||
const cache = new CacheHint({ type: "ephemeral" })
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
model,
|
||||
tools: [{ name: "lookup", description: "lookup", inputSchema: { type: "object", properties: {} }, cache }],
|
||||
@@ -1065,7 +1066,7 @@ describe("Bedrock Converse route", () => {
|
||||
it.effect("drops cachePoint markers past the 4-per-request cap", () =>
|
||||
Effect.gen(function* () {
|
||||
const cache = new CacheHint({ type: "ephemeral" })
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
model,
|
||||
system: [
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ConfigProvider, Effect, Schema } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { LLM, LLMEvent } from "../../src"
|
||||
import { CloudflareAIGateway, CloudflareWorkersAI } from "../../src/providers/cloudflare"
|
||||
import { compileRequest } from "../../src/route/client"
|
||||
import { LLMClient } from "../../src/route"
|
||||
import { it } from "../lib/effect"
|
||||
import { dynamicResponse } from "../lib/http"
|
||||
import { sseEvents } from "../lib/sse"
|
||||
@@ -34,7 +34,7 @@ describe("Cloudflare", () => {
|
||||
})
|
||||
expect(model.route.endpoint.baseURL).toBe("https://gateway.ai.cloudflare.com/v1/test-account/test-gateway/compat")
|
||||
|
||||
const prepared = yield* compileRequest(LLM.request({ model, prompt: "Say hello." }))
|
||||
const prepared = yield* LLMClient.prepare(LLM.request({ model, prompt: "Say hello." }))
|
||||
|
||||
expect(prepared.route).toBe("cloudflare-ai-gateway")
|
||||
expect(prepared.body).toMatchObject({
|
||||
@@ -129,7 +129,7 @@ describe("Cloudflare", () => {
|
||||
openai: { reasoningField: "reasoning", reasoningDetails: merged },
|
||||
})
|
||||
|
||||
const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] }))
|
||||
const replay = yield* LLMClient.prepare(LLM.request({ model, messages: [response.message] }))
|
||||
expect(replay.body.messages).toEqual([
|
||||
{ role: "assistant", content: "Hello", reasoning: "Thinking", reasoning_details: merged },
|
||||
])
|
||||
@@ -180,7 +180,7 @@ describe("Cloudflare", () => {
|
||||
|
||||
it.effect("allows a fully configured baseURL override", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
model: CloudflareAIGateway.configure({
|
||||
baseURL: "https://gateway.proxy.test/v1/custom/compat",
|
||||
@@ -208,7 +208,7 @@ describe("Cloudflare", () => {
|
||||
})
|
||||
expect(model.route.endpoint.baseURL).toBe("https://api.cloudflare.com/client/v4/accounts/test-account/ai/v1")
|
||||
|
||||
const prepared = yield* compileRequest(LLM.request({ model, prompt: "Say hello." }))
|
||||
const prepared = yield* LLMClient.prepare(LLM.request({ model, prompt: "Say hello." }))
|
||||
|
||||
expect(prepared.route).toBe("cloudflare-workers-ai")
|
||||
expect(prepared.body).toMatchObject({
|
||||
|
||||
@@ -2,7 +2,6 @@ import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { LLM, LLMError, LLMRequest, Message, ToolCallPart, ToolDefinition, Usage } from "../../src"
|
||||
import { Auth, LLMClient } from "../../src/route"
|
||||
import { compileRequest } from "../../src/route/client"
|
||||
import * as Gemini from "../../src/protocols/gemini"
|
||||
import { ProviderShared } from "../../src/protocols/shared"
|
||||
import { it } from "../lib/effect"
|
||||
@@ -27,7 +26,7 @@ const request = LLM.request({
|
||||
describe("Gemini route", () => {
|
||||
it.effect("prepares Gemini target", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(request)
|
||||
const prepared = yield* LLMClient.prepare(request)
|
||||
|
||||
expect(prepared.body).toEqual({
|
||||
contents: [{ role: "user", parts: [{ text: "Say hello." }] }],
|
||||
@@ -39,12 +38,12 @@ describe("Gemini route", () => {
|
||||
|
||||
it.effect("normalizes Gemini thinking options", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<Gemini.GeminiBody>(
|
||||
LLMRequest.update(request, {
|
||||
providerOptions: { gemini: { thinkingConfig: { thinkingBudget: 0, includeThoughts: false } } },
|
||||
}),
|
||||
)
|
||||
const filtered = yield* compileRequest(
|
||||
const filtered = yield* LLMClient.prepare<Gemini.GeminiBody>(
|
||||
LLMRequest.update(request, {
|
||||
providerOptions: { gemini: { thinkingConfig: { thinkingBudget: "invalid", includeThoughts: false } } },
|
||||
}),
|
||||
@@ -60,7 +59,7 @@ describe("Gemini route", () => {
|
||||
|
||||
it.effect("lowers chronological system updates to wrapped user text in order", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<Gemini.GeminiBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [Message.user("Before."), Message.system("Update."), Message.assistant("After.")],
|
||||
@@ -76,7 +75,7 @@ describe("Gemini route", () => {
|
||||
|
||||
it.effect("prepares multimodal user input and tool history", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
id: "req_tool_result",
|
||||
model,
|
||||
@@ -144,7 +143,7 @@ describe("Gemini route", () => {
|
||||
|
||||
it.effect("continues media tool results as inline model input without base64 text", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<Gemini.GeminiBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [
|
||||
@@ -189,7 +188,7 @@ describe("Gemini route", () => {
|
||||
|
||||
it.effect("strips matching data URLs to raw base64 inlineData", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<Gemini.GeminiBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [
|
||||
@@ -230,7 +229,7 @@ describe("Gemini route", () => {
|
||||
] as const)
|
||||
it.effect(`rejects ${name}`, () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* compileRequest(
|
||||
const error = yield* LLMClient.prepare(
|
||||
LLM.request({ model, messages: [Message.user({ type: "media", ...media })] }),
|
||||
).pipe(Effect.flip)
|
||||
expect(error.message).toMatch(/does not support|does not match|valid base64/)
|
||||
@@ -239,7 +238,7 @@ describe("Gemini route", () => {
|
||||
|
||||
it.effect("rejects oversized image input", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* compileRequest(
|
||||
const error = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [
|
||||
@@ -257,7 +256,7 @@ describe("Gemini route", () => {
|
||||
|
||||
it.effect("keeps tools and sends function calling mode NONE", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
id: "req_tool_choice_none",
|
||||
model,
|
||||
@@ -277,7 +276,7 @@ describe("Gemini route", () => {
|
||||
|
||||
it.effect("sanitizes integer enums, dangling required, untyped arrays, and scalar object keys", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
id: "req_schema_patch",
|
||||
model,
|
||||
@@ -458,7 +457,7 @@ describe("Gemini route", () => {
|
||||
response.events.findIndex((event) => event.type === "tool-call"),
|
||||
)
|
||||
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<Gemini.GeminiBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [
|
||||
@@ -692,7 +691,7 @@ describe("Gemini route", () => {
|
||||
|
||||
it.effect("rejects unsupported assistant media content", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* compileRequest(
|
||||
const error = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
id: "req_media",
|
||||
model,
|
||||
|
||||
@@ -4,7 +4,6 @@ import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { LLM } from "../../src"
|
||||
import { GoogleVertex, GoogleVertexChat, GoogleVertexMessages, GoogleVertexResponses } from "../../src/providers"
|
||||
import { LLMClient } from "../../src/route"
|
||||
import { compileRequest } from "../../src/route/client"
|
||||
import { it } from "../lib/effect"
|
||||
import { dynamicResponse } from "../lib/http"
|
||||
import { deltaChunk, finishChunk } from "../lib/openai-chunks"
|
||||
@@ -183,7 +182,7 @@ describe("Google Vertex providers", () => {
|
||||
|
||||
it.effect("protects the Vertex Messages API version from body overlays", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* compileRequest(
|
||||
const error = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
model: GoogleVertexMessages.configure({
|
||||
accessToken: "vertex-token",
|
||||
|
||||
@@ -5,7 +5,6 @@ import { OpenAIChat } from "../../src/protocols/openai-chat"
|
||||
import * as OpenAICompatible from "../../src/providers/openai-compatible"
|
||||
import * as OpenRouter from "../../src/providers/openrouter"
|
||||
import { LLMClient } from "../../src/route"
|
||||
import { compileRequest } from "../../src/route/client"
|
||||
import { recordedTests } from "../recorded-test"
|
||||
import { expectWeatherToolLoop, goldenWeatherToolLoopRequest, runWeatherToolLoop } from "../recorded-scenarios"
|
||||
|
||||
@@ -85,7 +84,9 @@ for (const item of cases) {
|
||||
),
|
||||
).toBe(true)
|
||||
|
||||
const replay = yield* compileRequest(LLM.request({ model: item.model, messages: [response.message] }))
|
||||
const replay = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
|
||||
LLM.request({ model: item.model, messages: [response.message] }),
|
||||
)
|
||||
expect(replay.body.messages).toMatchObject([
|
||||
{ role: "assistant", content: response.text, reasoning: response.reasoning },
|
||||
])
|
||||
|
||||
@@ -18,7 +18,6 @@ import * as OpenAI from "../../src/providers/openai"
|
||||
import * as OpenAIChat from "../../src/protocols/openai-chat"
|
||||
import { ProviderShared } from "../../src/protocols/shared"
|
||||
import { Auth, LLMClient } from "../../src/route"
|
||||
import { compileRequest } from "../../src/route/client"
|
||||
import { it } from "../lib/effect"
|
||||
import { dynamicResponse, fixedResponse, truncatedStream } from "../lib/http"
|
||||
import { deltaChunk, usageChunk } from "../lib/openai-chunks"
|
||||
@@ -43,7 +42,11 @@ const request = LLM.request({
|
||||
describe("OpenAI Chat route", () => {
|
||||
it.effect("prepares OpenAI Chat payload", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(request)
|
||||
// Pass the OpenAIChat payload type so `prepared.body` is statically
|
||||
// typed to the route's native shape — the assertions below read field
|
||||
// names without `unknown` casts.
|
||||
const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(request)
|
||||
const _typed: { readonly model: string; readonly stream: true } = prepared.body
|
||||
|
||||
expect(prepared.body).toEqual({
|
||||
model: "gpt-4o-mini",
|
||||
@@ -61,7 +64,7 @@ describe("OpenAI Chat route", () => {
|
||||
|
||||
it.effect("lowers chronological system updates to escaped user wrappers in order", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [
|
||||
@@ -84,7 +87,7 @@ describe("OpenAI Chat route", () => {
|
||||
|
||||
it.effect("replays canonical reasoning as OpenAI-compatible reasoning_content", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [
|
||||
@@ -102,7 +105,7 @@ describe("OpenAI Chat route", () => {
|
||||
|
||||
it.effect("writes reasoning to a configured custom field on every assistant message", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
|
||||
LLM.request({
|
||||
model: Model.update(model, { compatibility: { reasoningField: "vendor_reasoning" } }),
|
||||
messages: [
|
||||
@@ -128,7 +131,7 @@ describe("OpenAI Chat route", () => {
|
||||
|
||||
it.effect("rejects reasoning fields that conflict with assistant message fields", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* compileRequest(
|
||||
const error = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
model: Model.update(model, { compatibility: { reasoningField: "content" } }),
|
||||
messages: [Message.assistant([{ type: "reasoning", text: "thinking" }])],
|
||||
@@ -141,7 +144,7 @@ describe("OpenAI Chat route", () => {
|
||||
|
||||
it.effect("maps OpenAI provider options to Chat options", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
|
||||
LLM.request({
|
||||
model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).chat("gpt-4o-mini"),
|
||||
prompt: "think",
|
||||
@@ -156,7 +159,7 @@ describe("OpenAI Chat route", () => {
|
||||
|
||||
it.effect("passes through custom OpenAI-compatible reasoning effort strings", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
prompt: "think",
|
||||
@@ -250,7 +253,7 @@ describe("OpenAI Chat route", () => {
|
||||
|
||||
it.effect("prepares assistant tool-call and tool-result messages", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
id: "req_tool_result",
|
||||
model,
|
||||
@@ -288,7 +291,7 @@ describe("OpenAI Chat route", () => {
|
||||
it.effect("preserves structured tool errors for the model", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = { error: { type: "unknown", message: "Tool execution interrupted" } }
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [
|
||||
@@ -308,7 +311,7 @@ describe("OpenAI Chat route", () => {
|
||||
|
||||
it.effect("continues image tool results as vision input without base64 text", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [
|
||||
@@ -352,7 +355,7 @@ describe("OpenAI Chat route", () => {
|
||||
|
||||
it.effect("orders parallel tool responses before one aggregated vision message", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [
|
||||
@@ -402,7 +405,7 @@ describe("OpenAI Chat route", () => {
|
||||
|
||||
it.effect("aggregates consecutive tool images with a following system update", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [
|
||||
@@ -443,7 +446,7 @@ describe("OpenAI Chat route", () => {
|
||||
|
||||
it.effect("appends system updates without replacing multipart user content", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [
|
||||
@@ -471,7 +474,7 @@ describe("OpenAI Chat route", () => {
|
||||
] as const)
|
||||
it.effect(`rejects ${name}`, () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* compileRequest(
|
||||
const error = yield* LLMClient.prepare(
|
||||
LLM.request({ model, messages: [Message.user({ type: "media", ...media })] }),
|
||||
).pipe(Effect.flip)
|
||||
expect(error.message).toMatch(/does not support|does not match|valid base64/)
|
||||
@@ -480,7 +483,7 @@ describe("OpenAI Chat route", () => {
|
||||
|
||||
it.effect("rejects oversized image input", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* compileRequest(
|
||||
const error = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [
|
||||
@@ -498,7 +501,7 @@ describe("OpenAI Chat route", () => {
|
||||
|
||||
it.effect("prepares raw and data URL image media as vision input", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
|
||||
LLM.request({
|
||||
id: "req_media",
|
||||
model,
|
||||
@@ -525,7 +528,7 @@ describe("OpenAI Chat route", () => {
|
||||
|
||||
it.effect("lowers reasoning-only assistant history", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
|
||||
LLM.request({
|
||||
id: "req_reasoning",
|
||||
model,
|
||||
@@ -616,7 +619,9 @@ describe("OpenAI Chat route", () => {
|
||||
openai: { reasoningField: field },
|
||||
})
|
||||
|
||||
const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] }))
|
||||
const replay = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
|
||||
LLM.request({ model, messages: [response.message] }),
|
||||
)
|
||||
expect(replay.body.messages).toEqual([{ role: "assistant", content: "Hello", [field]: "thinking" }])
|
||||
}
|
||||
}),
|
||||
@@ -642,7 +647,9 @@ describe("OpenAI Chat route", () => {
|
||||
openai: { reasoningField: "vendor_reasoning" },
|
||||
})
|
||||
|
||||
const replay = yield* compileRequest(LLM.request({ model: custom, messages: [response.message] }))
|
||||
const replay = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
|
||||
LLM.request({ model: custom, messages: [response.message] }),
|
||||
)
|
||||
expect(replay.body.messages).toEqual([{ role: "assistant", content: "Hello", vendor_reasoning: "thinking" }])
|
||||
}),
|
||||
)
|
||||
@@ -685,7 +692,9 @@ describe("OpenAI Chat route", () => {
|
||||
openai: { reasoningField: "reasoning", reasoningDetails: details },
|
||||
})
|
||||
|
||||
const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] }))
|
||||
const replay = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
|
||||
LLM.request({ model, messages: [response.message] }),
|
||||
)
|
||||
expect(replay.body.messages).toEqual([
|
||||
{
|
||||
role: "assistant",
|
||||
@@ -728,7 +737,9 @@ describe("OpenAI Chat route", () => {
|
||||
openai: { reasoningDetails: details },
|
||||
})
|
||||
|
||||
const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] }))
|
||||
const replay = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
|
||||
LLM.request({ model, messages: [response.message] }),
|
||||
)
|
||||
expect(replay.body.messages).toEqual([{ role: "assistant", content: "Hello", reasoning_details: details }])
|
||||
}),
|
||||
)
|
||||
@@ -753,7 +764,9 @@ describe("OpenAI Chat route", () => {
|
||||
openai: { reasoningField: "reasoning", reasoningDetails: details },
|
||||
})
|
||||
|
||||
const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] }))
|
||||
const replay = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
|
||||
LLM.request({ model, messages: [response.message] }),
|
||||
)
|
||||
expect(replay.body.messages).toEqual([
|
||||
{ role: "assistant", content: "Hello", reasoning: "thinking", reasoning_details: details },
|
||||
])
|
||||
@@ -826,7 +839,9 @@ describe("OpenAI Chat route", () => {
|
||||
openai: { reasoningDetails: [] },
|
||||
})
|
||||
|
||||
const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] }))
|
||||
const replay = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
|
||||
LLM.request({ model, messages: [response.message] }),
|
||||
)
|
||||
expect(replay.body.messages).toEqual([{ role: "assistant", content: "Hello", reasoning_details: [] }])
|
||||
}),
|
||||
)
|
||||
@@ -874,7 +889,9 @@ describe("OpenAI Chat route", () => {
|
||||
response.events.findIndex(LLMEvent.is.textStart),
|
||||
)
|
||||
|
||||
const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] }))
|
||||
const replay = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
|
||||
LLM.request({ model, messages: [response.message] }),
|
||||
)
|
||||
expect(replay.body.messages).toEqual([
|
||||
{ role: "assistant", content: "Hello", reasoning: "thinking", reasoning_details: merged },
|
||||
])
|
||||
@@ -901,7 +918,9 @@ describe("OpenAI Chat route", () => {
|
||||
expect(response.events.filter(LLMEvent.is.reasoningStart)).toHaveLength(1)
|
||||
expect(response.events.filter(LLMEvent.is.reasoningEnd)).toHaveLength(1)
|
||||
|
||||
const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] }))
|
||||
const replay = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
|
||||
LLM.request({ model, messages: [response.message] }),
|
||||
)
|
||||
expect(replay.body.messages).toEqual([{ role: "assistant", content: null, reasoning_details: details }])
|
||||
}),
|
||||
)
|
||||
@@ -931,7 +950,7 @@ describe("OpenAI Chat route", () => {
|
||||
Effect.gen(function* () {
|
||||
const first = { type: "reasoning.text", text: "first", signature: "signed-0", index: 0 }
|
||||
const second = { type: "reasoning.text", text: "second", signature: "signed-1", index: 1 }
|
||||
const replay = yield* compileRequest(
|
||||
const replay = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [
|
||||
@@ -960,7 +979,7 @@ describe("OpenAI Chat route", () => {
|
||||
it.effect("retains scalar replay for mixed structured reasoning parts", () =>
|
||||
Effect.gen(function* () {
|
||||
const detail = { type: "reasoning.encrypted", data: "opaque", index: 0 }
|
||||
const replay = yield* compileRequest(
|
||||
const replay = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [
|
||||
@@ -985,7 +1004,7 @@ describe("OpenAI Chat route", () => {
|
||||
it.effect("replays native scalar reasoning alongside native details", () =>
|
||||
Effect.gen(function* () {
|
||||
const details = [{ type: "reasoning.encrypted", data: "opaque", index: 0 }]
|
||||
const replay = yield* compileRequest(
|
||||
const replay = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Effect, Schema } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { LLM, LLMRequest, Message, ToolCallPart, ToolChoice, ToolDefinition } from "../../src"
|
||||
import { Auth, LLMClient } from "../../src/route"
|
||||
import { compileRequest } from "../../src/route/client"
|
||||
import * as OpenAICompatible from "../../src/providers/openai-compatible"
|
||||
import * as OpenAICompatibleChat from "../../src/protocols/openai-compatible-chat"
|
||||
import { it } from "../lib/effect"
|
||||
@@ -53,7 +52,7 @@ const providerFamilies = [
|
||||
describe("OpenAI-compatible Chat route", () => {
|
||||
it.effect("prepares generic Chat target", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLMRequest.update(request, {
|
||||
tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })],
|
||||
toolChoice: ToolChoice.make({ type: "required" }),
|
||||
@@ -128,7 +127,7 @@ describe("OpenAI-compatible Chat route", () => {
|
||||
|
||||
it.effect("matches AI SDK compatible basic request body fixture", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(request)
|
||||
const prepared = yield* LLMClient.prepare(request)
|
||||
|
||||
expect(prepared.body).toEqual({
|
||||
model: "deepseek-chat",
|
||||
@@ -146,7 +145,7 @@ describe("OpenAI-compatible Chat route", () => {
|
||||
|
||||
it.effect("matches AI SDK compatible tool request body fixture", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
id: "req_tool_parity",
|
||||
model,
|
||||
|
||||
@@ -7,7 +7,6 @@ import { OpenResponses } from "../../src/protocols/open-responses"
|
||||
import { OpenAICompatibleResponses } from "../../src/protocols/openai-compatible-responses"
|
||||
import { OpenAIResponses } from "../../src/protocols/openai-responses"
|
||||
import { LLMClient } from "../../src/route"
|
||||
import { compileRequest } from "../../src/route/client"
|
||||
import { it } from "../lib/effect"
|
||||
import { fixedResponse } from "../lib/http"
|
||||
import { sseEvents } from "../lib/sse"
|
||||
@@ -24,7 +23,7 @@ describe("Open Responses-compatible route", () => {
|
||||
baseURL: "https://responses.example.test/v1",
|
||||
provider: "example",
|
||||
}).model("example-model")
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
model,
|
||||
system: "You are concise.",
|
||||
@@ -62,7 +61,7 @@ describe("Open Responses-compatible route", () => {
|
||||
apiKey: "test-key",
|
||||
baseURL: "https://responses.example.test/v1",
|
||||
}).model("example-model")
|
||||
const error = yield* compileRequest(
|
||||
const error = yield* LLMClient.prepare(
|
||||
LLM.request({ model, prompt: "Draw.", tools: [OpenAI.imageGeneration()] }),
|
||||
).pipe(Effect.flip)
|
||||
|
||||
@@ -77,7 +76,7 @@ describe("Open Responses-compatible route", () => {
|
||||
apiKey: "test-key",
|
||||
baseURL: "https://responses.example.test/v1",
|
||||
}).model("example-model")
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [
|
||||
@@ -103,7 +102,7 @@ describe("Open Responses-compatible route", () => {
|
||||
baseURL: "https://responses.example.test/v1",
|
||||
providerOptions: { openresponses: { reasoningEffort: "low", store: true } },
|
||||
}).model("example-model")
|
||||
const prepared = yield* compileRequest(LLM.request({ model, prompt: "Think." }))
|
||||
const prepared = yield* LLMClient.prepare(LLM.request({ model, prompt: "Think." }))
|
||||
|
||||
expect(prepared.body).toMatchObject({
|
||||
reasoning: { effort: "low" },
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
Usage,
|
||||
} from "../../src"
|
||||
import { Auth, LLMClient, RequestExecutor, WebSocketExecutor } from "../../src/route"
|
||||
import { compileRequest } from "../../src/route/client"
|
||||
import * as Azure from "../../src/providers/azure"
|
||||
import * as OpenAI from "../../src/providers/openai"
|
||||
import * as XAI from "../../src/providers/xai"
|
||||
@@ -57,7 +56,7 @@ const expectToolOutput = (body: OpenAIResponses.OpenAIResponsesBody): OpenAITool
|
||||
describe("OpenAI Responses route", () => {
|
||||
it.effect("prepares OpenAI Responses target", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(request)
|
||||
const prepared = yield* LLMClient.prepare(request)
|
||||
|
||||
expect(prepared.body).toEqual({
|
||||
model: "gpt-4.1-mini",
|
||||
@@ -75,7 +74,7 @@ describe("OpenAI Responses route", () => {
|
||||
|
||||
it.effect("lowers the hosted OpenAI image generation tool", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
prompt: "Show me a rooftop garden.",
|
||||
@@ -93,7 +92,7 @@ describe("OpenAI Responses route", () => {
|
||||
|
||||
it.effect("rejects invalid hosted image generation options locally", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* compileRequest(
|
||||
const error = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
model,
|
||||
prompt: "Show me a rooftop garden.",
|
||||
@@ -110,7 +109,7 @@ describe("OpenAI Responses route", () => {
|
||||
Effect.gen(function* () {
|
||||
const input = LLMRequest.update(request, { providerOptions: { openai: { serviceTier: "priority" } } })
|
||||
expect(input.providerOptions).toEqual({ openai: { serviceTier: "priority" } })
|
||||
const prepared = yield* compileRequest(input)
|
||||
const prepared = yield* LLMClient.prepare(input)
|
||||
|
||||
expect(prepared.body).toMatchObject({ service_tier: "priority" })
|
||||
expect(prepared.body).not.toHaveProperty("serviceTier")
|
||||
@@ -119,7 +118,7 @@ describe("OpenAI Responses route", () => {
|
||||
|
||||
it.effect("passes through custom OpenAI reasoning effort strings", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
LLMRequest.update(request, { providerOptions: { openai: { reasoningEffort: "experimental" } } }),
|
||||
)
|
||||
|
||||
@@ -129,7 +128,7 @@ describe("OpenAI Responses route", () => {
|
||||
|
||||
it.effect("omits unsupported semantic service tiers", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLMRequest.update(request, { providerOptions: { openai: { serviceTier: "unsupported" } } }),
|
||||
)
|
||||
|
||||
@@ -139,7 +138,7 @@ describe("OpenAI Responses route", () => {
|
||||
|
||||
it.effect("flattens top-level object unions in function schemas", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
LLMRequest.update(request, {
|
||||
tools: [
|
||||
ToolDefinition.make({
|
||||
@@ -192,7 +191,7 @@ describe("OpenAI Responses route", () => {
|
||||
|
||||
it.effect("lowers chronological system updates to escaped user wrappers in order", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [
|
||||
@@ -218,7 +217,7 @@ describe("OpenAI Responses route", () => {
|
||||
|
||||
it.effect("prepares OpenAI Responses WebSocket target", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLMRequest.update(request, {
|
||||
model: OpenAIResponses.webSocketRoute
|
||||
.with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") })
|
||||
@@ -396,7 +395,7 @@ describe("OpenAI Responses route", () => {
|
||||
|
||||
it.effect("prepares function call and function output input items", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
id: "req_tool_result",
|
||||
model,
|
||||
@@ -433,7 +432,7 @@ describe("OpenAI Responses route", () => {
|
||||
content: [],
|
||||
structured: {},
|
||||
}
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [
|
||||
@@ -454,7 +453,7 @@ describe("OpenAI Responses route", () => {
|
||||
|
||||
it.effect("keeps primitive tool errors as plain text", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [
|
||||
@@ -470,7 +469,7 @@ describe("OpenAI Responses route", () => {
|
||||
|
||||
it.effect("keeps non-JSON tool errors as plain text", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [
|
||||
@@ -488,7 +487,7 @@ describe("OpenAI Responses route", () => {
|
||||
// image data is not JSON-stringified into `function_call_output.output`.
|
||||
it.effect("lowers image tool-result content as structured input_image items", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
LLM.request({
|
||||
id: "req_tool_result_image",
|
||||
model,
|
||||
@@ -517,7 +516,7 @@ describe("OpenAI Responses route", () => {
|
||||
|
||||
it.effect("lowers single-image tool-result content as structured input_image array", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
LLM.request({
|
||||
id: "req_tool_result_image_only",
|
||||
model,
|
||||
@@ -541,7 +540,7 @@ describe("OpenAI Responses route", () => {
|
||||
|
||||
it.effect("lowers PDF tool-result content as structured input_file array", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
LLM.request({
|
||||
id: "req_tool_result_pdf",
|
||||
model,
|
||||
@@ -576,7 +575,7 @@ describe("OpenAI Responses route", () => {
|
||||
|
||||
it.effect("uses xAI inline file encoding for PDF tool results", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
LLM.request({
|
||||
model: xaiModel,
|
||||
messages: [
|
||||
@@ -611,7 +610,7 @@ describe("OpenAI Responses route", () => {
|
||||
|
||||
it.effect("rejects unsupported media in tool-result content with a clear error", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* compileRequest(
|
||||
const error = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
id: "req_tool_result_unsupported_media",
|
||||
model,
|
||||
@@ -634,7 +633,7 @@ describe("OpenAI Responses route", () => {
|
||||
|
||||
it.effect("prepares the composed native continuation request", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
continuationRequest({
|
||||
id: "req_native_continuation_openai",
|
||||
model,
|
||||
@@ -676,7 +675,7 @@ describe("OpenAI Responses route", () => {
|
||||
|
||||
it.effect("maps OpenAI provider options to Responses options", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
LLM.request({
|
||||
model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).model("gpt-5.2"),
|
||||
prompt: "think",
|
||||
@@ -701,7 +700,7 @@ describe("OpenAI Responses route", () => {
|
||||
|
||||
it.effect("accepts the full ResponseIncludable union", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
prompt: "hi",
|
||||
@@ -723,7 +722,7 @@ describe("OpenAI Responses route", () => {
|
||||
|
||||
it.effect("filters unknown includable values out of the include array", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
prompt: "hi",
|
||||
@@ -740,7 +739,7 @@ describe("OpenAI Responses route", () => {
|
||||
|
||||
it.effect("treats an explicit empty include as no include at all", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
LLM.request({ model, prompt: "hi", providerOptions: { openai: { include: [] } } }),
|
||||
)
|
||||
|
||||
@@ -750,7 +749,7 @@ describe("OpenAI Responses route", () => {
|
||||
|
||||
it.effect("treats an all-invalid include as no include at all", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
LLM.request({ model, prompt: "hi", providerOptions: { openai: { include: ["bogus.thing"] } } }),
|
||||
)
|
||||
|
||||
@@ -760,7 +759,7 @@ describe("OpenAI Responses route", () => {
|
||||
|
||||
it.effect("omits include when no include is set", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
LLM.request({ model, prompt: "hi", providerOptions: { openai: { store: false } } }),
|
||||
)
|
||||
|
||||
@@ -774,7 +773,7 @@ describe("OpenAI Responses route", () => {
|
||||
// reasoningSummary: "auto" by default. Without `include`, a follow-up
|
||||
// turn cannot replay reasoning state, so the facade also opts into
|
||||
// `reasoning.encrypted_content` automatically.
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
LLM.request({
|
||||
model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).responses("gpt-5.2"),
|
||||
prompt: "hi",
|
||||
@@ -789,7 +788,7 @@ describe("OpenAI Responses route", () => {
|
||||
|
||||
it.effect("lets callers opt out of the GPT-5 default include", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
LLM.request({
|
||||
model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).responses("gpt-5.2"),
|
||||
prompt: "hi",
|
||||
@@ -803,7 +802,7 @@ describe("OpenAI Responses route", () => {
|
||||
|
||||
it.effect("request OpenAI provider options override route defaults", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
LLM.request({
|
||||
model: OpenAI.configure({
|
||||
baseURL: "https://api.openai.test/v1/",
|
||||
@@ -935,7 +934,9 @@ describe("OpenAI Responses route", () => {
|
||||
},
|
||||
])
|
||||
|
||||
const prepared = yield* compileRequest(LLM.request({ model, messages: [response.message] }))
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
LLM.request({ model, messages: [response.message] }),
|
||||
)
|
||||
expect(prepared.body.input).toEqual([
|
||||
{
|
||||
role: "assistant",
|
||||
@@ -1269,7 +1270,7 @@ describe("OpenAI Responses route", () => {
|
||||
|
||||
it.effect("preserves assistant content order around reasoning items", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
LLM.request({
|
||||
id: "req_reasoning_order",
|
||||
model,
|
||||
@@ -1307,7 +1308,7 @@ describe("OpenAI Responses route", () => {
|
||||
|
||||
it.effect("references stored reasoning items by id", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [
|
||||
@@ -1329,7 +1330,7 @@ describe("OpenAI Responses route", () => {
|
||||
|
||||
it.effect("references stored provider-executed hosted tool results by id", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [
|
||||
@@ -1366,7 +1367,7 @@ describe("OpenAI Responses route", () => {
|
||||
it.effect("continues stateless hosted image generation with the generated image", () =>
|
||||
Effect.gen(function* () {
|
||||
const imageTool = OpenAI.imageGeneration({ action: "edit" })
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [
|
||||
@@ -1407,7 +1408,7 @@ describe("OpenAI Responses route", () => {
|
||||
|
||||
it.effect("joins streamed summary blocks into one continuation reasoning item", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
LLM.request({
|
||||
id: "req_multi_summary_continuation",
|
||||
model,
|
||||
@@ -1444,7 +1445,7 @@ describe("OpenAI Responses route", () => {
|
||||
|
||||
it.effect("skips non-persisted reasoning ids without encrypted state", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
id: "req_reasoning_without_encrypted_state",
|
||||
model,
|
||||
@@ -1761,7 +1762,7 @@ describe("OpenAI Responses route", () => {
|
||||
|
||||
it.effect("lowers user image and PDF content", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
LLM.request({
|
||||
id: "req_media",
|
||||
model,
|
||||
@@ -1792,7 +1793,7 @@ describe("OpenAI Responses route", () => {
|
||||
|
||||
it.effect("uses xAI inline file encoding for user PDFs", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||
LLM.request({
|
||||
model: xaiModel,
|
||||
messages: [
|
||||
@@ -1824,7 +1825,7 @@ describe("OpenAI Responses route", () => {
|
||||
|
||||
it.effect("rejects unsupported user media content", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* compileRequest(
|
||||
const error = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
id: "req_media",
|
||||
model,
|
||||
|
||||
@@ -2,7 +2,6 @@ import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { LLM, Message } from "../../src"
|
||||
import { LLMClient } from "../../src/route"
|
||||
import { compileRequest } from "../../src/route/client"
|
||||
import * as OpenRouter from "../../src/providers/openrouter"
|
||||
import { it } from "../lib/effect"
|
||||
import { fixedResponse } from "../lib/http"
|
||||
@@ -20,7 +19,7 @@ describe("OpenRouter", () => {
|
||||
})
|
||||
expect(model.route.endpoint.baseURL).toBe("https://openrouter.ai/api/v1")
|
||||
|
||||
const prepared = yield* compileRequest(LLM.request({ model, prompt: "Say hello." }))
|
||||
const prepared = yield* LLMClient.prepare(LLM.request({ model, prompt: "Say hello." }))
|
||||
|
||||
expect(prepared.route).toBe("openrouter")
|
||||
expect(prepared.body).toMatchObject({
|
||||
@@ -33,7 +32,7 @@ describe("OpenRouter", () => {
|
||||
|
||||
it.effect("applies OpenRouter payload options from the model helper", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare(
|
||||
LLM.request({
|
||||
model: OpenRouter.configure({
|
||||
apiKey: "test-key",
|
||||
@@ -101,7 +100,7 @@ describe("OpenRouter", () => {
|
||||
{ type: "reasoning.text", signature: "signed", format: "anthropic-claude-v1", index: 0 },
|
||||
{ type: "reasoning.encrypted", data: "opaque", format: "openai-responses-v1", index: 1 },
|
||||
]
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenRouter.OpenRouterBody>(
|
||||
LLM.request({
|
||||
model: OpenRouter.configure({ apiKey: "test-key" }).model("anthropic/claude-sonnet-4.6"),
|
||||
messages: [
|
||||
@@ -134,7 +133,7 @@ describe("OpenRouter", () => {
|
||||
{ type: "reasoning.encrypted", id: "state", data: "opaque" },
|
||||
{ type: "reasoning.encrypted", id: "state", data: "opaque" },
|
||||
]
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenRouter.OpenRouterBody>(
|
||||
LLM.request({
|
||||
model: OpenRouter.configure({ apiKey: "test-key" }).model("anthropic/claude-sonnet-4.6"),
|
||||
messages: [
|
||||
@@ -159,7 +158,7 @@ describe("OpenRouter", () => {
|
||||
{ type: "reasoning.text", id: "first", index: 0, text: "A", opaque: "first" },
|
||||
{ type: "reasoning.text", id: "second", index: 1, text: "B", opaque: "second" },
|
||||
]
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenRouter.OpenRouterBody>(
|
||||
LLM.request({
|
||||
model: OpenRouter.configure({ apiKey: "test-key" }).model("anthropic/claude-sonnet-4.6"),
|
||||
messages: [
|
||||
@@ -180,7 +179,7 @@ describe("OpenRouter", () => {
|
||||
|
||||
it.effect("omits scalar reasoning without continuation details", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenRouter.OpenRouterBody>(
|
||||
LLM.request({
|
||||
model: OpenRouter.configure({ apiKey: "test-key" }).model("anthropic/claude-sonnet-4.6"),
|
||||
messages: [Message.assistant({ type: "reasoning", text: "Thinking" })],
|
||||
|
||||
@@ -3,8 +3,7 @@ import { Effect } from "effect"
|
||||
import { LLM } from "../src"
|
||||
import { OpenAIChat } from "../src/protocols"
|
||||
import { ToolSchemaProjection } from "../src/protocols/utils/tool-schema"
|
||||
import { Auth } from "../src/route"
|
||||
import { compileRequest } from "../src/route/client"
|
||||
import { Auth, LLMClient } from "../src/route"
|
||||
import { it } from "./lib/effect"
|
||||
|
||||
describe("tool schema projections", () => {
|
||||
@@ -80,7 +79,7 @@ describe("tool schema projections", () => {
|
||||
const model = OpenAIChat.route
|
||||
.with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") })
|
||||
.model({ id: "kimi-k2", compatibility: { toolSchema: "moonshot" } })
|
||||
const prepared = yield* compileRequest(
|
||||
const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
|
||||
LLM.request({
|
||||
model,
|
||||
prompt: "Use the tool.",
|
||||
|
||||
@@ -69,7 +69,7 @@ export const DialogFork: Component = () => {
|
||||
const dir = base64Encode(sdk().directory)
|
||||
|
||||
sdk()
|
||||
.api.session.fork({ sessionID, boundary: { type: "before", messageID: item.id } })
|
||||
.api.session.fork({ sessionID, messageID: item.id })
|
||||
.then((forked) => {
|
||||
dialog.close()
|
||||
prompt.set(restored, undefined, { dir, id: forked.id })
|
||||
|
||||
@@ -562,7 +562,6 @@ export function useServerManagementController(options: { onSelect?: () => void;
|
||||
startEdit,
|
||||
resetForm,
|
||||
submitForm,
|
||||
canRemove: server.canRemove,
|
||||
handleRemove,
|
||||
handleFormChange: () => (isAddMode() ? handleAddChange : handleEditChange),
|
||||
handleFormNameChange: () => (isAddMode() ? handleAddNameChange : handleEditNameChange),
|
||||
@@ -650,15 +649,13 @@ export function ServerConnectionList(props: { controller: ReturnType<typeof useS
|
||||
</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</Show>
|
||||
<Show when={props.controller.canRemove(key)}>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => props.controller.handleRemove(ServerConnection.key(i))}
|
||||
class="text-text-on-critical-base hover:bg-surface-critical-weak"
|
||||
>
|
||||
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.delete")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</Show>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => props.controller.handleRemove(ServerConnection.key(i))}
|
||||
class="text-text-on-critical-base hover:bg-surface-critical-weak"
|
||||
>
|
||||
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.delete")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -21,7 +21,6 @@ export const ServerRowMenu: Component<{
|
||||
labels={serverMenuLabels(language)}
|
||||
canDefault={props.controller.canDefault()}
|
||||
isDefault={props.controller.defaultKey() === key}
|
||||
canRemove={props.controller.canRemove(key)}
|
||||
onEdit={props.onEdit}
|
||||
onSetDefault={() => props.controller.setDefault(key)}
|
||||
onRemoveDefault={() => props.controller.setDefault(null)}
|
||||
@@ -48,7 +47,6 @@ export const ServerRowMenuView: Component<{
|
||||
labels: ReturnType<typeof serverMenuLabels>
|
||||
canDefault: boolean
|
||||
isDefault: boolean
|
||||
canRemove: boolean
|
||||
onEdit: (server: ServerConnection.Http) => void
|
||||
onSetDefault: () => void
|
||||
onRemoveDefault: () => void
|
||||
@@ -86,10 +84,10 @@ export const ServerRowMenuView: Component<{
|
||||
<Show when={props.canDefault && props.isDefault}>
|
||||
<MenuV2.Item onSelect={props.onRemoveDefault}>{props.labels.defaultRemove}</MenuV2.Item>
|
||||
</Show>
|
||||
<Show when={props.canRemove}>
|
||||
<MenuV2.Separator />
|
||||
<MenuV2.Item onSelect={props.onRemove}>{props.labels.delete}</MenuV2.Item>
|
||||
</Show>
|
||||
<MenuV2.Separator />
|
||||
<MenuV2.Item disabled={builtin()} onSelect={props.onRemove}>
|
||||
{props.labels.delete}
|
||||
</MenuV2.Item>
|
||||
</MenuV2.Group>
|
||||
</MenuV2.Content>
|
||||
</MenuV2.Portal>
|
||||
|
||||
@@ -395,25 +395,27 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
|
||||
}}
|
||||
onReorder={(keys) => tabsStoreActions.reorder(keys)}
|
||||
/>
|
||||
<TooltipV2
|
||||
placement="bottom"
|
||||
value={
|
||||
<>
|
||||
{language.t("command.session.new")}
|
||||
<KeybindV2 keys={newTabTooltipKeybind(command)} variant="neutral" />
|
||||
</>
|
||||
}
|
||||
>
|
||||
<IconButtonV2
|
||||
type="button"
|
||||
variant="ghost-muted"
|
||||
size="large"
|
||||
class="shrink-0"
|
||||
icon={<IconV2 name="plus" />}
|
||||
onClick={openNewTab}
|
||||
aria-label={language.t("command.session.new")}
|
||||
/>
|
||||
</TooltipV2>
|
||||
<Show when={!creating()}>
|
||||
<TooltipV2
|
||||
placement="bottom"
|
||||
value={
|
||||
<>
|
||||
{language.t("command.session.new")}
|
||||
<KeybindV2 keys={newTabTooltipKeybind(command)} variant="neutral" />
|
||||
</>
|
||||
}
|
||||
>
|
||||
<IconButtonV2
|
||||
type="button"
|
||||
variant="ghost-muted"
|
||||
size="large"
|
||||
class="shrink-0"
|
||||
icon={<IconV2 name="plus" />}
|
||||
onClick={openNewTab}
|
||||
aria-label={language.t("command.session.new")}
|
||||
/>
|
||||
</TooltipV2>
|
||||
</Show>
|
||||
<div class="flex-1" />
|
||||
<TitlebarV2Right state={v2RightState()} />
|
||||
</div>
|
||||
|
||||
@@ -178,17 +178,6 @@ export function resolveServerList(input: {
|
||||
return [...deduped.values()]
|
||||
}
|
||||
|
||||
export function canRemoveServer(input: {
|
||||
key: ServerConnection.Key
|
||||
provided?: Array<ServerConnection.Any>
|
||||
stored: StoredServer[]
|
||||
}) {
|
||||
if (input.provided?.some((server) => ServerConnection.key(server) === input.key)) return false
|
||||
return input.stored.some((server) =>
|
||||
typeof server === "string" ? server === input.key : ("type" in server ? server.http.url : server.url) === input.key,
|
||||
)
|
||||
}
|
||||
|
||||
export namespace ServerConnection {
|
||||
type Base = { displayName?: string; label?: string }
|
||||
|
||||
@@ -323,10 +312,6 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
||||
})
|
||||
}
|
||||
|
||||
function canRemove(key: ServerConnection.Key) {
|
||||
return canRemoveServer({ key, provided: props.servers, stored: store.list })
|
||||
}
|
||||
|
||||
const isReady = Object.assign(
|
||||
createMemo(() => ready() && !!state.active),
|
||||
{ promise: ready.promise },
|
||||
@@ -365,7 +350,6 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
||||
setActive,
|
||||
add,
|
||||
remove,
|
||||
canRemove,
|
||||
scope,
|
||||
projects: {
|
||||
...projects,
|
||||
|
||||
@@ -60,7 +60,6 @@ export function createHomeProjectsController(home: HomeController) {
|
||||
defaultKey: serverManagement.defaultKey,
|
||||
setDefault: (conn: ServerConnection.Any | undefined) =>
|
||||
serverManagement.setDefault(conn ? ServerConnection.key(conn) : null),
|
||||
canRemove: (conn: ServerConnection.Any) => serverManagement.canRemove(ServerConnection.key(conn)),
|
||||
remove: (conn: ServerConnection.Any) => serverManagement.handleRemove(ServerConnection.key(conn)),
|
||||
edit: (conn: ServerConnection.Http) => dialog.show(() => <DialogServerV2 mode="edit" server={conn} />),
|
||||
focus: home.selection.focusServer,
|
||||
|
||||
@@ -47,7 +47,6 @@ export type HomeProjectsViewProps = {
|
||||
onToggleCollapsed: (server: ServerConnection.Any) => void
|
||||
onEditServer: (server: ServerConnection.Http) => void
|
||||
onSetDefaultServer: (server: ServerConnection.Any | undefined) => void
|
||||
canRemoveServer: (server: ServerConnection.Any) => boolean
|
||||
onRemoveServer: (server: ServerConnection.Any) => void
|
||||
onMoveProject: (server: ServerConnection.Any, worktree: string, index: number) => void
|
||||
onSelectProject: (server: ServerConnection.Any, directory: string) => void
|
||||
@@ -193,7 +192,6 @@ function HomeServerRow(props: {
|
||||
onToggleCollapsed: HomeProjectsViewProps["onToggleCollapsed"]
|
||||
onEditServer: HomeProjectsViewProps["onEditServer"]
|
||||
onSetDefaultServer: HomeProjectsViewProps["onSetDefaultServer"]
|
||||
canRemoveServer: HomeProjectsViewProps["canRemoveServer"]
|
||||
onRemoveServer: HomeProjectsViewProps["onRemoveServer"]
|
||||
onSetContextMenuOpen: HomeProjectsContextMenuProps["onSetContextMenuOpen"]
|
||||
onChooseProject: HomeProjectsViewProps["onChooseProject"]
|
||||
@@ -279,7 +277,6 @@ function HomeServerRow(props: {
|
||||
labels={serverMenuLabels(props.language)}
|
||||
canDefault={props.canDefaultServer()}
|
||||
isDefault={props.defaultServerKey() === ServerConnection.key(props.server)}
|
||||
canRemove={props.canRemoveServer(props.server)}
|
||||
onEdit={props.onEditServer}
|
||||
onSetDefault={() => props.onSetDefaultServer(props.server)}
|
||||
onRemoveDefault={() => props.onSetDefaultServer(undefined)}
|
||||
|
||||
@@ -24,7 +24,6 @@ export function HomeProjects(props: { projects: HomeProjectsController; scroll:
|
||||
onToggleCollapsed={props.projects.server.toggleCollapsed}
|
||||
onEditServer={props.projects.server.edit}
|
||||
onSetDefaultServer={props.projects.server.setDefault}
|
||||
canRemoveServer={props.projects.server.canRemove}
|
||||
onRemoveServer={props.projects.server.remove}
|
||||
onMoveProject={props.projects.project.move}
|
||||
onSelectProject={props.projects.project.select}
|
||||
|
||||
@@ -26,6 +26,7 @@ function setup(
|
||||
return new Response(undefined, { status: 204 })
|
||||
if (request.method === "POST" && request.url.endsWith("/prompt")) {
|
||||
return Response.json({
|
||||
admittedSeq: 1,
|
||||
id: "msg_1",
|
||||
sessionID: "ses_1",
|
||||
timeCreated: 1,
|
||||
@@ -52,7 +53,6 @@ function setup(
|
||||
}
|
||||
|
||||
describe("createCompatibleApi", () => {
|
||||
/*
|
||||
test("routes V1 archive through the legacy session update", async () => {
|
||||
const { api, requests } = setup("v1")
|
||||
await api.session.archive({ sessionID: "ses_1", directory: "/repo" })
|
||||
@@ -63,7 +63,6 @@ describe("createCompatibleApi", () => {
|
||||
expect(requests[0]!.method).toBe("PATCH")
|
||||
expect(await requests[0]!.json()).toMatchObject({ time: { archived: expect.any(Number) } })
|
||||
})
|
||||
*/
|
||||
|
||||
test("converts current prompts to the V1 prompt contract", async () => {
|
||||
const { api, requests } = setup("v1")
|
||||
@@ -146,7 +145,6 @@ describe("createCompatibleApi", () => {
|
||||
expect(detections).toBe(1)
|
||||
})
|
||||
|
||||
/*
|
||||
test("keeps V2 session actions on the current API", async () => {
|
||||
const { api, requests } = setup("v2")
|
||||
await api.session.archive({ sessionID: "ses_1" })
|
||||
@@ -154,7 +152,6 @@ describe("createCompatibleApi", () => {
|
||||
expect(new URL(requests[0]!.url).pathname).toBe("/api/session/ses_1/archive")
|
||||
expect(requests[0]!.method).toBe("POST")
|
||||
})
|
||||
*/
|
||||
|
||||
test("uses the global V1 session search endpoint", async () => {
|
||||
const { api, requests } = setup("v1")
|
||||
|
||||
@@ -27,7 +27,7 @@ type CompatibleSessionApi = Omit<
|
||||
shell: (input: SessionShellInput & LegacyPrompt) => Promise<SessionShellOutput>
|
||||
compact: (input: SessionCompactInput & { model?: LegacyPrompt["model"] }) => Promise<SessionCompactOutput>
|
||||
rename: (input: Parameters<SessionApi["rename"]>[0] & LegacyLocation) => ReturnType<SessionApi["rename"]>
|
||||
// archive: (input: Parameters<SessionApi["archive"]>[0] & LegacyLocation) => ReturnType<SessionApi["archive"]>
|
||||
archive: (input: Parameters<SessionApi["archive"]>[0] & LegacyLocation) => ReturnType<SessionApi["archive"]>
|
||||
remove: (input: Parameters<SessionApi["remove"]>[0] & LegacyLocation) => ReturnType<SessionApi["remove"]>
|
||||
}
|
||||
type CompatiblePermissionApi = Omit<ServerApi["permission"], "reply"> & {
|
||||
@@ -183,9 +183,9 @@ function createV1Api(input: CompatibleInput): CompatibleApi {
|
||||
async rename(value: Parameters<ServerApi["session"]["rename"]>[0] & LegacyLocation) {
|
||||
await legacy(value).session.update({ sessionID: value.sessionID, title: value.title })
|
||||
},
|
||||
// async archive(value: Parameters<ServerApi["session"]["archive"]>[0] & LegacyLocation) {
|
||||
// await legacy(value).session.update({ sessionID: value.sessionID, time: { archived: Date.now() } })
|
||||
// },
|
||||
async archive(value: Parameters<ServerApi["session"]["archive"]>[0] & LegacyLocation) {
|
||||
await legacy(value).session.update({ sessionID: value.sessionID, time: { archived: Date.now() } })
|
||||
},
|
||||
async remove(value: Parameters<ServerApi["session"]["remove"]>[0] & LegacyLocation) {
|
||||
await legacy(value).session.delete(value)
|
||||
},
|
||||
@@ -229,6 +229,7 @@ function createV1Api(input: CompatibleInput): CompatibleApi {
|
||||
],
|
||||
})
|
||||
return {
|
||||
admittedSeq: 0,
|
||||
id: value.id ?? "",
|
||||
sessionID: value.sessionID,
|
||||
timeCreated: Date.now(),
|
||||
@@ -254,6 +255,7 @@ function createV1Api(input: CompatibleInput): CompatibleApi {
|
||||
})),
|
||||
})
|
||||
return {
|
||||
admittedSeq: 0,
|
||||
id: value.id ?? "",
|
||||
sessionID: value.sessionID,
|
||||
timeCreated: Date.now(),
|
||||
@@ -278,6 +280,7 @@ function createV1Api(input: CompatibleInput): CompatibleApi {
|
||||
modelID: value.model.modelID,
|
||||
})
|
||||
return {
|
||||
admittedSeq: 0,
|
||||
id: value.id ?? "",
|
||||
sessionID: value.sessionID,
|
||||
timeCreated: Date.now(),
|
||||
|
||||
@@ -56,16 +56,6 @@
|
||||
"@lydell/node-pty-linux-x64": "1.2.0-beta.12",
|
||||
"@lydell/node-pty-win32-arm64": "1.2.0-beta.12",
|
||||
"@lydell/node-pty-win32-x64": "1.2.0-beta.12",
|
||||
"@ff-labs/fff-bin-darwin-arm64": "0.10.1",
|
||||
"@ff-labs/fff-bin-linux-arm64-gnu": "0.10.1",
|
||||
"@ff-labs/fff-bin-linux-x64-gnu": "0.10.1",
|
||||
"@ff-labs/fff-bin-win32-arm64": "0.10.1",
|
||||
"@ff-labs/fff-bin-win32-x64": "0.10.1",
|
||||
"@yuuang/ffi-rs-darwin-arm64": "1.3.2",
|
||||
"@yuuang/ffi-rs-linux-arm64-gnu": "1.3.2",
|
||||
"@yuuang/ffi-rs-linux-x64-gnu": "1.3.2",
|
||||
"@yuuang/ffi-rs-win32-arm64-msvc": "1.3.2",
|
||||
"@yuuang/ffi-rs-win32-x64-msvc": "1.3.2",
|
||||
"@parcel/watcher-darwin-arm64": "2.5.1",
|
||||
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-x64-glibc": "2.5.1",
|
||||
|
||||
@@ -5,7 +5,6 @@ import { rm } from "fs/promises"
|
||||
import path from "path"
|
||||
import { Script } from "@opencode-ai/script"
|
||||
import { createSolidTransformPlugin } from "@opentui/solid/bun-plugin"
|
||||
import type { BunPlugin } from "bun"
|
||||
import pkg from "../package.json"
|
||||
import { modelsData } from "./generate"
|
||||
|
||||
@@ -23,7 +22,7 @@ await rm(outdir, { recursive: true, force: true })
|
||||
const singleFlag = process.argv.includes("--single")
|
||||
const baselineFlag = process.argv.includes("--baseline")
|
||||
const skipInstall = process.argv.includes("--skip-install")
|
||||
const solidPlugin = createSolidTransformPlugin()
|
||||
const plugin = createSolidTransformPlugin()
|
||||
|
||||
const allTargets: {
|
||||
os: string
|
||||
@@ -56,16 +55,6 @@ const targets = singleFlag
|
||||
if (!skipInstall) await $`bun install --os="*" --cpu="*" @opentui/core@${pkg.dependencies["@opentui/core"]}`
|
||||
|
||||
for (const item of targets) {
|
||||
const parcelWatcherPackage = `@parcel/watcher-${item.os}-${item.arch}${item.os === "linux" ? `-${item.abi ?? "glibc"}` : ""}`
|
||||
const parcelWatcherPlugin: BunPlugin = {
|
||||
name: "parcel-watcher-binding",
|
||||
setup(build) {
|
||||
build.onLoad({ filter: /filesystem\/watcher-binding\.ts$/ }, () => ({
|
||||
contents: `import binding from ${JSON.stringify(parcelWatcherPackage)}; export default () => binding`,
|
||||
loader: "js",
|
||||
}))
|
||||
},
|
||||
}
|
||||
const target = [
|
||||
binary,
|
||||
item.os === "win32" ? "windows" : item.os,
|
||||
@@ -80,7 +69,7 @@ for (const item of targets) {
|
||||
const result = await Bun.build({
|
||||
entrypoints: ["./src/index.ts"],
|
||||
tsconfig: "./tsconfig.json",
|
||||
plugins: [solidPlugin, parcelWatcherPlugin],
|
||||
plugins: [plugin],
|
||||
external: ["node-gyp"],
|
||||
format: "esm",
|
||||
minify: true,
|
||||
|
||||
@@ -37,8 +37,6 @@ export async function collectNodeAssets(target: NodeTarget) {
|
||||
...(target.platform === "linux" ? { libc: "glibc" as const } : {}),
|
||||
}),
|
||||
{ key: target.parcelWatcherAsset, source: fileURLToPath(import.meta.resolve(target.parcelWatcherPackage)) },
|
||||
{ key: target.fffAsset, source: fileURLToPath(import.meta.resolve(target.fffPackage)) },
|
||||
{ key: target.fffFfiAsset, source: fileURLToPath(import.meta.resolve(target.fffFfiPackage)) },
|
||||
{
|
||||
key: photonWasmAsset,
|
||||
source: fileURLToPath(import.meta.resolve(photonWasmAsset)),
|
||||
|
||||
@@ -13,7 +13,7 @@ const directory = path.join(import.meta.dir, "..", "dist", ...(nodeBuild ? ["nod
|
||||
const binary = path.join(directory, `opencode2${nodeBuild ? "-node" : ""}${process.platform === "win32" ? ".exe" : ""}`)
|
||||
if (!(await Bun.file(binary).exists())) throw new Error(`Missing compiled CLI in ${directory}`)
|
||||
|
||||
const root = await fs.realpath(await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-smoke-")))
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-smoke-"))
|
||||
const env = {
|
||||
...process.env,
|
||||
HOME: root,
|
||||
@@ -29,7 +29,6 @@ const processes: Array<ReturnType<typeof Bun.spawn>> = []
|
||||
const errors: Array<Promise<string>> = []
|
||||
let failure: unknown
|
||||
try {
|
||||
await fs.mkdir(path.join(root, ".opencode"))
|
||||
spawnService()
|
||||
spawnService()
|
||||
const registration = await waitForRegistration()
|
||||
@@ -50,11 +49,6 @@ try {
|
||||
{ signal: AbortSignal.timeout(5_000) },
|
||||
)
|
||||
if (tokenOpenApi.status !== 200) throw new Error("Compiled application rejected query authentication")
|
||||
if ((await pluginIDs(info.url, headers)).includes("smoke")) throw new Error("Smoke plugin existed before creation")
|
||||
const plugin = path.join(root, ".opencode", "plugins", "smoke.ts")
|
||||
await fs.mkdir(path.dirname(plugin), { recursive: true })
|
||||
await fs.writeFile(plugin, pluginSource())
|
||||
await waitForPlugin(info.url, headers)
|
||||
|
||||
const unauthorizedHealth = await fetch(new URL("/api/health", info.url), {
|
||||
signal: AbortSignal.timeout(5_000),
|
||||
@@ -94,7 +88,6 @@ try {
|
||||
} finally {
|
||||
processes.forEach((process) => process.kill())
|
||||
await Promise.all(processes.map((process) => process.exited))
|
||||
if (failure) errors.push(fs.readFile(path.join(root, "data", "opencode", "log", "opencode.log"), "utf8").catch(() => ""))
|
||||
}
|
||||
|
||||
const output = await Promise.all(errors)
|
||||
@@ -140,31 +133,3 @@ async function waitForReady(url: string, headers: HeadersInit) {
|
||||
function exitsWithin(process: Bun.Subprocess, milliseconds: number) {
|
||||
return Promise.race([process.exited.then(() => true), Bun.sleep(milliseconds).then(() => false)])
|
||||
}
|
||||
|
||||
function pluginSource() {
|
||||
return 'export default { id: "smoke", setup: async () => {} }\n'
|
||||
}
|
||||
|
||||
async function pluginIDs(url: string, headers: HeadersInit) {
|
||||
const endpoint = new URL("/api/plugin", url)
|
||||
endpoint.searchParams.set("location[directory]", root)
|
||||
const response = await fetch(endpoint, { headers, signal: AbortSignal.timeout(5_000) })
|
||||
const body: unknown = await response.json()
|
||||
if (typeof body !== "object" || body === null || !("data" in body) || !Array.isArray(body.data)) {
|
||||
throw new Error("Compiled service returned an invalid plugin list")
|
||||
}
|
||||
return body.data.flatMap((plugin) =>
|
||||
typeof plugin === "object" && plugin !== null && "id" in plugin && typeof plugin.id === "string"
|
||||
? [plugin.id]
|
||||
: [],
|
||||
)
|
||||
}
|
||||
|
||||
async function waitForPlugin(url: string, headers: HeadersInit) {
|
||||
const deadline = Date.now() + 10_000
|
||||
while (Date.now() < deadline) {
|
||||
if ((await pluginIDs(url, headers)).includes("smoke")) return
|
||||
await Bun.sleep(25)
|
||||
}
|
||||
throw new Error("Compiled service did not discover the created plugin")
|
||||
}
|
||||
|
||||
@@ -244,10 +244,7 @@ export function make(input: { readonly client: OpenCodeClient; readonly connecti
|
||||
return {}
|
||||
},
|
||||
forkSession: async (params) => {
|
||||
const forked = await input.client.session.fork({
|
||||
sessionID: params.sessionId,
|
||||
boundary: { type: "through" },
|
||||
})
|
||||
const forked = await input.client.session.fork({ sessionID: params.sessionId })
|
||||
const state = await attach(forked, forked.location.directory, params.mcpServers ?? [])
|
||||
await replay(state)
|
||||
return { sessionId: state.id, configOptions: configOptions(state) }
|
||||
|
||||
@@ -14,23 +14,10 @@ const ServerParams = {
|
||||
),
|
||||
}
|
||||
|
||||
const PermissionParams = {
|
||||
auto: Flag.boolean("auto").pipe(
|
||||
Flag.withDescription("Auto-approve permissions that are not explicitly denied"),
|
||||
Flag.withDefault(false),
|
||||
),
|
||||
yolo: Flag.boolean("yolo").pipe(Flag.withDefault(false), Flag.withHidden),
|
||||
dangerouslySkipPermissions: Flag.boolean("dangerously-skip-permissions").pipe(
|
||||
Flag.withDefault(false),
|
||||
Flag.withHidden,
|
||||
),
|
||||
}
|
||||
|
||||
export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME : "opencode", {
|
||||
description: "OpenCode 2.0 preview command line interface",
|
||||
params: {
|
||||
...ServerParams,
|
||||
...PermissionParams,
|
||||
directory: Argument.string("directory").pipe(
|
||||
Argument.withDescription("Directory to start OpenCode in"),
|
||||
Argument.optional,
|
||||
@@ -209,7 +196,11 @@ export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCO
|
||||
),
|
||||
title: Flag.string("title").pipe(Flag.withDescription("Session title"), Flag.optional),
|
||||
thinking: Flag.boolean("thinking").pipe(Flag.withDescription("Show thinking blocks"), Flag.withDefault(false)),
|
||||
...PermissionParams,
|
||||
auto: Flag.boolean("auto").pipe(
|
||||
Flag.withDescription("Auto-approve permissions that are not explicitly denied"),
|
||||
Flag.withDefault(false),
|
||||
),
|
||||
yolo: Flag.boolean("yolo").pipe(Flag.withDefault(false), Flag.withHidden),
|
||||
},
|
||||
}),
|
||||
Spec.make("service", {
|
||||
|
||||
@@ -59,7 +59,6 @@ export default Runtime.handler(Commands, (input) =>
|
||||
continue: input.continue,
|
||||
sessionID: Option.getOrUndefined(input.session),
|
||||
prompt: Option.getOrUndefined(input.prompt),
|
||||
auto: input.auto || input.yolo || input.dangerouslySkipPermissions,
|
||||
},
|
||||
config: {
|
||||
path: config.path,
|
||||
|
||||
@@ -24,7 +24,7 @@ export default Runtime.handler(Commands.commands.run, (input) =>
|
||||
file: [...input.file],
|
||||
title: Option.getOrUndefined(input.title),
|
||||
thinking: input.thinking,
|
||||
auto: input.auto || input.yolo || input.dangerouslySkipPermissions,
|
||||
auto: input.auto || input.yolo,
|
||||
}),
|
||||
)
|
||||
}),
|
||||
|
||||
@@ -11,8 +11,6 @@ export function nodeTarget(platform: string, arch: string) {
|
||||
const targetArch = arch as "arm64" | "x64"
|
||||
const nodePtyPackage = `@lydell/node-pty-${targetPlatform}-${targetArch}`
|
||||
const parcelWatcherPackage = `@parcel/watcher-${targetPlatform}-${targetArch}${targetPlatform === "linux" ? "-glibc" : ""}`
|
||||
const fffPackage = `@ff-labs/fff-bin-${targetPlatform}-${targetArch}${targetPlatform === "linux" ? "-gnu" : ""}`
|
||||
const fffFfiPackage = `@yuuang/ffi-rs-${targetPlatform}-${targetArch}${targetPlatform === "linux" ? "-gnu" : targetPlatform === "win32" ? "-msvc" : ""}`
|
||||
|
||||
return {
|
||||
platform: targetPlatform,
|
||||
@@ -21,10 +19,6 @@ export function nodeTarget(platform: string, arch: string) {
|
||||
nodePtyEntryAsset: `${nodePtyPackage}/lib/index.js`,
|
||||
parcelWatcherPackage,
|
||||
parcelWatcherAsset: `${parcelWatcherPackage}/watcher.node`,
|
||||
fffPackage,
|
||||
fffAsset: `${fffPackage}/${targetPlatform === "darwin" ? "libfff_c.dylib" : targetPlatform === "win32" ? "fff_c.dll" : "libfff_c.so"}`,
|
||||
fffFfiPackage,
|
||||
fffFfiAsset: `${fffFfiPackage}/ffi-rs.${targetPlatform}-${targetArch}${targetPlatform === "linux" ? "-gnu" : targetPlatform === "win32" ? "-msvc" : ""}.node`,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ async function selectSession(input: {
|
||||
return {
|
||||
session: input.fork
|
||||
? await input.client.session
|
||||
.fork({ sessionID: explicit.id, boundary: { type: "through" } }, ...requestOptions(input.signal))
|
||||
.fork({ sessionID: explicit.id }, ...requestOptions(input.signal))
|
||||
.catch((error) => {
|
||||
throw new SessionTargetMutationError(error)
|
||||
})
|
||||
@@ -118,11 +118,9 @@ async function selectSession(input: {
|
||||
if (!selected) return { session: undefined, location }
|
||||
return {
|
||||
session: input.fork
|
||||
? await input.client.session
|
||||
.fork({ sessionID: selected.id, boundary: { type: "through" } }, ...requestOptions(input.signal))
|
||||
.catch((error) => {
|
||||
throw new SessionTargetMutationError(error)
|
||||
})
|
||||
? await input.client.session.fork({ sessionID: selected.id }, ...requestOptions(input.signal)).catch((error) => {
|
||||
throw new SessionTargetMutationError(error)
|
||||
})
|
||||
: selected,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,7 +267,7 @@ async function run(input: {
|
||||
values.push(...input.turn(messageID))
|
||||
wake?.()
|
||||
wake = undefined
|
||||
return ok({ id: messageID, sessionID: "ses_1", timeCreated: 1 }) as never
|
||||
return ok({ admittedSeq: 1, id: messageID, sessionID: "ses_1", timeCreated: 1 }) as never
|
||||
})
|
||||
await runNonInteractivePrompt({
|
||||
client: sdk,
|
||||
|
||||
@@ -30,44 +30,6 @@ function runtimeRequirePlugin(): Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
function fffNodePlugin(): Plugin {
|
||||
return {
|
||||
name: "opencode:fff-node",
|
||||
enforce: "pre",
|
||||
transform(code, id) {
|
||||
const normalized = id.replaceAll("\\", "/")
|
||||
if (normalized.endsWith("/ffi-rs/index.js")) {
|
||||
const start = code.indexOf("if (!nativeBinding) {")
|
||||
if (start === -1) this.error("Failed to rewrite ffi-rs native binding loader")
|
||||
return `const unavailable = () => { throw new Error("ffi-rs native binding unavailable") }
|
||||
const nativeBinding = globalThis.__OPENCODE_FFF_FFI ?? {
|
||||
DataType: new Proxy({}, { get: (target, key) => target[key] ?? key }),
|
||||
PointerType: {},
|
||||
FFITypeTag: {},
|
||||
open: unavailable,
|
||||
close: unavailable,
|
||||
load: unavailable,
|
||||
isNullPointer: unavailable,
|
||||
createPointer: unavailable,
|
||||
restorePointer: unavailable,
|
||||
unwrapPointer: unavailable,
|
||||
wrapPointer: unavailable,
|
||||
freePointer: unavailable,
|
||||
}
|
||||
const loadError = undefined
|
||||
${code.slice(start)}`
|
||||
}
|
||||
if (!normalized.endsWith("/fff-node/dist/src/binary.js")) return
|
||||
const transformed = code.replace(
|
||||
"export function findBinary() {",
|
||||
"export function findBinary() { if (process.env.FFF_BINARY_PATH) return process.env.FFF_BINARY_PATH;",
|
||||
)
|
||||
if (transformed === code) this.error("Failed to rewrite FFF binary loader")
|
||||
return transformed
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const resolve = {
|
||||
alias: [
|
||||
{ find: /^solid-js\/store$/, replacement: "solid-js/store/dist/store.js" },
|
||||
@@ -194,11 +156,6 @@ process.env.OTUI_ASSET_ROOT = __ocAssetRoot
|
||||
process.env.OPENCODE_NODE_PTY_PATH = __ocPath.join(__ocAssetRoot, ${JSON.stringify(input.target.nodePtyEntryAsset)})
|
||||
process.env.OPENCODE_PARCEL_WATCHER_PATH = __ocPath.join(__ocAssetRoot, ${JSON.stringify(input.target.parcelWatcherAsset)})
|
||||
process.env.OPENCODE_PHOTON_WASM_PATH = __ocPath.join(__ocAssetRoot, ${JSON.stringify(photonWasmAsset)})
|
||||
process.env.FFF_BINARY_PATH = __ocPath.join(__ocAssetRoot, ${JSON.stringify(input.target.fffAsset)})
|
||||
process.env.OPENCODE_FFF_FFI_PATH = __ocPath.join(__ocAssetRoot, ${JSON.stringify(input.target.fffFfiAsset)})
|
||||
try {
|
||||
globalThis.__OPENCODE_FFF_FFI = require(process.env.OPENCODE_FFF_FFI_PATH)
|
||||
} catch {}
|
||||
globalThis.__OPENCODE_PHOTON_WASM_PATH = process.env.OPENCODE_PHOTON_WASM_PATH
|
||||
if (process.platform === "linux") process.env.OPENTUI_LIBC = "glibc"`
|
||||
}
|
||||
@@ -217,7 +174,6 @@ export function mainConfig(input: NodeBuildInput): UserConfig {
|
||||
plugins: [
|
||||
rawTextPlugin(),
|
||||
runtimeRequirePlugin(),
|
||||
fffNodePlugin(),
|
||||
solid({
|
||||
solid: {
|
||||
generate: "universal",
|
||||
|
||||
@@ -136,7 +136,7 @@ export type Endpoint5_4Input = { readonly sessionID: Session.ID }
|
||||
export type Endpoint5_4Output = void
|
||||
export type SessionRemoveOperation<E = never> = (input: Endpoint5_4Input) => Effect.Effect<Endpoint5_4Output, E>
|
||||
|
||||
export type Endpoint5_5Input = { readonly sessionID: Session.ID; readonly boundary: Session.ForkRequestBoundary }
|
||||
export type Endpoint5_5Input = { readonly sessionID: Session.ID; readonly messageID?: SessionMessage.ID | undefined }
|
||||
export type Endpoint5_5Output = Session.Info
|
||||
export type SessionForkOperation<E = never> = (input: Endpoint5_5Input) => Effect.Effect<Endpoint5_5Output, E>
|
||||
|
||||
@@ -152,15 +152,19 @@ export type Endpoint5_8Input = { readonly sessionID: Session.ID; readonly title:
|
||||
export type Endpoint5_8Output = void
|
||||
export type SessionRenameOperation<E = never> = (input: Endpoint5_8Input) => Effect.Effect<Endpoint5_8Output, E>
|
||||
|
||||
export type Endpoint5_9Input = {
|
||||
export type Endpoint5_9Input = { readonly sessionID: Session.ID }
|
||||
export type Endpoint5_9Output = void
|
||||
export type SessionArchiveOperation<E = never> = (input: Endpoint5_9Input) => Effect.Effect<Endpoint5_9Output, E>
|
||||
|
||||
export type Endpoint5_10Input = {
|
||||
readonly sessionID: Session.ID
|
||||
readonly directory: AbsolutePath
|
||||
readonly workspaceID?: Workspace.ID | undefined
|
||||
}
|
||||
export type Endpoint5_9Output = void
|
||||
export type SessionMoveOperation<E = never> = (input: Endpoint5_9Input) => Effect.Effect<Endpoint5_9Output, E>
|
||||
export type Endpoint5_10Output = void
|
||||
export type SessionMoveOperation<E = never> = (input: Endpoint5_10Input) => Effect.Effect<Endpoint5_10Output, E>
|
||||
|
||||
export type Endpoint5_10Input = {
|
||||
export type Endpoint5_11Input = {
|
||||
readonly sessionID: Session.ID
|
||||
readonly id?: SessionMessage.ID | undefined
|
||||
readonly text: string
|
||||
@@ -170,10 +174,10 @@ export type Endpoint5_10Input = {
|
||||
readonly delivery?: "steer" | "queue" | undefined
|
||||
readonly resume?: boolean | undefined
|
||||
}
|
||||
export type Endpoint5_10Output = SessionPending.User
|
||||
export type SessionPromptOperation<E = never> = (input: Endpoint5_10Input) => Effect.Effect<Endpoint5_10Output, E>
|
||||
export type Endpoint5_11Output = SessionPending.User
|
||||
export type SessionPromptOperation<E = never> = (input: Endpoint5_11Input) => Effect.Effect<Endpoint5_11Output, E>
|
||||
|
||||
export type Endpoint5_11Input = {
|
||||
export type Endpoint5_12Input = {
|
||||
readonly sessionID: Session.ID
|
||||
readonly id?: SessionMessage.ID | undefined
|
||||
readonly command: string
|
||||
@@ -185,19 +189,19 @@ export type Endpoint5_11Input = {
|
||||
readonly delivery?: "steer" | "queue" | undefined
|
||||
readonly resume?: boolean | undefined
|
||||
}
|
||||
export type Endpoint5_11Output = SessionPending.User
|
||||
export type SessionCommandOperation<E = never> = (input: Endpoint5_11Input) => Effect.Effect<Endpoint5_11Output, E>
|
||||
export type Endpoint5_12Output = SessionPending.User
|
||||
export type SessionCommandOperation<E = never> = (input: Endpoint5_12Input) => Effect.Effect<Endpoint5_12Output, E>
|
||||
|
||||
export type Endpoint5_12Input = {
|
||||
export type Endpoint5_13Input = {
|
||||
readonly sessionID: Session.ID
|
||||
readonly id?: SessionMessage.ID | undefined
|
||||
readonly skill: Skill.ID
|
||||
readonly resume?: boolean | undefined
|
||||
}
|
||||
export type Endpoint5_12Output = void
|
||||
export type SessionSkillOperation<E = never> = (input: Endpoint5_12Input) => Effect.Effect<Endpoint5_12Output, E>
|
||||
export type Endpoint5_13Output = void
|
||||
export type SessionSkillOperation<E = never> = (input: Endpoint5_13Input) => Effect.Effect<Endpoint5_13Output, E>
|
||||
|
||||
export type Endpoint5_13Input = {
|
||||
export type Endpoint5_14Input = {
|
||||
readonly sessionID: Session.ID
|
||||
readonly id?: SessionMessage.ID | undefined
|
||||
readonly text: string
|
||||
@@ -206,81 +210,81 @@ export type Endpoint5_13Input = {
|
||||
readonly delivery?: "steer" | "queue" | undefined
|
||||
readonly resume?: boolean | undefined
|
||||
}
|
||||
export type Endpoint5_13Output = SessionPending.Synthetic
|
||||
export type SessionSyntheticOperation<E = never> = (input: Endpoint5_13Input) => Effect.Effect<Endpoint5_13Output, E>
|
||||
export type Endpoint5_14Output = SessionPending.Synthetic
|
||||
export type SessionSyntheticOperation<E = never> = (input: Endpoint5_14Input) => Effect.Effect<Endpoint5_14Output, E>
|
||||
|
||||
export type Endpoint5_14Input = {
|
||||
export type Endpoint5_15Input = {
|
||||
readonly sessionID: Session.ID
|
||||
readonly id?: Event.ID | undefined
|
||||
readonly command: string
|
||||
}
|
||||
export type Endpoint5_14Output = void
|
||||
export type SessionShellOperation<E = never> = (input: Endpoint5_14Input) => Effect.Effect<Endpoint5_14Output, E>
|
||||
export type Endpoint5_15Output = void
|
||||
export type SessionShellOperation<E = never> = (input: Endpoint5_15Input) => Effect.Effect<Endpoint5_15Output, E>
|
||||
|
||||
export type Endpoint5_15Input = { readonly sessionID: Session.ID; readonly id?: SessionMessage.ID | undefined }
|
||||
export type Endpoint5_15Output = SessionPending.Compaction
|
||||
export type SessionCompactOperation<E = never> = (input: Endpoint5_15Input) => Effect.Effect<Endpoint5_15Output, E>
|
||||
export type Endpoint5_16Input = { readonly sessionID: Session.ID; readonly id?: SessionMessage.ID | undefined }
|
||||
export type Endpoint5_16Output = SessionPending.Compaction
|
||||
export type SessionCompactOperation<E = never> = (input: Endpoint5_16Input) => Effect.Effect<Endpoint5_16Output, E>
|
||||
|
||||
export type Endpoint5_16Input = { readonly sessionID: Session.ID }
|
||||
export type Endpoint5_16Output = void
|
||||
export type SessionWaitOperation<E = never> = (input: Endpoint5_16Input) => Effect.Effect<Endpoint5_16Output, E>
|
||||
export type Endpoint5_17Input = { readonly sessionID: Session.ID }
|
||||
export type Endpoint5_17Output = void
|
||||
export type SessionWaitOperation<E = never> = (input: Endpoint5_17Input) => Effect.Effect<Endpoint5_17Output, E>
|
||||
|
||||
export type Endpoint5_17Input = {
|
||||
export type Endpoint5_18Input = {
|
||||
readonly sessionID: Session.ID
|
||||
readonly messageID: SessionMessage.ID
|
||||
readonly files?: boolean | undefined
|
||||
}
|
||||
export type Endpoint5_17Output = Session.Revert
|
||||
export type SessionRevertStageOperation<E = never> = (input: Endpoint5_17Input) => Effect.Effect<Endpoint5_17Output, E>
|
||||
|
||||
export type Endpoint5_18Input = { readonly sessionID: Session.ID }
|
||||
export type Endpoint5_18Output = void
|
||||
export type SessionRevertClearOperation<E = never> = (input: Endpoint5_18Input) => Effect.Effect<Endpoint5_18Output, E>
|
||||
export type Endpoint5_18Output = Session.Revert
|
||||
export type SessionRevertStageOperation<E = never> = (input: Endpoint5_18Input) => Effect.Effect<Endpoint5_18Output, E>
|
||||
|
||||
export type Endpoint5_19Input = { readonly sessionID: Session.ID }
|
||||
export type Endpoint5_19Output = void
|
||||
export type SessionRevertCommitOperation<E = never> = (input: Endpoint5_19Input) => Effect.Effect<Endpoint5_19Output, E>
|
||||
export type SessionRevertClearOperation<E = never> = (input: Endpoint5_19Input) => Effect.Effect<Endpoint5_19Output, E>
|
||||
|
||||
export type Endpoint5_20Input = { readonly sessionID: Session.ID }
|
||||
export type Endpoint5_20Output = ReadonlyArray<SessionMessage.Info>
|
||||
export type SessionContextOperation<E = never> = (input: Endpoint5_20Input) => Effect.Effect<Endpoint5_20Output, E>
|
||||
export type Endpoint5_20Output = void
|
||||
export type SessionRevertCommitOperation<E = never> = (input: Endpoint5_20Input) => Effect.Effect<Endpoint5_20Output, E>
|
||||
|
||||
export type Endpoint5_21Input = { readonly sessionID: Session.ID }
|
||||
export type Endpoint5_21Output = ReadonlyArray<SessionPending.Info>
|
||||
export type SessionPendingListOperation<E = never> = (input: Endpoint5_21Input) => Effect.Effect<Endpoint5_21Output, E>
|
||||
export type Endpoint5_21Output = ReadonlyArray<SessionMessage.Info>
|
||||
export type SessionContextOperation<E = never> = (input: Endpoint5_21Input) => Effect.Effect<Endpoint5_21Output, E>
|
||||
|
||||
export type Endpoint5_22Input = { readonly sessionID: Session.ID }
|
||||
export type Endpoint5_22Output = ReadonlyArray<InstructionEntry.Info>
|
||||
export type SessionInstructionsEntryListOperation<E = never> = (
|
||||
input: Endpoint5_22Input,
|
||||
) => Effect.Effect<Endpoint5_22Output, E>
|
||||
export type Endpoint5_22Output = ReadonlyArray<SessionPending.Info>
|
||||
export type SessionPendingListOperation<E = never> = (input: Endpoint5_22Input) => Effect.Effect<Endpoint5_22Output, E>
|
||||
|
||||
export type Endpoint5_23Input = {
|
||||
export type Endpoint5_23Input = { readonly sessionID: Session.ID }
|
||||
export type Endpoint5_23Output = ReadonlyArray<InstructionEntry.Info>
|
||||
export type SessionInstructionsEntryListOperation<E = never> = (
|
||||
input: Endpoint5_23Input,
|
||||
) => Effect.Effect<Endpoint5_23Output, E>
|
||||
|
||||
export type Endpoint5_24Input = {
|
||||
readonly sessionID: Session.ID
|
||||
readonly key: InstructionEntry.Key
|
||||
readonly value: Schema.Json
|
||||
}
|
||||
export type Endpoint5_23Output = void
|
||||
export type SessionInstructionsEntryPutOperation<E = never> = (
|
||||
input: Endpoint5_23Input,
|
||||
) => Effect.Effect<Endpoint5_23Output, E>
|
||||
|
||||
export type Endpoint5_24Input = { readonly sessionID: Session.ID; readonly key: InstructionEntry.Key }
|
||||
export type Endpoint5_24Output = void
|
||||
export type SessionInstructionsEntryRemoveOperation<E = never> = (
|
||||
export type SessionInstructionsEntryPutOperation<E = never> = (
|
||||
input: Endpoint5_24Input,
|
||||
) => Effect.Effect<Endpoint5_24Output, E>
|
||||
|
||||
export type Endpoint5_25Input = { readonly sessionID: Session.ID; readonly prompt: string }
|
||||
export type Endpoint5_25Output = { readonly text: string }
|
||||
export type SessionGenerateOperation<E = never> = (input: Endpoint5_25Input) => Effect.Effect<Endpoint5_25Output, E>
|
||||
export type Endpoint5_25Input = { readonly sessionID: Session.ID; readonly key: InstructionEntry.Key }
|
||||
export type Endpoint5_25Output = void
|
||||
export type SessionInstructionsEntryRemoveOperation<E = never> = (
|
||||
input: Endpoint5_25Input,
|
||||
) => Effect.Effect<Endpoint5_25Output, E>
|
||||
|
||||
export type Endpoint5_26Input = {
|
||||
export type Endpoint5_26Input = { readonly sessionID: Session.ID; readonly prompt: string }
|
||||
export type Endpoint5_26Output = { readonly text: string }
|
||||
export type SessionGenerateOperation<E = never> = (input: Endpoint5_26Input) => Effect.Effect<Endpoint5_26Output, E>
|
||||
|
||||
export type Endpoint5_27Input = {
|
||||
readonly sessionID: Session.ID
|
||||
readonly after?: Event.Seq | undefined
|
||||
readonly follow?: boolean | undefined
|
||||
}
|
||||
export type Endpoint5_26Output =
|
||||
export type Endpoint5_27Output =
|
||||
| (
|
||||
| {
|
||||
readonly id: Event.ID
|
||||
@@ -323,6 +327,15 @@ export type Endpoint5_26Output =
|
||||
readonly location?: Location.Ref | undefined
|
||||
readonly data: { readonly sessionID: Session.ID; readonly title: string }
|
||||
}
|
||||
| {
|
||||
readonly id: Event.ID
|
||||
readonly created: DateTime.Utc
|
||||
readonly metadata?: { readonly [x: string]: unknown } | undefined
|
||||
readonly type: "session.archived"
|
||||
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
|
||||
readonly location?: Location.Ref | undefined
|
||||
readonly data: { readonly sessionID: Session.ID }
|
||||
}
|
||||
| {
|
||||
readonly id: Event.ID
|
||||
readonly created: DateTime.Utc
|
||||
@@ -342,10 +355,8 @@ export type Endpoint5_26Output =
|
||||
readonly data: {
|
||||
readonly sessionID: Session.ID
|
||||
readonly parentID: Session.ID
|
||||
readonly boundary: Session.ForkBoundary
|
||||
readonly instructions?:
|
||||
| { readonly [x: string & Brand.Brand<"Instruction.Key">]: string & Brand.Brand<"Instruction.Hash"> }
|
||||
| undefined
|
||||
readonly parentSeq: number
|
||||
readonly from?: SessionMessage.ID | undefined
|
||||
}
|
||||
}
|
||||
| {
|
||||
@@ -827,19 +838,19 @@ export type Endpoint5_26Output =
|
||||
}
|
||||
)
|
||||
| EventLog.Synced
|
||||
export type SessionLogOperation<E = never> = (input: Endpoint5_26Input) => Stream.Stream<Endpoint5_26Output, E>
|
||||
|
||||
export type Endpoint5_27Input = { readonly sessionID: Session.ID }
|
||||
export type Endpoint5_27Output = void
|
||||
export type SessionInterruptOperation<E = never> = (input: Endpoint5_27Input) => Effect.Effect<Endpoint5_27Output, E>
|
||||
export type SessionLogOperation<E = never> = (input: Endpoint5_27Input) => Stream.Stream<Endpoint5_27Output, E>
|
||||
|
||||
export type Endpoint5_28Input = { readonly sessionID: Session.ID }
|
||||
export type Endpoint5_28Output = void
|
||||
export type SessionBackgroundOperation<E = never> = (input: Endpoint5_28Input) => Effect.Effect<Endpoint5_28Output, E>
|
||||
export type SessionInterruptOperation<E = never> = (input: Endpoint5_28Input) => Effect.Effect<Endpoint5_28Output, E>
|
||||
|
||||
export type Endpoint5_29Input = { readonly sessionID: Session.ID; readonly messageID: SessionMessage.ID }
|
||||
export type Endpoint5_29Output = SessionMessage.Info
|
||||
export type SessionMessageOperation<E = never> = (input: Endpoint5_29Input) => Effect.Effect<Endpoint5_29Output, E>
|
||||
export type Endpoint5_29Input = { readonly sessionID: Session.ID }
|
||||
export type Endpoint5_29Output = void
|
||||
export type SessionBackgroundOperation<E = never> = (input: Endpoint5_29Input) => Effect.Effect<Endpoint5_29Output, E>
|
||||
|
||||
export type Endpoint5_30Input = { readonly sessionID: Session.ID; readonly messageID: SessionMessage.ID }
|
||||
export type Endpoint5_30Output = SessionMessage.Info
|
||||
export type SessionMessageOperation<E = never> = (input: Endpoint5_30Input) => Effect.Effect<Endpoint5_30Output, E>
|
||||
|
||||
export interface SessionApi<E = never> {
|
||||
readonly list: SessionListOperation<E>
|
||||
@@ -851,6 +862,7 @@ export interface SessionApi<E = never> {
|
||||
readonly switchAgent: SessionSwitchAgentOperation<E>
|
||||
readonly switchModel: SessionSwitchModelOperation<E>
|
||||
readonly rename: SessionRenameOperation<E>
|
||||
readonly archive: SessionArchiveOperation<E>
|
||||
readonly move: SessionMoveOperation<E>
|
||||
readonly prompt: SessionPromptOperation<E>
|
||||
readonly command: SessionCommandOperation<E>
|
||||
|
||||
@@ -76,6 +76,8 @@ import type {
|
||||
Endpoint5_28Output,
|
||||
Endpoint5_29Input,
|
||||
Endpoint5_29Output,
|
||||
Endpoint5_30Input,
|
||||
Endpoint5_30Output,
|
||||
Endpoint6_0Input,
|
||||
Endpoint6_0Output,
|
||||
Endpoint7_0Input,
|
||||
@@ -329,7 +331,7 @@ const Endpoint5_4 = (raw: RawClient["server.session"]) => (input: Endpoint5_4Inp
|
||||
|
||||
const Endpoint5_5 = (raw: RawClient["server.session"]) => (input: Endpoint5_5Input) =>
|
||||
preserveEffect<Endpoint5_5Output>()(
|
||||
raw["session.fork"]({ params: { sessionID: input["sessionID"] }, payload: { boundary: input["boundary"] } }).pipe(
|
||||
raw["session.fork"]({ params: { sessionID: input["sessionID"] }, payload: { messageID: input["messageID"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
),
|
||||
@@ -358,14 +360,19 @@ const Endpoint5_8 = (raw: RawClient["server.session"]) => (input: Endpoint5_8Inp
|
||||
|
||||
const Endpoint5_9 = (raw: RawClient["server.session"]) => (input: Endpoint5_9Input) =>
|
||||
preserveEffect<Endpoint5_9Output>()(
|
||||
raw["session.archive"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
const Endpoint5_10 = (raw: RawClient["server.session"]) => (input: Endpoint5_10Input) =>
|
||||
preserveEffect<Endpoint5_10Output>()(
|
||||
raw["session.move"]({
|
||||
params: { sessionID: input["sessionID"] },
|
||||
payload: { directory: input["directory"], workspaceID: input["workspaceID"] },
|
||||
}).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
const Endpoint5_10 = (raw: RawClient["server.session"]) => (input: Endpoint5_10Input) =>
|
||||
preserveEffect<Endpoint5_10Output>()(
|
||||
const Endpoint5_11 = (raw: RawClient["server.session"]) => (input: Endpoint5_11Input) =>
|
||||
preserveEffect<Endpoint5_11Output>()(
|
||||
raw["session.prompt"]({
|
||||
params: { sessionID: input["sessionID"] },
|
||||
payload: {
|
||||
@@ -383,8 +390,8 @@ const Endpoint5_10 = (raw: RawClient["server.session"]) => (input: Endpoint5_10I
|
||||
),
|
||||
)
|
||||
|
||||
const Endpoint5_11 = (raw: RawClient["server.session"]) => (input: Endpoint5_11Input) =>
|
||||
preserveEffect<Endpoint5_11Output>()(
|
||||
const Endpoint5_12 = (raw: RawClient["server.session"]) => (input: Endpoint5_12Input) =>
|
||||
preserveEffect<Endpoint5_12Output>()(
|
||||
raw["session.command"]({
|
||||
params: { sessionID: input["sessionID"] },
|
||||
payload: {
|
||||
@@ -404,16 +411,16 @@ const Endpoint5_11 = (raw: RawClient["server.session"]) => (input: Endpoint5_11I
|
||||
),
|
||||
)
|
||||
|
||||
const Endpoint5_12 = (raw: RawClient["server.session"]) => (input: Endpoint5_12Input) =>
|
||||
preserveEffect<Endpoint5_12Output>()(
|
||||
const Endpoint5_13 = (raw: RawClient["server.session"]) => (input: Endpoint5_13Input) =>
|
||||
preserveEffect<Endpoint5_13Output>()(
|
||||
raw["session.skill"]({
|
||||
params: { sessionID: input["sessionID"] },
|
||||
payload: { id: input["id"], skill: input["skill"], resume: input["resume"] },
|
||||
}).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
const Endpoint5_13 = (raw: RawClient["server.session"]) => (input: Endpoint5_13Input) =>
|
||||
preserveEffect<Endpoint5_13Output>()(
|
||||
const Endpoint5_14 = (raw: RawClient["server.session"]) => (input: Endpoint5_14Input) =>
|
||||
preserveEffect<Endpoint5_14Output>()(
|
||||
raw["session.synthetic"]({
|
||||
params: { sessionID: input["sessionID"] },
|
||||
payload: {
|
||||
@@ -430,29 +437,29 @@ const Endpoint5_13 = (raw: RawClient["server.session"]) => (input: Endpoint5_13I
|
||||
),
|
||||
)
|
||||
|
||||
const Endpoint5_14 = (raw: RawClient["server.session"]) => (input: Endpoint5_14Input) =>
|
||||
preserveEffect<Endpoint5_14Output>()(
|
||||
const Endpoint5_15 = (raw: RawClient["server.session"]) => (input: Endpoint5_15Input) =>
|
||||
preserveEffect<Endpoint5_15Output>()(
|
||||
raw["session.shell"]({
|
||||
params: { sessionID: input["sessionID"] },
|
||||
payload: { id: input["id"], command: input["command"] },
|
||||
}).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
const Endpoint5_15 = (raw: RawClient["server.session"]) => (input: Endpoint5_15Input) =>
|
||||
preserveEffect<Endpoint5_15Output>()(
|
||||
const Endpoint5_16 = (raw: RawClient["server.session"]) => (input: Endpoint5_16Input) =>
|
||||
preserveEffect<Endpoint5_16Output>()(
|
||||
raw["session.compact"]({ params: { sessionID: input["sessionID"] }, payload: { id: input["id"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
),
|
||||
)
|
||||
|
||||
const Endpoint5_16 = (raw: RawClient["server.session"]) => (input: Endpoint5_16Input) =>
|
||||
preserveEffect<Endpoint5_16Output>()(
|
||||
const Endpoint5_17 = (raw: RawClient["server.session"]) => (input: Endpoint5_17Input) =>
|
||||
preserveEffect<Endpoint5_17Output>()(
|
||||
raw["session.wait"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
const Endpoint5_17 = (raw: RawClient["server.session"]) => (input: Endpoint5_17Input) =>
|
||||
preserveEffect<Endpoint5_17Output>()(
|
||||
const Endpoint5_18 = (raw: RawClient["server.session"]) => (input: Endpoint5_18Input) =>
|
||||
preserveEffect<Endpoint5_18Output>()(
|
||||
raw["session.revert.stage"]({
|
||||
params: { sessionID: input["sessionID"] },
|
||||
payload: { messageID: input["messageID"], files: input["files"] },
|
||||
@@ -462,27 +469,19 @@ const Endpoint5_17 = (raw: RawClient["server.session"]) => (input: Endpoint5_17I
|
||||
),
|
||||
)
|
||||
|
||||
const Endpoint5_18 = (raw: RawClient["server.session"]) => (input: Endpoint5_18Input) =>
|
||||
preserveEffect<Endpoint5_18Output>()(
|
||||
raw["session.revert.clear"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
const Endpoint5_19 = (raw: RawClient["server.session"]) => (input: Endpoint5_19Input) =>
|
||||
preserveEffect<Endpoint5_19Output>()(
|
||||
raw["session.revert.commit"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)),
|
||||
raw["session.revert.clear"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
const Endpoint5_20 = (raw: RawClient["server.session"]) => (input: Endpoint5_20Input) =>
|
||||
preserveEffect<Endpoint5_20Output>()(
|
||||
raw["session.context"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
),
|
||||
raw["session.revert.commit"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
const Endpoint5_21 = (raw: RawClient["server.session"]) => (input: Endpoint5_21Input) =>
|
||||
preserveEffect<Endpoint5_21Output>()(
|
||||
raw["session.pending.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
||||
raw["session.context"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
),
|
||||
@@ -490,7 +489,7 @@ const Endpoint5_21 = (raw: RawClient["server.session"]) => (input: Endpoint5_21I
|
||||
|
||||
const Endpoint5_22 = (raw: RawClient["server.session"]) => (input: Endpoint5_22Input) =>
|
||||
preserveEffect<Endpoint5_22Output>()(
|
||||
raw["session.instructions.entry.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
||||
raw["session.pending.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
),
|
||||
@@ -498,29 +497,37 @@ const Endpoint5_22 = (raw: RawClient["server.session"]) => (input: Endpoint5_22I
|
||||
|
||||
const Endpoint5_23 = (raw: RawClient["server.session"]) => (input: Endpoint5_23Input) =>
|
||||
preserveEffect<Endpoint5_23Output>()(
|
||||
raw["session.instructions.entry.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
),
|
||||
)
|
||||
|
||||
const Endpoint5_24 = (raw: RawClient["server.session"]) => (input: Endpoint5_24Input) =>
|
||||
preserveEffect<Endpoint5_24Output>()(
|
||||
raw["session.instructions.entry.put"]({
|
||||
params: { sessionID: input["sessionID"], key: input["key"] },
|
||||
payload: { value: input["value"] },
|
||||
}).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
const Endpoint5_24 = (raw: RawClient["server.session"]) => (input: Endpoint5_24Input) =>
|
||||
preserveEffect<Endpoint5_24Output>()(
|
||||
const Endpoint5_25 = (raw: RawClient["server.session"]) => (input: Endpoint5_25Input) =>
|
||||
preserveEffect<Endpoint5_25Output>()(
|
||||
raw["session.instructions.entry.remove"]({ params: { sessionID: input["sessionID"], key: input["key"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
),
|
||||
)
|
||||
|
||||
const Endpoint5_25 = (raw: RawClient["server.session"]) => (input: Endpoint5_25Input) =>
|
||||
preserveEffect<Endpoint5_25Output>()(
|
||||
const Endpoint5_26 = (raw: RawClient["server.session"]) => (input: Endpoint5_26Input) =>
|
||||
preserveEffect<Endpoint5_26Output>()(
|
||||
raw["session.generate"]({ params: { sessionID: input["sessionID"] }, payload: { prompt: input["prompt"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
),
|
||||
)
|
||||
|
||||
const Endpoint5_26 = (raw: RawClient["server.session"]) => (input: Endpoint5_26Input) =>
|
||||
preserveStream<Endpoint5_26Output>()(
|
||||
const Endpoint5_27 = (raw: RawClient["server.session"]) => (input: Endpoint5_27Input) =>
|
||||
preserveStream<Endpoint5_27Output>()(
|
||||
Stream.unwrap(
|
||||
raw["session.log"]({
|
||||
params: { sessionID: input["sessionID"] },
|
||||
@@ -532,18 +539,18 @@ const Endpoint5_26 = (raw: RawClient["server.session"]) => (input: Endpoint5_26I
|
||||
),
|
||||
)
|
||||
|
||||
const Endpoint5_27 = (raw: RawClient["server.session"]) => (input: Endpoint5_27Input) =>
|
||||
preserveEffect<Endpoint5_27Output>()(
|
||||
raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
const Endpoint5_28 = (raw: RawClient["server.session"]) => (input: Endpoint5_28Input) =>
|
||||
preserveEffect<Endpoint5_28Output>()(
|
||||
raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)),
|
||||
raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
const Endpoint5_29 = (raw: RawClient["server.session"]) => (input: Endpoint5_29Input) =>
|
||||
preserveEffect<Endpoint5_29Output>()(
|
||||
raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
const Endpoint5_30 = (raw: RawClient["server.session"]) => (input: Endpoint5_30Input) =>
|
||||
preserveEffect<Endpoint5_30Output>()(
|
||||
raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
@@ -560,23 +567,24 @@ const adaptGroup5 = (raw: RawClient["server.session"]) => ({
|
||||
switchAgent: Endpoint5_6(raw),
|
||||
switchModel: Endpoint5_7(raw),
|
||||
rename: Endpoint5_8(raw),
|
||||
move: Endpoint5_9(raw),
|
||||
prompt: Endpoint5_10(raw),
|
||||
command: Endpoint5_11(raw),
|
||||
skill: Endpoint5_12(raw),
|
||||
synthetic: Endpoint5_13(raw),
|
||||
shell: Endpoint5_14(raw),
|
||||
compact: Endpoint5_15(raw),
|
||||
wait: Endpoint5_16(raw),
|
||||
revert: { stage: Endpoint5_17(raw), clear: Endpoint5_18(raw), commit: Endpoint5_19(raw) },
|
||||
context: Endpoint5_20(raw),
|
||||
pending: { list: Endpoint5_21(raw) },
|
||||
instructions: { entry: { list: Endpoint5_22(raw), put: Endpoint5_23(raw), remove: Endpoint5_24(raw) } },
|
||||
generate: Endpoint5_25(raw),
|
||||
log: Endpoint5_26(raw),
|
||||
interrupt: Endpoint5_27(raw),
|
||||
background: Endpoint5_28(raw),
|
||||
message: Endpoint5_29(raw),
|
||||
archive: Endpoint5_9(raw),
|
||||
move: Endpoint5_10(raw),
|
||||
prompt: Endpoint5_11(raw),
|
||||
command: Endpoint5_12(raw),
|
||||
skill: Endpoint5_13(raw),
|
||||
synthetic: Endpoint5_14(raw),
|
||||
shell: Endpoint5_15(raw),
|
||||
compact: Endpoint5_16(raw),
|
||||
wait: Endpoint5_17(raw),
|
||||
revert: { stage: Endpoint5_18(raw), clear: Endpoint5_19(raw), commit: Endpoint5_20(raw) },
|
||||
context: Endpoint5_21(raw),
|
||||
pending: { list: Endpoint5_22(raw) },
|
||||
instructions: { entry: { list: Endpoint5_23(raw), put: Endpoint5_24(raw), remove: Endpoint5_25(raw) } },
|
||||
generate: Endpoint5_26(raw),
|
||||
log: Endpoint5_27(raw),
|
||||
interrupt: Endpoint5_28(raw),
|
||||
background: Endpoint5_29(raw),
|
||||
message: Endpoint5_30(raw),
|
||||
})
|
||||
|
||||
const Endpoint6_0 = (raw: RawClient["server.message"]) => (input: Endpoint6_0Input) =>
|
||||
|
||||
@@ -28,6 +28,8 @@ import type {
|
||||
SessionSwitchModelOutput,
|
||||
SessionRenameInput,
|
||||
SessionRenameOutput,
|
||||
SessionArchiveInput,
|
||||
SessionArchiveOutput,
|
||||
SessionMoveInput,
|
||||
SessionMoveOutput,
|
||||
SessionPromptInput,
|
||||
@@ -510,7 +512,7 @@ export function make(options: ClientOptions) {
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/fork`,
|
||||
body: { boundary: input["boundary"] },
|
||||
body: { messageID: input["messageID"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [404, 400, 401],
|
||||
empty: false,
|
||||
@@ -553,6 +555,17 @@ export function make(options: ClientOptions) {
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
archive: (input: SessionArchiveInput, requestOptions?: RequestOptions) =>
|
||||
request<SessionArchiveOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/archive`,
|
||||
successStatus: 204,
|
||||
declaredStatuses: [404, 400, 401],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
move: (input: SessionMoveInput, requestOptions?: RequestOptions) =>
|
||||
request<SessionMoveOutput>(
|
||||
{
|
||||
|
||||
@@ -14,8 +14,6 @@ export type PermissionEffect = "allow" | "deny" | "ask"
|
||||
|
||||
export type PluginInfo = { id: string }
|
||||
|
||||
export type SessionForkBoundary = { type: "before"; messageID: string } | { type: "through"; messageID: string }
|
||||
|
||||
export type MoneyUSD = number
|
||||
|
||||
export type TokenUsageInfo = {
|
||||
@@ -45,7 +43,13 @@ export type PromptMention = { start: number; end: number; text: string }
|
||||
|
||||
export type SessionPendingSyntheticData = { text: string; description?: string; metadata?: { [x: string]: JsonValue } }
|
||||
|
||||
export type SessionPendingCompaction = { id: string; sessionID: string; timeCreated: number; type: "compaction" }
|
||||
export type SessionPendingCompaction = {
|
||||
admittedSeq: number
|
||||
id: string
|
||||
sessionID: string
|
||||
timeCreated: number
|
||||
type: "compaction"
|
||||
}
|
||||
|
||||
export type SessionMessageAgentSelected = {
|
||||
id: string
|
||||
@@ -607,6 +611,16 @@ export type SessionRenamed = {
|
||||
data: { sessionID: string; title: string }
|
||||
}
|
||||
|
||||
export type SessionArchived = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: { [x: string]: any }
|
||||
type: "session.archived"
|
||||
durable: { aggregateID: string; seq: number; version: 1 }
|
||||
location?: LocationRef
|
||||
data: { sessionID: string }
|
||||
}
|
||||
|
||||
export type SessionDeleted = {
|
||||
id: string
|
||||
created: number
|
||||
@@ -624,7 +638,7 @@ export type SessionForked = {
|
||||
type: "session.forked"
|
||||
durable: { aggregateID: string; seq: number; version: 2 }
|
||||
location?: LocationRef
|
||||
data: { sessionID: string; parentID: string; boundary: SessionForkBoundary; instructions?: { [x: string]: string } }
|
||||
data: { sessionID: string; parentID: string; parentSeq: number; from?: string }
|
||||
}
|
||||
|
||||
export type SessionInputPromoted = {
|
||||
@@ -1206,6 +1220,7 @@ export type PromptFileAttachment = {
|
||||
export type PromptAgentAttachment = { name: string; mention?: PromptMention }
|
||||
|
||||
export type SessionPendingSynthetic = {
|
||||
admittedSeq: number
|
||||
id: string
|
||||
sessionID: string
|
||||
timeCreated: number
|
||||
@@ -1750,7 +1765,7 @@ export type PermissionRuleset = Array<PermissionRule>
|
||||
export type SessionInfo = {
|
||||
id: string
|
||||
parentID?: string
|
||||
fork?: { sessionID: string; boundary: SessionForkBoundary }
|
||||
fork?: { sessionID: string; messageID?: string }
|
||||
projectID: string
|
||||
agent?: string
|
||||
model?: ModelRef
|
||||
@@ -1961,6 +1976,7 @@ export type AgentInfo = {
|
||||
export type SessionsResponse = { data: Array<SessionInfo>; cursor: { previous?: string | null; next?: string | null } }
|
||||
|
||||
export type SessionPendingUser = {
|
||||
admittedSeq: number
|
||||
id: string
|
||||
sessionID: string
|
||||
timeCreated: number
|
||||
@@ -2153,6 +2169,7 @@ export type SessionEventDurable =
|
||||
| SessionModelSelected
|
||||
| SessionMoved
|
||||
| SessionRenamed
|
||||
| SessionArchived
|
||||
| SessionDeleted
|
||||
| SessionForked
|
||||
| SessionInputPromoted
|
||||
@@ -2247,6 +2264,7 @@ export type V2Event =
|
||||
| SessionModelSelected
|
||||
| SessionMoved
|
||||
| SessionRenamed
|
||||
| SessionArchived
|
||||
| SessionUsageUpdated
|
||||
| SessionDeleted
|
||||
| SessionForked
|
||||
@@ -2696,9 +2714,7 @@ export type SessionRemoveOutput = void
|
||||
|
||||
export type SessionForkInput = {
|
||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||
readonly boundary: {
|
||||
readonly boundary: { readonly type: "before"; readonly messageID: string } | { readonly type: "through" }
|
||||
}["boundary"]
|
||||
readonly messageID?: { readonly messageID?: string | undefined }["messageID"]
|
||||
}
|
||||
|
||||
export type SessionForkOutput = { data: SessionInfo }["data"]
|
||||
@@ -2726,6 +2742,10 @@ export type SessionRenameInput = {
|
||||
|
||||
export type SessionRenameOutput = void
|
||||
|
||||
export type SessionArchiveInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
|
||||
|
||||
export type SessionArchiveOutput = void
|
||||
|
||||
export type SessionMoveInput = {
|
||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||
readonly directory: { readonly directory: string; readonly workspaceID?: string }["directory"]
|
||||
|
||||
@@ -268,6 +268,7 @@ const session = {
|
||||
|
||||
const admission = {
|
||||
data: {
|
||||
admittedSeq: 0,
|
||||
id: "msg_test",
|
||||
sessionID: "ses_test",
|
||||
type: "user",
|
||||
@@ -280,6 +281,7 @@ const admission = {
|
||||
const compactionAdmission = {
|
||||
data: {
|
||||
type: "compaction",
|
||||
admittedSeq: 1,
|
||||
id: "msg_compaction",
|
||||
sessionID: "ses_test",
|
||||
timeCreated: 1_717_171_717_000,
|
||||
|
||||
@@ -304,6 +304,7 @@ test("session.pending.list uses the public HTTP contract", async () => {
|
||||
const requests: Array<{ method: string; url: string }> = []
|
||||
const pending = [
|
||||
{
|
||||
admittedSeq: 3,
|
||||
id: "msg_pending",
|
||||
sessionID: "ses_test",
|
||||
timeCreated: 1_717_171_717_000,
|
||||
@@ -431,6 +432,7 @@ test("session methods use the public HTTP contract", async () => {
|
||||
sessionID: "ses_test",
|
||||
model: { id: "claude", providerID: "anthropic" },
|
||||
})
|
||||
await client.session.archive({ sessionID: "ses_test" })
|
||||
const admitted = await client.session.prompt({
|
||||
sessionID: "ses_test",
|
||||
text: "Hello",
|
||||
@@ -466,6 +468,7 @@ test("session methods use the public HTTP contract", async () => {
|
||||
["POST", "http://localhost:3000/api/session"],
|
||||
["POST", "http://localhost:3000/api/session/ses_test/agent"],
|
||||
["POST", "http://localhost:3000/api/session/ses_test/model"],
|
||||
["POST", "http://localhost:3000/api/session/ses_test/archive"],
|
||||
["POST", "http://localhost:3000/api/session/ses_test/prompt"],
|
||||
["POST", "http://localhost:3000/api/session/ses_test/generate"],
|
||||
["POST", "http://localhost:3000/api/session/ses_test/synthetic"],
|
||||
@@ -546,6 +549,7 @@ const session = {
|
||||
|
||||
const admission = {
|
||||
data: {
|
||||
admittedSeq: 0,
|
||||
id: "msg_test",
|
||||
sessionID: "ses_test",
|
||||
type: "user",
|
||||
@@ -557,6 +561,7 @@ const admission = {
|
||||
|
||||
const syntheticAdmission = {
|
||||
data: {
|
||||
admittedSeq: 1,
|
||||
id: "msg_synthetic",
|
||||
sessionID: "ses_test",
|
||||
type: "synthetic",
|
||||
@@ -569,6 +574,7 @@ const syntheticAdmission = {
|
||||
const compactionAdmission = {
|
||||
data: {
|
||||
type: "compaction",
|
||||
admittedSeq: 1,
|
||||
id: "msg_compaction",
|
||||
sessionID: "ses_test",
|
||||
timeCreated: 1_717_171_717_000,
|
||||
|
||||
@@ -92,7 +92,6 @@
|
||||
"@lydell/node-pty": "catalog:",
|
||||
"@modelcontextprotocol/sdk": "1.29.0",
|
||||
"@ff-labs/fff-bun": "0.10.1",
|
||||
"@ff-labs/fff-node": "0.10.1",
|
||||
"@opencode-ai/codemode": "workspace:*",
|
||||
"@opencode-ai/effect-drizzle-sqlite": "workspace:*",
|
||||
"@opencode-ai/effect-sqlite-node": "workspace:*",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"id": "db37a97f-9b5e-4c87-be8b-4feace35136c",
|
||||
"id": "a4ba73b4-21bc-41ab-a415-94e2ca38d798",
|
||||
"prevIds": [
|
||||
"a4ba73b4-21bc-41ab-a415-94e2ca38d798"
|
||||
"5f0a1db8-d4bf-42c3-becb-96b46fe66bed"
|
||||
],
|
||||
"ddl": [
|
||||
{
|
||||
@@ -1266,7 +1266,17 @@
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "fork_boundary",
|
||||
"name": "fork_message_id",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "fork_seq",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
|
||||
@@ -6,9 +6,7 @@ import { Instructions } from "../instructions/index"
|
||||
import { CodeModeCatalog } from "./catalog"
|
||||
|
||||
// prettier-ignore
|
||||
const prompt = (hasMoreTools: boolean) => `The Code Mode tool catalog below is ${hasMoreTools ? "partial" : "complete"}.
|
||||
|
||||
${hasMoreTools ? "The Code Mode catalog and `search` results are" : "This catalog is"} the complete set of tools available within Code Mode. Tools presented elsewhere are not available in this runtime.${hasMoreTools ? `
|
||||
const prompt = (hasMoreTools: boolean) => `The Code Mode tool catalog below is ${hasMoreTools ? "partial" : "complete"}.${hasMoreTools ? `
|
||||
|
||||
## Search
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ type CollectedFiles = {
|
||||
|
||||
// Invariant model-facing guidance; the changing tool catalog is delivered through Instructions.
|
||||
const description = [
|
||||
"Run JavaScript in a confined Code Mode runtime to orchestrate tool calls and compose their results.",
|
||||
"Run JavaScript to orchestrate tool calls and compose their results through `{ code }` in a confined Code Mode runtime.",
|
||||
"Imports, direct filesystem access, and timers are unavailable. Do not use `fetch`; all external access goes through `tools`.",
|
||||
"Within `{ code }`, the only callable tools are those explicitly listed in the Code Mode catalog instructions or returned by `search`. Inside `{ code }`, ignore tools shown outside the Code Mode catalog. They are not available in the Code Mode runtime.",
|
||||
'Call tools through `tools` using only exact paths and signatures from the catalog. Do not infer or normalize tool names; preserve bracket notation such as `tools.<namespace>["tool-name"](input)`.',
|
||||
|
||||
@@ -393,8 +393,7 @@ export const layer = (options?: Options) => Layer.effect(
|
||||
const key = JSON.stringify(target)
|
||||
if (watched.has(key)) continue
|
||||
watched.add(key)
|
||||
const stream = yield* watcher.subscribe(target)
|
||||
yield* stream.pipe(
|
||||
yield* watcher.subscribe(target).pipe(
|
||||
Stream.runForEach((update) => PubSub.publish(updates, update)),
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
|
||||
-1
@@ -57,6 +57,5 @@ export const migrations = (
|
||||
import("./migration/20260716020354_kv"),
|
||||
import("./migration/20260722011141_delete_tool_progress_events"),
|
||||
import("./migration/20260722170000_canonical_tool_results"),
|
||||
import("./migration/20260729022634_session_fork_boundary"),
|
||||
])
|
||||
).map((module) => module.default) satisfies DatabaseMigration.Migration[]
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260729022634_session_fork_boundary",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`ALTER TABLE \`session\` ADD \`fork_boundary\` text;`)
|
||||
yield* tx.run(`ALTER TABLE \`session\` DROP COLUMN \`fork_message_id\`;`)
|
||||
yield* tx.run(`ALTER TABLE \`session\` DROP COLUMN \`fork_seq\`;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
@@ -213,7 +213,8 @@ export default {
|
||||
\`workspace_id\` text,
|
||||
\`parent_id\` text,
|
||||
\`fork_session_id\` text,
|
||||
\`fork_boundary\` text,
|
||||
\`fork_message_id\` text,
|
||||
\`fork_seq\` integer,
|
||||
\`slug\` text NOT NULL,
|
||||
\`directory\` text NOT NULL,
|
||||
\`path\` text,
|
||||
|
||||
@@ -1,52 +1,71 @@
|
||||
import type {
|
||||
DirItem,
|
||||
DirSearchResult,
|
||||
FileItem,
|
||||
GrepCursor,
|
||||
GrepMatch,
|
||||
GrepResult,
|
||||
InitOptions,
|
||||
MixedItem,
|
||||
MixedSearchResult,
|
||||
SearchResult,
|
||||
} from "@ff-labs/fff-node"
|
||||
|
||||
const { FileFinder } = await import("@ff-labs/fff-node").catch(() => ({ FileFinder: undefined }))
|
||||
|
||||
export type Result<T> = { ok: true; value: T } | { ok: false; error: string }
|
||||
|
||||
export type Init = InitOptions
|
||||
export interface Init {
|
||||
basePath: string
|
||||
frecencyDbPath?: string
|
||||
historyDbPath?: string
|
||||
useUnsafeNoLock?: boolean
|
||||
disableMmapCache?: boolean
|
||||
disableContentIndexing?: boolean
|
||||
disableWatch?: boolean
|
||||
aiMode?: boolean
|
||||
logFilePath?: string
|
||||
logLevel?: "trace" | "debug" | "info" | "warn" | "error"
|
||||
enableFsRootScanning?: boolean
|
||||
enableHomeDirScanning?: boolean
|
||||
}
|
||||
|
||||
export interface File {
|
||||
relativePath: string
|
||||
fileName: string
|
||||
modified: number
|
||||
}
|
||||
|
||||
export interface Directory {
|
||||
relativePath: string
|
||||
dirName: string
|
||||
maxAccessFrecency: number
|
||||
}
|
||||
|
||||
export type Mixed = { type: "file"; item: File } | { type: "directory"; item: Directory }
|
||||
|
||||
export interface Search {
|
||||
items: FileItem[]
|
||||
scores: SearchResult["scores"]
|
||||
items: File[]
|
||||
scores: Array<{ total: number }>
|
||||
totalMatched: number
|
||||
totalFiles: number
|
||||
}
|
||||
|
||||
export interface DirSearch {
|
||||
items: DirItem[]
|
||||
scores: DirSearchResult["scores"]
|
||||
items: Directory[]
|
||||
scores: Array<{ total: number }>
|
||||
totalMatched: number
|
||||
totalDirs: number
|
||||
}
|
||||
|
||||
export interface MixedSearch {
|
||||
items: MixedItem[]
|
||||
scores: MixedSearchResult["scores"]
|
||||
items: Mixed[]
|
||||
scores: Array<{ total: number }>
|
||||
totalMatched: number
|
||||
totalFiles: number
|
||||
totalDirs: number
|
||||
}
|
||||
|
||||
export type File = FileItem
|
||||
export type Directory = DirItem
|
||||
export type Mixed = MixedItem
|
||||
export type Cursor = GrepCursor | null
|
||||
export type Hit = GrepMatch
|
||||
export type Cursor = null
|
||||
|
||||
export interface Hit {
|
||||
relativePath: string
|
||||
fileName: string
|
||||
lineNumber: number
|
||||
byteOffset: number
|
||||
lineContent: string
|
||||
matchRanges: [number, number][]
|
||||
contextBefore?: string[]
|
||||
contextAfter?: string[]
|
||||
}
|
||||
|
||||
export interface Grep {
|
||||
items: GrepResult["items"]
|
||||
items: Hit[]
|
||||
totalMatched: number
|
||||
totalFilesSearched: number
|
||||
totalFiles: number
|
||||
@@ -109,30 +128,11 @@ export interface Picker {
|
||||
}
|
||||
|
||||
export function available() {
|
||||
return FileFinder?.isAvailable() ?? false
|
||||
return false
|
||||
}
|
||||
|
||||
export function create(opts: Init): Result<Picker> {
|
||||
if (!FileFinder) return { ok: false, error: "fff unavailable on node runtime" }
|
||||
const made = FileFinder.create(opts)
|
||||
if (!made.ok) return made
|
||||
const pick = made.value
|
||||
return {
|
||||
ok: true,
|
||||
value: {
|
||||
destroy: () => pick.destroy(),
|
||||
isScanning: () => pick.isScanning(),
|
||||
waitForScan: (timeoutMs) => pick.waitForScan(timeoutMs),
|
||||
refreshGitStatus: () => pick.refreshGitStatus(),
|
||||
fileSearch: (query, next) => pick.fileSearch(query, next),
|
||||
glob: (pattern, next) => pick.glob(pattern, next),
|
||||
directorySearch: (query, next) => pick.directorySearch(query, next),
|
||||
mixedSearch: (query, next) => pick.mixedSearch(query, next),
|
||||
grep: (query, next) => pick.grep(query, next),
|
||||
trackQuery: (query, file) => pick.trackQuery(query, file),
|
||||
getHistoricalQuery: (offset) => pick.getHistoricalQuery(offset),
|
||||
},
|
||||
}
|
||||
export function create(_opts: Init): Result<Picker> {
|
||||
return { ok: false, error: "fff unavailable on node runtime" }
|
||||
}
|
||||
|
||||
export * as Fff from "./fff.node"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user