mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-02 16:26:14 -04:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4086aa8079 | |||
| dd014120cd | |||
| bf7f0cb521 | |||
| adbd22833c | |||
| 634b9b21dd | |||
| 5d729521d3 | |||
| c391983ae0 | |||
| f158abd694 | |||
| 0e3da4b4f1 | |||
| 003b22edda | |||
| d5f6c088f0 | |||
| e872bd3c8f | |||
| 85ea15e56d | |||
| 6a543791b6 | |||
| 47c6840752 | |||
| 14e4c3dfd7 | |||
| ee6577936d | |||
| 8285a18f18 | |||
| cfcd8de62a | |||
| c2db18c59b | |||
| 70393a1d18 | |||
| eab59ea021 | |||
| 4569659b03 | |||
| b5afaed053 | |||
| 97f5fb3399 | |||
| 18843682ad | |||
| 9eb9c1db7d | |||
| 8846336e11 | |||
| f6b5f6b6b3 | |||
| 02df1e9737 | |||
| 5b84e39f6d | |||
| 6d3f84b4ff | |||
| 255e9bbf06 | |||
| 25b6a179e2 | |||
| 80874c241f | |||
| f99469bdf8 | |||
| fe0eb4ea8f | |||
| a484f87680 | |||
| 3b6ab392ac | |||
| 6c567dc745 | |||
| 92567700f6 | |||
| 90c84639e3 |
@@ -4,6 +4,13 @@
|
||||
- The default branch in this repo is `dev`.
|
||||
- Local `main` ref may not exist; use `dev` or `origin/dev` for diffs.
|
||||
|
||||
## Live V2 TUI Testing
|
||||
|
||||
- Run `bun run dev:live` from a development worktree to test its TUI against the currently elected `opencode2` background server and live sessions.
|
||||
- Pass a directory after the script when needed, for example `bun run dev:live /path/to/project`.
|
||||
- The script discovers the server with `opencode2 service status`, injects its private local credential from `opencode2 service get password`, and uses the `next` TUI storage channel so tabs and other client-local state match the installed client.
|
||||
- Prefer `dev:live` over plain `bun run dev` for this workflow. An implicit managed-service connection may replace the live server when the worktree client version differs; explicit `--server` warns and continues without replacing it.
|
||||
|
||||
## Branch Names
|
||||
|
||||
Use a short branch name of at most three words, separated by hyphens. Do not use slashes or type prefixes such as `feat/` or `fix/`.
|
||||
|
||||
@@ -88,7 +88,6 @@
|
||||
"@tanstack/solid-query": "5.91.4",
|
||||
"@tanstack/solid-virtual": "catalog:",
|
||||
"@thisbeyond/solid-dnd": "0.7.5",
|
||||
"diff": "catalog:",
|
||||
"effect": "catalog:",
|
||||
"fuzzysort": "catalog:",
|
||||
"ghostty-web": "github:anomalyco/ghostty-web#83c0a07b8628b748aed073b232cb4b52a6ca11c1",
|
||||
@@ -384,7 +383,6 @@
|
||||
"@opencode-ai/ai": "workspace:*",
|
||||
"@opencode-ai/codemode": "workspace:*",
|
||||
"@opencode-ai/effect-drizzle-sqlite": "workspace:*",
|
||||
"@opencode-ai/effect-sqlite-node": "workspace:*",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
@@ -405,7 +403,6 @@
|
||||
"ignore": "7.0.5",
|
||||
"immer": "11.1.4",
|
||||
"jsonc-parser": "3.3.1",
|
||||
"semver": "^7.6.3",
|
||||
"tree-sitter-bash": "0.25.0",
|
||||
"tree-sitter-powershell": "0.25.10",
|
||||
"turndown": "7.2.0",
|
||||
@@ -427,7 +424,6 @@
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@types/semver": "catalog:",
|
||||
"@types/turndown": "5.0.5",
|
||||
"@types/which": "3.0.4",
|
||||
"drizzle-kit": "catalog:",
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"packageManager": "bun@1.3.14",
|
||||
"scripts": {
|
||||
"dev": "bun run --cwd packages/cli --conditions=browser src/index.ts",
|
||||
"dev:live": "OPENCODE_TUI_CHANNEL=next OPENCODE_PASSWORD=\"$(opencode2 service get password)\" bun run dev --server \"$(opencode2 service status)\"",
|
||||
"dev:desktop": "bun --cwd packages/desktop dev",
|
||||
"dev:web": "bun --cwd packages/app dev",
|
||||
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
## Conventions
|
||||
|
||||
Per-type constructors live on the type, not as top-level re-exports. Use `Message.system(...)`, `Message.user(...)`, `Message.assistant(...)`, `Message.tool(...)`, `Model.make(...)`, `ToolDefinition.make(...)`, `ToolCallPart.make(...)`, `ToolResultPart.make(...)`, `ToolChoice.make(...)`, `ToolChoice.named(...)`, `SystemPart.make(...)`, and `GenerationOptions.make(...)` directly. The top-level `LLM` namespace is reserved for request-shaped call APIs: `LLM.request`, `LLM.generate`, `LLM.stream`, `LLM.updateRequest`, and `LLM.generateObject`. Two ways to construct the same thing is one too many.
|
||||
Per-type constructors live on the type, not as top-level re-exports. Use `Message.system(...)`, `Message.user(...)`, `Message.assistant(...)`, `Message.tool(...)`, `LanguageModel.make(...)`, `ToolDefinition.make(...)`, `ToolCallPart.make(...)`, `ToolResultPart.make(...)`, `ToolChoice.make(...)`, `ToolChoice.named(...)`, `SystemPart.make(...)`, and `GenerationOptions.make(...)` directly. The top-level `LLM` namespace is reserved for request-shaped call APIs: `LLM.request`, `LLM.generate`, `LLM.stream`, `LLM.updateRequest`, and `LLM.generateObject`. Two ways to construct the same thing is one too many.
|
||||
|
||||
- Keep provider-defined string enums forward-compatible. Expose known values for autocomplete while accepting future values with `Known | (string & {})`; use `Schema.String` at runtime unless rejecting unknown values is required for correctness.
|
||||
|
||||
@@ -76,7 +76,7 @@ export const route = Route.make({
|
||||
})
|
||||
```
|
||||
|
||||
Route defaults are request-shaping defaults such as `headers`, `limits`, `generation`, `providerOptions`, and `http`. Endpoint host/query belongs on the route endpoint. Selected `Model` values carry only model id, provider id, and the configured route value. Model capability/catalog metadata lives outside this package; protocol support is enforced by request lowering and typed `LLMError`s.
|
||||
Route defaults are request-shaping defaults such as `headers`, `limits`, `generation`, `providerOptions`, and `http`. Endpoint host/query belongs on the route endpoint. Selected `LanguageModel` values carry only model id, provider id, and the configured route value. Model capability/catalog metadata lives outside this package; protocol support is enforced by request lowering and typed `AIError`s.
|
||||
|
||||
The four-axis decomposition is the reason DeepSeek, TogetherAI, Cerebras, Baseten, Fireworks, and DeepInfra all reuse `OpenAIChat.protocol` verbatim — each provider deployment is a 5-15 line `Route.make(...)` call instead of a 300-400 line route clone. Bug fixes in one protocol propagate to every consumer of that protocol in a single commit.
|
||||
|
||||
@@ -128,7 +128,7 @@ const selected = model("gpt-5", {
|
||||
})
|
||||
```
|
||||
|
||||
Keep semantic APIs as separate entrypoints, such as OpenAI `chat` and `responses`. Keep transport choices inside the semantic entrypoint settings, so OpenAI Responses HTTP and WebSocket share one entrypoint. Provider facades may still expose named selectors such as `responsesWebSocket` for direct typed call sites; the package-like contract maps its settings to those selectors before returning an executable `Model`.
|
||||
Keep semantic APIs as separate entrypoints, such as OpenAI `chat` and `responses`. Keep transport choices inside the semantic entrypoint settings, so OpenAI Responses HTTP and WebSocket share one entrypoint. Provider facades may still expose named selectors such as `responsesWebSocket` for direct typed call sites; the package-like contract maps its settings to those selectors before returning an executable `LanguageModel`.
|
||||
|
||||
Do not expose `Route` in provider package settings. Route composition stays an implementation detail behind `model(...)`.
|
||||
|
||||
@@ -138,10 +138,10 @@ Do not expose `Route` in provider package settings. Route composition stays an i
|
||||
packages/ai/src/
|
||||
schema/ canonical Schema model, split by concern
|
||||
ids.ts branded IDs, literal types, ProviderMetadata
|
||||
options.ts Generation/Provider/Http options, Limits, Model, cache policy
|
||||
options.ts Generation/Provider/Http options, Limits, LanguageModel, cache policy
|
||||
messages.ts content parts, Message, ToolDefinition, LLMRequest
|
||||
events.ts Usage, individual events, LLMEvent, LLMResponse
|
||||
errors.ts error reasons, LLMError, ToolFailure
|
||||
errors.ts error reasons, AIError, ToolFailure
|
||||
index.ts barrel
|
||||
llm.ts request constructors and convenience helpers
|
||||
route/
|
||||
|
||||
@@ -96,7 +96,7 @@ contains identity, capabilities, pricing metadata, provider-specific option
|
||||
types, reusable request-behavior defaults, and hidden execution behavior.
|
||||
|
||||
Normal users do not need to learn the current `Route` composite. Protocol,
|
||||
endpoint, auth, transport, and hooks are bound behind `Model`.
|
||||
endpoint, auth, transport, and hooks are bound behind `LanguageModel`.
|
||||
|
||||
### Request
|
||||
|
||||
@@ -539,7 +539,7 @@ Hosted tools do not pretend to have local handlers, and callers do not inspect a
|
||||
|
||||
### Run stream
|
||||
|
||||
`LLM.stream` returns an Effect `Stream<RunEvent, LLMError, Requirements>`.
|
||||
`LLM.stream` returns an Effect `Stream<RunEvent, AIError, Requirements>`.
|
||||
Run events explicitly expose orchestration boundaries:
|
||||
|
||||
```ts
|
||||
@@ -828,11 +828,11 @@ portable semantic guarantee.
|
||||
|
||||
## Error Model
|
||||
|
||||
The Effect error channel is a tagged domain union rather than one `LLMError`
|
||||
The Effect error channel is a tagged domain union rather than one `AIError`
|
||||
wrapper with nested reasons. Illustrative categories:
|
||||
|
||||
```ts
|
||||
type LLMError =
|
||||
type AIError =
|
||||
| AuthenticationError
|
||||
| InvalidRequestError
|
||||
| UnsupportedCapabilityError
|
||||
@@ -1079,7 +1079,7 @@ The redesign intentionally removes or changes these current concepts:
|
||||
| `LLM.generate` means one turn | `LLM.generate` means complete run |
|
||||
| `LLMClient.generate/stream` | `LLM.generateTurn/streamTurn` for one turn |
|
||||
| `LLMClient.layer` requirement | Standard Effect requirements exposed directly |
|
||||
| Public `Route` mental model | Hidden behind executable `Model` |
|
||||
| Public `Route` mental model | Hidden behind executable `LanguageModel` |
|
||||
| `Provider.make` structural helper | Experimental declarative `Provider.define` |
|
||||
| Schema classes as canonical values | Plain immutable values plus schema subpath |
|
||||
| `LLM.updateRequest` | Object spread |
|
||||
@@ -1089,7 +1089,7 @@ The redesign intentionally removes or changes these current concepts:
|
||||
| `generateObject` | Typed `output` option on `generate` |
|
||||
| One event union for provider output | Separate `TurnEvent` and `RunEvent` unions |
|
||||
| `providerExecuted` dispatch check | Distinct hosted-tool constructors |
|
||||
| One wrapped `LLMError` | Tagged domain error union |
|
||||
| One wrapped `AIError` | Tagged domain error union |
|
||||
|
||||
OpenCode should migrate to `generateTurn` / `streamTurn`, preserving its durable
|
||||
prompt admission, persistence, permission, tool settlement, and continuation
|
||||
|
||||
@@ -195,7 +195,7 @@ The hosted result is represented as a provider-executed tool call and tool resul
|
||||
- **`LLM.request({...})`** — build a provider-neutral `LLMRequest`. Accepts ergonomic inputs (`system: string`, `prompt: string`) that normalize into the canonical Schema classes.
|
||||
- **`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.
|
||||
- **`LanguageModel.make(...)` / `ToolCallPart.make(...)` / `ToolResultPart.make(...)` / `ToolDefinition.make(...)`** — model and tool-related constructors from the canonical schema model.
|
||||
- **`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`.
|
||||
|
||||
+23
-23
@@ -60,7 +60,7 @@ Other `aisdk:` packages, including Google Vertex, Azure, and Bedrock, currently
|
||||
| `@ai-sdk/google-vertex/xai` | Vertex Chat / Responses | Partial / usable | Decide Chat/Responses selection for catalog models, add runner mapping and recorded coverage, and review xAI-specific request options. |
|
||||
| `@ai-sdk/azure` | Azure OpenAI Chat/Responses facade | Partial | Map runner/catalog metadata to native Azure, handle resourceName/baseURL/apiVersion variants, add AAD/token auth story, and verify Chat vs Responses deployment selection. |
|
||||
| `@ai-sdk/amazon-bedrock` | Bedrock Converse | Partial | Add default AWS credential chain/profile support, region/inference-profile model ID handling, provider option parity via `additionalModelRequestFields`, guardrails/performance config, and runner/catalog mapping. |
|
||||
| `@ai-sdk/amazon-bedrock/mantle` | Bedrock Mantle OpenAI-compatible Chat/Responses namespace | Missing | Decide native Mantle shape, likely separate from Converse because it uses OpenAI-compatible Chat/Responses semantics over Bedrock. Add package mapping and tests. |
|
||||
| `@ai-sdk/amazon-bedrock/mantle` | Bedrock Mantle OpenAI-compatible Chat/Responses namespace | Partial / usable | Add default AWS credential chain/profile support; native catalog mapping currently requires bearer auth or explicit static credentials. |
|
||||
|
||||
## Highest-Risk Gaps
|
||||
|
||||
@@ -71,30 +71,31 @@ Other `aisdk:` packages, including Google Vertex, Azure, and Bedrock, currently
|
||||
5. Azure is only a provider facade, not a full runtime replacement. Native Azure exists, but the catalog runner does not select it, and token auth/resource variants need review.
|
||||
6. Provider option typing is uneven. OpenAI, Anthropic, Gemini, Bedrock, and OpenRouter each expose a small typed subset plus raw HTTP overlays; this is useful but not equivalent to AI SDK provider option coverage.
|
||||
7. Structured output is not provider-native yet. `LLM.generateObject` still uses a synthetic tool strategy, while the future design expects native structured output where reliable and tool fallback where needed.
|
||||
8. Package/namespace boundaries for the current native loading set are explicit in docs and exports. Other exported provider facades are not catalog package entrypoints until they implement the contract. Vertex xAI still needs catalog API selection; the missing native boundary is Bedrock Mantle.
|
||||
9. Recorded coverage is uneven. OpenAI, Anthropic, Gemini, Bedrock Converse, Cloudflare, OpenRouter, and several OpenAI-compatible Chat providers have cassettes. Azure, Vertex, and Mantle need first-class recorded scenarios before switching defaults.
|
||||
8. Package/namespace boundaries for the current native loading set are explicit in docs and exports. Other exported provider facades are not catalog package entrypoints until they implement the contract. Vertex xAI still needs catalog API selection.
|
||||
9. Recorded coverage is uneven. OpenAI, Anthropic, Gemini, Bedrock Converse, Bedrock Mantle, Cloudflare, OpenRouter, and several OpenAI-compatible Chat providers have cassettes. Azure and Vertex still need first-class recorded scenarios before switching defaults.
|
||||
|
||||
## Native Namespace Shape
|
||||
|
||||
These are implementation/API slices, not separate npm packages.
|
||||
|
||||
| API slice | Package-like entrypoint | Purpose |
|
||||
| ----------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
||||
| OpenAI Chat | `@opencode-ai/ai/providers/openai/chat` | OpenAI `/chat/completions` semantics. |
|
||||
| OpenAI Responses | `@opencode-ai/ai/providers/openai/responses` | OpenAI `/responses` semantics with HTTP/WebSocket selected through settings. |
|
||||
| OpenAI-compatible Chat | `@opencode-ai/ai/providers/openai-compatible` | Generic OpenAI-compatible `/chat/completions`. |
|
||||
| Open Responses-compatible | `@opencode-ai/ai/providers/openai-compatible/responses` | Generic provider-neutral `/responses`. |
|
||||
| Anthropic-compatible Messages | `@opencode-ai/ai/providers/anthropic-compatible` | Generic Anthropic-compatible `/messages`. |
|
||||
| Anthropic Messages | `@opencode-ai/ai/providers/anthropic` | Anthropic Messages API. |
|
||||
| Gemini Developer API | `@opencode-ai/ai/providers/google` | Google AI Studio Gemini API. |
|
||||
| Vertex Gemini | `@opencode-ai/ai/providers/google-vertex/gemini` | Vertex Gemini API; `providers/google-vertex` is the default alias. |
|
||||
| Vertex Chat | `@opencode-ai/ai/providers/google-vertex/chat` | Vertex OpenAI-compatible Chat Completions for MaaS models. |
|
||||
| Vertex Responses | `@opencode-ai/ai/providers/google-vertex/responses` | Vertex Open Responses for Grok models. |
|
||||
| Vertex Messages | `@opencode-ai/ai/providers/google-vertex/messages` | Vertex-hosted Anthropic Messages API. |
|
||||
| Bedrock Converse | `@opencode-ai/ai/providers/amazon-bedrock` | AWS Bedrock Converse API. |
|
||||
| Bedrock Mantle | Missing | AWS Bedrock Mantle OpenAI-compatible APIs. |
|
||||
| Azure OpenAI Chat | `@opencode-ai/ai/providers/azure/chat` | Azure specialization of OpenAI Chat. |
|
||||
| Azure OpenAI Responses | `@opencode-ai/ai/providers/azure/responses` | Azure specialization of OpenAI Responses. |
|
||||
| API slice | Package-like entrypoint | Purpose |
|
||||
| ----------------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
||||
| OpenAI Chat | `@opencode-ai/ai/providers/openai/chat` | OpenAI `/chat/completions` semantics. |
|
||||
| OpenAI Responses | `@opencode-ai/ai/providers/openai/responses` | OpenAI `/responses` semantics with HTTP/WebSocket selected through settings. |
|
||||
| OpenAI-compatible Chat | `@opencode-ai/ai/providers/openai-compatible` | Generic OpenAI-compatible `/chat/completions`. |
|
||||
| Open Responses-compatible | `@opencode-ai/ai/providers/openai-compatible/responses` | Generic provider-neutral `/responses`. |
|
||||
| Anthropic-compatible Messages | `@opencode-ai/ai/providers/anthropic-compatible` | Generic Anthropic-compatible `/messages`. |
|
||||
| Anthropic Messages | `@opencode-ai/ai/providers/anthropic` | Anthropic Messages API. |
|
||||
| Gemini Developer API | `@opencode-ai/ai/providers/google` | Google AI Studio Gemini API. |
|
||||
| Vertex Gemini | `@opencode-ai/ai/providers/google-vertex/gemini` | Vertex Gemini API; `providers/google-vertex` is the default alias. |
|
||||
| Vertex Chat | `@opencode-ai/ai/providers/google-vertex/chat` | Vertex OpenAI-compatible Chat Completions for MaaS models. |
|
||||
| Vertex Responses | `@opencode-ai/ai/providers/google-vertex/responses` | Vertex Open Responses for Grok models. |
|
||||
| Vertex Messages | `@opencode-ai/ai/providers/google-vertex/messages` | Vertex-hosted Anthropic Messages API. |
|
||||
| Bedrock Converse | `@opencode-ai/ai/providers/amazon-bedrock` | AWS Bedrock Converse API. |
|
||||
| Bedrock Mantle Chat | `@opencode-ai/ai/providers/amazon-bedrock/mantle/chat` | AWS Bedrock Mantle OpenAI-compatible Chat API. |
|
||||
| Bedrock Mantle Responses | `@opencode-ai/ai/providers/amazon-bedrock/mantle/responses` | AWS Bedrock Mantle OpenAI-compatible Responses API. |
|
||||
| Azure OpenAI Chat | `@opencode-ai/ai/providers/azure/chat` | Azure specialization of OpenAI Chat. |
|
||||
| Azure OpenAI Responses | `@opencode-ai/ai/providers/azure/responses` | Azure specialization of OpenAI Responses. |
|
||||
|
||||
## Suggested Next Work Slices
|
||||
|
||||
@@ -103,6 +104,5 @@ These are implementation/API slices, not separate npm packages.
|
||||
3. Bring Bedrock native auth/config to AI SDK parity: region, profile, default AWS credential chain, bearer token env, endpoint override, and cross-region inference profile handling.
|
||||
4. Add runner/catalog mappings and recorded scenarios for the native Vertex Gemini, Chat, Responses, and Messages entrypoints.
|
||||
5. Decide Chat/Responses selection for `@ai-sdk/google-vertex/xai` catalog models.
|
||||
6. Add Bedrock Mantle as a separate OpenAI-compatible Bedrock namespace after deciding whether it uses Chat, Responses, or both by model.
|
||||
7. Expand typed provider options from the existing V1 lowerer knowledge in `packages/core/src/v1/config/provider-options.ts` before adding more raw overlay examples.
|
||||
8. Add recorded provider tests for Azure, Vertex Gemini, Vertex Chat, Vertex Responses, Vertex Messages, Bedrock credential-chain behavior, and Mantle before making native runtime the default for those packages.
|
||||
6. Expand typed provider options from the existing V1 lowerer knowledge in `packages/core/src/v1/config/provider-options.ts` before adding more raw overlay examples.
|
||||
7. Add recorded provider tests for Azure, Vertex Gemini, Vertex Chat, Vertex Responses, Vertex Messages, and Bedrock credential-chain behavior before making native runtime the default for those packages.
|
||||
|
||||
@@ -33,7 +33,7 @@ Keep durable identity separate from runtime capability:
|
||||
|
||||
- Durable identity is small serializable data like `{ providerID, modelID }` for
|
||||
config, sessions, logs, and catalogs.
|
||||
- Runtime capability is a `Model` with a route value, protocol, transport, auth,
|
||||
- Runtime capability is a `LanguageModel` with a route value, protocol, transport, auth,
|
||||
and defaults. It is allowed to contain functions and schemas.
|
||||
- If persisted identity needs to become executable, resolve it through an app
|
||||
boundary first. Do not make `LLMRequest` recover behavior from a global route
|
||||
@@ -137,7 +137,7 @@ starts hiding the real provider-specific config.
|
||||
- accepts model id only
|
||||
- returns executable models
|
||||
- does not accept endpoint/auth/deployment overrides
|
||||
4. **Model**
|
||||
4. **Language Model**
|
||||
- model id
|
||||
- route value
|
||||
- provider id
|
||||
@@ -164,7 +164,7 @@ execution mechanism:
|
||||
```ts
|
||||
type ProviderFacade<APIs, Config> = {
|
||||
readonly id: ProviderID
|
||||
readonly model: (id: string) => Model
|
||||
readonly model: (id: string) => LanguageModel
|
||||
readonly configure: (input?: Config) => ProviderFacade<APIs, Config>
|
||||
} & APIs
|
||||
```
|
||||
@@ -181,8 +181,8 @@ export const OpenAI = {
|
||||
configure: configureOpenAI,
|
||||
} satisfies ProviderFacade<
|
||||
{
|
||||
responses: (id: string) => Model
|
||||
chat: (id: string) => Model
|
||||
responses: (id: string) => LanguageModel
|
||||
chat: (id: string) => LanguageModel
|
||||
},
|
||||
OpenAIConfig
|
||||
>
|
||||
@@ -528,7 +528,7 @@ The chosen split is:
|
||||
```txt
|
||||
Route = execution mechanics
|
||||
Provider facade = configured route group
|
||||
Model = selected executable model carrying route value
|
||||
LanguageModel = selected executable model carrying route value
|
||||
App boundary = explicit durable-config -> typed-provider call
|
||||
```
|
||||
|
||||
@@ -549,13 +549,13 @@ App boundary = explicit durable-config -> typed-provider call
|
||||
entrypoint maps its scoped `transport` setting before constructing the model.
|
||||
- No separate public `LLMClient.layerWithWebSocket`. The runtime should expose one
|
||||
client layer with the available transport capabilities.
|
||||
- No executable `ModelRef`. The executable handle is `Model`; durable model
|
||||
- No executable `ModelRef`. The executable handle is `LanguageModel`; durable model
|
||||
identity stays separate and cannot execute on its own.
|
||||
|
||||
## Implementation Todo
|
||||
|
||||
- [x] Replace the current executable `ModelRef` with `Model`.
|
||||
- [x] Change `Model.route` to carry a route value, not a `RouteID` string.
|
||||
- [x] Replace the current executable `ModelRef` with `LanguageModel`.
|
||||
- [x] Change `LanguageModel.route` to carry a route value, not a `RouteID` string.
|
||||
- [ ] Keep a separate durable model identity type for persisted/session/catalog
|
||||
data, likely `{ providerID, modelID }`, and make it clear that it cannot
|
||||
execute without resolver context.
|
||||
@@ -566,7 +566,7 @@ App boundary = explicit durable-config -> typed-provider call
|
||||
- [x] Remove endpoint/auth escape hatches from route model selection; callers must
|
||||
configure endpoint/auth through `route.with(...)` or provider facades before
|
||||
calling `.model(...)`.
|
||||
- [x] Remove request-shaping defaults from `Model`; selected models now carry only
|
||||
- [x] Remove request-shaping defaults from `LanguageModel`; selected models now carry only
|
||||
id, provider, and configured route while defaults live on routes or requests.
|
||||
- [x] Rework `LLMClient.stream` / `generate` to read
|
||||
`request.model.route` directly instead of calling `registeredRoute(...)`.
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { RequestExecutor } from "./route/executor"
|
||||
import type { ImageOptions, ImageRequest, ImageRequestFor, ImageResponse } from "./image"
|
||||
import type { LLMError } from "./schema"
|
||||
import type { AIError } from "./schema"
|
||||
|
||||
export type Execute = RequestExecutor.Interface["execute"]
|
||||
|
||||
export interface Interface {
|
||||
readonly generate: <Options extends ImageOptions>(
|
||||
request: ImageRequestFor<Options>,
|
||||
) => Effect.Effect<ImageResponse, LLMError>
|
||||
) => Effect.Effect<ImageResponse, AIError>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/ImageClient") {}
|
||||
|
||||
export const generate = <Options extends ImageOptions>(
|
||||
request: ImageRequestFor<Options>,
|
||||
): Effect.Effect<ImageResponse, LLMError> =>
|
||||
): Effect.Effect<ImageResponse, AIError> =>
|
||||
Effect.gen(function* () {
|
||||
const client = yield* Service
|
||||
return yield* client.generate(request)
|
||||
}) as Effect.Effect<ImageResponse, LLMError>
|
||||
}) as Effect.Effect<ImageResponse, AIError>
|
||||
|
||||
export const layer: Layer.Layer<Service, never, RequestExecutor.Service> = Layer.effect(
|
||||
Service,
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import { HttpOptions, InvalidRequestReason, LLMError, ModelID, ProviderID, ProviderMetadata, Usage } from "./schema"
|
||||
import { HttpOptions, InvalidRequestReason, AIError, ModelID, ProviderID, ProviderMetadata, Usage } from "./schema"
|
||||
import { ImageClient, Service, type Execute as ImageExecute } from "./image-client"
|
||||
|
||||
export interface ImageRoute<Options extends ImageOptions = ImageOptions> {
|
||||
readonly id: string
|
||||
readonly generate: (
|
||||
request: ImageRequestFor<Options>,
|
||||
execute: ImageExecute,
|
||||
) => Effect.Effect<ImageResponse, LLMError>
|
||||
readonly generate: (request: ImageRequestFor<Options>, execute: ImageExecute) => Effect.Effect<ImageResponse, AIError>
|
||||
}
|
||||
|
||||
export type ImageOptions = Record<string, unknown>
|
||||
@@ -146,13 +143,13 @@ export function request(input: ImageRequest | ImageRequestInput) {
|
||||
|
||||
export function generate<const Model extends object>(
|
||||
input: ImageRequestInput<Model>,
|
||||
): Effect.Effect<ImageResponse, LLMError, Service>
|
||||
export function generate(input: ImageRequest): Effect.Effect<ImageResponse, LLMError, Service>
|
||||
): Effect.Effect<ImageResponse, AIError, Service>
|
||||
export function generate(input: ImageRequest): Effect.Effect<ImageResponse, AIError, Service>
|
||||
export function generate(input: ImageRequest | ImageRequestInput) {
|
||||
return Effect.try({
|
||||
try: () => (input instanceof ImageRequest ? input : request(input)),
|
||||
catch: (error) =>
|
||||
new LLMError({
|
||||
new AIError({
|
||||
module: "Image",
|
||||
method: "generate",
|
||||
reason: new InvalidRequestReason({ message: error instanceof Error ? error.message : String(error) }),
|
||||
|
||||
@@ -5,8 +5,8 @@ export { Provider } from "./provider"
|
||||
export { ProviderPackage } from "./provider-package"
|
||||
export { isContextOverflow, isContextOverflowFailure } from "./provider-error"
|
||||
export type {
|
||||
RouteModelInput,
|
||||
RouteRoutedModelInput,
|
||||
RouteLanguageModelInput,
|
||||
RouteRoutedLanguageModelInput,
|
||||
Interface as LLMClientShape,
|
||||
Service as LLMClientService,
|
||||
} from "./route/client"
|
||||
@@ -33,7 +33,7 @@ export type {
|
||||
export * as LLM from "./llm"
|
||||
export type {
|
||||
Definition as ProviderDefinition,
|
||||
ModelFactory as ProviderModelFactory,
|
||||
ModelOptions as ProviderModelOptions,
|
||||
LanguageModelFactory as ProviderLanguageModelFactory,
|
||||
LanguageModelOptions as ProviderLanguageModelOptions,
|
||||
} from "./provider"
|
||||
export type { Definition as ProviderPackageDefinition, Settings as ProviderPackageSettings } from "./provider-package"
|
||||
|
||||
+28
-21
@@ -4,33 +4,33 @@ import {
|
||||
GenerationOptions,
|
||||
HttpOptions,
|
||||
InvalidProviderOutputReason,
|
||||
LLMError,
|
||||
AIError,
|
||||
LLMEvent,
|
||||
LLMRequest,
|
||||
LLMResponse,
|
||||
Message,
|
||||
Model,
|
||||
LanguageModel,
|
||||
SystemPart,
|
||||
ToolChoice,
|
||||
ToolDefinition,
|
||||
type ContentPart,
|
||||
type ModelProviderOptions,
|
||||
type LanguageModelProviderOptions,
|
||||
} 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<SelectedLanguageModel extends LanguageModel = LanguageModel> = Omit<
|
||||
ConstructorParameters<typeof LLMRequest>[0],
|
||||
"model" | "system" | "messages" | "tools" | "toolChoice" | "generation" | "http" | "providerOptions"
|
||||
> & {
|
||||
readonly model: SelectedModel
|
||||
readonly model: SelectedLanguageModel
|
||||
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?: NoInfer<LanguageModelProviderOptions<SelectedLanguageModel>>
|
||||
readonly http?: HttpOptions.Input
|
||||
}
|
||||
|
||||
@@ -38,7 +38,9 @@ export const generate = LLMClient.generate
|
||||
|
||||
export const stream = LLMClient.stream
|
||||
|
||||
export const request = <const SelectedModel extends Model>(input: RequestInput<SelectedModel>) => {
|
||||
export const request = <const SelectedLanguageModel extends LanguageModel>(
|
||||
input: RequestInput<SelectedLanguageModel>,
|
||||
) => {
|
||||
const {
|
||||
system: requestSystem,
|
||||
prompt,
|
||||
@@ -66,7 +68,10 @@ 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<SelectedLanguageModel extends LanguageModel = LanguageModel> = Omit<
|
||||
RequestInput<SelectedLanguageModel>,
|
||||
"tools" | "toolChoice"
|
||||
>
|
||||
|
||||
export class GenerateObjectResponse<T> {
|
||||
constructor(
|
||||
@@ -83,13 +88,15 @@ 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>,
|
||||
SelectedLanguageModel extends LanguageModel = LanguageModel,
|
||||
> extends GenerateObjectBase<SelectedLanguageModel> {
|
||||
readonly schema: S
|
||||
}
|
||||
|
||||
export interface GenerateObjectDynamicOptions<SelectedModel extends Model = Model>
|
||||
extends GenerateObjectBase<SelectedModel> {
|
||||
export interface GenerateObjectDynamicOptions<SelectedLanguageModel extends LanguageModel = LanguageModel>
|
||||
extends GenerateObjectBase<SelectedLanguageModel> {
|
||||
/** Raw JSON Schema object describing the expected output shape. */
|
||||
readonly jsonSchema: JsonSchema.JsonSchema
|
||||
}
|
||||
@@ -108,7 +115,7 @@ const runGenerateObject = Effect.fn("LLM.generateObject")(function* (
|
||||
(event) => LLMEvent.is.toolCall(event) && event.name === GENERATE_OBJECT_TOOL_NAME,
|
||||
)
|
||||
if (!call || !LLMEvent.is.toolCall(call))
|
||||
return yield* new LLMError({
|
||||
return yield* new AIError({
|
||||
module: "LLM",
|
||||
method: "generateObject",
|
||||
reason: new InvalidProviderOutputReason({
|
||||
@@ -118,7 +125,7 @@ const runGenerateObject = Effect.fn("LLM.generateObject")(function* (
|
||||
const object = yield* tool._decode(call.input).pipe(
|
||||
Effect.mapError(
|
||||
(error) =>
|
||||
new LLMError({
|
||||
new AIError({
|
||||
module: "LLM",
|
||||
method: "generateObject",
|
||||
reason: new InvalidProviderOutputReason({
|
||||
@@ -138,16 +145,16 @@ const runGenerateObject = Effect.fn("LLM.generateObject")(function* (
|
||||
* Two input modes:
|
||||
*
|
||||
* 1. `schema: EffectSchema<T>` — `.object` is decoded and typed as `T`.
|
||||
* Decode failures surface as `LLMError`.
|
||||
* Decode failures surface as `AIError`.
|
||||
* 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>,
|
||||
): Effect.Effect<GenerateObjectResponse<Schema.Schema.Type<S>>, LLMError>
|
||||
export function generateObject<const SelectedModel extends Model>(
|
||||
options: GenerateObjectDynamicOptions<SelectedModel>,
|
||||
): Effect.Effect<GenerateObjectResponse<unknown>, LLMError>
|
||||
export function generateObject<const SelectedLanguageModel extends LanguageModel, S extends ToolSchema<any>>(
|
||||
options: GenerateObjectOptions<S, SelectedLanguageModel>,
|
||||
): Effect.Effect<GenerateObjectResponse<Schema.Schema.Type<S>>, AIError>
|
||||
export function generateObject<const SelectedLanguageModel extends LanguageModel>(
|
||||
options: GenerateObjectDynamicOptions<SelectedLanguageModel>,
|
||||
): Effect.Effect<GenerateObjectResponse<unknown>, AIError>
|
||||
export function generateObject(options: GenerateObjectOptions<ToolSchema<any>> | GenerateObjectDynamicOptions) {
|
||||
if ("schema" in options) {
|
||||
const { schema, ...rest } = options
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Endpoint } from "../route/endpoint"
|
||||
import { Framing } from "../route/framing"
|
||||
import { Protocol } from "../route/protocol"
|
||||
import {
|
||||
LLMError,
|
||||
AIError,
|
||||
LLMEvent,
|
||||
mergeJsonRecords,
|
||||
Usage,
|
||||
@@ -242,16 +242,14 @@ const AnthropicUsage = Schema.StructWithRest(
|
||||
cache_creation_input_tokens: optionalNull(Schema.Number),
|
||||
cache_read_input_tokens: optionalNull(Schema.Number),
|
||||
server_tool_use: optionalNull(
|
||||
Schema.StructWithRest(
|
||||
Schema.Struct({ web_search_requests: Schema.optional(Schema.Number) }),
|
||||
[Schema.Record(Schema.String, Schema.Unknown)],
|
||||
),
|
||||
Schema.StructWithRest(Schema.Struct({ web_search_requests: Schema.optional(Schema.Number) }), [
|
||||
Schema.Record(Schema.String, Schema.Unknown),
|
||||
]),
|
||||
),
|
||||
output_tokens_details: optionalNull(
|
||||
Schema.StructWithRest(
|
||||
Schema.Struct({ thinking_tokens: Schema.optional(Schema.Number) }),
|
||||
[Schema.Record(Schema.String, Schema.Unknown)],
|
||||
),
|
||||
Schema.StructWithRest(Schema.Struct({ thinking_tokens: Schema.optional(Schema.Number) }), [
|
||||
Schema.Record(Schema.String, Schema.Unknown),
|
||||
]),
|
||||
),
|
||||
}),
|
||||
[Schema.Record(Schema.String, Schema.Unknown)],
|
||||
@@ -725,8 +723,7 @@ const mergeUsage = (left: Usage | undefined, right: Usage | undefined) => {
|
||||
reasoningTokens,
|
||||
totalTokens: ProviderShared.totalTokens(inputTokens, outputTokens, undefined),
|
||||
providerMetadata: {
|
||||
anthropic:
|
||||
mergeJsonRecords(left.providerMetadata?.["anthropic"], right.providerMetadata?.["anthropic"]) ?? {},
|
||||
anthropic: mergeJsonRecords(left.providerMetadata?.["anthropic"], right.providerMetadata?.["anthropic"]) ?? {},
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -816,7 +813,8 @@ const onContentBlockStart = (state: ParserState, event: AnthropicEvent): StepRes
|
||||
if (block.type === "thinking" && block.thinking !== undefined) {
|
||||
const events: LLMEvent[] = []
|
||||
const id = `reasoning-${event.index ?? 0}`
|
||||
const providerMetadata = block.signature === undefined ? undefined : anthropicMetadata({ signature: block.signature })
|
||||
const providerMetadata =
|
||||
block.signature === undefined ? undefined : anthropicMetadata({ signature: block.signature })
|
||||
const lifecycle = Lifecycle.reasoningStart(state.lifecycle, events, id, providerMetadata)
|
||||
return [
|
||||
{
|
||||
@@ -980,7 +978,7 @@ const providerErrorMessage = (event: AnthropicEvent): string => {
|
||||
}
|
||||
|
||||
const onError = (event: AnthropicEvent) =>
|
||||
new LLMError({
|
||||
new AIError({
|
||||
module: ADAPTER,
|
||||
method: "stream",
|
||||
reason: classifyProviderFailure({ message: providerErrorMessage(event), code: event.error?.type }),
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Route } from "../route/client"
|
||||
import { Endpoint } from "../route/endpoint"
|
||||
import { Protocol } from "../route/protocol"
|
||||
import {
|
||||
LLMError,
|
||||
AIError,
|
||||
LLMEvent,
|
||||
Usage,
|
||||
type CacheHint,
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
type FinishReasonDetails,
|
||||
type JsonSchema,
|
||||
type LLMRequest,
|
||||
type ModelToolSchemaCompatibility,
|
||||
type LanguageModelToolSchemaCompatibility,
|
||||
type ProviderMetadata,
|
||||
type ReasoningPart,
|
||||
type ToolCallPart,
|
||||
@@ -232,7 +232,7 @@ const lowerToolSpec = (tool: ToolDefinition, inputSchema: JsonSchema): BedrockTo
|
||||
})
|
||||
|
||||
const lowerTools = (
|
||||
compatibility: ModelToolSchemaCompatibility | undefined,
|
||||
compatibility: LanguageModelToolSchemaCompatibility | undefined,
|
||||
breakpoints: BedrockCache.Breakpoints,
|
||||
tools: ReadonlyArray<ToolDefinition>,
|
||||
): BedrockTool[] => {
|
||||
@@ -274,9 +274,7 @@ const reasoningSignature = (part: ReasoningPart) => {
|
||||
|
||||
const reasoningRedactedData = (part: ReasoningPart) => {
|
||||
const bedrock = part.providerMetadata?.bedrock
|
||||
return ProviderShared.isRecord(bedrock) && typeof bedrock.redactedData === "string"
|
||||
? bedrock.redactedData
|
||||
: undefined
|
||||
return ProviderShared.isRecord(bedrock) && typeof bedrock.redactedData === "string" ? bedrock.redactedData : undefined
|
||||
}
|
||||
|
||||
const lowerToolCall = (part: ToolCallPart): BedrockToolUseBlock => ({
|
||||
@@ -656,7 +654,7 @@ const step = (state: ParserState, event: BedrockEvent) =>
|
||||
] as const
|
||||
).find((entry) => entry[1] !== undefined)
|
||||
if (exception) {
|
||||
return yield* new LLMError({
|
||||
return yield* new AIError({
|
||||
module: ADAPTER,
|
||||
method: "stream",
|
||||
reason: classifyProviderFailure({
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { Auth, type Definition as AuthDefinition } from "../route/auth"
|
||||
import {
|
||||
InvalidProviderOutputReason,
|
||||
LLMError,
|
||||
AIError,
|
||||
Usage,
|
||||
mergeHttpOptions,
|
||||
mergeJsonRecords,
|
||||
@@ -125,7 +125,7 @@ const nativeOptions = (options: GoogleImageOptions | undefined) => {
|
||||
}
|
||||
|
||||
const invalidOutput = (message: string, providerMetadata?: ProviderMetadata) =>
|
||||
new LLMError({
|
||||
new AIError({
|
||||
module: ADAPTER,
|
||||
method: "generate",
|
||||
reason: new InvalidProviderOutputReason({ message, route: ADAPTER, providerMetadata }),
|
||||
@@ -285,7 +285,7 @@ export const model = (input: ModelInput) => {
|
||||
return ImageModel.make<GoogleImageOptions>({ id: input.id, provider: "google", route, http: input.http })
|
||||
}
|
||||
|
||||
const googleImagePart = (image: ImageInput): Effect.Effect<Record<string, unknown>, LLMError> => {
|
||||
const googleImagePart = (image: ImageInput): Effect.Effect<Record<string, unknown>, AIError> => {
|
||||
if (image.type === "bytes")
|
||||
return Effect.succeed({ inlineData: { mimeType: image.mediaType, data: Encoding.encodeBase64(image.data) } })
|
||||
if (image.type === "file-uri") return Effect.succeed({ fileData: { mimeType: image.mediaType, fileUri: image.uri } })
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Content } from "@opencode-ai/schema/tool"
|
||||
import { HttpTransport } from "../route/transport"
|
||||
import { Protocol } from "../route/protocol"
|
||||
import {
|
||||
LLMError,
|
||||
AIError,
|
||||
LLMEvent,
|
||||
Usage,
|
||||
type FinishReason,
|
||||
@@ -434,10 +434,7 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (reques
|
||||
const groups = content.reduce<Array<{ phase: MessagePhase | null | undefined; parts: TextPart[] }>>(
|
||||
(groups, part) => {
|
||||
const metadata = part.providerMetadata?.[providerMetadataKey]
|
||||
const phase =
|
||||
ProviderShared.isRecord(metadata)
|
||||
? messagePhase(metadata.phase, extension)
|
||||
: undefined
|
||||
const phase = ProviderShared.isRecord(metadata) ? messagePhase(metadata.phase, extension) : undefined
|
||||
const group = groups.at(-1)
|
||||
if (group && group.phase === phase) group.parts.push(part)
|
||||
else groups.push({ phase, parts: [part] })
|
||||
@@ -646,10 +643,7 @@ const onOutputTextDelta = (state: ParserState, event: Event, id: string): StepRe
|
||||
const phase = state.messagePhases[id]
|
||||
const metadata = phase === undefined ? undefined : providerMetadata(state, { phase })
|
||||
const lifecycle = Lifecycle.textStart(state.lifecycle, events, id, metadata)
|
||||
return [
|
||||
{ ...state, lifecycle: Lifecycle.textDelta(lifecycle, events, id, event.delta) },
|
||||
events,
|
||||
]
|
||||
return [{ ...state, lifecycle: Lifecycle.textDelta(lifecycle, events, id, event.delta) }, events]
|
||||
}
|
||||
|
||||
const onOutputTextDone = (state: ParserState, event: Event, id: string): StepResult => {
|
||||
@@ -975,7 +969,7 @@ const providerErrorMessage = (event: Event, fallback: string): string => {
|
||||
const providerError = (state: ParserState, event: Event, fallback: string) => {
|
||||
const code = event.code || event.error?.code || event.response?.error?.code || undefined
|
||||
const message = providerErrorMessage(event, fallback)
|
||||
return new LLMError({
|
||||
return new AIError({
|
||||
module: state.id,
|
||||
method: "stream",
|
||||
reason: classifyProviderFailure({ message, code }),
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Endpoint } from "../route/endpoint"
|
||||
import { HttpTransport } from "../route/transport"
|
||||
import { Protocol } from "../route/protocol"
|
||||
import {
|
||||
LLMError,
|
||||
AIError,
|
||||
LLMEvent,
|
||||
Usage,
|
||||
type FinishReason,
|
||||
@@ -555,7 +555,7 @@ const mapFinishReason = (reason: string | null | undefined): FinishReason => {
|
||||
// OpenAI Chat reports `prompt_tokens` (inclusive total) with a
|
||||
// cached-read and cache-write subsets, and `completion_tokens` (inclusive
|
||||
// total) with a `reasoning_tokens` subset. We pass the inclusive totals
|
||||
// through and derive the non-cached breakdown so the `LLM.Usage` contract is
|
||||
// through and derive the non-cached breakdown so the `AI.Usage` contract is
|
||||
// satisfied on both sides.
|
||||
const mapUsage = (usage: OpenAIChatEvent["usage"]): Usage | undefined => {
|
||||
if (!usage) return undefined
|
||||
@@ -645,7 +645,7 @@ const reasoningMetadata = (field: ParserState["reasoningField"], details?: Reado
|
||||
const step = (state: ParserState, event: OpenAIChatEvent) =>
|
||||
Effect.gen(function* () {
|
||||
if (event.error)
|
||||
return yield* new LLMError({
|
||||
return yield* new AIError({
|
||||
module: ADAPTER,
|
||||
method: "stream",
|
||||
reason: classifyProviderFailure({
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Route, type RouteRoutedModelInput } from "../route/client"
|
||||
import { Route, type RouteRoutedLanguageModelInput } from "../route/client"
|
||||
import { Endpoint } from "../route/endpoint"
|
||||
import { Framing } from "../route/framing"
|
||||
import * as OpenAIChat from "./openai-chat"
|
||||
|
||||
const ADAPTER = "openai-compatible-chat"
|
||||
|
||||
export type OpenAICompatibleChatModelInput = RouteRoutedModelInput
|
||||
export type OpenAICompatibleChatLanguageModelInput = RouteRoutedLanguageModelInput
|
||||
|
||||
/**
|
||||
* Route for non-OpenAI providers that expose an OpenAI Chat-compatible
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Route, type RouteRoutedModelInput } from "../route/client"
|
||||
import { Route, type RouteRoutedLanguageModelInput } from "../route/client"
|
||||
import { Endpoint } from "../route/endpoint"
|
||||
import { OpenResponses } from "./open-responses"
|
||||
|
||||
const ADAPTER = "openai-compatible-responses"
|
||||
|
||||
export type OpenAICompatibleResponsesModelInput = RouteRoutedModelInput
|
||||
export type OpenAICompatibleResponsesLanguageModelInput = RouteRoutedLanguageModelInput
|
||||
|
||||
/**
|
||||
* Deployment adapter for providers that expose an Open Responses-compatible
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { Auth, type Definition as AuthDefinition } from "../route/auth"
|
||||
import {
|
||||
InvalidProviderOutputReason,
|
||||
LLMError,
|
||||
AIError,
|
||||
Usage,
|
||||
mergeHttpOptions,
|
||||
mergeJsonRecords,
|
||||
@@ -85,7 +85,7 @@ const nativeOptions = (options: OpenAIImageOptions | undefined) => {
|
||||
}
|
||||
|
||||
const invalidOutput = (message: string) =>
|
||||
new LLMError({
|
||||
new AIError({
|
||||
module: ADAPTER,
|
||||
method: "generate",
|
||||
reason: new InvalidProviderOutputReason({ message, route: ADAPTER }),
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Headers, HttpClientRequest } from "effect/unstable/http"
|
||||
import {
|
||||
InvalidProviderOutputReason,
|
||||
InvalidRequestReason,
|
||||
LLMError,
|
||||
AIError,
|
||||
type ContentPart,
|
||||
type LLMRequest,
|
||||
type MediaPart,
|
||||
@@ -41,7 +41,7 @@ export interface ToolAccumulator {
|
||||
* when at least one is defined. Returns `undefined` when neither input nor
|
||||
* output is known so routes don't publish a misleading `0`.
|
||||
*
|
||||
* Under the additive `LLM.Usage` contract, `inputTokens` and `outputTokens`
|
||||
* Under the additive `AI.Usage` contract, `inputTokens` and `outputTokens`
|
||||
* are the non-cached input and visible output only. The provider-supplied
|
||||
* `total` is the source of truth when present; the computed fallback
|
||||
* under-counts cache and reasoning by design and exists mainly so
|
||||
@@ -88,7 +88,7 @@ export const sumTokens = (...values: ReadonlyArray<number | undefined>): number
|
||||
}
|
||||
|
||||
export const eventError = (route: string, message: string, raw?: string) =>
|
||||
new LLMError({
|
||||
new AIError({
|
||||
module: "ProviderShared",
|
||||
method: "stream",
|
||||
reason: new InvalidProviderOutputReason({ route, message, raw }),
|
||||
@@ -238,9 +238,9 @@ export const errorText = (error: unknown) => {
|
||||
* `decodeChunk` sees one JSON string per element. The SSE channel emits a
|
||||
* `Retry` control event on its error channel; we drop it here (we don't
|
||||
* implement client-driven retries) so the public error channel stays
|
||||
* `LLMError`.
|
||||
* `AIError`.
|
||||
*/
|
||||
export const sseFraming = (bytes: Stream.Stream<Uint8Array, LLMError>): Stream.Stream<string, LLMError> =>
|
||||
export const sseFraming = (bytes: Stream.Stream<Uint8Array, AIError>): Stream.Stream<string, AIError> =>
|
||||
bytes.pipe(
|
||||
Stream.decodeText(),
|
||||
Stream.pipeThroughChannel(Sse.decode()),
|
||||
@@ -257,7 +257,7 @@ export const sseFraming = (bytes: Stream.Stream<Uint8Array, LLMError>): Stream.S
|
||||
* lands here.
|
||||
*/
|
||||
export const invalidRequest = (message: string) =>
|
||||
new LLMError({
|
||||
new AIError({
|
||||
module: "ProviderShared",
|
||||
method: "request",
|
||||
reason: new InvalidRequestReason({ message }),
|
||||
@@ -304,7 +304,7 @@ export const unsupportedContent = (
|
||||
* Build a `validate` step from a Schema decoder. Replaces the per-route
|
||||
* lambda body `(payload) => decode(payload).pipe(Effect.mapError((e) =>
|
||||
* invalid(e.message)))`. Any decode error is translated into
|
||||
* `LLMError` carrying the original parse-error message.
|
||||
* `AIError` carrying the original parse-error message.
|
||||
*/
|
||||
export const validateWith =
|
||||
<A, I, E extends { readonly message: string }>(decode: (input: I) => Effect.Effect<A, E>) =>
|
||||
|
||||
@@ -22,6 +22,8 @@ const signRequest = (input: {
|
||||
readonly body: string
|
||||
readonly headers: Headers.Headers
|
||||
readonly credentials: Credentials
|
||||
readonly service: string
|
||||
readonly name: string
|
||||
}) =>
|
||||
Effect.tryPromise({
|
||||
try: async () => {
|
||||
@@ -34,23 +36,26 @@ const signRequest = (input: {
|
||||
accessKeyId: input.credentials.accessKeyId,
|
||||
secretAccessKey: input.credentials.secretAccessKey,
|
||||
sessionToken: input.credentials.sessionToken,
|
||||
service: "bedrock",
|
||||
service: input.service,
|
||||
}).sign()
|
||||
return Object.fromEntries(signed.headers.entries())
|
||||
},
|
||||
catch: (error) =>
|
||||
ProviderShared.invalidRequest(
|
||||
`Bedrock Converse SigV4 signing failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
`${input.name} SigV4 signing failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
),
|
||||
})
|
||||
|
||||
/** Sign the exact JSON bytes with SigV4 using credentials configured on the route. */
|
||||
export const sigV4 = (credentials: Credentials | undefined) =>
|
||||
export const sigV4 = (
|
||||
credentials: Credentials | undefined,
|
||||
options: { readonly service?: string; readonly name?: string } = {},
|
||||
) =>
|
||||
Auth.custom((input: AuthInput) => {
|
||||
return Effect.gen(function* () {
|
||||
if (!credentials) {
|
||||
return yield* ProviderShared.invalidRequest(
|
||||
"Bedrock Converse requires either route bearer auth or AWS credentials configured on the route",
|
||||
`${options.name ?? "Bedrock Converse"} requires either route bearer auth or AWS credentials configured on the route`,
|
||||
)
|
||||
}
|
||||
const headersForSigning = Headers.set(input.headers, "content-type", "application/json")
|
||||
@@ -59,6 +64,8 @@ export const sigV4 = (credentials: Credentials | undefined) =>
|
||||
body: input.body,
|
||||
headers: headersForSigning,
|
||||
credentials,
|
||||
service: options.service ?? "bedrock",
|
||||
name: options.name ?? "Bedrock Converse",
|
||||
})
|
||||
return Headers.setAll(headersForSigning, signed)
|
||||
})
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Effect, Encoding } from "effect"
|
||||
import type { ImageInput } from "../../image"
|
||||
import { InvalidRequestReason, LLMError } from "../../schema"
|
||||
import { InvalidRequestReason, AIError } from "../../schema"
|
||||
|
||||
const invalid = (module: string, message: string) =>
|
||||
new LLMError({
|
||||
new AIError({
|
||||
module,
|
||||
method: "generate",
|
||||
reason: new InvalidRequestReason({ message }),
|
||||
@@ -15,7 +15,7 @@ export const dataUrl = (input: Extract<ImageInput, { readonly type: "bytes" }>)
|
||||
export const decodeDataUrl = (
|
||||
url: string,
|
||||
module: string,
|
||||
): Effect.Effect<{ readonly mediaType: string; readonly data: Uint8Array } | undefined, LLMError> => {
|
||||
): Effect.Effect<{ readonly mediaType: string; readonly data: Uint8Array } | undefined, AIError> => {
|
||||
if (!url.startsWith("data:")) return Effect.succeed(undefined)
|
||||
const match = /^data:([^;,]+);base64,(.*)$/s.exec(url)
|
||||
if (!match) return Effect.fail(invalid(module, "Image data URLs must contain a MIME type and base64 data"))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { JsonSchema, ModelToolSchemaCompatibility } from "../../schema"
|
||||
import type { JsonSchema, LanguageModelToolSchemaCompatibility } from "../../schema"
|
||||
import { isRecord } from "../../utils/record"
|
||||
import { GeminiToolSchema } from "./gemini-tool-schema"
|
||||
|
||||
@@ -69,7 +69,7 @@ const gemini = (schema: JsonSchema): JsonSchema => GeminiToolSchema.convert(sche
|
||||
|
||||
const modelCompatibility = (
|
||||
schema: JsonSchema,
|
||||
compatibility: ModelToolSchemaCompatibility | undefined,
|
||||
compatibility: LanguageModelToolSchemaCompatibility | undefined,
|
||||
): JsonSchema => {
|
||||
if (compatibility === undefined) return schema
|
||||
switch (compatibility) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect } from "effect"
|
||||
import { LLMError, LLMEvent, type ProviderMetadata, type ToolCall, type ToolInputError } from "../../schema"
|
||||
import { AIError, LLMEvent, type ProviderMetadata, type ToolCall, type ToolInputError } from "../../schema"
|
||||
import { eventError, parseToolInput, type ToolAccumulator } from "../shared"
|
||||
|
||||
type StreamKey = string | number
|
||||
@@ -112,8 +112,8 @@ const appendTool = <K extends StreamKey>(
|
||||
}
|
||||
}
|
||||
|
||||
export const isError = <K extends StreamKey>(result: AppendOutcome<K> | LLMError): result is LLMError =>
|
||||
result instanceof LLMError
|
||||
export const isError = <K extends StreamKey>(result: AppendOutcome<K> | AIError): result is AIError =>
|
||||
result instanceof AIError
|
||||
|
||||
/**
|
||||
* Register a tool call whose start event arrived before any argument deltas.
|
||||
@@ -138,7 +138,7 @@ export const appendOrStart = <K extends StreamKey>(
|
||||
key: K,
|
||||
delta: { readonly id?: string; readonly name?: string; readonly text: string },
|
||||
missingToolMessage: string,
|
||||
): AppendOutcome<K> | LLMError => {
|
||||
): AppendOutcome<K> | AIError => {
|
||||
const current = tools[key]
|
||||
const id = current?.id ?? delta.id
|
||||
const name = current?.name ?? delta.name
|
||||
@@ -167,7 +167,7 @@ export const appendExisting = <K extends StreamKey>(
|
||||
key: K,
|
||||
text: string,
|
||||
missingToolMessage: string,
|
||||
): AppendOutcome<K> | LLMError => {
|
||||
): AppendOutcome<K> | AIError => {
|
||||
const current = tools[key]
|
||||
if (!current) return eventError(route, missingToolMessage)
|
||||
if (text.length === 0) return { tools, tool: current, events: [] }
|
||||
|
||||
@@ -4,7 +4,7 @@ import { GeneratedImage, ImageModel, ImageResponse, type ImageRequestFor, type I
|
||||
import { Auth, type Definition as AuthDefinition } from "../route/auth"
|
||||
import {
|
||||
InvalidProviderOutputReason,
|
||||
LLMError,
|
||||
AIError,
|
||||
Usage,
|
||||
mergeHttpOptions,
|
||||
mergeJsonRecords,
|
||||
@@ -95,7 +95,7 @@ const nativeOptions = (options: XAIImageOptions | undefined) => {
|
||||
}
|
||||
|
||||
const invalidOutput = (message: string) =>
|
||||
new LLMError({
|
||||
new AIError({
|
||||
module: ADAPTER,
|
||||
method: "generate",
|
||||
reason: new InvalidProviderOutputReason({ message, route: ADAPTER }),
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Effect, Schema } from "effect"
|
||||
import { Headers, HttpClientRequest } from "effect/unstable/http"
|
||||
import { GeneratedImage, ImageModel, ImageResponse, type ImageRequestFor, type ImageRoute } from "../image"
|
||||
import { Auth, type Definition as AuthDefinition } from "../route/auth"
|
||||
import { InvalidProviderOutputReason, LLMError, mergeHttpOptions, mergeJsonRecords, type HttpOptions } from "../schema"
|
||||
import { InvalidProviderOutputReason, AIError, mergeHttpOptions, mergeJsonRecords, type HttpOptions } from "../schema"
|
||||
import { ProviderShared } from "./shared"
|
||||
import { ImageInputs } from "./utils/image-input"
|
||||
|
||||
@@ -58,7 +58,7 @@ const nativeOptions = (options: ZAIImageOptions | undefined) => {
|
||||
}
|
||||
|
||||
const invalidOutput = (message: string) =>
|
||||
new LLMError({
|
||||
new AIError({
|
||||
module: ADAPTER,
|
||||
method: "generate",
|
||||
reason: new InvalidProviderOutputReason({ message, route: ADAPTER }),
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
AuthenticationReason,
|
||||
ContentPolicyReason,
|
||||
InvalidRequestReason,
|
||||
LLMError,
|
||||
AIError,
|
||||
ProviderErrorEvent,
|
||||
ProviderInternalReason,
|
||||
QuotaExceededReason,
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
|
||||
const patterns = [
|
||||
/prompt is too long/i,
|
||||
/request_too_large/i,
|
||||
/input is too long for requested model/i,
|
||||
/exceeds the context window/i,
|
||||
/exceeds (?:the )?(?:model'?s )?maximum context length(?: of [\d,]+ tokens?|\s*\([\d,]+\))/i,
|
||||
@@ -33,7 +32,6 @@ const patterns = [
|
||||
/context window exceeds limit/i,
|
||||
/exceeded model token limit/i,
|
||||
/context[_ ]length[_ ]exceeded/i,
|
||||
/request entity too large/i,
|
||||
/context length is only \d+ tokens/i,
|
||||
/input length.*exceeds.*context length/i,
|
||||
/prompt too long; exceeded (?:max )?context length/i,
|
||||
@@ -44,14 +42,18 @@ const patterns = [
|
||||
/token limit exceeded/i,
|
||||
]
|
||||
|
||||
const payloadPatterns = [/request_too_large/i, /request entity too large/i, /payload too large/i, /request too large/i]
|
||||
|
||||
const exclusions = [/^(throttling error|service unavailable):/i, /rate limit/i, /too many requests/i]
|
||||
|
||||
export const isContextOverflow = (message: string) =>
|
||||
!exclusions.some((pattern) => pattern.test(message)) &&
|
||||
(patterns.some((pattern) => pattern.test(message)) || /^4(00|13)\s*(status code)?\s*\(no body\)/i.test(message))
|
||||
(patterns.some((pattern) => pattern.test(message)) || /^400\s*(status code)?\s*\(no body\)/i.test(message))
|
||||
|
||||
export const isPayloadTooLarge = (message: string) => payloadPatterns.some((pattern) => pattern.test(message))
|
||||
|
||||
export const isContextOverflowFailure = (failure: unknown) =>
|
||||
failure instanceof LLMError
|
||||
failure instanceof AIError
|
||||
? failure.reason._tag === "InvalidRequest" && failure.reason.classification === "context-overflow"
|
||||
: Schema.is(ProviderErrorEvent)(failure) && failure.classification === "context-overflow"
|
||||
|
||||
@@ -84,7 +86,7 @@ export interface ProviderFailure {
|
||||
|
||||
// Keep HTTP failures and provider-reported stream failures on one typed path so
|
||||
// session retry policy never needs provider-specific string matching.
|
||||
export function classifyProviderFailure(input: ProviderFailure): LLMError["reason"] {
|
||||
export function classifyProviderFailure(input: ProviderFailure): AIError["reason"] {
|
||||
const body = input.http?.body ?? ""
|
||||
const codes = [input.code, ...providerCodes(body), ...providerCodes(input.message)]
|
||||
.filter((code): code is string => code !== undefined)
|
||||
@@ -100,6 +102,8 @@ export function classifyProviderFailure(input: ProviderFailure): LLMError["reaso
|
||||
isContextOverflow(text))
|
||||
)
|
||||
return new InvalidRequestReason({ ...common, classification: "context-overflow" })
|
||||
if (input.status === 413 || isPayloadTooLarge(text))
|
||||
return new InvalidRequestReason({ ...common, classification: "payload-too-large" })
|
||||
if (CONTENT_POLICY_TEXT.test(text)) return new ContentPolicyReason(common)
|
||||
if (codes.some((code) => QUOTA_CODES.has(code)) || (input.status === 429 && QUOTA_TEXT.test(text)))
|
||||
return new QuotaExceededReason(common)
|
||||
@@ -142,12 +146,7 @@ export function classifyProviderFailure(input: ProviderFailure): LLMError["reaso
|
||||
retryAfterMs: input.retryAfterMs,
|
||||
})
|
||||
if (codes.some((code) => INVALID_REQUEST_CODES.has(code))) return new InvalidRequestReason(common)
|
||||
if (
|
||||
input.status === 400 ||
|
||||
input.status === 404 ||
|
||||
input.status === 413 ||
|
||||
input.status === 422
|
||||
)
|
||||
if (input.status === 400 || input.status === 404 || input.status === 413 || input.status === 422)
|
||||
return new InvalidRequestReason(common)
|
||||
return new UnknownProviderReason({ ...common, status: input.status })
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Model, ProviderOptions } from "./schema"
|
||||
import type { LanguageModel, ProviderOptions } from "./schema"
|
||||
|
||||
export interface Settings extends Readonly<Record<string, unknown>> {
|
||||
readonly baseURL?: string
|
||||
@@ -15,7 +15,7 @@ export interface Definition<
|
||||
ProviderSettings extends Settings = Settings,
|
||||
Options extends ProviderOptions = ProviderOptions,
|
||||
> {
|
||||
readonly model: (modelID: string, settings: ProviderSettings) => Model<Options>
|
||||
readonly model: (modelID: string, settings: ProviderSettings) => LanguageModel<Options>
|
||||
}
|
||||
|
||||
export * as ProviderPackage from "./provider-package"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Model, ModelID, ProviderID } from "./schema"
|
||||
import type { LanguageModel, ModelID, ProviderID } from "./schema"
|
||||
|
||||
export type ModelOptions = Pick<Model.Input, "defaults" | "compatibility">
|
||||
export type LanguageModelOptions = Pick<LanguageModel.Input, "defaults" | "compatibility">
|
||||
|
||||
/**
|
||||
* Advanced structural provider definition helper. Built-in providers should
|
||||
@@ -8,23 +8,23 @@ export type ModelOptions = Pick<Model.Input, "defaults" | "compatibility">
|
||||
* chosen before model selection. The optional `apis` map remains for external
|
||||
* structural providers that expose multiple route selectors behind one provider.
|
||||
*/
|
||||
export type ModelFactory<Options extends ModelOptions = ModelOptions> = (
|
||||
export type LanguageModelFactory<Options extends LanguageModelOptions = LanguageModelOptions> = (
|
||||
id: string | ModelID,
|
||||
options?: Options,
|
||||
) => Model
|
||||
) => LanguageModel
|
||||
|
||||
type AnyModelFactory = (...args: never[]) => Model
|
||||
type AnyLanguageModelFactory = (...args: never[]) => LanguageModel
|
||||
|
||||
export interface Definition<Factory extends AnyModelFactory = ModelFactory> {
|
||||
export interface Definition<Factory extends AnyLanguageModelFactory = LanguageModelFactory> {
|
||||
readonly id: ProviderID
|
||||
readonly model: Factory
|
||||
readonly apis?: Record<string, AnyModelFactory>
|
||||
readonly apis?: Record<string, AnyLanguageModelFactory>
|
||||
}
|
||||
|
||||
type DefinitionShape = {
|
||||
readonly id: ProviderID
|
||||
readonly model: (...args: never[]) => Model
|
||||
readonly apis?: Record<string, (...args: never[]) => Model>
|
||||
readonly model: (...args: never[]) => LanguageModel
|
||||
readonly apis?: Record<string, (...args: never[]) => LanguageModel>
|
||||
}
|
||||
|
||||
type NoExtraFields<Input, Shape> = Input & Record<Exclude<keyof Input, keyof Shape>, never>
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import { Auth } from "../route/auth"
|
||||
import type { Route as RouteDef, RouteDefaultsInput } from "../route/client"
|
||||
import type { ProviderPackage } from "../provider-package"
|
||||
import { OpenAIChat } from "../protocols/openai-chat"
|
||||
import { OpenAIResponses } from "../protocols/openai-responses"
|
||||
import { BedrockAuth, type Credentials } from "../protocols/utils/bedrock-auth"
|
||||
import { ProviderID, type ModelID } from "../schema"
|
||||
import { withOpenAIOptions, type OpenAIProviderOptionsInput } from "./openai-options"
|
||||
|
||||
export const id = ProviderID.make("amazon-bedrock")
|
||||
|
||||
export type Config = RouteDefaultsInput & {
|
||||
readonly apiKey?: string
|
||||
readonly baseURL?: string
|
||||
readonly credentials?: Credentials
|
||||
readonly region?: string
|
||||
readonly providerOptions?: OpenAIProviderOptionsInput
|
||||
}
|
||||
|
||||
export interface Settings extends ProviderPackage.Settings {
|
||||
readonly apiKey?: string
|
||||
readonly auth?: "bearer" | "sigv4"
|
||||
readonly baseURL?: string
|
||||
readonly credentials?: Credentials
|
||||
readonly region?: string
|
||||
readonly providerOptions?: OpenAIProviderOptionsInput
|
||||
}
|
||||
|
||||
const responsesRoute = OpenAIResponses.route.with({
|
||||
id: "bedrock-mantle-responses",
|
||||
provider: id,
|
||||
})
|
||||
|
||||
const chatRoute = OpenAIChat.route.with({
|
||||
id: "bedrock-mantle-chat",
|
||||
provider: id,
|
||||
})
|
||||
|
||||
export const routes = [responsesRoute, chatRoute]
|
||||
|
||||
const configuredRoute = <Body, Prepared>(route: RouteDef<Body, Prepared>, input: Config) => {
|
||||
const region = input.region ?? input.credentials?.region ?? "us-east-1"
|
||||
const credentials = input.credentials === undefined ? undefined : { ...input.credentials, region }
|
||||
return route.with({
|
||||
endpoint: { baseURL: input.baseURL ?? `https://bedrock-mantle.${region}.api.aws/v1` },
|
||||
auth:
|
||||
input.apiKey === undefined
|
||||
? BedrockAuth.sigV4(credentials, { service: "bedrock-mantle", name: "Bedrock Mantle" })
|
||||
: Auth.bearer(input.apiKey),
|
||||
})
|
||||
}
|
||||
|
||||
const defaults = (input: Config) => {
|
||||
const { apiKey: _, baseURL: _baseURL, credentials: _credentials, region: _region, ...rest } = input
|
||||
return rest
|
||||
}
|
||||
|
||||
export const configure = (input: Config = {}) => {
|
||||
const configuredResponsesRoute = configuredRoute(responsesRoute, input)
|
||||
const configuredChatRoute = configuredRoute(chatRoute, input)
|
||||
const modelDefaults = defaults(input)
|
||||
const responses = (modelID: string | ModelID) =>
|
||||
configuredResponsesRoute
|
||||
.with(withOpenAIOptions(modelID, modelDefaults))
|
||||
.model<OpenAIProviderOptionsInput>({ id: modelID })
|
||||
const chat = (modelID: string | ModelID) =>
|
||||
configuredChatRoute
|
||||
.with(withOpenAIOptions(modelID, modelDefaults))
|
||||
.model<OpenAIProviderOptionsInput>({ id: modelID })
|
||||
|
||||
return {
|
||||
id,
|
||||
model: chat,
|
||||
chat,
|
||||
responses,
|
||||
configure,
|
||||
}
|
||||
}
|
||||
|
||||
export const provider = configure()
|
||||
|
||||
const config = (settings: Settings): Config => {
|
||||
if (settings.auth === "bearer" && settings.apiKey === undefined)
|
||||
throw new Error("Amazon Bedrock Mantle bearer auth requires apiKey")
|
||||
if (settings.auth === "sigv4" && settings.apiKey !== undefined)
|
||||
throw new Error("Amazon Bedrock Mantle SigV4 auth does not accept apiKey")
|
||||
return {
|
||||
apiKey: settings.auth === "sigv4" ? undefined : settings.apiKey,
|
||||
baseURL: settings.baseURL,
|
||||
credentials: settings.credentials,
|
||||
headers: settings.headers === undefined ? undefined : { ...settings.headers },
|
||||
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
|
||||
limits: settings.limits,
|
||||
providerOptions: settings.providerOptions,
|
||||
region: settings.region,
|
||||
}
|
||||
}
|
||||
|
||||
export const chatModel: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (
|
||||
modelID,
|
||||
settings,
|
||||
) => configure(config(settings)).chat(modelID)
|
||||
export const responsesModel: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (
|
||||
modelID,
|
||||
settings,
|
||||
) => configure(config(settings)).responses(modelID)
|
||||
export const model = chatModel
|
||||
@@ -0,0 +1,2 @@
|
||||
export { chatModel as model } from "../amazon-bedrock-mantle"
|
||||
export type { Settings } from "../amazon-bedrock-mantle"
|
||||
@@ -0,0 +1,2 @@
|
||||
export { chatModel as model } from "../../amazon-bedrock-mantle"
|
||||
export type { Settings } from "../../amazon-bedrock-mantle"
|
||||
@@ -0,0 +1,2 @@
|
||||
export { responsesModel as model } from "../../amazon-bedrock-mantle"
|
||||
export type { Settings } from "../../amazon-bedrock-mantle"
|
||||
@@ -14,7 +14,7 @@ const routeAuth = Auth.remove("authorization")
|
||||
// (helper builds the URL) or `baseURL` directly.
|
||||
type AzureURL = AtLeastOne<{ readonly resourceName: string; readonly baseURL: string }>
|
||||
|
||||
export type ModelOptions = AzureURL &
|
||||
export type LanguageModelOptions = AzureURL &
|
||||
RouteDefaultsInput &
|
||||
ProviderAuthOption<"optional"> & {
|
||||
readonly apiVersion?: string
|
||||
@@ -22,7 +22,7 @@ export type ModelOptions = AzureURL &
|
||||
readonly useCompletionUrls?: boolean
|
||||
readonly providerOptions?: OpenAIProviderOptionsInput
|
||||
}
|
||||
export type Config = ModelOptions
|
||||
export type Config = LanguageModelOptions
|
||||
|
||||
export type Settings = ProviderPackage.Settings &
|
||||
AzureURL & {
|
||||
|
||||
@@ -9,14 +9,14 @@ export const id = ProviderID.make("github-copilot")
|
||||
|
||||
// GitHub Copilot has no canonical public URL — callers (opencode, etc.) must
|
||||
// supply `baseURL` explicitly.
|
||||
export type ModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
|
||||
export type LanguageModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
|
||||
ProviderAuthOption<"optional"> & {
|
||||
readonly baseURL: string
|
||||
readonly endpoint?: "chat" | "responses"
|
||||
readonly providerOptions?: OpenAIProviderOptionsInput
|
||||
}
|
||||
|
||||
export const shouldUseResponsesApi = (modelID: string | ModelID, endpoint?: ModelOptions["endpoint"]) => {
|
||||
export const shouldUseResponsesApi = (modelID: string | ModelID, endpoint?: LanguageModelOptions["endpoint"]) => {
|
||||
if (endpoint) return endpoint === "responses"
|
||||
const model = String(modelID)
|
||||
const match = /^gpt-(\d+)/.exec(model)
|
||||
@@ -29,24 +29,24 @@ export const routes = [OpenAIResponses.route, OpenAIChat.route]
|
||||
const chatRoute = OpenAIChat.route.with({ provider: id })
|
||||
const responsesRoute = OpenAIResponses.route.with({ provider: id })
|
||||
|
||||
const defaults = (options: ModelOptions) => {
|
||||
const defaults = (options: LanguageModelOptions) => {
|
||||
const { apiKey: _, auth: _auth, baseURL: _baseURL, endpoint: _endpoint, ...rest } = options
|
||||
return rest
|
||||
}
|
||||
|
||||
const configuredResponsesRoute = (options: ModelOptions) =>
|
||||
const configuredResponsesRoute = (options: LanguageModelOptions) =>
|
||||
responsesRoute.with({
|
||||
endpoint: { baseURL: options.baseURL },
|
||||
auth: AuthOptions.bearer(options, []),
|
||||
})
|
||||
|
||||
const configuredChatRoute = (options: ModelOptions) =>
|
||||
const configuredChatRoute = (options: LanguageModelOptions) =>
|
||||
chatRoute.with({
|
||||
endpoint: { baseURL: options.baseURL },
|
||||
auth: AuthOptions.bearer(options, []),
|
||||
})
|
||||
|
||||
export const configure = (options: ModelOptions) => {
|
||||
export const configure = (options: LanguageModelOptions) => {
|
||||
const responsesRoute = configuredResponsesRoute(options)
|
||||
const chatRoute = configuredChatRoute(options)
|
||||
const responses = (modelID: string | ModelID) =>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export * as Anthropic from "./anthropic"
|
||||
export * as AnthropicCompatible from "./anthropic-compatible"
|
||||
export * as AmazonBedrock from "./amazon-bedrock"
|
||||
export * as AmazonBedrockMantle from "./amazon-bedrock-mantle"
|
||||
export * as Azure from "./azure"
|
||||
export * as Cloudflare from "./cloudflare"
|
||||
export { CloudflareAIGateway, CloudflareWorkersAI } from "./cloudflare"
|
||||
|
||||
@@ -76,7 +76,7 @@ export type OpenRouterProviderOptionsInput = ProviderOptions & {
|
||||
readonly openrouter?: OpenRouterOptions
|
||||
}
|
||||
|
||||
export type ModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
|
||||
export type LanguageModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
|
||||
ProviderAuthOption<"optional"> & {
|
||||
readonly baseURL?: string
|
||||
readonly providerOptions?: OpenRouterProviderOptionsInput
|
||||
@@ -175,7 +175,7 @@ export const route = Route.make({
|
||||
|
||||
export const routes = [route]
|
||||
|
||||
const configuredRoute = (input: ModelOptions) => {
|
||||
const configuredRoute = (input: LanguageModelOptions) => {
|
||||
const { apiKey: _, auth: _auth, baseURL, ...rest } = input
|
||||
return route.with({
|
||||
...rest,
|
||||
@@ -184,7 +184,7 @@ const configuredRoute = (input: ModelOptions) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const configure = (input: ModelOptions = {}) => {
|
||||
export const configure = (input: LanguageModelOptions = {}) => {
|
||||
const route = configuredRoute(input)
|
||||
return {
|
||||
id,
|
||||
|
||||
@@ -16,7 +16,7 @@ export type XAIProviderOptionsInput = ProviderOptions & {
|
||||
readonly xai?: OpenAIOptionsInput
|
||||
}
|
||||
|
||||
export type ModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
|
||||
export type LanguageModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
|
||||
ProviderAuthOption<"optional"> & {
|
||||
readonly baseURL?: string
|
||||
readonly providerOptions?: XAIProviderOptionsInput
|
||||
@@ -53,7 +53,7 @@ export const routes = [responsesRoute, chatRoute]
|
||||
|
||||
const auth = (options: ProviderAuthOption<"optional">) => AuthOptions.bearer(options, "XAI_API_KEY")
|
||||
|
||||
const configuredResponsesRoute = (input: ModelOptions) => {
|
||||
const configuredResponsesRoute = (input: LanguageModelOptions) => {
|
||||
const { apiKey: _, auth: _auth, baseURL, ...rest } = input
|
||||
return responsesRoute.with({
|
||||
...rest,
|
||||
@@ -62,7 +62,7 @@ const configuredResponsesRoute = (input: ModelOptions) => {
|
||||
})
|
||||
}
|
||||
|
||||
const configuredChatRoute = (input: ModelOptions) => {
|
||||
const configuredChatRoute = (input: LanguageModelOptions) => {
|
||||
const { apiKey: _, auth: _auth, baseURL, ...rest } = input
|
||||
return chatRoute.with({
|
||||
...rest,
|
||||
@@ -71,7 +71,7 @@ const configuredChatRoute = (input: ModelOptions) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const configure = (input: ModelOptions = {}) => {
|
||||
export const configure = (input: LanguageModelOptions = {}) => {
|
||||
const responsesRoute = configuredResponsesRoute(input)
|
||||
const chatRoute = configuredChatRoute(input)
|
||||
const responses = (modelID: string | ModelID) => responsesRoute.model<XAIProviderOptionsInput>({ id: modelID })
|
||||
|
||||
@@ -22,13 +22,17 @@ export type ProviderAuthOption<Mode extends ApiKeyMode> =
|
||||
| AuthOverride
|
||||
| (Mode extends "optional" ? OptionalApiKeyAuth : RequiredApiKeyAuth)
|
||||
|
||||
export type ModelOptions<Base, Mode extends ApiKeyMode> = Omit<Base, "apiKey" | "auth"> & ProviderAuthOption<Mode>
|
||||
export type LanguageModelOptions<Base, Mode extends ApiKeyMode> = Omit<Base, "apiKey" | "auth"> &
|
||||
ProviderAuthOption<Mode>
|
||||
|
||||
export type ModelArgs<Base, Mode extends ApiKeyMode> = Mode extends "optional"
|
||||
? readonly [options?: ModelOptions<Base, Mode>]
|
||||
: readonly [options: ModelOptions<Base, Mode>]
|
||||
export type LanguageModelArgs<Base, Mode extends ApiKeyMode> = Mode extends "optional"
|
||||
? readonly [options?: LanguageModelOptions<Base, Mode>]
|
||||
: readonly [options: LanguageModelOptions<Base, Mode>]
|
||||
|
||||
export type ModelFactory<Base, Mode extends ApiKeyMode, Model> = (id: string, ...args: ModelArgs<Base, Mode>) => Model
|
||||
export type LanguageModelFactory<Base, Mode extends ApiKeyMode, LanguageModel> = (
|
||||
id: string,
|
||||
...args: LanguageModelArgs<Base, Mode>
|
||||
) => LanguageModel
|
||||
|
||||
/**
|
||||
* Require at least one of the keys in `T`. Use for option shapes where any
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Config, Effect, Redacted } from "effect"
|
||||
import { Headers } from "effect/unstable/http"
|
||||
import { AuthenticationReason, InvalidRequestReason, LLMError, type HttpOptions } from "../schema"
|
||||
import { AuthenticationReason, InvalidRequestReason, AIError, type HttpOptions } from "../schema"
|
||||
|
||||
export class MissingCredentialError extends Error {
|
||||
readonly _tag = "MissingCredentialError"
|
||||
@@ -11,7 +11,7 @@ export class MissingCredentialError extends Error {
|
||||
}
|
||||
|
||||
export type CredentialError = MissingCredentialError | Config.ConfigError
|
||||
export type AuthError = CredentialError | LLMError
|
||||
export type AuthError = CredentialError | AIError
|
||||
type Secret = string | Redacted.Redacted | Config.Config<string | Redacted.Redacted>
|
||||
|
||||
export interface AuthInput {
|
||||
@@ -100,7 +100,7 @@ export const headers = (input: Headers.Input) =>
|
||||
|
||||
export const remove = (name: string) => auth((input) => Effect.succeed(Headers.remove(input.headers, name)))
|
||||
|
||||
export const custom = (apply: (input: AuthInput) => Effect.Effect<Headers.Headers, LLMError>) => auth(apply)
|
||||
export const custom = (apply: (input: AuthInput) => Effect.Effect<Headers.Headers, AIError>) => auth(apply)
|
||||
|
||||
export const passthrough = none
|
||||
|
||||
@@ -134,9 +134,9 @@ export function bearerHeader(name: string, source?: Secret | Credential) {
|
||||
return render(source)
|
||||
}
|
||||
|
||||
const toLLMError = (error: AuthError): LLMError => {
|
||||
const toAIError = (error: AuthError): AIError => {
|
||||
if (error instanceof MissingCredentialError || error instanceof Config.ConfigError) {
|
||||
return new LLMError({
|
||||
return new AIError({
|
||||
module: "Auth",
|
||||
method: "apply",
|
||||
reason:
|
||||
@@ -150,7 +150,7 @@ const toLLMError = (error: AuthError): LLMError => {
|
||||
|
||||
export const toEffect =
|
||||
(input: Definition) =>
|
||||
(authInput: AuthInput): Effect.Effect<Headers.Headers, LLMError> =>
|
||||
input.apply(authInput).pipe(Effect.mapError(toLLMError))
|
||||
(authInput: AuthInput): Effect.Effect<Headers.Headers, AIError> =>
|
||||
input.apply(authInput).pipe(Effect.mapError(toAIError))
|
||||
|
||||
export * as Auth from "./auth"
|
||||
|
||||
@@ -10,15 +10,15 @@ 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 { ProtocolID, ProviderOptions } from "../schema"
|
||||
import {
|
||||
AIError,
|
||||
GenerationOptions,
|
||||
HttpOptions,
|
||||
LLMRequest,
|
||||
LLMResponse,
|
||||
Model,
|
||||
ModelLimits,
|
||||
LLMError as LLMErrorClass,
|
||||
LanguageModel,
|
||||
LanguageModelLimits,
|
||||
LLMEvent,
|
||||
ProviderID,
|
||||
mergeGenerationOptions,
|
||||
@@ -30,7 +30,7 @@ export interface RouteBody<Body> {
|
||||
/** Schema for the validated provider-native body sent as the JSON request. */
|
||||
readonly schema: Schema.Codec<Body, unknown>
|
||||
/** Build the provider-native body from a common `LLMRequest`. */
|
||||
readonly from: (request: LLMRequest) => Effect.Effect<Body, LLMError>
|
||||
readonly from: (request: LLMRequest) => Effect.Effect<Body, AIError>
|
||||
}
|
||||
|
||||
export interface Route<Body, Prepared = unknown> {
|
||||
@@ -45,17 +45,19 @@ 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: <Options extends ProviderOptions = ProviderOptions>(
|
||||
input: RouteMappedLanguageModelInput,
|
||||
) => LanguageModel<Options>
|
||||
readonly prepareTransport: (
|
||||
body: Body,
|
||||
request: LLMRequest,
|
||||
options?: StreamOptions,
|
||||
) => Effect.Effect<Prepared, LLMError>
|
||||
) => Effect.Effect<Prepared, AIError>
|
||||
readonly streamPrepared: (
|
||||
prepared: Prepared,
|
||||
request: LLMRequest,
|
||||
runtime: TransportRuntime,
|
||||
) => Stream.Stream<LLMEvent, LLMError>
|
||||
) => Stream.Stream<LLMEvent, AIError>
|
||||
}
|
||||
|
||||
// Route registries intentionally erase body generics after construction.
|
||||
@@ -66,13 +68,13 @@ export type AnyRoute = Route<any, any>
|
||||
|
||||
export type HttpOptionsInput = HttpOptions.Input
|
||||
|
||||
export type RouteModelInput = Omit<Model.Input, "provider" | "route">
|
||||
export type RouteLanguageModelInput = Omit<LanguageModel.Input, "provider" | "route">
|
||||
|
||||
export type RouteRoutedModelInput = Omit<Model.Input, "route">
|
||||
export type RouteRoutedLanguageModelInput = Omit<LanguageModel.Input, "route">
|
||||
|
||||
export interface RouteDefaults {
|
||||
readonly headers?: Record<string, string>
|
||||
readonly limits?: ModelLimits
|
||||
readonly limits?: LanguageModelLimits
|
||||
readonly generation?: GenerationOptions
|
||||
readonly providerOptions?: ProviderOptions
|
||||
readonly http?: HttpOptions
|
||||
@@ -80,7 +82,7 @@ export interface RouteDefaults {
|
||||
|
||||
export interface RouteDefaultsInput {
|
||||
readonly headers?: Record<string, string>
|
||||
readonly limits?: ModelLimits.Input
|
||||
readonly limits?: LanguageModelLimits.Input
|
||||
readonly generation?: GenerationOptions.Input
|
||||
readonly providerOptions?: ProviderOptions
|
||||
readonly http?: HttpOptions.Input
|
||||
@@ -94,14 +96,17 @@ export interface RoutePatch<Body, Prepared> extends RouteDefaultsInput {
|
||||
readonly endpoint?: EndpointPatch<Body>
|
||||
}
|
||||
|
||||
type RouteMappedModelInput = RouteModelInput | RouteRoutedModelInput
|
||||
type RouteMappedLanguageModelInput = RouteLanguageModelInput | RouteRoutedLanguageModelInput
|
||||
|
||||
const makeRouteModel = <Options extends ProviderOptions = ProviderOptions>(route: AnyRoute, mapped: RouteMappedModelInput) => {
|
||||
const makeRouteLanguageModel = <Options extends ProviderOptions = ProviderOptions>(
|
||||
route: AnyRoute,
|
||||
mapped: RouteMappedLanguageModelInput,
|
||||
) => {
|
||||
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 LanguageModel.make<Options>({
|
||||
...mapped,
|
||||
provider,
|
||||
route,
|
||||
@@ -114,7 +119,7 @@ const mergeRouteDefaults = (base: RouteDefaults | undefined, patch: RouteDefault
|
||||
...base,
|
||||
...patch,
|
||||
headers,
|
||||
limits: patch.limits === undefined ? base?.limits : ModelLimits.make(patch.limits),
|
||||
limits: patch.limits === undefined ? base?.limits : LanguageModelLimits.make(patch.limits),
|
||||
generation: mergeGenerationOptions(generationOptions(base?.generation), generationOptions(patch.generation)),
|
||||
providerOptions: mergeProviderOptions(base?.providerOptions, patch.providerOptions),
|
||||
http: mergeHttpOptions(
|
||||
@@ -154,11 +159,11 @@ export interface StreamOptions {
|
||||
}
|
||||
|
||||
export interface StreamMethod {
|
||||
(request: LLMRequest, options?: StreamOptions): Stream.Stream<LLMEvent, LLMError>
|
||||
(request: LLMRequest, options?: StreamOptions): Stream.Stream<LLMEvent, AIError>
|
||||
}
|
||||
|
||||
export interface GenerateMethod {
|
||||
(request: LLMRequest, options?: StreamOptions): Effect.Effect<LLMResponse, LLMError>
|
||||
(request: LLMRequest, options?: StreamOptions): Effect.Effect<LLMResponse, AIError>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/LLMClient") {}
|
||||
@@ -222,11 +227,11 @@ export interface MakeTransportInput<Body, Prepared, Frame, Event, State> {
|
||||
|
||||
const streamError = (route: string, message: string, cause: Cause.Cause<unknown>) => {
|
||||
const failed = cause.reasons.find(Cause.isFailReason)?.error
|
||||
if (failed instanceof LLMErrorClass) return failed
|
||||
if (failed instanceof AIError) return failed
|
||||
return ProviderShared.eventError(route, message, Cause.pretty(cause))
|
||||
}
|
||||
|
||||
const requireTerminalEvent = (route: string) => (events: Stream.Stream<LLMEvent, LLMError>) =>
|
||||
const requireTerminalEvent = (route: string) => (events: Stream.Stream<LLMEvent, AIError>) =>
|
||||
Stream.suspend(() => {
|
||||
let terminal = false
|
||||
return events.pipe(
|
||||
@@ -292,8 +297,8 @@ function makeFromTransport<Body, Prepared, Frame, Event, State>(
|
||||
defaults: mergeRouteDefaults(route.defaults, defaults),
|
||||
})
|
||||
},
|
||||
model: <Options extends ProviderOptions = ProviderOptions>(input: RouteMappedModelInput) =>
|
||||
makeRouteModel<Options>(route, input),
|
||||
model: <Options extends ProviderOptions = ProviderOptions>(input: RouteMappedLanguageModelInput) =>
|
||||
makeRouteLanguageModel<Options>(route, input),
|
||||
prepareTransport: (body, request, options) =>
|
||||
routeInput.transport.prepare({
|
||||
body,
|
||||
@@ -417,18 +422,18 @@ const generateWith = (stream: Interface["stream"]) =>
|
||||
)
|
||||
})
|
||||
|
||||
export function stream(request: LLMRequest, options?: StreamOptions): Stream.Stream<LLMEvent, LLMError> {
|
||||
export function stream(request: LLMRequest, options?: StreamOptions): Stream.Stream<LLMEvent, AIError> {
|
||||
return Stream.unwrap(
|
||||
Effect.gen(function* () {
|
||||
return (yield* Service).stream(request, options)
|
||||
}),
|
||||
) as Stream.Stream<LLMEvent, LLMError>
|
||||
) as Stream.Stream<LLMEvent, AIError>
|
||||
}
|
||||
|
||||
export function generate(request: LLMRequest, options?: StreamOptions): Effect.Effect<LLMResponse, LLMError> {
|
||||
export function generate(request: LLMRequest, options?: StreamOptions): Effect.Effect<LLMResponse, AIError> {
|
||||
return Effect.gen(function* () {
|
||||
return yield* (yield* Service).generate(request, options)
|
||||
}) as Effect.Effect<LLMResponse, LLMError>
|
||||
}) as Effect.Effect<LLMResponse, AIError>
|
||||
}
|
||||
|
||||
export const streamRequest = (request: LLMRequest, options?: StreamOptions) =>
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
HttpRateLimitDetails,
|
||||
HttpRequestDetails,
|
||||
HttpResponseDetails,
|
||||
LLMError,
|
||||
AIError,
|
||||
TransportReason,
|
||||
} from "../schema"
|
||||
import { classifyProviderFailure } from "../provider-error"
|
||||
@@ -20,10 +20,10 @@ import { classifyProviderFailure } from "../provider-error"
|
||||
export interface Interface {
|
||||
readonly execute: (
|
||||
request: HttpClientRequest.HttpClientRequest,
|
||||
) => Effect.Effect<HttpClientResponse.HttpClientResponse, LLMError>
|
||||
) => Effect.Effect<HttpClientResponse.HttpClientResponse, AIError>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/LLM/RequestExecutor") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/AI/RequestExecutor") {}
|
||||
|
||||
const BODY_LIMIT = 16_384
|
||||
const REDACTED = "<redacted>"
|
||||
@@ -220,7 +220,7 @@ const statusError =
|
||||
const retryAfter = retryAfterMs(headers)
|
||||
const rateLimit = rateLimitDetails(headers, retryAfter)
|
||||
const details = responseBody(body, request)
|
||||
return yield* new LLMError({
|
||||
return yield* new AIError({
|
||||
module: "RequestExecutor",
|
||||
method: "execute",
|
||||
reason: classifyProviderFailure({
|
||||
@@ -246,7 +246,7 @@ const toHttpError = (redactedNames: ReadonlyArray<string | RegExp>) => (error: u
|
||||
readonly kind?: string | undefined
|
||||
readonly request?: HttpClientRequest.HttpClientRequest | undefined
|
||||
}) =>
|
||||
new LLMError({
|
||||
new AIError({
|
||||
module: "RequestExecutor",
|
||||
method: "execute",
|
||||
reason: new TransportReason({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Stream } from "effect"
|
||||
import * as ProviderShared from "../protocols/shared"
|
||||
import type { LLMError } from "../schema"
|
||||
import type { AIError } from "../schema"
|
||||
|
||||
/**
|
||||
* Decode a streaming HTTP response body into provider-protocol frames.
|
||||
@@ -18,7 +18,7 @@ import type { LLMError } from "../schema"
|
||||
*/
|
||||
export interface Definition<Frame> {
|
||||
readonly id: string
|
||||
readonly frame: (bytes: Stream.Stream<Uint8Array, LLMError>) => Stream.Stream<Frame, LLMError>
|
||||
readonly frame: (bytes: Stream.Stream<Uint8Array, AIError>) => Stream.Stream<Frame, AIError>
|
||||
}
|
||||
|
||||
/** Server-Sent Events framing. Used by every JSON-streaming HTTP provider. */
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export { Route, LLMClient } from "./client"
|
||||
export type {
|
||||
Route as RouteShape,
|
||||
RouteModelInput,
|
||||
RouteRoutedModelInput,
|
||||
RouteLanguageModelInput,
|
||||
RouteRoutedLanguageModelInput,
|
||||
RouteDefaults,
|
||||
RouteDefaultsInput,
|
||||
AnyRoute,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Schema, type Effect } from "effect"
|
||||
import type { LLMError, LLMEvent, LLMRequest, ProtocolID } from "../schema"
|
||||
import type { AIError, LLMEvent, LLMRequest, ProtocolID } from "../schema"
|
||||
|
||||
/**
|
||||
* The semantic API contract of one model server family.
|
||||
@@ -47,7 +47,7 @@ export interface ProtocolBody<Body> {
|
||||
/** Schema for the validated provider-native body sent as the JSON request. */
|
||||
readonly schema: Schema.Codec<Body, unknown>
|
||||
/** Build the provider-native body from a common `LLMRequest`. */
|
||||
readonly from: (request: LLMRequest) => Effect.Effect<Body, LLMError>
|
||||
readonly from: (request: LLMRequest) => Effect.Effect<Body, AIError>
|
||||
}
|
||||
|
||||
export interface ProtocolStream<Frame, Event, State> {
|
||||
@@ -56,7 +56,7 @@ export interface ProtocolStream<Frame, Event, State> {
|
||||
/** Initial parser state. Called once per response with the resolved request. */
|
||||
readonly initial: (request: LLMRequest) => State
|
||||
/** Translate one event into emitted `LLMEvent`s plus the next state. */
|
||||
readonly step: (state: State, event: Event) => Effect.Effect<readonly [State, ReadonlyArray<LLMEvent>], LLMError>
|
||||
readonly step: (state: State, event: Event) => Effect.Effect<readonly [State, ReadonlyArray<LLMEvent>], AIError>
|
||||
/** Optional request-completion signal for transports that do not end naturally. */
|
||||
readonly terminal?: (event: Event) => boolean
|
||||
/** Optional flush emitted when the framed stream ends. */
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Endpoint } from "../endpoint"
|
||||
import { Auth } from "../auth"
|
||||
import type { Interface as RequestExecutorInterface } from "../executor"
|
||||
import type { Interface as WebSocketExecutorInterface } from "./websocket"
|
||||
import type { LLMError, LLMRequest } from "../../schema"
|
||||
import type { AIError, LLMRequest } from "../../schema"
|
||||
|
||||
export interface TransportRuntime {
|
||||
readonly http: RequestExecutorInterface
|
||||
@@ -21,12 +21,8 @@ export type HttpRequestTransform = (request: HttpRequest) => Effect.Effect<void>
|
||||
|
||||
export interface Transport<Body, Prepared, Frame> {
|
||||
readonly id: string
|
||||
readonly prepare: (input: TransportPrepareInput<Body>) => Effect.Effect<Prepared, LLMError>
|
||||
readonly frames: (
|
||||
prepared: Prepared,
|
||||
request: LLMRequest,
|
||||
runtime: TransportRuntime,
|
||||
) => Stream.Stream<Frame, LLMError>
|
||||
readonly prepare: (input: TransportPrepareInput<Body>) => Effect.Effect<Prepared, AIError>
|
||||
readonly frames: (prepared: Prepared, request: LLMRequest, runtime: TransportRuntime) => Stream.Stream<Frame, AIError>
|
||||
}
|
||||
|
||||
export interface TransportPrepareInput<Body> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Cause, Context, Effect, Layer, Queue, Stream } from "effect"
|
||||
import { Headers } from "effect/unstable/http"
|
||||
import { LLMError, TransportReason } from "../../schema"
|
||||
import { AIError, TransportReason } from "../../schema"
|
||||
import * as HttpTransport from "./http"
|
||||
import type { Transport } from "./index"
|
||||
|
||||
@@ -10,13 +10,13 @@ export interface WebSocketRequest {
|
||||
}
|
||||
|
||||
export interface WebSocketConnection {
|
||||
readonly sendText: (message: string) => Effect.Effect<void, LLMError>
|
||||
readonly messages: Stream.Stream<string | Uint8Array, LLMError>
|
||||
readonly sendText: (message: string) => Effect.Effect<void, AIError>
|
||||
readonly messages: Stream.Stream<string | Uint8Array, AIError>
|
||||
readonly close: Effect.Effect<void, never>
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
readonly open: (input: WebSocketRequest) => Effect.Effect<WebSocketConnection, LLMError>
|
||||
readonly open: (input: WebSocketRequest) => Effect.Effect<WebSocketConnection, AIError>
|
||||
}
|
||||
|
||||
type WebSocketConstructorWithHeaders = new (
|
||||
@@ -24,14 +24,14 @@ type WebSocketConstructorWithHeaders = new (
|
||||
options?: { readonly headers?: Headers.Headers },
|
||||
) => globalThis.WebSocket
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/LLM/WebSocketExecutor") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/AI/WebSocketExecutor") {}
|
||||
|
||||
const transportError = (
|
||||
method: string,
|
||||
message: string,
|
||||
input: { readonly url?: string; readonly kind?: string } = {},
|
||||
) =>
|
||||
new LLMError({
|
||||
new AIError({
|
||||
module: "WebSocketExecutor",
|
||||
method,
|
||||
reason: new TransportReason({ message, url: input.url, kind: input.kind }),
|
||||
@@ -59,7 +59,7 @@ const waitOpen = (ws: globalThis.WebSocket, input: WebSocketRequest) => {
|
||||
}),
|
||||
)
|
||||
}
|
||||
return Effect.callback<void, LLMError>((resume, signal) => {
|
||||
return Effect.callback<void, AIError>((resume, signal) => {
|
||||
const cleanup = () => {
|
||||
ws.removeEventListener("open", onOpen)
|
||||
ws.removeEventListener("error", onError)
|
||||
@@ -138,10 +138,10 @@ export const layer: Layer.Layer<Service> = Layer.succeed(Service, Service.of({ o
|
||||
export const fromWebSocket = (
|
||||
ws: globalThis.WebSocket,
|
||||
input: WebSocketRequest,
|
||||
): Effect.Effect<WebSocketConnection, LLMError> =>
|
||||
): Effect.Effect<WebSocketConnection, AIError> =>
|
||||
Effect.gen(function* () {
|
||||
yield* waitOpen(ws, input)
|
||||
const messages = yield* Queue.bounded<string | Uint8Array, LLMError | Cause.Done<void>>(128)
|
||||
const messages = yield* Queue.bounded<string | Uint8Array, AIError | Cause.Done<void>>(128)
|
||||
|
||||
const onMessage = (event: MessageEvent) => {
|
||||
if (typeof event.data === "string") return Queue.offerUnsafe(messages, event.data)
|
||||
@@ -213,7 +213,7 @@ export interface JsonPrepared {
|
||||
}
|
||||
|
||||
export interface JsonInput<Body, Message> {
|
||||
readonly toMessage: (body: Body | Record<string, unknown>) => Effect.Effect<Message, LLMError>
|
||||
readonly toMessage: (body: Body | Record<string, unknown>) => Effect.Effect<Message, AIError>
|
||||
readonly encodeMessage: (message: Message) => string
|
||||
}
|
||||
|
||||
|
||||
@@ -2,28 +2,28 @@ import { Schema } from "effect"
|
||||
import { Tool } from "@opencode-ai/schema/tool"
|
||||
import { ModelID, ProviderID, ProviderMetadata, RouteID } from "./ids"
|
||||
|
||||
export const ProviderFailureClassification = Schema.Literal("context-overflow")
|
||||
export const ProviderFailureClassification = Schema.Literals(["context-overflow", "payload-too-large"])
|
||||
export type ProviderFailureClassification = typeof ProviderFailureClassification.Type
|
||||
|
||||
export class HttpRequestDetails extends Schema.Class<HttpRequestDetails>("LLM.HttpRequestDetails")({
|
||||
export class HttpRequestDetails extends Schema.Class<HttpRequestDetails>("AI.HttpRequestDetails")({
|
||||
method: Schema.String,
|
||||
url: Schema.String,
|
||||
headers: Schema.Record(Schema.String, Schema.String),
|
||||
}) {}
|
||||
|
||||
export class HttpResponseDetails extends Schema.Class<HttpResponseDetails>("LLM.HttpResponseDetails")({
|
||||
export class HttpResponseDetails extends Schema.Class<HttpResponseDetails>("AI.HttpResponseDetails")({
|
||||
status: Schema.Number,
|
||||
headers: Schema.Record(Schema.String, Schema.String),
|
||||
}) {}
|
||||
|
||||
export class HttpRateLimitDetails extends Schema.Class<HttpRateLimitDetails>("LLM.HttpRateLimitDetails")({
|
||||
export class HttpRateLimitDetails extends Schema.Class<HttpRateLimitDetails>("AI.HttpRateLimitDetails")({
|
||||
retryAfterMs: Schema.optional(Schema.Number),
|
||||
limit: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
||||
remaining: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
||||
reset: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
||||
}) {}
|
||||
|
||||
export class HttpContext extends Schema.Class<HttpContext>("LLM.HttpContext")({
|
||||
export class HttpContext extends Schema.Class<HttpContext>("AI.HttpContext")({
|
||||
request: HttpRequestDetails,
|
||||
response: Schema.optional(HttpResponseDetails),
|
||||
body: Schema.optional(Schema.String),
|
||||
@@ -32,7 +32,7 @@ export class HttpContext extends Schema.Class<HttpContext>("LLM.HttpContext")({
|
||||
rateLimit: Schema.optional(HttpRateLimitDetails),
|
||||
}) {}
|
||||
|
||||
export class InvalidRequestReason extends Schema.Class<InvalidRequestReason>("LLM.Error.InvalidRequest")({
|
||||
export class InvalidRequestReason extends Schema.Class<InvalidRequestReason>("AI.Error.InvalidRequest")({
|
||||
_tag: Schema.tag("InvalidRequest"),
|
||||
message: Schema.String,
|
||||
parameter: Schema.optional(Schema.String),
|
||||
@@ -41,18 +41,18 @@ export class InvalidRequestReason extends Schema.Class<InvalidRequestReason>("LL
|
||||
http: Schema.optional(HttpContext),
|
||||
}) {}
|
||||
|
||||
export class NoRouteReason extends Schema.Class<NoRouteReason>("LLM.Error.NoRoute")({
|
||||
export class NoRouteReason extends Schema.Class<NoRouteReason>("AI.Error.NoRoute")({
|
||||
_tag: Schema.tag("NoRoute"),
|
||||
route: RouteID,
|
||||
provider: ProviderID,
|
||||
model: ModelID,
|
||||
}) {
|
||||
get message() {
|
||||
return `No LLM route for ${this.provider}/${this.model} using ${this.route}`
|
||||
return `No AI route for ${this.provider}/${this.model} using ${this.route}`
|
||||
}
|
||||
}
|
||||
|
||||
export class AuthenticationReason extends Schema.Class<AuthenticationReason>("LLM.Error.Authentication")({
|
||||
export class AuthenticationReason extends Schema.Class<AuthenticationReason>("AI.Error.Authentication")({
|
||||
_tag: Schema.tag("Authentication"),
|
||||
message: Schema.String,
|
||||
kind: Schema.Literals(["missing", "invalid", "expired", "insufficient-permissions", "unknown"]),
|
||||
@@ -60,7 +60,7 @@ export class AuthenticationReason extends Schema.Class<AuthenticationReason>("LL
|
||||
http: Schema.optional(HttpContext),
|
||||
}) {}
|
||||
|
||||
export class RateLimitReason extends Schema.Class<RateLimitReason>("LLM.Error.RateLimit")({
|
||||
export class RateLimitReason extends Schema.Class<RateLimitReason>("AI.Error.RateLimit")({
|
||||
_tag: Schema.tag("RateLimit"),
|
||||
message: Schema.String,
|
||||
retryAfterMs: Schema.optional(Schema.Number),
|
||||
@@ -69,21 +69,21 @@ export class RateLimitReason extends Schema.Class<RateLimitReason>("LLM.Error.Ra
|
||||
http: Schema.optional(HttpContext),
|
||||
}) {}
|
||||
|
||||
export class QuotaExceededReason extends Schema.Class<QuotaExceededReason>("LLM.Error.QuotaExceeded")({
|
||||
export class QuotaExceededReason extends Schema.Class<QuotaExceededReason>("AI.Error.QuotaExceeded")({
|
||||
_tag: Schema.tag("QuotaExceeded"),
|
||||
message: Schema.String,
|
||||
providerMetadata: Schema.optional(ProviderMetadata),
|
||||
http: Schema.optional(HttpContext),
|
||||
}) {}
|
||||
|
||||
export class ContentPolicyReason extends Schema.Class<ContentPolicyReason>("LLM.Error.ContentPolicy")({
|
||||
export class ContentPolicyReason extends Schema.Class<ContentPolicyReason>("AI.Error.ContentPolicy")({
|
||||
_tag: Schema.tag("ContentPolicy"),
|
||||
message: Schema.String,
|
||||
providerMetadata: Schema.optional(ProviderMetadata),
|
||||
http: Schema.optional(HttpContext),
|
||||
}) {}
|
||||
|
||||
export class ProviderInternalReason extends Schema.Class<ProviderInternalReason>("LLM.Error.ProviderInternal")({
|
||||
export class ProviderInternalReason extends Schema.Class<ProviderInternalReason>("AI.Error.ProviderInternal")({
|
||||
_tag: Schema.tag("ProviderInternal"),
|
||||
message: Schema.String,
|
||||
status: Schema.optional(Schema.Number),
|
||||
@@ -92,7 +92,7 @@ export class ProviderInternalReason extends Schema.Class<ProviderInternalReason>
|
||||
http: Schema.optional(HttpContext),
|
||||
}) {}
|
||||
|
||||
export class TransportReason extends Schema.Class<TransportReason>("LLM.Error.Transport")({
|
||||
export class TransportReason extends Schema.Class<TransportReason>("AI.Error.Transport")({
|
||||
_tag: Schema.tag("Transport"),
|
||||
message: Schema.String,
|
||||
kind: Schema.optional(Schema.String),
|
||||
@@ -101,7 +101,7 @@ export class TransportReason extends Schema.Class<TransportReason>("LLM.Error.Tr
|
||||
}) {}
|
||||
|
||||
export class InvalidProviderOutputReason extends Schema.Class<InvalidProviderOutputReason>(
|
||||
"LLM.Error.InvalidProviderOutput",
|
||||
"AI.Error.InvalidProviderOutput",
|
||||
)({
|
||||
_tag: Schema.tag("InvalidProviderOutput"),
|
||||
message: Schema.String,
|
||||
@@ -110,7 +110,7 @@ export class InvalidProviderOutputReason extends Schema.Class<InvalidProviderOut
|
||||
providerMetadata: Schema.optional(ProviderMetadata),
|
||||
}) {}
|
||||
|
||||
export class UnknownProviderReason extends Schema.Class<UnknownProviderReason>("LLM.Error.UnknownProvider")({
|
||||
export class UnknownProviderReason extends Schema.Class<UnknownProviderReason>("AI.Error.UnknownProvider")({
|
||||
_tag: Schema.tag("UnknownProvider"),
|
||||
message: Schema.String,
|
||||
status: Schema.optional(Schema.Number),
|
||||
@@ -118,7 +118,7 @@ export class UnknownProviderReason extends Schema.Class<UnknownProviderReason>("
|
||||
http: Schema.optional(HttpContext),
|
||||
}) {}
|
||||
|
||||
export const LLMErrorReason = Schema.Union([
|
||||
export const AIErrorReason = Schema.Union([
|
||||
InvalidRequestReason,
|
||||
NoRouteReason,
|
||||
AuthenticationReason,
|
||||
@@ -130,12 +130,12 @@ export const LLMErrorReason = Schema.Union([
|
||||
InvalidProviderOutputReason,
|
||||
UnknownProviderReason,
|
||||
]).pipe(Schema.toTaggedUnion("_tag"))
|
||||
export type LLMErrorReason = Schema.Schema.Type<typeof LLMErrorReason>
|
||||
export type AIErrorReason = Schema.Schema.Type<typeof AIErrorReason>
|
||||
|
||||
export class LLMError extends Schema.TaggedErrorClass<LLMError>()("LLM.Error", {
|
||||
export class AIError extends Schema.TaggedErrorClass<AIError>()("AI.Error", {
|
||||
module: Schema.String,
|
||||
method: Schema.String,
|
||||
reason: LLMErrorReason,
|
||||
reason: AIErrorReason,
|
||||
}) {
|
||||
override readonly cause = this.reason
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ import { ProviderFailureClassification } from "./errors"
|
||||
* — for fields we don't normalize and for billing-level audit trails.
|
||||
* Matches the same escape-hatch field on `LLMEvent`.
|
||||
*/
|
||||
export class Usage extends Schema.Class<Usage>("LLM.Usage")({
|
||||
export class Usage extends Schema.Class<Usage>("AI.Usage")({
|
||||
inputTokens: Schema.optional(Schema.Number),
|
||||
outputTokens: Schema.optional(Schema.Number),
|
||||
nonCachedInputTokens: Schema.optional(Schema.Number),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Schema } from "effect"
|
||||
import { LLM, ProviderMetadata } from "@opencode-ai/schema/llm"
|
||||
import { ProviderMetadata } from "@opencode-ai/schema/ai"
|
||||
import { LLM } from "@opencode-ai/schema/llm"
|
||||
|
||||
export { ProviderMetadata }
|
||||
|
||||
@@ -11,10 +12,10 @@ export type ProtocolID = Schema.Schema.Type<typeof ProtocolID>
|
||||
export const RouteID = Schema.String
|
||||
export type RouteID = Schema.Schema.Type<typeof RouteID>
|
||||
|
||||
export const ModelID = Schema.String.pipe(Schema.brand("LLM.ModelID"))
|
||||
export const ModelID = Schema.String.pipe(Schema.brand("AI.ModelID"))
|
||||
export type ModelID = typeof ModelID.Type
|
||||
|
||||
export const ProviderID = Schema.String.pipe(Schema.brand("LLM.ProviderID"))
|
||||
export const ProviderID = Schema.String.pipe(Schema.brand("AI.ProviderID"))
|
||||
export type ProviderID = typeof ProviderID.Type
|
||||
|
||||
export const ResponseID = Schema.String
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Schema } from "effect"
|
||||
import { Tool } from "@opencode-ai/schema/tool"
|
||||
import { JsonSchema, MessageRole, ProviderMetadata } from "./ids"
|
||||
import { CacheHint, CachePolicy, GenerationOptions, HttpOptions, ModelSchema, ProviderOptions } from "./options"
|
||||
import { CacheHint, CachePolicy, GenerationOptions, HttpOptions, LanguageModelSchema, ProviderOptions } from "./options"
|
||||
import { isRecord } from "../utils/record"
|
||||
|
||||
const systemPartSchema = Schema.Struct({
|
||||
@@ -263,7 +263,7 @@ export namespace ToolChoice {
|
||||
|
||||
export class LLMRequest extends Schema.Class<LLMRequest>("LLM.Request")({
|
||||
id: Schema.optional(Schema.String),
|
||||
model: ModelSchema,
|
||||
model: LanguageModelSchema,
|
||||
system: Schema.Array(SystemPart),
|
||||
messages: Schema.Array(Message),
|
||||
tools: Schema.Array(ToolDefinition),
|
||||
|
||||
@@ -50,7 +50,7 @@ export const mergeProviderOptions = (
|
||||
return Object.keys(result).length === 0 ? undefined : result
|
||||
}
|
||||
|
||||
export class HttpOptions extends Schema.Class<HttpOptions>("LLM.HttpOptions")({
|
||||
export class HttpOptions extends Schema.Class<HttpOptions>("AI.HttpOptions")({
|
||||
body: Schema.optional(JsonSchema),
|
||||
headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
||||
query: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
||||
@@ -121,32 +121,32 @@ export const mergeGenerationOptions = (...items: ReadonlyArray<GenerationOptions
|
||||
return Object.values(result).some((value) => value !== undefined) ? result : undefined
|
||||
}
|
||||
|
||||
export class ModelLimits extends Schema.Class<ModelLimits>("LLM.ModelLimits")({
|
||||
export class LanguageModelLimits extends Schema.Class<LanguageModelLimits>("LLM.LanguageModelLimits")({
|
||||
context: Schema.optional(Schema.Number),
|
||||
input: Schema.optional(Schema.Number),
|
||||
output: Schema.optional(Schema.Number),
|
||||
}) {}
|
||||
|
||||
export namespace ModelLimits {
|
||||
export type Input = ModelLimits | ConstructorParameters<typeof ModelLimits>[0]
|
||||
export namespace LanguageModelLimits {
|
||||
export type Input = LanguageModelLimits | ConstructorParameters<typeof LanguageModelLimits>[0]
|
||||
|
||||
/** Normalize model limit input into the canonical `ModelLimits` class. */
|
||||
/** Normalize model limit input into the canonical `LanguageModelLimits` class. */
|
||||
export const make = (input: Input | undefined) =>
|
||||
input instanceof ModelLimits ? input : new ModelLimits(input ?? {})
|
||||
input instanceof LanguageModelLimits ? input : new LanguageModelLimits(input ?? {})
|
||||
}
|
||||
|
||||
export class ModelDefaults extends Schema.Class<ModelDefaults>("LLM.ModelDefaults")({
|
||||
limits: Schema.optional(ModelLimits),
|
||||
export class LanguageModelDefaults extends Schema.Class<LanguageModelDefaults>("LLM.LanguageModelDefaults")({
|
||||
limits: Schema.optional(LanguageModelLimits),
|
||||
generation: Schema.optional(GenerationOptions),
|
||||
providerOptions: Schema.optional(ProviderOptions),
|
||||
http: Schema.optional(HttpOptions),
|
||||
}) {}
|
||||
|
||||
export namespace ModelDefaults {
|
||||
export namespace LanguageModelDefaults {
|
||||
export type Input =
|
||||
| ModelDefaults
|
||||
| LanguageModelDefaults
|
||||
| {
|
||||
readonly limits?: ModelLimits.Input
|
||||
readonly limits?: LanguageModelLimits.Input
|
||||
readonly generation?: GenerationOptions.Input
|
||||
readonly providerOptions?: ProviderOptions
|
||||
readonly http?: HttpOptions.Input
|
||||
@@ -154,9 +154,9 @@ export namespace ModelDefaults {
|
||||
|
||||
/** Normalize selected-model request defaults without applying precedence. */
|
||||
export const make = (input: Input) => {
|
||||
if (input instanceof ModelDefaults) return input
|
||||
return new ModelDefaults({
|
||||
limits: input.limits === undefined ? undefined : ModelLimits.make(input.limits),
|
||||
if (input instanceof LanguageModelDefaults) return input
|
||||
return new LanguageModelDefaults({
|
||||
limits: input.limits === undefined ? undefined : LanguageModelLimits.make(input.limits),
|
||||
generation: input.generation === undefined ? undefined : GenerationOptions.make(input.generation),
|
||||
providerOptions: input.providerOptions,
|
||||
http: input.http === undefined ? undefined : HttpOptions.make(input.http),
|
||||
@@ -164,34 +164,39 @@ export namespace ModelDefaults {
|
||||
}
|
||||
}
|
||||
|
||||
export const ModelToolSchemaCompatibility = Schema.Literals(["gemini", "moonshot"])
|
||||
export type ModelToolSchemaCompatibility = Schema.Schema.Type<typeof ModelToolSchemaCompatibility>
|
||||
export const LanguageModelToolSchemaCompatibility = Schema.Literals(["gemini", "moonshot"])
|
||||
export type LanguageModelToolSchemaCompatibility = Schema.Schema.Type<typeof LanguageModelToolSchemaCompatibility>
|
||||
|
||||
export const ModelMaxTokensFieldCompatibility = Schema.Literals(["max_completion_tokens", "max_tokens"])
|
||||
export type ModelMaxTokensFieldCompatibility = Schema.Schema.Type<typeof ModelMaxTokensFieldCompatibility>
|
||||
export const LanguageModelMaxTokensFieldCompatibility = Schema.Literals(["max_completion_tokens", "max_tokens"])
|
||||
export type LanguageModelMaxTokensFieldCompatibility = Schema.Schema.Type<
|
||||
typeof LanguageModelMaxTokensFieldCompatibility
|
||||
>
|
||||
|
||||
export class ModelCompatibility extends Schema.Class<ModelCompatibility>("LLM.ModelCompatibility")({
|
||||
toolSchema: Schema.optional(ModelToolSchemaCompatibility),
|
||||
export class LanguageModelCompatibility extends Schema.Class<LanguageModelCompatibility>(
|
||||
"LLM.LanguageModelCompatibility",
|
||||
)({
|
||||
toolSchema: Schema.optional(LanguageModelToolSchemaCompatibility),
|
||||
reasoningField: Schema.optional(Schema.String),
|
||||
maxTokensField: Schema.optional(ModelMaxTokensFieldCompatibility),
|
||||
maxTokensField: Schema.optional(LanguageModelMaxTokensFieldCompatibility),
|
||||
}) {}
|
||||
|
||||
export namespace ModelCompatibility {
|
||||
export type Input = ModelCompatibility | ConstructorParameters<typeof ModelCompatibility>[0]
|
||||
export namespace LanguageModelCompatibility {
|
||||
export type Input = LanguageModelCompatibility | ConstructorParameters<typeof LanguageModelCompatibility>[0]
|
||||
|
||||
/** Normalize model/upstream compatibility metadata without projecting requests. */
|
||||
export const make = (input: Input) => (input instanceof ModelCompatibility ? input : new ModelCompatibility(input))
|
||||
export const make = (input: Input) =>
|
||||
input instanceof LanguageModelCompatibility ? input : new LanguageModelCompatibility(input)
|
||||
}
|
||||
|
||||
export class Model<Options extends ProviderOptions = ProviderOptions> {
|
||||
export class LanguageModel<Options extends ProviderOptions = ProviderOptions> {
|
||||
declare protected readonly _ProviderOptions: Options
|
||||
readonly id: ModelID
|
||||
readonly provider: ProviderID
|
||||
readonly route: AnyRoute
|
||||
readonly defaults?: ModelDefaults
|
||||
readonly compatibility?: ModelCompatibility
|
||||
readonly defaults?: LanguageModelDefaults
|
||||
readonly compatibility?: LanguageModelCompatibility
|
||||
|
||||
constructor(input: Model.ConstructorInput) {
|
||||
constructor(input: LanguageModel.ConstructorInput) {
|
||||
this.id = input.id
|
||||
this.provider = input.provider
|
||||
this.route = input.route
|
||||
@@ -199,17 +204,18 @@ 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<Options extends ProviderOptions = ProviderOptions>(input: LanguageModel.Input) {
|
||||
return new LanguageModel<Options>({
|
||||
id: ModelID.make(input.id),
|
||||
provider: ProviderID.make(input.provider),
|
||||
route: input.route,
|
||||
defaults: input.defaults === undefined ? undefined : ModelDefaults.make(input.defaults),
|
||||
compatibility: input.compatibility === undefined ? undefined : ModelCompatibility.make(input.compatibility),
|
||||
defaults: input.defaults === undefined ? undefined : LanguageModelDefaults.make(input.defaults),
|
||||
compatibility:
|
||||
input.compatibility === undefined ? undefined : LanguageModelCompatibility.make(input.compatibility),
|
||||
})
|
||||
}
|
||||
|
||||
static input<Options extends ProviderOptions>(model: Model<Options>): Model.ConstructorInput {
|
||||
static input<Options extends ProviderOptions>(model: LanguageModel<Options>): LanguageModel.ConstructorInput {
|
||||
return {
|
||||
id: model.id,
|
||||
provider: model.provider,
|
||||
@@ -219,37 +225,40 @@ export class Model<Options extends ProviderOptions = ProviderOptions> {
|
||||
}
|
||||
}
|
||||
|
||||
static update<Options extends ProviderOptions>(model: Model<Options>, patch: Partial<Model.Input>) {
|
||||
static update<Options extends ProviderOptions>(model: LanguageModel<Options>, patch: Partial<LanguageModel.Input>) {
|
||||
if (Object.keys(patch).length === 0) return model
|
||||
return Model.make<Options>({
|
||||
...Model.input(model),
|
||||
return LanguageModel.make<Options>({
|
||||
...LanguageModel.input(model),
|
||||
...patch,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Model {
|
||||
export namespace LanguageModel {
|
||||
export type ConstructorInput = {
|
||||
readonly id: ModelID
|
||||
readonly provider: ProviderID
|
||||
readonly route: AnyRoute
|
||||
readonly defaults?: ModelDefaults
|
||||
readonly compatibility?: ModelCompatibility
|
||||
readonly defaults?: LanguageModelDefaults
|
||||
readonly compatibility?: LanguageModelCompatibility
|
||||
}
|
||||
|
||||
export type Input = Omit<ConstructorInput, "id" | "provider" | "defaults" | "compatibility"> & {
|
||||
readonly id: string | ModelID
|
||||
readonly provider: string | ProviderID
|
||||
readonly defaults?: ModelDefaults.Input
|
||||
readonly compatibility?: ModelCompatibility.Input
|
||||
readonly defaults?: LanguageModelDefaults.Input
|
||||
readonly compatibility?: LanguageModelCompatibility.Input
|
||||
}
|
||||
}
|
||||
|
||||
export type ModelInput = Model.Input
|
||||
export type LanguageModelInput = LanguageModel.Input
|
||||
|
||||
export type ModelProviderOptions<SelectedModel> = SelectedModel extends Model<infer Options> ? Options : never
|
||||
export type LanguageModelProviderOptions<SelectedModel> =
|
||||
SelectedModel extends LanguageModel<infer Options> ? Options : never
|
||||
|
||||
export const ModelSchema = Schema.declare((value): value is Model => value instanceof Model, { expected: "LLM.Model" })
|
||||
export const LanguageModelSchema = Schema.declare((value): value is LanguageModel => value instanceof LanguageModel, {
|
||||
expected: "LLM.LanguageModel",
|
||||
})
|
||||
|
||||
export class CacheHint extends Schema.Class<CacheHint>("LLM.CacheHint")({
|
||||
type: Schema.Literals(["ephemeral", "persistent"]),
|
||||
|
||||
@@ -5,13 +5,13 @@ import {
|
||||
LLMEvent,
|
||||
LLMResponse,
|
||||
type FinishReasonDetails,
|
||||
type LLMError,
|
||||
type AIError,
|
||||
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 Response = readonly LLMEvent[] | Stream.Stream<LLMEvent, AIError>
|
||||
|
||||
export type Gate = Readonly<{ started: Effect.Effect<void>; release: Effect.Effect<void> }>
|
||||
|
||||
@@ -63,7 +63,7 @@ export const textWithUsage = (value: string, id: string, inputTokens: number) =>
|
||||
|
||||
export const tool = (id: string, name: string, input: unknown) => toolCalls(LLMEvent.toolCall({ id, name, input }))
|
||||
|
||||
export const failAfter = (error: LLMError, ...events: readonly LLMEvent[]) =>
|
||||
export const failAfter = (error: AIError, ...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)
|
||||
|
||||
@@ -3,11 +3,11 @@ 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 { LanguageModel } from "../src/schema"
|
||||
import { testEffect } from "./lib/effect"
|
||||
import { dynamicResponse } from "./lib/http"
|
||||
|
||||
const updateModel = (model: Model, patch: Partial<Model.Input>) => Model.update(model, patch)
|
||||
const updateModel = (model: LanguageModel, patch: Partial<LanguageModel.Input>) => LanguageModel.update(model, patch)
|
||||
|
||||
const Json = Schema.fromJsonString(Schema.Unknown)
|
||||
const encodeJson = Schema.encodeSync(Json)
|
||||
@@ -86,7 +86,7 @@ const configuredGemini = gemini.with({ endpoint: { baseURL: "https://fake.local"
|
||||
|
||||
const request = LLM.request({
|
||||
id: "req_1",
|
||||
model: Model.make({
|
||||
model: LanguageModel.make({
|
||||
id: "fake-model",
|
||||
provider: "fake-provider",
|
||||
route: configuredFake,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Config } from "effect"
|
||||
import { Auth } from "../src/route"
|
||||
import type { ModelFactory } from "../src/route/auth-options"
|
||||
import type { LanguageModelFactory } from "../src/route/auth-options"
|
||||
import * as OpenAIChat from "../src/protocols/openai-chat"
|
||||
import * as AmazonBedrock from "../src/providers/amazon-bedrock"
|
||||
import * as Anthropic from "../src/providers/anthropic"
|
||||
@@ -23,13 +23,13 @@ type BaseOptions = {
|
||||
readonly headers?: Record<string, string>
|
||||
}
|
||||
|
||||
type Model = {
|
||||
type LanguageModel = {
|
||||
readonly id: string
|
||||
}
|
||||
|
||||
declare const auth: Auth.Definition
|
||||
declare const optionalAuthModel: ModelFactory<BaseOptions, "optional", Model>
|
||||
declare const requiredAuthModel: ModelFactory<BaseOptions, "required", Model>
|
||||
declare const optionalAuthModel: LanguageModelFactory<BaseOptions, "optional", LanguageModel>
|
||||
declare const requiredAuthModel: LanguageModelFactory<BaseOptions, "required", LanguageModel>
|
||||
const configApiKey = Config.redacted("OPENAI_API_KEY")
|
||||
|
||||
OpenAIChat.route.model({ id: "gpt-4.1-mini" })
|
||||
|
||||
@@ -4,12 +4,12 @@ import { Headers } from "effect/unstable/http"
|
||||
import { LLM } from "../src"
|
||||
import { Auth } from "../src/route/auth"
|
||||
import * as OpenAIChat from "../src/protocols/openai-chat"
|
||||
import { Model } from "../src/schema"
|
||||
import { LanguageModel } from "../src/schema"
|
||||
import { it } from "./lib/effect"
|
||||
|
||||
const request = LLM.request({
|
||||
id: "req_auth",
|
||||
model: Model.make({ id: "fake-model", provider: "fake", route: OpenAIChat.route }),
|
||||
model: LanguageModel.make({ id: "fake-model", provider: "fake", route: OpenAIChat.route }),
|
||||
prompt: "hello",
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { LLM, Message, ToolCallPart, ToolDefinition, ToolResultPart, type ContentPart, type Model } from "../src"
|
||||
import {
|
||||
LLM,
|
||||
Message,
|
||||
ToolCallPart,
|
||||
ToolDefinition,
|
||||
ToolResultPart,
|
||||
type ContentPart,
|
||||
type LanguageModel,
|
||||
} from "../src"
|
||||
|
||||
export const basicContinuation = ["system", "user-text", "assistant-text", "user-follow-up"] as const
|
||||
export const toolContinuation = ["tool-call", "tool-result"] as const
|
||||
@@ -40,7 +48,7 @@ export const continuationTool = ToolDefinition.make({
|
||||
|
||||
export function continuationRequest(input: {
|
||||
readonly id: string
|
||||
readonly model: Model
|
||||
readonly model: LanguageModel
|
||||
readonly features: ReadonlyArray<ContinuationFeature>
|
||||
readonly image?: string
|
||||
}) {
|
||||
|
||||
@@ -2,11 +2,11 @@ import { describe, expect, test } from "bun:test"
|
||||
import { LLM } from "../src"
|
||||
import * as OpenAIChat from "../src/protocols/openai-chat"
|
||||
import { Endpoint } from "../src/route"
|
||||
import { Model } from "../src/schema"
|
||||
import { LanguageModel } from "../src/schema"
|
||||
|
||||
const request = () =>
|
||||
LLM.request({
|
||||
model: Model.make({
|
||||
model: LanguageModel.make({
|
||||
id: "model-1",
|
||||
provider: "test",
|
||||
route: OpenAIChat.route,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer, Ref } from "effect"
|
||||
import { Headers, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
|
||||
import { LLM, LLMError } from "../src"
|
||||
import { LLM, AIError } from "../src"
|
||||
import { LLMClient, RequestExecutor } from "../src/route"
|
||||
import * as OpenAIChat from "../src/protocols/openai-chat"
|
||||
import { dynamicResponse } from "./lib/http"
|
||||
@@ -58,13 +58,13 @@ const countedResponsesLayer = (attempts: Ref.Ref<number>, responses: ReadonlyArr
|
||||
),
|
||||
)
|
||||
|
||||
const expectLLMError = (error: unknown) => {
|
||||
expect(error).toBeInstanceOf(LLMError)
|
||||
if (!(error instanceof LLMError)) throw new Error("expected LLMError")
|
||||
const expectAIError = (error: unknown) => {
|
||||
expect(error).toBeInstanceOf(AIError)
|
||||
if (!(error instanceof AIError)) throw new Error("expected AIError")
|
||||
return error
|
||||
}
|
||||
|
||||
const errorHttp = (error: LLMError) => ("http" in error.reason ? error.reason.http : undefined)
|
||||
const errorHttp = (error: AIError) => ("http" in error.reason ? error.reason.http : undefined)
|
||||
|
||||
describe("RequestExecutor", () => {
|
||||
it.effect("classifies context overflow responses", () =>
|
||||
@@ -72,7 +72,7 @@ describe("RequestExecutor", () => {
|
||||
const executor = yield* RequestExecutor.Service
|
||||
const error = yield* executor.execute(request).pipe(Effect.flip)
|
||||
|
||||
expectLLMError(error)
|
||||
expectAIError(error)
|
||||
expect(error.reason).toMatchObject({ _tag: "InvalidRequest", classification: "context-overflow" })
|
||||
}).pipe(
|
||||
Effect.provide(
|
||||
@@ -85,14 +85,17 @@ describe("RequestExecutor", () => {
|
||||
),
|
||||
)
|
||||
|
||||
it.effect("does not classify generic HTTP 413 payload errors as context overflow", () =>
|
||||
it.effect("classifies generic HTTP 413 payload errors", () =>
|
||||
Effect.gen(function* () {
|
||||
const executor = yield* RequestExecutor.Service
|
||||
const error = yield* executor.execute(request).pipe(Effect.flip)
|
||||
|
||||
expectLLMError(error)
|
||||
expect(error.reason).toMatchObject({ _tag: "InvalidRequest" })
|
||||
expect("classification" in error.reason ? error.reason.classification : undefined).toBeUndefined()
|
||||
expectAIError(error)
|
||||
expect(error.reason).toMatchObject({
|
||||
_tag: "InvalidRequest",
|
||||
classification: "payload-too-large",
|
||||
http: { response: { status: 413 } },
|
||||
})
|
||||
}).pipe(Effect.provide(responsesLayer([new Response("request too large", { status: 413 })]))),
|
||||
)
|
||||
|
||||
@@ -101,7 +104,7 @@ describe("RequestExecutor", () => {
|
||||
const executor = yield* RequestExecutor.Service
|
||||
const error = yield* executor.execute(request).pipe(Effect.flip)
|
||||
|
||||
expectLLMError(error)
|
||||
expectAIError(error)
|
||||
expect(error.reason).toMatchObject({ _tag: "InvalidRequest" })
|
||||
expect("classification" in error.reason ? error.reason.classification : undefined).toBeUndefined()
|
||||
}).pipe(Effect.provide(responsesLayer([new Response("invalid parameter", { status: 400 })]))),
|
||||
@@ -114,7 +117,7 @@ describe("RequestExecutor", () => {
|
||||
const executor = yield* RequestExecutor.Service
|
||||
const error = yield* executor.execute(request).pipe(Effect.flip)
|
||||
|
||||
expectLLMError(error)
|
||||
expectAIError(error)
|
||||
expect(error.reason).toMatchObject({ _tag: "RateLimit" })
|
||||
}).pipe(Effect.provide(responsesLayer([new Response(body, { status: 400 })])))
|
||||
|
||||
@@ -131,7 +134,7 @@ describe("RequestExecutor", () => {
|
||||
const executor = yield* RequestExecutor.Service
|
||||
const error = yield* executor.execute(request).pipe(Effect.flip)
|
||||
|
||||
expectLLMError(error)
|
||||
expectAIError(error)
|
||||
expect(error.reason).toMatchObject({ _tag: "ProviderInternal" })
|
||||
}).pipe(Effect.provide(responsesLayer([new Response(body, { status: 400 })])))
|
||||
|
||||
@@ -145,7 +148,7 @@ describe("RequestExecutor", () => {
|
||||
const executor = yield* RequestExecutor.Service
|
||||
const error = yield* executor.execute(request).pipe(Effect.flip)
|
||||
|
||||
expectLLMError(error)
|
||||
expectAIError(error)
|
||||
expect(error).toMatchObject({
|
||||
reason: {
|
||||
_tag: "RateLimit",
|
||||
@@ -187,7 +190,7 @@ describe("RequestExecutor", () => {
|
||||
const executor = yield* RequestExecutor.Service
|
||||
const error = yield* executor.execute(request).pipe(Effect.flip)
|
||||
|
||||
expectLLMError(error)
|
||||
expectAIError(error)
|
||||
expect(errorHttp(error)?.request.headers["x-safe"]).toBe("<redacted>")
|
||||
expect(errorHttp(error)?.response?.headers["x-safe"]).toBe("<redacted>")
|
||||
}).pipe(
|
||||
@@ -201,7 +204,7 @@ describe("RequestExecutor", () => {
|
||||
const executor = yield* RequestExecutor.Service
|
||||
const error = yield* executor.execute(request).pipe(Effect.flip)
|
||||
|
||||
expectLLMError(error)
|
||||
expectAIError(error)
|
||||
expect(error.reason).toMatchObject({ _tag: "RateLimit" })
|
||||
expect(error.reason._tag === "RateLimit" ? error.reason.rateLimit : undefined).toEqual({
|
||||
retryAfterMs: 0,
|
||||
@@ -234,7 +237,7 @@ describe("RequestExecutor", () => {
|
||||
const executor = yield* RequestExecutor.Service
|
||||
const error = yield* executor.execute(request).pipe(Effect.flip)
|
||||
|
||||
expectLLMError(error)
|
||||
expectAIError(error)
|
||||
expect(error.reason).toMatchObject({ _tag: "ProviderInternal" })
|
||||
expect(errorHttp(error)?.rateLimit).toEqual({
|
||||
retryAfterMs: 0,
|
||||
@@ -277,7 +280,7 @@ describe("RequestExecutor", () => {
|
||||
),
|
||||
)
|
||||
|
||||
expectLLMError(error)
|
||||
expectAIError(error)
|
||||
expect(error.reason).toMatchObject({ _tag: "ProviderInternal", status: 503 })
|
||||
expect(yield* Ref.get(attempts)).toBe(1)
|
||||
}),
|
||||
@@ -290,7 +293,7 @@ describe("RequestExecutor", () => {
|
||||
const executor = yield* RequestExecutor.Service
|
||||
const error = yield* executor.execute(request).pipe(Effect.flip)
|
||||
|
||||
expectLLMError(error)
|
||||
expectAIError(error)
|
||||
expect(error.reason).toMatchObject({ _tag: "ProviderInternal", status })
|
||||
}).pipe(
|
||||
Effect.provide(
|
||||
@@ -313,7 +316,7 @@ describe("RequestExecutor", () => {
|
||||
const executor = yield* RequestExecutor.Service
|
||||
const error = yield* executor.execute(request).pipe(Effect.flip)
|
||||
|
||||
expectLLMError(error)
|
||||
expectAIError(error)
|
||||
expect(error.reason).toMatchObject({ _tag: "Authentication" })
|
||||
expect(errorHttp(error)?.bodyTruncated).toBe(true)
|
||||
expect(errorHttp(error)?.body).toHaveLength(16_384)
|
||||
@@ -332,7 +335,7 @@ describe("RequestExecutor", () => {
|
||||
const executor = yield* RequestExecutor.Service
|
||||
const error = yield* executor.execute(request).pipe(Effect.flip)
|
||||
|
||||
expectLLMError(error)
|
||||
expectAIError(error)
|
||||
expect(errorHttp(error)?.body).toContain('"key":"<redacted>"')
|
||||
expect(errorHttp(error)?.body).toContain("api_key=<redacted>")
|
||||
expect(errorHttp(error)?.body).not.toContain("body-secret")
|
||||
@@ -353,7 +356,7 @@ describe("RequestExecutor", () => {
|
||||
const executor = yield* RequestExecutor.Service
|
||||
const error = yield* executor.execute(secretRequest).pipe(Effect.flip)
|
||||
|
||||
expectLLMError(error)
|
||||
expectAIError(error)
|
||||
expect(errorHttp(error)?.body).toContain("provider echoed <redacted>")
|
||||
expect(errorHttp(error)?.body).toContain("authorization <redacted>")
|
||||
expect(errorHttp(error)?.body).not.toContain("query-secret-123")
|
||||
@@ -392,7 +395,7 @@ describe("RequestExecutor", () => {
|
||||
Effect.flip,
|
||||
)
|
||||
|
||||
expectLLMError(error)
|
||||
expectAIError(error)
|
||||
expect(error.reason).toMatchObject({ _tag: "InvalidProviderOutput" })
|
||||
expect(yield* Ref.get(attempts)).toBe(1)
|
||||
}),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { ImageInput, LLM, LLMClient, Provider } from "@opencode-ai/ai"
|
||||
import { AIError, ImageInput, LanguageModel, LLM, LLMClient, Provider } from "@opencode-ai/ai"
|
||||
import { Route, Protocol } from "@opencode-ai/ai/route"
|
||||
import { Provider as ProviderSubpath } from "@opencode-ai/ai/provider"
|
||||
import {
|
||||
@@ -26,6 +26,8 @@ describe("public exports", () => {
|
||||
expect(LLM.request).toBeFunction()
|
||||
expect(LLMClient.Service).toBeFunction()
|
||||
expect(LLMClient.layer).toBeDefined()
|
||||
expect(AIError).toBeFunction()
|
||||
expect(LanguageModel.make).toBeFunction()
|
||||
expect(ImageInput.bytes).toBeFunction()
|
||||
expect(Provider.make).toBeFunction()
|
||||
expect(ProviderSubpath.make).toBe(Provider.make)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
import { Schema } from "effect"
|
||||
import { LLM, type Model, type ModelProviderOptions, type ProviderOptions } from "../src"
|
||||
import { LLM, type LanguageModel, type LanguageModelProviderOptions, type ProviderOptions } from "../src"
|
||||
import { OpenAIChat } from "../src/protocols"
|
||||
|
||||
interface ExampleOptions {
|
||||
@@ -40,8 +40,8 @@ LLM.generateObject({
|
||||
providerOptions: { example: { mode: false } },
|
||||
})
|
||||
|
||||
declare const generic: Model
|
||||
declare const generic: LanguageModel
|
||||
LLM.request({ model: generic, prompt: "Hello", providerOptions: { arbitrary: { option: true } } })
|
||||
|
||||
const options: ModelProviderOptions<typeof model> = { example: { mode: "fast" } }
|
||||
const options: LanguageModelProviderOptions<typeof model> = { example: { mode: "fast" } }
|
||||
void options
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
GenerationOptions,
|
||||
LLMRequest,
|
||||
Message,
|
||||
Model,
|
||||
LanguageModel,
|
||||
ToolCallPart,
|
||||
ToolChoice,
|
||||
ToolDefinition,
|
||||
@@ -20,13 +20,13 @@ describe("llm constructors", () => {
|
||||
test("builds canonical schema classes from ergonomic input", () => {
|
||||
const request = LLM.request({
|
||||
id: "req_1",
|
||||
model: Model.make({ id: "fake-model", provider: "fake", route: chatRoute }),
|
||||
model: LanguageModel.make({ id: "fake-model", provider: "fake", route: chatRoute }),
|
||||
system: "You are concise.",
|
||||
prompt: "Say hello.",
|
||||
})
|
||||
|
||||
expect(request).toBeInstanceOf(LLMRequest)
|
||||
expect(request.model).toBeInstanceOf(Model)
|
||||
expect(request.model).toBeInstanceOf(LanguageModel)
|
||||
expect(request.messages[0]).toBeInstanceOf(Message)
|
||||
expect(request.system).toEqual([{ type: "text", text: "You are concise." }])
|
||||
expect(request.messages[0]?.content).toEqual([{ type: "text", text: "Say hello." }])
|
||||
@@ -37,7 +37,7 @@ describe("llm constructors", () => {
|
||||
test("updates requests without spreading schema class instances", () => {
|
||||
const base = LLM.request({
|
||||
id: "req_1",
|
||||
model: Model.make({ id: "fake-model", provider: "fake", route: chatRoute }),
|
||||
model: LanguageModel.make({ id: "fake-model", provider: "fake", route: chatRoute }),
|
||||
prompt: "Say hello.",
|
||||
})
|
||||
const updated = LLMRequest.update(base, {
|
||||
@@ -54,7 +54,7 @@ describe("llm constructors", () => {
|
||||
|
||||
test("keeps request options separate from route defaults", () => {
|
||||
const request = LLM.request({
|
||||
model: Model.make({
|
||||
model: LanguageModel.make({
|
||||
id: "fake-model",
|
||||
provider: "fake",
|
||||
route: chatRoute.with({
|
||||
@@ -81,7 +81,7 @@ describe("llm constructors", () => {
|
||||
test("updates canonical requests from the request datatype", () => {
|
||||
const base = LLM.request({
|
||||
id: "req_1",
|
||||
model: Model.make({ id: "fake-model", provider: "fake", route: chatRoute }),
|
||||
model: LanguageModel.make({ id: "fake-model", provider: "fake", route: chatRoute }),
|
||||
prompt: "Say hello.",
|
||||
})
|
||||
const updated = LLMRequest.update(base, { messages: [...base.messages, Message.assistant("Hi.")] })
|
||||
@@ -94,19 +94,19 @@ describe("llm constructors", () => {
|
||||
})
|
||||
|
||||
test("updates canonical models from the model datatype", () => {
|
||||
const base = Model.make({
|
||||
const base = LanguageModel.make({
|
||||
id: "fake-model",
|
||||
provider: "fake",
|
||||
route: chatRoute,
|
||||
})
|
||||
const updated = Model.update(base, {
|
||||
const updated = LanguageModel.update(base, {
|
||||
route: responsesRoute,
|
||||
defaults: { generation: { maxTokens: 20 } },
|
||||
compatibility: { toolSchema: "gemini" },
|
||||
})
|
||||
const updatedInput = Model.input(updated)
|
||||
const updatedInput = LanguageModel.input(updated)
|
||||
|
||||
expect(updated).toBeInstanceOf(Model)
|
||||
expect(updated).toBeInstanceOf(LanguageModel)
|
||||
expect(String(updated.id)).toBe("fake-model")
|
||||
expect(updated.route).toBe(responsesRoute)
|
||||
expect(updated.defaults?.generation).toEqual({ maxTokens: 20 })
|
||||
@@ -114,7 +114,7 @@ describe("llm constructors", () => {
|
||||
expect(updatedInput.defaults).toBe(updated.defaults)
|
||||
expect(updatedInput.compatibility).toBe(updated.compatibility)
|
||||
expect(String(updatedInput.provider)).toBe("fake")
|
||||
expect(Model.update(updated, {})).toBe(updated)
|
||||
expect(LanguageModel.update(updated, {})).toBe(updated)
|
||||
})
|
||||
|
||||
test("carries model defaults and compatibility through route model selection", () => {
|
||||
@@ -155,7 +155,7 @@ describe("llm constructors", () => {
|
||||
expect(ToolChoice.make("required")).toEqual(new ToolChoice({ type: "required" }))
|
||||
expect(
|
||||
LLM.request({
|
||||
model: Model.make({
|
||||
model: LanguageModel.make({
|
||||
id: "fake-model",
|
||||
provider: "fake",
|
||||
route: chatRoute,
|
||||
@@ -181,7 +181,7 @@ describe("llm constructors", () => {
|
||||
{ type: "text", text: "Use parameterized SQL.", cache: new CacheHint({ type: "ephemeral" }) },
|
||||
])
|
||||
const request = LLM.request({
|
||||
model: Model.make({ id: "fake-model", provider: "fake", route: chatRoute }),
|
||||
model: LanguageModel.make({ id: "fake-model", provider: "fake", route: chatRoute }),
|
||||
system: "Initial operator prompt.",
|
||||
messages: [Message.user("Review this."), update],
|
||||
})
|
||||
|
||||
@@ -6,7 +6,6 @@ describe("provider error classification", () => {
|
||||
test("classifies provider token limit messages as context overflow", () => {
|
||||
const messages = [
|
||||
"tokens in request more than max tokens allowed",
|
||||
'{"error":{"type":"request_too_large","message":"Request exceeds the maximum size"}}',
|
||||
"Requested token count exceeds the model's maximum context length of 131072 tokens.",
|
||||
"Input length (265330) exceeds model's maximum context length (262144).",
|
||||
"Input length 131393 exceeds the maximum allowed input length of 131040 tokens.",
|
||||
@@ -19,6 +18,24 @@ describe("provider error classification", () => {
|
||||
expect(messages.every(isContextOverflow)).toBe(true)
|
||||
})
|
||||
|
||||
test("classifies request size failures separately from context overflow", () => {
|
||||
const failures = [
|
||||
classifyProviderFailure({ message: "request too large", status: 413 }),
|
||||
classifyProviderFailure({
|
||||
message: '{"error":{"type":"request_too_large","message":"Request exceeds the maximum size"}}',
|
||||
status: 400,
|
||||
}),
|
||||
classifyProviderFailure({ message: "upstream request entity too large", status: 502 }),
|
||||
]
|
||||
|
||||
expect(failures).toEqual(
|
||||
failures.map((failure) =>
|
||||
expect.objectContaining({ _tag: "InvalidRequest", classification: "payload-too-large" }),
|
||||
),
|
||||
)
|
||||
expect(isContextOverflow("413 status code (no body)")).toBe(false)
|
||||
})
|
||||
|
||||
test("does not classify rate limits as context overflow", () => {
|
||||
const messages = [
|
||||
"Throttling error: Too many tokens, please wait before trying again.",
|
||||
@@ -59,9 +76,10 @@ describe("provider error classification", () => {
|
||||
})
|
||||
|
||||
test("classifies transient client statuses as provider internal", () => {
|
||||
expect(
|
||||
[408, 409].map((status) => classifyProviderFailure({ message: `HTTP ${status}`, status })._tag),
|
||||
).toEqual(["ProviderInternal", "ProviderInternal"])
|
||||
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", () => {
|
||||
|
||||
@@ -23,12 +23,16 @@ describe("provider package entrypoints", () => {
|
||||
import("@opencode-ai/ai/providers/google-vertex/messages"),
|
||||
import("@opencode-ai/ai/providers/openrouter"),
|
||||
import("@opencode-ai/ai/providers/xai"),
|
||||
import("@opencode-ai/ai/providers/amazon-bedrock/mantle"),
|
||||
import("@opencode-ai/ai/providers/amazon-bedrock/mantle/chat"),
|
||||
import("@opencode-ai/ai/providers/amazon-bedrock/mantle/responses"),
|
||||
])
|
||||
|
||||
for (const module of modules) expect(module.model).toBeFunction()
|
||||
expect(modules[0].model).toBe(modules[1].model)
|
||||
expect(modules[8].model).toBe(modules[9].model)
|
||||
expect(modules[12].model).toBe(modules[13].model)
|
||||
expect(modules[19].model).toBe(modules[20].model)
|
||||
})
|
||||
|
||||
test("maps OpenRouter and xAI package settings onto executable models", async () => {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Provider } from "../src/provider"
|
||||
import { ProviderID, type Model } from "../src/schema"
|
||||
import { ProviderID, type LanguageModel } from "../src/schema"
|
||||
|
||||
declare const model: (id: string) => Model
|
||||
declare const requiredModel: (id: string, options: { readonly baseURL: string }) => Model
|
||||
declare const chat: (id: string, options: { readonly apiKey: string }) => Model
|
||||
declare const model: (id: string) => LanguageModel
|
||||
declare const requiredModel: (id: string, options: { readonly baseURL: string }) => LanguageModel
|
||||
declare const chat: (id: string, options: { readonly apiKey: string }) => LanguageModel
|
||||
|
||||
Provider.make({
|
||||
id: ProviderID.make("example"),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { LLM, LLMError, Message, ToolCallPart } from "../../src"
|
||||
import { LLM, AIError, Message, ToolCallPart } from "../../src"
|
||||
import { LLMClient } from "../../src/route"
|
||||
import * as Anthropic from "../../src/providers/anthropic"
|
||||
import { weatherToolName } from "../recorded-scenarios"
|
||||
@@ -37,7 +37,7 @@ describe("Anthropic Messages sad-path recorded", () => {
|
||||
Effect.gen(function* () {
|
||||
const error = yield* LLMClient.generate(malformedToolOrderRequest).pipe(Effect.flip)
|
||||
|
||||
expect(error).toBeInstanceOf(LLMError)
|
||||
expect(error).toBeInstanceOf(AIError)
|
||||
expect(error.reason).toMatchObject({ _tag: "InvalidRequest" })
|
||||
expect(error.message).toContain("HTTP 400")
|
||||
}),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { CacheHint, LLM, LLMError, LLMRequest, Message, ToolCallPart, ToolDefinition, Usage } from "../../src"
|
||||
import { CacheHint, LLM, AIError, 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"
|
||||
@@ -684,9 +684,7 @@ describe("Anthropic Messages route", () => {
|
||||
),
|
||||
)
|
||||
|
||||
expect(response.toolCalls).toMatchObject([
|
||||
{ id: "call_1", name: "lookup", input: { query: "weather" } },
|
||||
])
|
||||
expect(response.toolCalls).toMatchObject([{ id: "call_1", name: "lookup", input: { query: "weather" } }])
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -935,7 +933,7 @@ describe("Anthropic Messages route", () => {
|
||||
|
||||
const error = yield* LLMClient.generate(request).pipe(Effect.provide(fixedResponse(body)), Effect.flip)
|
||||
|
||||
expect(error).toBeInstanceOf(LLMError)
|
||||
expect(error).toBeInstanceOf(AIError)
|
||||
expect(error.message).toContain("Invalid JSON input for anthropic-messages tool call web_search")
|
||||
}),
|
||||
)
|
||||
@@ -1009,7 +1007,7 @@ describe("Anthropic Messages route", () => {
|
||||
Effect.flip,
|
||||
)
|
||||
|
||||
expect(error).toBeInstanceOf(LLMError)
|
||||
expect(error).toBeInstanceOf(AIError)
|
||||
expect(error.reason).toMatchObject({ _tag: "InvalidRequest" })
|
||||
expect(error.message).toContain("HTTP 400")
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { LLM } from "../../src"
|
||||
import { AmazonBedrockMantle } from "../../src/providers"
|
||||
import { compileRequest, LLMClient } from "../../src/route/client"
|
||||
import { it } from "../lib/effect"
|
||||
import { dynamicResponse } from "../lib/http"
|
||||
import { recordedTests } from "../recorded-test"
|
||||
|
||||
const credentials = {
|
||||
region: "us-east-2",
|
||||
accessKeyId: "AKIAIOSFODNN7EXAMPLE",
|
||||
secretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
||||
}
|
||||
|
||||
describe("Amazon Bedrock Mantle provider", () => {
|
||||
it.effect("uses Chat by default and exposes Responses", () =>
|
||||
Effect.gen(function* () {
|
||||
const provider = AmazonBedrockMantle.configure({ credentials })
|
||||
const chat = yield* compileRequest(LLM.request({ model: provider.model("openai.gpt-oss-120b"), prompt: "Hi" }))
|
||||
const responses = yield* compileRequest(
|
||||
LLM.request({ model: provider.responses("openai.gpt-oss-120b"), prompt: "Hi" }),
|
||||
)
|
||||
|
||||
expect(chat).toMatchObject({
|
||||
route: "bedrock-mantle-chat",
|
||||
protocol: "openai-chat",
|
||||
body: { model: "openai.gpt-oss-120b" },
|
||||
})
|
||||
expect(responses).toMatchObject({
|
||||
route: "bedrock-mantle-responses",
|
||||
protocol: "openai-responses",
|
||||
body: { model: "openai.gpt-oss-120b", store: false },
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("uses the Mantle endpoint and signing service", () =>
|
||||
Effect.gen(function* () {
|
||||
const seen: Array<{ readonly url: string; readonly authorization: string | undefined }> = []
|
||||
const model = AmazonBedrockMantle.configure({ credentials, region: "us-west-1" }).responses("openai.gpt-oss-120b")
|
||||
yield* LLMClient.generate(LLM.request({ model, prompt: "Hi" })).pipe(
|
||||
Effect.provide(
|
||||
dynamicResponse((input) =>
|
||||
Effect.gen(function* () {
|
||||
const request = yield* HttpClientRequest.toWeb(input.request)
|
||||
seen.push({ url: request.url, authorization: request.headers.get("authorization") ?? undefined })
|
||||
return input.respond("", { headers: { "content-type": "text/event-stream" } })
|
||||
}),
|
||||
),
|
||||
),
|
||||
Effect.flip,
|
||||
)
|
||||
|
||||
expect(seen[0]?.url).toBe("https://bedrock-mantle.us-west-1.api.aws/v1/responses")
|
||||
expect(seen[0]?.authorization).toContain("/us-west-1/bedrock-mantle/aws4_request")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("supports bearer authentication and custom base URLs", () =>
|
||||
Effect.gen(function* () {
|
||||
const seen: Array<{ readonly url: string; readonly authorization: string | undefined }> = []
|
||||
const model = AmazonBedrockMantle.configure({
|
||||
apiKey: "test-key",
|
||||
baseURL: "https://mantle.test/v1",
|
||||
}).chat("openai.gpt-oss-safeguard-20b")
|
||||
yield* LLMClient.generate(LLM.request({ model, prompt: "Hi" })).pipe(
|
||||
Effect.provide(
|
||||
dynamicResponse((input) =>
|
||||
Effect.gen(function* () {
|
||||
const request = yield* HttpClientRequest.toWeb(input.request)
|
||||
seen.push({ url: request.url, authorization: request.headers.get("authorization") ?? undefined })
|
||||
return input.respond("", { headers: { "content-type": "text/event-stream" } })
|
||||
}),
|
||||
),
|
||||
),
|
||||
Effect.flip,
|
||||
)
|
||||
|
||||
expect(seen).toEqual([{ url: "https://mantle.test/v1/chat/completions", authorization: "Bearer test-key" }])
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
const recorded = recordedTests({
|
||||
prefix: "bedrock-mantle",
|
||||
provider: "amazon-bedrock",
|
||||
protocol: "openai-responses",
|
||||
requires: ["AWS_BEARER_TOKEN_BEDROCK"],
|
||||
metadata: { model: "openai.gpt-oss-120b" },
|
||||
})
|
||||
|
||||
describe("Amazon Bedrock Mantle recorded", () => {
|
||||
recorded.effect("streams text", () =>
|
||||
Effect.gen(function* () {
|
||||
const response = yield* LLMClient.generate(
|
||||
LLM.request({
|
||||
model: AmazonBedrockMantle.configure({
|
||||
apiKey: process.env.AWS_BEARER_TOKEN_BEDROCK ?? "fixture",
|
||||
region: "us-east-1",
|
||||
}).responses("openai.gpt-oss-120b"),
|
||||
prompt: "Reply with exactly: hello",
|
||||
generation: { maxTokens: 256, temperature: 0 },
|
||||
}),
|
||||
)
|
||||
|
||||
expect(response.text.trim().toLowerCase()).toBe("hello")
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { LLM, LLMError, LLMRequest, Message, ToolCallPart, ToolDefinition, Usage } from "../../src"
|
||||
import { LLM, AIError, 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"
|
||||
@@ -712,7 +712,7 @@ describe("Gemini route", () => {
|
||||
Effect.flip,
|
||||
)
|
||||
|
||||
expect(error).toBeInstanceOf(LLMError)
|
||||
expect(error).toBeInstanceOf(AIError)
|
||||
expect(error.reason).toMatchObject({ _tag: "InvalidProviderOutput" })
|
||||
expect(error.message).toContain("Invalid google/gemini stream event")
|
||||
}),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { LLM, LLMEvent, LLMResponse, Model } from "../../src"
|
||||
import { LLM, LLMEvent, LLMResponse, LanguageModel } from "../../src"
|
||||
import { OpenAIChat } from "../../src/protocols/openai-chat"
|
||||
import * as OpenAICompatible from "../../src/providers/openai-compatible"
|
||||
import * as OpenRouter from "../../src/providers/openrouter"
|
||||
@@ -12,7 +12,7 @@ import { expectWeatherToolLoop, goldenWeatherToolLoopRequest, runWeatherToolLoop
|
||||
const cases = [
|
||||
{
|
||||
name: "OpenRouter",
|
||||
model: Model.update(
|
||||
model: LanguageModel.update(
|
||||
OpenRouter.configure({
|
||||
apiKey: process.env.OPENROUTER_API_KEY ?? "fixture",
|
||||
providerOptions: { openrouter: { reasoning: { max_tokens: 1024 } } },
|
||||
@@ -25,7 +25,7 @@ const cases = [
|
||||
},
|
||||
{
|
||||
name: "Vercel AI Gateway",
|
||||
model: Model.update(
|
||||
model: LanguageModel.update(
|
||||
OpenAICompatible.configure({
|
||||
provider: "vercel-ai-gateway",
|
||||
baseURL: "https://ai-gateway.vercel.sh/v1",
|
||||
|
||||
@@ -4,11 +4,11 @@ import { HttpClientRequest } from "effect/unstable/http"
|
||||
import {
|
||||
HttpOptions,
|
||||
LLM,
|
||||
LLMError,
|
||||
AIError,
|
||||
LLMEvent,
|
||||
LLMRequest,
|
||||
Message,
|
||||
Model,
|
||||
LanguageModel,
|
||||
ToolCallPart,
|
||||
ToolDefinition,
|
||||
Usage,
|
||||
@@ -104,7 +104,7 @@ describe("OpenAI Chat route", () => {
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
LLM.request({
|
||||
model: Model.update(model, { compatibility: { reasoningField: "vendor_reasoning" } }),
|
||||
model: LanguageModel.update(model, { compatibility: { reasoningField: "vendor_reasoning" } }),
|
||||
messages: [
|
||||
Message.assistant([
|
||||
{
|
||||
@@ -130,7 +130,7 @@ describe("OpenAI Chat route", () => {
|
||||
Effect.gen(function* () {
|
||||
const error = yield* compileRequest(
|
||||
LLM.request({
|
||||
model: Model.update(model, { compatibility: { reasoningField: "content" } }),
|
||||
model: LanguageModel.update(model, { compatibility: { reasoningField: "content" } }),
|
||||
messages: [Message.assistant([{ type: "reasoning", text: "thinking" }])],
|
||||
}),
|
||||
).pipe(Effect.flip)
|
||||
@@ -171,7 +171,9 @@ describe("OpenAI Chat route", () => {
|
||||
it.effect("adds native query params to the Chat Completions URL", () =>
|
||||
LLMClient.generate(
|
||||
LLMRequest.update(request, {
|
||||
model: Model.update(model, { route: model.route.with({ endpoint: { query: { "api-version": "v1" } } }) }),
|
||||
model: LanguageModel.update(model, {
|
||||
route: model.route.with({ endpoint: { query: { "api-version": "v1" } } }),
|
||||
}),
|
||||
}),
|
||||
).pipe(
|
||||
Effect.provide(
|
||||
@@ -624,7 +626,7 @@ describe("OpenAI Chat route", () => {
|
||||
|
||||
it.effect("parses and replays a configured custom reasoning field", () =>
|
||||
Effect.gen(function* () {
|
||||
const custom = Model.update(model, { compatibility: { reasoningField: "vendor_reasoning" } })
|
||||
const custom = LanguageModel.update(model, { compatibility: { reasoningField: "vendor_reasoning" } })
|
||||
const response = yield* LLMClient.generate(LLMRequest.update(request, { model: custom })).pipe(
|
||||
Effect.provide(
|
||||
fixedResponse(
|
||||
@@ -1172,7 +1174,7 @@ describe("OpenAI Chat route", () => {
|
||||
Effect.flip,
|
||||
)
|
||||
|
||||
expect(error).toBeInstanceOf(LLMError)
|
||||
expect(error).toBeInstanceOf(AIError)
|
||||
expect(error.reason).toMatchObject({ _tag: "InvalidRequest" })
|
||||
expect(error.message).toContain("HTTP 400")
|
||||
}),
|
||||
|
||||
@@ -3,11 +3,11 @@ import { ConfigProvider, Effect, Layer, Stream } from "effect"
|
||||
import { Headers, HttpClientRequest } from "effect/unstable/http"
|
||||
import {
|
||||
LLM,
|
||||
LLMError,
|
||||
AIError,
|
||||
LLMEvent,
|
||||
LLMRequest,
|
||||
Message,
|
||||
Model,
|
||||
LanguageModel,
|
||||
ToolCallPart,
|
||||
ToolDefinition,
|
||||
ToolResultPart,
|
||||
@@ -304,7 +304,9 @@ describe("OpenAI Responses route", () => {
|
||||
Effect.gen(function* () {
|
||||
yield* LLMClient.generate(
|
||||
LLMRequest.update(request, {
|
||||
model: Model.update(model, { route: model.route.with({ endpoint: { query: { "api-version": "v1" } } }) }),
|
||||
model: LanguageModel.update(model, {
|
||||
route: model.route.with({ endpoint: { query: { "api-version": "v1" } } }),
|
||||
}),
|
||||
}),
|
||||
).pipe(
|
||||
Effect.provide(
|
||||
@@ -985,9 +987,7 @@ describe("OpenAI Responses route", () => {
|
||||
|
||||
for (const event of events) {
|
||||
const error = yield* LLMClient.generate(request).pipe(
|
||||
Effect.provide(
|
||||
fixedResponse(sseEvents(event, { type: "response.completed", response: { id: "resp_1" } })),
|
||||
),
|
||||
Effect.provide(fixedResponse(sseEvents(event, { type: "response.completed", response: { id: "resp_1" } }))),
|
||||
Effect.flip,
|
||||
)
|
||||
expect(error.reason._tag).toBe("InvalidProviderOutput")
|
||||
@@ -1843,7 +1843,7 @@ describe("OpenAI Responses route", () => {
|
||||
Effect.flip,
|
||||
)
|
||||
|
||||
expect(error).toBeInstanceOf(LLMError)
|
||||
expect(error).toBeInstanceOf(AIError)
|
||||
expect(error.reason).toMatchObject({ _tag: "RateLimit", message: "rate_limit_exceeded: Slow down" })
|
||||
}),
|
||||
)
|
||||
@@ -2037,7 +2037,7 @@ describe("OpenAI Responses route", () => {
|
||||
Effect.flip,
|
||||
)
|
||||
|
||||
expect(error).toBeInstanceOf(LLMError)
|
||||
expect(error).toBeInstanceOf(AIError)
|
||||
expect(error.reason).toMatchObject({ _tag: "InvalidRequest" })
|
||||
expect(error.message).toContain("HTTP 400")
|
||||
}),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Schema, Stream } from "effect"
|
||||
import { LLM, LLMResponse, Message, ToolDefinition, type Model } from "../../src"
|
||||
import { LLM, LLMResponse, Message, ToolDefinition, type LanguageModel } from "../../src"
|
||||
import { AmazonBedrock, Anthropic, Google, OpenAI, XAI } from "../../src/providers"
|
||||
import { LLMClient } from "../../src/route"
|
||||
import { Tool } from "../../src/tool"
|
||||
@@ -28,7 +28,7 @@ const targets: ReadonlyArray<{
|
||||
readonly requires: string
|
||||
readonly filename: string
|
||||
readonly maxTokens: number
|
||||
readonly model: Model
|
||||
readonly model: LanguageModel
|
||||
}> = [
|
||||
{
|
||||
id: "openai",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { HttpRecorder } from "@opencode-ai/http-recorder"
|
||||
import { describe } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import type { Model } from "../src"
|
||||
import type { LanguageModel } from "../src"
|
||||
import { goldenScenarioTags, goldenScenarioTitle, runGoldenScenario, type GoldenScenarioID } from "./recorded-scenarios"
|
||||
import { recordedTests } from "./recorded-test"
|
||||
import { kebab } from "./recorded-utils"
|
||||
@@ -22,7 +22,7 @@ type ScenarioInput =
|
||||
|
||||
type TargetInput = {
|
||||
readonly name: string
|
||||
readonly model: Model
|
||||
readonly model: LanguageModel
|
||||
readonly protocol?: string
|
||||
readonly requires?: ReadonlyArray<string>
|
||||
readonly transport?: Transport
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
toDefinitions,
|
||||
type ContentPart,
|
||||
type FinishReason,
|
||||
type Model,
|
||||
type LanguageModel,
|
||||
} from "../src"
|
||||
import { LLMClient } from "../src/route"
|
||||
import { Tool } from "../src/tool"
|
||||
@@ -54,7 +54,7 @@ export const weatherRuntimeTool = Tool.make({
|
||||
|
||||
export const weatherToolLoopRequest = (input: {
|
||||
readonly id: string
|
||||
readonly model: Model
|
||||
readonly model: LanguageModel
|
||||
readonly system?: string
|
||||
readonly maxTokens?: number
|
||||
readonly temperature?: number | false
|
||||
@@ -73,7 +73,7 @@ export const weatherToolLoopRequest = (input: {
|
||||
|
||||
export const goldenWeatherToolLoopRequest = (input: {
|
||||
readonly id: string
|
||||
readonly model: Model
|
||||
readonly model: LanguageModel
|
||||
readonly maxTokens?: number
|
||||
readonly temperature?: number | false
|
||||
}) =>
|
||||
@@ -163,7 +163,7 @@ export const expectGoldenWeatherToolLoop = (events: ReadonlyArray<LLMEvent>) =>
|
||||
|
||||
export interface GoldenScenarioContext {
|
||||
readonly id: string
|
||||
readonly model: Model
|
||||
readonly model: LanguageModel
|
||||
readonly maxTokens?: number
|
||||
readonly temperature?: number | false
|
||||
}
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Schema } from "effect"
|
||||
import { Effect, Schema } from "effect"
|
||||
import * as OpenAIChat from "../src/protocols/openai-chat"
|
||||
import * as OpenAIResponses from "../src/protocols/openai-responses"
|
||||
import { ContentPart, LLMEvent, LLMRequest, Model, ModelID, ProviderID, Usage } from "../src/schema"
|
||||
import {
|
||||
AIError,
|
||||
ContentPart,
|
||||
InvalidRequestReason,
|
||||
LLMEvent,
|
||||
LLMRequest,
|
||||
LanguageModel,
|
||||
ModelID,
|
||||
ProviderID,
|
||||
Usage,
|
||||
} from "../src/schema"
|
||||
import { ProviderShared } from "../src/protocols/shared"
|
||||
|
||||
const model = new Model({
|
||||
const model = new LanguageModel({
|
||||
id: ModelID.make("fake-model"),
|
||||
provider: ProviderID.make("fake-provider"),
|
||||
route: OpenAIChat.route,
|
||||
@@ -33,7 +43,7 @@ describe("llm schema", () => {
|
||||
|
||||
test("accepts custom route ids", () => {
|
||||
const decoded = decodeLLMRequest({
|
||||
model: Model.update(model, { route: OpenAIResponses.route }),
|
||||
model: LanguageModel.update(model, { route: OpenAIResponses.route }),
|
||||
system: [],
|
||||
messages: [],
|
||||
tools: [],
|
||||
@@ -51,9 +61,7 @@ describe("llm schema", () => {
|
||||
expect(
|
||||
LLMEvent.stepFinish({ index: 0, reason: { normalized: "stop" }, usage: { inputTokens: 1 } }).usage,
|
||||
).toBeInstanceOf(Usage)
|
||||
expect(LLMEvent.finish({ reason: { normalized: "stop" }, usage: { outputTokens: 2 } }).usage).toBeInstanceOf(
|
||||
Usage,
|
||||
)
|
||||
expect(LLMEvent.finish({ reason: { normalized: "stop" }, usage: { outputTokens: 2 } }).usage).toBeInstanceOf(Usage)
|
||||
})
|
||||
|
||||
test("content part tagged union exposes guards", () => {
|
||||
@@ -62,7 +70,7 @@ describe("llm schema", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("LLM.Usage", () => {
|
||||
describe("AI.Usage", () => {
|
||||
test("subtractTokens clamps non-sensical breakdowns to zero", () => {
|
||||
// Defense against a provider reporting cached_tokens > prompt_tokens or
|
||||
// reasoning_tokens > completion_tokens — the negative would otherwise
|
||||
@@ -88,3 +96,15 @@ describe("LLM.Usage", () => {
|
||||
expect(new Usage({}).visibleOutputTokens).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
test("AI errors expose the shared runtime tag", async () => {
|
||||
const error = new AIError({
|
||||
module: "test",
|
||||
method: "call",
|
||||
reason: new InvalidRequestReason({ message: "invalid" }),
|
||||
})
|
||||
expect(error._tag).toBe("AI.Error")
|
||||
expect(
|
||||
await Effect.runPromise(Effect.fail(error).pipe(Effect.catchTag("AI.Error", () => Effect.succeed("caught")))),
|
||||
).toBe("caught")
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { LLMError } from "../src/schema"
|
||||
import { AIError } from "../src/schema"
|
||||
import { ToolStream } from "../src/protocols/utils/tool-stream"
|
||||
import { it } from "./lib/effect"
|
||||
|
||||
@@ -67,7 +67,7 @@ describe("ToolStream", () => {
|
||||
Effect.gen(function* () {
|
||||
const error = ToolStream.appendExisting(ADAPTER, ToolStream.empty<number>(), 0, "{}", "missing tool")
|
||||
|
||||
expect(error).toBeInstanceOf(LLMError)
|
||||
expect(error).toBeInstanceOf(AIError)
|
||||
if (ToolStream.isError(error)) expect(error.reason.message).toBe("missing tool")
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -133,7 +133,6 @@ export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCO
|
||||
description: "Manage plugins",
|
||||
commands: [Spec.make("list", { description: "List active plugins" })],
|
||||
}),
|
||||
Spec.make("migrate", { description: "Migrate v1 data to v2" }),
|
||||
Spec.make("mini", {
|
||||
description: "Start the minimal interactive interface",
|
||||
params: {
|
||||
|
||||
@@ -45,7 +45,11 @@ export default Runtime.handler(Commands, (input) =>
|
||||
const runPromise = Effect.runPromiseWith(context)
|
||||
const service = server.service
|
||||
yield* run({
|
||||
app: { name: process.env.OPENCODE_CLIENT ?? "cli", version: OPENCODE_VERSION, channel: OPENCODE_CHANNEL },
|
||||
app: {
|
||||
name: process.env.OPENCODE_CLIENT ?? "cli",
|
||||
version: OPENCODE_VERSION,
|
||||
channel: process.env.OPENCODE_TUI_CHANNEL ?? OPENCODE_CHANNEL,
|
||||
},
|
||||
server: {
|
||||
endpoint: server.endpoint,
|
||||
service: service
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { Effect } from "effect"
|
||||
import { Commands } from "../commands"
|
||||
import { Runtime } from "../../framework/runtime"
|
||||
|
||||
export default Runtime.handler(Commands.commands.migrate, (_input) => Effect.log("No migrations to run."))
|
||||
@@ -116,13 +116,6 @@ export function migrateV1(legacy: TuiConfigV1.Info | undefined, kv: Record<strin
|
||||
: { grouping: kv.exploration_grouping ? ("auto" as const) : ("none" as const) }),
|
||||
},
|
||||
}),
|
||||
...(kv.dismissed_getting_started === undefined
|
||||
? {}
|
||||
: {
|
||||
hints: {
|
||||
onboarding: !kv.dismissed_getting_started,
|
||||
},
|
||||
}),
|
||||
...(kv.animations_enabled === undefined ? {} : { animations: kv.animations_enabled }),
|
||||
...(legacy?.mouse === undefined ? {} : { mouse: legacy.mouse }),
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@ const Handlers = Runtime.handlers(Commands, {
|
||||
plugin: {
|
||||
list: () => import("./commands/handlers/plugin/list"),
|
||||
},
|
||||
migrate: () => import("./commands/handlers/migrate"),
|
||||
mini: () => import("./commands/handlers/mini"),
|
||||
run: () => import("./commands/handlers/run"),
|
||||
pair: () => import("./commands/handlers/pair"),
|
||||
|
||||
@@ -235,10 +235,6 @@ async function renderToolError(part: SessionMessageAssistantTool, directory: str
|
||||
UI.println(UI.Style.TEXT_NORMAL + "✗", UI.Style.TEXT_NORMAL + `${info.title} failed`)
|
||||
}
|
||||
|
||||
function warning(message: string) {
|
||||
UI.println(UI.Style.TEXT_WARNING_BOLD + "!", UI.Style.TEXT_NORMAL, message)
|
||||
}
|
||||
|
||||
function errorMessage(error: unknown) {
|
||||
if (error instanceof Error) return error.message
|
||||
if (typeof error === "object" && error !== null && "message" in error && typeof error.message === "string")
|
||||
|
||||
@@ -74,12 +74,12 @@ test("migrates tui and kv config into cli.json", async () => {
|
||||
terminal: { title: false },
|
||||
prompt: { editor: false, paste: "full" },
|
||||
session: { sidebar: "hide", scrollbar: true, thinking: "show", grouping: "none" },
|
||||
hints: { onboarding: false },
|
||||
animations: false,
|
||||
mouse: false,
|
||||
})
|
||||
expect(config).not.toHaveProperty("skipped_version")
|
||||
expect(config).not.toHaveProperty("which_key")
|
||||
expect(config).not.toHaveProperty("hints")
|
||||
expect((await Bun.file(path.join(directory, "cli.json")).json()).keybinds).toEqual({ leader: "ctrl+o" })
|
||||
expect(await Bun.file(path.join(directory, "cli.json")).exists()).toBe(true)
|
||||
expect(await Bun.file(path.join(directory, "tui.json")).exists()).toBe(true)
|
||||
|
||||
@@ -398,7 +398,7 @@ export type Endpoint5_26Output =
|
||||
readonly location?: Location.Ref | undefined
|
||||
readonly data: {
|
||||
readonly sessionID: Session.ID
|
||||
readonly error: { readonly type: string; readonly message: string }
|
||||
readonly error: { readonly type: string; readonly message: string; readonly status?: number | undefined }
|
||||
}
|
||||
}
|
||||
| {
|
||||
@@ -524,7 +524,7 @@ export type Endpoint5_26Output =
|
||||
readonly data: {
|
||||
readonly sessionID: Session.ID
|
||||
readonly assistantMessageID: SessionMessage.ID
|
||||
readonly error: { readonly type: string; readonly message: string }
|
||||
readonly error: { readonly type: string; readonly message: string; readonly status?: number | undefined }
|
||||
readonly cost?: (number & Brand.Brand<"Money.USD">) | undefined
|
||||
readonly tokens?:
|
||||
| {
|
||||
@@ -686,7 +686,7 @@ export type Endpoint5_26Output =
|
||||
readonly sessionID: Session.ID
|
||||
readonly assistantMessageID: SessionMessage.ID
|
||||
readonly callID: string
|
||||
readonly error: { readonly type: string; readonly message: string }
|
||||
readonly error: { readonly type: string; readonly message: string; readonly status?: number | undefined }
|
||||
readonly content?:
|
||||
| readonly [
|
||||
(
|
||||
@@ -726,7 +726,7 @@ export type Endpoint5_26Output =
|
||||
readonly assistantMessageID: SessionMessage.ID
|
||||
readonly attempt: number
|
||||
readonly at: number
|
||||
readonly error: { readonly type: string; readonly message: string }
|
||||
readonly error: { readonly type: string; readonly message: string; readonly status?: number | undefined }
|
||||
}
|
||||
}
|
||||
| {
|
||||
@@ -776,7 +776,7 @@ export type Endpoint5_26Output =
|
||||
readonly data: {
|
||||
readonly sessionID: Session.ID
|
||||
readonly reason: "auto" | "manual"
|
||||
readonly error: { readonly type: string; readonly message: string }
|
||||
readonly error: { readonly type: string; readonly message: string; readonly status?: number | undefined }
|
||||
readonly inputID?: SessionMessage.ID | undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ export type ToolTextContent = { type: "text"; text: string }
|
||||
|
||||
export type ToolFileContent = { type: "file"; uri: string; mime: string; name?: string | null }
|
||||
|
||||
export type SessionStructuredError = { type: string; message: string }
|
||||
export type SessionStructuredError = { type: string; message: string; status?: number }
|
||||
|
||||
export type SessionMessageCompactionRunning = {
|
||||
type: "compaction"
|
||||
|
||||
@@ -56,7 +56,6 @@
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@types/semver": "catalog:",
|
||||
"@types/turndown": "5.0.5",
|
||||
"@types/which": "3.0.4",
|
||||
"@parcel/watcher-darwin-arm64": "2.5.1",
|
||||
@@ -100,7 +99,6 @@
|
||||
"@ff-labs/fff-node": "0.10.1",
|
||||
"@opencode-ai/codemode": "workspace:*",
|
||||
"@opencode-ai/effect-drizzle-sqlite": "workspace:*",
|
||||
"@opencode-ai/effect-sqlite-node": "workspace:*",
|
||||
"@opencode-ai/ai": "workspace:*",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
@@ -122,7 +120,6 @@
|
||||
"immer": "11.1.4",
|
||||
"ignore": "7.0.5",
|
||||
"jsonc-parser": "3.3.1",
|
||||
"semver": "^7.6.3",
|
||||
"turndown": "7.2.0",
|
||||
"tree-sitter-bash": "0.25.0",
|
||||
"tree-sitter-powershell": "0.25.10",
|
||||
|
||||
@@ -10,32 +10,172 @@ export interface Mapping {
|
||||
readonly body?: Readonly<Record<string, unknown>>
|
||||
}
|
||||
|
||||
export function map(packageName: string | undefined, settings: Readonly<Record<string, unknown>>): Mapping | undefined {
|
||||
const baseSettings = mapBaseSettings(settings)
|
||||
switch (packageName) {
|
||||
export interface MapInput {
|
||||
readonly packageName: string | undefined
|
||||
readonly settings: Readonly<Record<string, unknown>>
|
||||
readonly modelID: string
|
||||
}
|
||||
|
||||
export function map(input: MapInput): Mapping | undefined {
|
||||
const baseSettings = mapBaseSettings(input.settings)
|
||||
switch (input.packageName) {
|
||||
case "@ai-sdk/amazon-bedrock":
|
||||
return {
|
||||
package: "@opencode-ai/ai/providers/amazon-bedrock",
|
||||
settings: mapBedrockSettings(input.settings, baseSettings),
|
||||
...mapBedrockRequest(input),
|
||||
}
|
||||
case "@ai-sdk/amazon-bedrock/mantle":
|
||||
return mapBedrockMantle(input, baseSettings)
|
||||
case "@ai-sdk/google":
|
||||
return {
|
||||
package: "@opencode-ai/ai/providers/google",
|
||||
settings: {
|
||||
...baseSettings,
|
||||
...mapAPIKey(settings),
|
||||
...mapGoogleOptions(settings),
|
||||
...mapAPIKey(input.settings),
|
||||
...mapGoogleOptions(input.settings),
|
||||
},
|
||||
}
|
||||
case "@openrouter/ai-sdk-provider":
|
||||
return mapOpenRouter(settings, baseSettings)
|
||||
return mapOpenRouter(input.settings, baseSettings)
|
||||
case "@ai-sdk/xai":
|
||||
return {
|
||||
package: "@opencode-ai/ai/providers/xai",
|
||||
settings: {
|
||||
...baseSettings,
|
||||
...mapAPIKey(settings),
|
||||
...mapXAIOptions(settings),
|
||||
...mapAPIKey(input.settings),
|
||||
...mapXAIOptions(input.settings),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mapBedrockMantle(input: MapInput, baseSettings: Readonly<Record<string, unknown>>): Mapping | undefined {
|
||||
const settings = input.settings
|
||||
const chat = input.modelID === "openai.gpt-oss-safeguard-20b" || input.modelID === "openai.gpt-oss-safeguard-120b"
|
||||
return {
|
||||
package: `@opencode-ai/ai/providers/amazon-bedrock/mantle/${chat ? "chat" : "responses"}`,
|
||||
settings: {
|
||||
...mapBedrockSettings(settings, baseSettings),
|
||||
...mapOpenAIOptions(settings),
|
||||
},
|
||||
...(isStringRecord(settings.headers) ? { headers: settings.headers } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
function mapBedrockSettings(
|
||||
settings: Readonly<Record<string, unknown>>,
|
||||
baseSettings: Readonly<Record<string, unknown>>,
|
||||
) {
|
||||
const apiKey =
|
||||
typeof settings.apiKey === "string"
|
||||
? settings.apiKey
|
||||
: typeof settings.bearerToken === "string"
|
||||
? settings.bearerToken
|
||||
: undefined
|
||||
const credentials = mapBedrockCredentials(settings)
|
||||
return {
|
||||
...baseSettings,
|
||||
...(typeof settings.baseURL !== "string" && typeof settings.endpoint === "string"
|
||||
? { baseURL: settings.endpoint }
|
||||
: {}),
|
||||
...(apiKey === undefined ? {} : { apiKey }),
|
||||
...(credentials === undefined ? {} : { credentials }),
|
||||
...(typeof settings.region === "string" ? { region: settings.region } : {}),
|
||||
...(typeof settings.topP === "number" ? { topP: settings.topP } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
function mapBedrockRequest(input: MapInput): Pick<Mapping, "headers" | "body"> {
|
||||
const settings = input.settings
|
||||
const headers = isStringRecord(settings.headers) ? settings.headers : undefined
|
||||
const additional = isRecord(settings.additionalModelRequestFields) ? settings.additionalModelRequestFields : {}
|
||||
const reasoning = isRecord(settings.reasoningConfig) ? settings.reasoningConfig : undefined
|
||||
const anthropic = input.modelID.includes("anthropic")
|
||||
const openai = input.modelID.startsWith("openai.")
|
||||
const effort = typeof reasoning?.maxReasoningEffort === "string" ? reasoning.maxReasoningEffort : undefined
|
||||
const type = typeof reasoning?.type === "string" ? reasoning.type : undefined
|
||||
const budget = typeof reasoning?.budgetTokens === "number" ? reasoning.budgetTokens : undefined
|
||||
const display = typeof reasoning?.display === "string" ? reasoning.display : undefined
|
||||
const betas = Array.isArray(settings.anthropicBeta)
|
||||
? settings.anthropicBeta.filter((item): item is string => typeof item === "string")
|
||||
: []
|
||||
const existingBetas = Array.isArray(additional.anthropic_beta)
|
||||
? additional.anthropic_beta.filter((item): item is string => typeof item === "string")
|
||||
: []
|
||||
const fields = Provider.mergeOverlay(additional, {
|
||||
...(betas.length > 0 ? { anthropic_beta: [...existingBetas, ...betas] } : {}),
|
||||
...(anthropic && type === "enabled" && budget !== undefined
|
||||
? { thinking: { type: "enabled", budget_tokens: budget } }
|
||||
: {}),
|
||||
...(anthropic && type === "adaptive"
|
||||
? { thinking: { type: "adaptive", ...(display === undefined ? {} : { display }) } }
|
||||
: {}),
|
||||
...(anthropic && effort !== undefined
|
||||
? {
|
||||
output_config: {
|
||||
...(isRecord(additional.output_config) ? additional.output_config : {}),
|
||||
effort,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(!anthropic && openai && effort !== undefined ? { reasoning_effort: effort } : {}),
|
||||
...(!anthropic && !openai && effort !== undefined
|
||||
? {
|
||||
reasoningConfig: {
|
||||
...(type === undefined || type === "adaptive" ? {} : { type }),
|
||||
...(budget === undefined ? {} : { budgetTokens: budget }),
|
||||
maxReasoningEffort: effort,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
const body = {
|
||||
...(fields && Object.keys(fields).length > 0 ? { additionalModelRequestFields: fields } : {}),
|
||||
...(typeof settings.serviceTier === "string" ? { serviceTier: { type: settings.serviceTier } } : {}),
|
||||
}
|
||||
return {
|
||||
...(headers === undefined ? {} : { headers }),
|
||||
...(Object.keys(body).length === 0 ? {} : { body }),
|
||||
}
|
||||
}
|
||||
|
||||
function mapBedrockCredentials(settings: Readonly<Record<string, unknown>>) {
|
||||
const credentials = isRecord(settings.credentials) ? settings.credentials : settings
|
||||
const region =
|
||||
typeof settings.region === "string"
|
||||
? settings.region
|
||||
: typeof credentials.region === "string"
|
||||
? credentials.region
|
||||
: undefined
|
||||
if (
|
||||
region === undefined ||
|
||||
typeof credentials.accessKeyId !== "string" ||
|
||||
typeof credentials.secretAccessKey !== "string"
|
||||
)
|
||||
return undefined
|
||||
return {
|
||||
region,
|
||||
accessKeyId: credentials.accessKeyId,
|
||||
secretAccessKey: credentials.secretAccessKey,
|
||||
...(typeof credentials.sessionToken === "string" ? { sessionToken: credentials.sessionToken } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
function mapOpenAIOptions(settings: Readonly<Record<string, unknown>>) {
|
||||
const options = {
|
||||
...(typeof settings.reasoningEffort === "string" ? { reasoningEffort: settings.reasoningEffort } : {}),
|
||||
...(typeof settings.reasoningSummary === "string" ? { reasoningSummary: settings.reasoningSummary } : {}),
|
||||
...(Array.isArray(settings.include) ? { include: settings.include } : {}),
|
||||
...(typeof settings.store === "boolean" ? { store: settings.store } : {}),
|
||||
...(typeof settings.promptCacheKey === "string" ? { promptCacheKey: settings.promptCacheKey } : {}),
|
||||
...(typeof settings.textVerbosity === "string" ? { textVerbosity: settings.textVerbosity } : {}),
|
||||
...(typeof settings.serviceTier === "string" ? { serviceTier: settings.serviceTier } : {}),
|
||||
}
|
||||
if (Object.keys(options).length === 0) return {}
|
||||
return { providerOptions: { openai: options } }
|
||||
}
|
||||
|
||||
function mapBaseSettings(settings: Readonly<Record<string, unknown>>) {
|
||||
return {
|
||||
...(typeof settings.baseURL === "string" ? { baseURL: settings.baseURL } : {}),
|
||||
|
||||
@@ -18,8 +18,8 @@ import {
|
||||
FinishReason,
|
||||
InvalidProviderOutputReason,
|
||||
LLMEvent,
|
||||
LLMError,
|
||||
Model,
|
||||
AIError,
|
||||
LanguageModel,
|
||||
ProviderID,
|
||||
ProviderMetadata,
|
||||
ToolResultValue,
|
||||
@@ -182,7 +182,7 @@ export interface Interface {
|
||||
readonly runSDK: (event: SDKEvent) => Effect.Effect<SDKEvent>
|
||||
readonly runLanguage: (event: LanguageEvent) => Effect.Effect<LanguageEvent>
|
||||
readonly language: (model: Info) => Effect.Effect<LanguageModelV3, InitError>
|
||||
readonly model: (model: Info) => Effect.Effect<Model, InitError>
|
||||
readonly model: (model: Info) => Effect.Effect<LanguageModel, InitError>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/AISDK") {}
|
||||
@@ -338,11 +338,12 @@ function modelFromLanguage(info: Info, language: LanguageModelV3) {
|
||||
from: (request) => Effect.succeed(callOptions(request)),
|
||||
},
|
||||
with: () => route,
|
||||
model: (input) => Model.make({ ...input, provider: "provider" in input ? input.provider : info.providerID, route }),
|
||||
model: (input) =>
|
||||
LanguageModel.make({ ...input, provider: "provider" in input ? input.provider : info.providerID, route }),
|
||||
prepareTransport: (body) => Effect.succeed(body),
|
||||
streamPrepared: (prepared) => streamLanguage(language, prepared as LanguageModelV3CallOptions),
|
||||
}
|
||||
return Model.make({
|
||||
return LanguageModel.make({
|
||||
id: info.modelID ?? info.id,
|
||||
provider: info.providerID,
|
||||
route,
|
||||
@@ -555,7 +556,7 @@ function streamLanguage(language: LanguageModelV3, options: LanguageModelV3CallO
|
||||
function streamPartEvents(
|
||||
state: { step: number; toolNames: Record<string, string> },
|
||||
event: LanguageModelV3StreamPart,
|
||||
): Effect.Effect<ReadonlyArray<LLMEvent>, LLMError> {
|
||||
): Effect.Effect<ReadonlyArray<LLMEvent>, AIError> {
|
||||
switch (event.type) {
|
||||
case "stream-start":
|
||||
case "response-metadata":
|
||||
@@ -720,10 +721,10 @@ function messageValue(input: unknown) {
|
||||
|
||||
function llmError(method: string, error: unknown) {
|
||||
const reason =
|
||||
error instanceof LLMError
|
||||
error instanceof AIError
|
||||
? new InvalidProviderOutputReason({ message: error.message })
|
||||
: new UnknownProviderReason({ message: error instanceof Error ? error.message : String(error) })
|
||||
return new LLMError({
|
||||
return new AIError({
|
||||
module: "AISDK",
|
||||
method,
|
||||
reason,
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
export * as MoveSession from "./move-session"
|
||||
|
||||
import { Context, DateTime, Effect, Layer, Schema } from "effect"
|
||||
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { FSUtil } from "@opencode-ai/util/fs-util"
|
||||
import { Git } from "../git"
|
||||
import { Global } from "@opencode-ai/util/global"
|
||||
import { Project } from "../project"
|
||||
import { Session } from "../session"
|
||||
import { SessionExecution } from "../session/execution"
|
||||
import { SessionSchema } from "../session/schema"
|
||||
import { SessionStore } from "../session/store"
|
||||
import { AbsolutePath } from "../schema"
|
||||
import path from "path"
|
||||
|
||||
export const Destination = Schema.Struct({
|
||||
directory: AbsolutePath,
|
||||
}).annotate({ identifier: "MoveSession.Destination" })
|
||||
export type Destination = typeof Destination.Type
|
||||
|
||||
export const Input = Schema.Struct({
|
||||
sessionID: SessionSchema.ID,
|
||||
destination: Destination,
|
||||
moveChanges: Schema.optional(Schema.Boolean),
|
||||
}).annotate({ identifier: "MoveSession.Input" })
|
||||
export type Input = typeof Input.Type
|
||||
|
||||
export class DestinationProjectMismatchError extends Schema.TaggedErrorClass<DestinationProjectMismatchError>()(
|
||||
"MoveSession.DestinationProjectMismatchError",
|
||||
{
|
||||
expected: Project.ID,
|
||||
actual: Project.ID,
|
||||
},
|
||||
) {}
|
||||
|
||||
export class DestinationNotFoundError extends Schema.TaggedErrorClass<DestinationNotFoundError>()(
|
||||
"MoveSession.DestinationNotFoundError",
|
||||
{ directory: AbsolutePath },
|
||||
) {}
|
||||
|
||||
export class DestinationNotDirectoryError extends Schema.TaggedErrorClass<DestinationNotDirectoryError>()(
|
||||
"MoveSession.DestinationNotDirectoryError",
|
||||
{ directory: AbsolutePath },
|
||||
) {}
|
||||
|
||||
export class ApplyChangesError extends Schema.TaggedErrorClass<ApplyChangesError>()("MoveSession.ApplyChangesError", {
|
||||
message: Schema.String,
|
||||
}) {}
|
||||
|
||||
export class CaptureChangesError extends Schema.TaggedErrorClass<CaptureChangesError>()(
|
||||
"MoveSession.CaptureChangesError",
|
||||
{
|
||||
message: Schema.String,
|
||||
},
|
||||
) {}
|
||||
|
||||
export class ResetSourceChangesError extends Schema.TaggedErrorClass<ResetSourceChangesError>()(
|
||||
"MoveSession.ResetSourceChangesError",
|
||||
{
|
||||
directory: AbsolutePath,
|
||||
message: Schema.String,
|
||||
cause: Schema.optional(Schema.Defect()),
|
||||
},
|
||||
) {}
|
||||
|
||||
export type Error =
|
||||
| Session.NotFoundError
|
||||
| DestinationProjectMismatchError
|
||||
| DestinationNotFoundError
|
||||
| DestinationNotDirectoryError
|
||||
| Session.DestinationNotFoundError
|
||||
| Session.DestinationNotDirectoryError
|
||||
| CaptureChangesError
|
||||
| ApplyChangesError
|
||||
| ResetSourceChangesError
|
||||
|
||||
export interface Interface {
|
||||
readonly moveSession: (input: Input) => Effect.Effect<void, Error>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/ControlPlaneMoveSession") {}
|
||||
|
||||
const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const git = yield* Git.Service
|
||||
const fs = yield* FSUtil.Service
|
||||
const global = yield* Global.Service
|
||||
const project = yield* Project.Service
|
||||
const sessions = yield* SessionStore.Service
|
||||
const session = yield* Session.Service
|
||||
const execution = yield* SessionExecution.Service
|
||||
|
||||
const moveSession = Effect.fn("MoveSession.moveSession")(function* (input: Input) {
|
||||
const current = yield* sessions.get(input.sessionID)
|
||||
if (!current) return yield* new Session.NotFoundError({ sessionID: input.sessionID })
|
||||
const value = input.destination.directory.trim()
|
||||
const expanded = value === "~" ? global.home : value.startsWith("~/") ? path.join(global.home, value.slice(2)) : value
|
||||
const directory = AbsolutePath.make(path.resolve(current.location.directory, expanded))
|
||||
const destinationInfo = yield* fs.stat(directory).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
||||
if (!destinationInfo) return yield* new DestinationNotFoundError({ directory })
|
||||
if (destinationInfo.type !== "Directory") return yield* new DestinationNotDirectoryError({ directory })
|
||||
if (current.location.directory === directory) return
|
||||
|
||||
const source = yield* project.resolve(current.location.directory)
|
||||
const destination = yield* project.resolve(directory)
|
||||
if (input.moveChanges && current.projectID !== destination.id) {
|
||||
return yield* new DestinationProjectMismatchError({ expected: current.projectID, actual: destination.id })
|
||||
}
|
||||
// A move must not race active execution: a mid-drain relocation would let
|
||||
// the source Location dispatch a request assembled under stale instructions
|
||||
// and history. Serialize like removal does — stop the drain, then move.
|
||||
yield* execution.interrupt(input.sessionID)
|
||||
yield* execution.awaitIdle(input.sessionID)
|
||||
|
||||
const moveChanges = input.moveChanges && source.directory !== destination.directory
|
||||
const sourceRepository = moveChanges ? yield* git.repo.discover(current.location.directory) : undefined
|
||||
if (moveChanges && !sourceRepository)
|
||||
return yield* new CaptureChangesError({ message: "Source is not a Git repository" })
|
||||
const patch = sourceRepository
|
||||
? yield* git.change
|
||||
.capture({ repository: sourceRepository, path: current.location.directory })
|
||||
.pipe(Effect.mapError((error) => new CaptureChangesError({ message: error.message })))
|
||||
: Git.ChangeSet.make("")
|
||||
if (patch) {
|
||||
const repository = yield* git.repo.discover(directory)
|
||||
if (!repository) return yield* new ApplyChangesError({ message: "Destination is not a Git repository" })
|
||||
yield* git.change
|
||||
.apply({ repository, path: directory, changes: patch })
|
||||
.pipe(Effect.mapError((error) => new ApplyChangesError({ message: error.message })))
|
||||
}
|
||||
|
||||
yield* session.move({
|
||||
sessionID: input.sessionID,
|
||||
directory,
|
||||
})
|
||||
|
||||
if (patch) {
|
||||
const repository = yield* git.repo.discover(current.location.directory)
|
||||
if (!repository)
|
||||
return yield* new ResetSourceChangesError({
|
||||
directory: current.location.directory,
|
||||
message: "Source is not a Git repository",
|
||||
})
|
||||
yield* git.change
|
||||
.discard({
|
||||
repository,
|
||||
path: current.location.directory,
|
||||
index: "preserve",
|
||||
untracked: "remove",
|
||||
})
|
||||
.pipe(
|
||||
Effect.mapError(
|
||||
(error) =>
|
||||
new ResetSourceChangesError({
|
||||
directory: current.location.directory,
|
||||
message: error.message,
|
||||
cause: error.cause,
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return Service.of({ moveSession })
|
||||
}),
|
||||
)
|
||||
|
||||
export const node = makeGlobalNode({
|
||||
service: Service,
|
||||
layer,
|
||||
deps: [
|
||||
FSUtil.node,
|
||||
Git.node,
|
||||
Global.node,
|
||||
Project.node,
|
||||
Session.node,
|
||||
SessionStore.node,
|
||||
SessionExecution.node,
|
||||
],
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as Generate from "./generate"
|
||||
|
||||
import { LLM, LLMClient, LLMError } from "@opencode-ai/ai"
|
||||
import { LLM, LLMClient, AIError } from "@opencode-ai/ai"
|
||||
import { Context, Effect, Layer, Schema } from "effect"
|
||||
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { llmClient } from "./effect/app-node-platform"
|
||||
@@ -57,7 +57,7 @@ export const layer = Layer.effect(
|
||||
})
|
||||
const response = yield* llm.generate(LLM.request({ model: resolved.model, prompt: input.prompt })).pipe(
|
||||
Effect.mapError(
|
||||
(error: LLMError) =>
|
||||
(error: AIError) =>
|
||||
new UnavailableError({
|
||||
message: error.message,
|
||||
service: resolved.ref.providerID,
|
||||
|
||||
@@ -148,6 +148,3 @@ export function buildLocationServiceMap(
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// This is temporary for backwards compatibility
|
||||
export const locationServiceMapLayer = buildLocationServiceMap()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export * as ModelResolver from "./model-resolver"
|
||||
|
||||
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { Model } from "@opencode-ai/ai"
|
||||
import { LanguageModel } from "@opencode-ai/ai"
|
||||
// ast-grep-ignore: no-star-import
|
||||
import * as AnthropicMessages from "@opencode-ai/ai/protocols/anthropic-messages"
|
||||
// ast-grep-ignore: no-star-import
|
||||
@@ -50,7 +50,7 @@ export type Error = VariantUnavailableError | UnsupportedPackageError | Integrat
|
||||
|
||||
export interface Resolved {
|
||||
/** Route-level model for provider requests; its id is the provider API model id, which may differ from the catalog id. */
|
||||
readonly model: Model
|
||||
readonly model: LanguageModel
|
||||
/** Selected catalog identity. Durable records and displays must use this, never the API model id. */
|
||||
readonly ref: Ref
|
||||
/** Catalog capabilities used to shape requests before provider lowering. */
|
||||
@@ -134,14 +134,14 @@ export const withVariant = (
|
||||
|
||||
export interface Dependencies {
|
||||
readonly loadPackage?: (specifier: string) => Effect.Effect<Provider.ProviderPackage, Provider.LoadError>
|
||||
readonly loadAISDK?: (model: Info) => Effect.Effect<Model, AISDK.InitError>
|
||||
readonly loadAISDK?: (model: Info) => Effect.Effect<LanguageModel, AISDK.InitError>
|
||||
}
|
||||
|
||||
export const fromCatalogModel = (
|
||||
model: Info,
|
||||
credential?: Credential.Value,
|
||||
dependencies?: Dependencies,
|
||||
): Effect.Effect<Model, UnsupportedPackageError> => {
|
||||
): Effect.Effect<LanguageModel, UnsupportedPackageError> => {
|
||||
const resolved = produce(model, (draft) => {
|
||||
if (draft.settings?.apiKey === "") delete draft.settings.apiKey
|
||||
if (credential?.type === "key" && credential.metadata !== undefined)
|
||||
@@ -176,14 +176,19 @@ export const fromCatalogModel = (
|
||||
)
|
||||
}
|
||||
const configured = { ...resolved.settings, ...credential?.metadata }
|
||||
const mapping = Provider.isAISDK(resolved.package) ? AISDKNative.map(packageName, configured) : undefined
|
||||
const mapping = Provider.isAISDK(resolved.package)
|
||||
? AISDKNative.map({
|
||||
packageName,
|
||||
settings: configured,
|
||||
modelID: resolved.modelID ?? resolved.id,
|
||||
})
|
||||
: undefined
|
||||
const native = mapping?.package ?? resolved.package
|
||||
if (Provider.isAISDK(resolved.package) && !mapping) {
|
||||
if (!dependencies?.loadAISDK) return Effect.fail(unsupported(resolved))
|
||||
const runtime = produce(resolved, (draft) => {
|
||||
draft.settings = Provider.mergeOverlay(draft.settings, {
|
||||
...(credential?.type === "key" ? { apiKey: credential.key } : {}),
|
||||
...(credential?.type === "oauth" ? { apiKey: credential.access } : {}),
|
||||
...nativeCredentialSettings(resolved.package ?? "", credential),
|
||||
...credential?.metadata,
|
||||
})
|
||||
})
|
||||
@@ -207,7 +212,7 @@ export const fromCatalogModel = (
|
||||
return yield* Effect.try({
|
||||
try: () => {
|
||||
const runtime = module.model(resolved.modelID ?? resolved.id, settings)
|
||||
return Model.update(runtime, {
|
||||
return LanguageModel.update(runtime, {
|
||||
provider: resolved.providerID,
|
||||
compatibility: resolved.compatibility
|
||||
? Object.assign({}, runtime.compatibility, resolved.compatibility)
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
export * as LayerMapExample from "./layer-map.example"
|
||||
|
||||
import { Context, Effect, Layer, LayerMap } from "effect"
|
||||
import { Npm } from "@opencode-ai/util/npm"
|
||||
|
||||
/**
|
||||
* Tutorial: split global services from context-specific services.
|
||||
*
|
||||
* Use this pattern when part of the app should be constructed once at the app edge,
|
||||
* while another part should be cached per request/project/workspace key.
|
||||
*
|
||||
* In this example:
|
||||
* - Npm.Service is the global service. It is not keyed by request context and should
|
||||
* be provided once by the application runtime.
|
||||
* - ConfigService is context-specific. It is built from a RequestContext key and is
|
||||
* cached by LayerMap for that key.
|
||||
* - ConfigServiceMap.layer owns the cache. Provide it once globally, then each
|
||||
* request can provide ConfigServiceMap.get(context) to select the right instance.
|
||||
*
|
||||
* Lifetime model:
|
||||
* - ConfigServiceMap.layer has the app/global lifetime and depends on Npm.Service.
|
||||
* - ConfigServiceMap.get(context) has the request/context lifetime and provides
|
||||
* ConfigService for exactly that context key.
|
||||
* - The cached ConfigService entry stays alive while something is using it. Once idle,
|
||||
* it remains cached for idleTimeToLive, then its scope is finalized.
|
||||
* - invalidate(context) removes the cache entry for future lookups. Active users keep
|
||||
* running on the old instance; the next lookup can create a fresh instance.
|
||||
*
|
||||
* Key model:
|
||||
* - Keys can be strings, structs, classes, arrays, etc.
|
||||
* - Prefer primitive or immutable keys. Effect uses Hash / Equal semantics for cache
|
||||
* lookup, so mutating an object after it has been used as a key is a bug.
|
||||
*/
|
||||
|
||||
export type RequestContext = {
|
||||
readonly directory: string
|
||||
readonly workspace: string
|
||||
}
|
||||
|
||||
export class RequestContextRef extends Context.Service<RequestContextRef, RequestContext>()(
|
||||
"@opencode/example/RequestContextRef",
|
||||
) {}
|
||||
|
||||
export interface ConfigServiceShape {
|
||||
readonly directory: string
|
||||
readonly workspace: string
|
||||
readonly nextUse: () => Effect.Effect<number>
|
||||
readonly which: Npm.Interface["which"]
|
||||
}
|
||||
|
||||
export class ConfigService extends Context.Service<ConfigService, ConfigServiceShape>()(
|
||||
"@opencode/example/ConfigService",
|
||||
) {}
|
||||
|
||||
const configServiceLayer = Layer.effect(
|
||||
ConfigService,
|
||||
Effect.gen(function* () {
|
||||
const context = yield* RequestContextRef
|
||||
const npm = yield* Npm.Service
|
||||
|
||||
let useCount = 0
|
||||
|
||||
return ConfigService.of({
|
||||
directory: context.directory,
|
||||
workspace: context.workspace,
|
||||
nextUse: () => Effect.succeed(++useCount),
|
||||
which: npm.which,
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
export class ConfigServiceMap extends LayerMap.Service<ConfigServiceMap>()("@opencode/example/ConfigServiceMap", {
|
||||
lookup: (context: RequestContext) =>
|
||||
configServiceLayer.pipe(Layer.provide(Layer.succeed(RequestContextRef, RequestContextRef.of(context)))),
|
||||
idleTimeToLive: "5 minutes",
|
||||
}) {}
|
||||
|
||||
export const appLayer = ConfigServiceMap.layer
|
||||
|
||||
export const readConfig = Effect.fn("LayerMapExample.readConfig")(function* () {
|
||||
const config = yield* ConfigService
|
||||
|
||||
return {
|
||||
directory: config.directory,
|
||||
workspace: config.workspace,
|
||||
useCount: yield* config.nextUse(),
|
||||
}
|
||||
})
|
||||
|
||||
export const handleRequest = Effect.fn("LayerMapExample.handleRequest")(function* (context: RequestContext) {
|
||||
return yield* readConfig().pipe(Effect.provide(ConfigServiceMap.get(context)))
|
||||
})
|
||||
|
||||
export const invalidateContext = (context: RequestContext) => ConfigServiceMap.invalidate(context)
|
||||
@@ -120,7 +120,6 @@ export const providerLayerWithCell = (cell: Cell) =>
|
||||
)
|
||||
|
||||
export const layer = layerWithCell(defaultCell)
|
||||
export const providerLayer = providerLayerWithCell(defaultCell)
|
||||
|
||||
export const node = makeGlobalNode({ service: Service, layer, deps: [] })
|
||||
|
||||
|
||||
@@ -34,6 +34,15 @@ export type ProviderPackage = ProviderPackageDefinition
|
||||
const packages = new Map<string, Promise<unknown>>()
|
||||
const builtins = new Map<string, () => Promise<unknown>>([
|
||||
["@opencode-ai/ai/providers/amazon-bedrock", () => import("@opencode-ai/ai/providers/amazon-bedrock")],
|
||||
["@opencode-ai/ai/providers/amazon-bedrock/mantle", () => import("@opencode-ai/ai/providers/amazon-bedrock/mantle")],
|
||||
[
|
||||
"@opencode-ai/ai/providers/amazon-bedrock/mantle/chat",
|
||||
() => import("@opencode-ai/ai/providers/amazon-bedrock/mantle/chat"),
|
||||
],
|
||||
[
|
||||
"@opencode-ai/ai/providers/amazon-bedrock/mantle/responses",
|
||||
() => import("@opencode-ai/ai/providers/amazon-bedrock/mantle/responses"),
|
||||
],
|
||||
["@opencode-ai/ai/providers/anthropic", () => import("@opencode-ai/ai/providers/anthropic")],
|
||||
["@opencode-ai/ai/providers/azure", () => import("@opencode-ai/ai/providers/azure")],
|
||||
["@opencode-ai/ai/providers/azure/chat", () => import("@opencode-ai/ai/providers/azure/chat")],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as SessionCompaction from "./compaction"
|
||||
|
||||
import { LLM, LLMClient, LLMError, LLMEvent, Message, type LLMRequest, type Model } from "@opencode-ai/ai"
|
||||
import { LLM, LLMClient, AIError, LLMEvent, Message, type LLMRequest, type LanguageModel } from "@opencode-ai/ai"
|
||||
import { SessionError } from "@opencode-ai/schema/session-error"
|
||||
import { Context, Effect, Layer, Stream } from "effect"
|
||||
import { Config } from "../config"
|
||||
@@ -64,7 +64,7 @@ type Dependencies = {
|
||||
readonly app: App.Info
|
||||
readonly bus: Bus.Interface
|
||||
readonly llm: {
|
||||
readonly stream: (request: LLMRequest) => Stream.Stream<LLMEvent, LLMError>
|
||||
readonly stream: (request: LLMRequest) => Stream.Stream<LLMEvent, AIError>
|
||||
}
|
||||
readonly models: SessionRunnerModel.Interface
|
||||
readonly config: Settings
|
||||
@@ -73,7 +73,7 @@ type Dependencies = {
|
||||
export type AutoInput = {
|
||||
readonly session: SessionSchema.Info
|
||||
readonly messages: readonly SessionMessage.Info[]
|
||||
readonly model: Model
|
||||
readonly model: LanguageModel
|
||||
readonly cost: Info["cost"]
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ export type ManualInput = {
|
||||
|
||||
type Plan = {
|
||||
readonly session: SessionSchema.Info
|
||||
readonly model: Model
|
||||
readonly model: LanguageModel
|
||||
readonly cost: Info["cost"]
|
||||
readonly reason: SessionMessage.Compaction["reason"]
|
||||
readonly prompt: string
|
||||
@@ -282,7 +282,7 @@ const make = (dependencies: Dependencies) => {
|
||||
}
|
||||
return Effect.void
|
||||
}),
|
||||
Effect.catchTag("LLM.Error", (error) =>
|
||||
Effect.catchTag("AI.Error", (error) =>
|
||||
Effect.sync(() => {
|
||||
failure = toSessionError(error)
|
||||
}),
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
export * as SessionGenerate from "./generate"
|
||||
|
||||
import type { LLMError } from "@opencode-ai/ai"
|
||||
import type { AIError } from "@opencode-ai/ai"
|
||||
import { Context, type Effect } from "effect"
|
||||
import type { Instructions } from "../instructions"
|
||||
import type { AgentNotFoundError } from "./error"
|
||||
import type { SessionRunnerModel } from "./runner/model"
|
||||
import type { SessionSchema } from "./schema"
|
||||
|
||||
export type Error = AgentNotFoundError | Instructions.InitializationBlocked | SessionRunnerModel.Error | LLMError
|
||||
export type Error = AgentNotFoundError | Instructions.InitializationBlocked | SessionRunnerModel.Error | AIError
|
||||
|
||||
export interface Interface {
|
||||
/** Generates text from current Session context without mutating the Session. */
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user