mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-24 06:33:01 -04:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 48ea84109e | |||
| 6adb98c266 | |||
| 83ffd292f8 | |||
| 8fb534d03e | |||
| d4a0f163a9 | |||
| a9caf9154e | |||
| e0be1c0e13 | |||
| 8a03b87c1f | |||
| 173e3b0d48 | |||
| 1df9092f07 |
@@ -6,7 +6,7 @@ permissions:
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [dev, beta]
|
||||
branches: [dev, beta, v2]
|
||||
paths:
|
||||
- "bun.lock"
|
||||
- "package.json"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-yYd6nV4YRnTYqf5W5T7oXNo7bSgr1Vzjhy7d5YjA5Vo=",
|
||||
"x86_64-linux": "sha256-IxkSw0gK/qkMHZGVHqjwgM9BKhzbQX6hyF9SWUNtpzg=",
|
||||
"aarch64-linux": "sha256-YVjpbil0QswVwi6NtVYFq3xCqpsfveG1chlNVCVI0MU=",
|
||||
"aarch64-darwin": "sha256-CdL2mI84pawH2H5i9qu8A6IWbkmKOYHlJS+DI/Mafdw=",
|
||||
"x86_64-darwin": "sha256-NtswwfU5WYv99bEmI4XeLwjhBGcS9ZMYLRo4MQRNtLo="
|
||||
|
||||
@@ -131,54 +131,6 @@ Keep semantic APIs as separate entrypoints, such as OpenAI `chat` and `responses
|
||||
|
||||
Do not expose `Route` in provider package settings. Route composition stays an implementation detail behind `model(...)`.
|
||||
|
||||
### Folder layout
|
||||
|
||||
```
|
||||
packages/ai/src/
|
||||
schema/ canonical Schema model, split by concern
|
||||
ids.ts branded IDs, literal types, ProviderMetadata
|
||||
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, AIError, ToolFailure
|
||||
index.ts barrel
|
||||
llm.ts request constructors and convenience helpers
|
||||
route/
|
||||
index.ts @opencode-ai/ai/route advanced barrel
|
||||
client.ts Route.make + LLMClient.stream/generate
|
||||
executor.ts RequestExecutor service + transport error mapping
|
||||
protocol.ts Protocol type + Protocol.make
|
||||
endpoint.ts Endpoint type + Endpoint.path
|
||||
auth.ts Auth type + Auth.bearer / Auth.header / Auth.none
|
||||
auth-options.ts ProviderAuthOption shape, AuthOptions.bearer, AtLeastOne helper
|
||||
framing.ts Framing type + Framing.sse
|
||||
transport/ transport implementations
|
||||
index.ts Transport execution types + HttpTransport / WebSocketTransport namespaces
|
||||
websocket-channel.ts generic sequential channel executor/driver contract
|
||||
http.ts HttpTransport.httpJson — POST + framing
|
||||
websocket.ts direct one-request channel executor + raw socket adapter
|
||||
protocols/
|
||||
shared.ts ProviderShared toolkit used inside protocol impls
|
||||
openai-chat.ts protocol + route (compose OpenAIChat.protocol)
|
||||
open-responses.ts provider-neutral Responses protocol baseline
|
||||
open-responses-channel.ts provider-neutral Responses WebSocket transport factory
|
||||
openai-responses.ts OpenAI tools/events and channel policy composed over OpenResponses
|
||||
anthropic-messages.ts
|
||||
gemini.ts
|
||||
bedrock-converse.ts
|
||||
bedrock-event-stream.ts framing for AWS event-stream binary frames
|
||||
openai-compatible-chat.ts route that reuses OpenAIChat.protocol, no canonical URL
|
||||
openai-compatible-responses.ts deployment adapter that reuses OpenResponses.protocol, no canonical URL
|
||||
utils/ per-protocol helpers (auth, cache, media, tool-stream, ...)
|
||||
providers/
|
||||
openai-compatible.ts generic Chat helper + family model helpers
|
||||
openai-compatible-responses.ts generic Responses helper
|
||||
openai-compatible-profile.ts family defaults (deepseek, togetherai, ...)
|
||||
azure.ts / amazon-bedrock.ts / cloudflare.ts / google.ts / xai.ts / openai.ts / anthropic.ts / openrouter.ts
|
||||
tool.ts typed Tool.make helper
|
||||
tool-runtime.ts narrow one-call typed tool dispatcher
|
||||
```
|
||||
|
||||
The dependency arrow points down: `providers/*.ts` files import protocol routes and auth-option utilities; protocol modules import `endpoint`, `auth`, `framing`, and transport pieces. Protocols do not import provider facades. Lower-level modules know nothing about provider catalog metadata. `OpenAIResponses` composes the provider-neutral `OpenResponses` protocol; the baseline never imports the OpenAI extension.
|
||||
|
||||
### Shared protocol helpers
|
||||
|
||||
@@ -34,10 +34,8 @@ Run `LLMClient.stream(request)` instead of `generate` when you want incremental
|
||||
Use `Image.generate` with an image model for direct asset generation:
|
||||
|
||||
```ts
|
||||
import { Effect, Layer } from "effect"
|
||||
import { Image, ImageClient, ImageInput } from "@opencode-ai/ai"
|
||||
import { Image, ImageInput } from "@opencode-ai/ai"
|
||||
import { OpenAI } from "@opencode-ai/ai/providers"
|
||||
import { RequestExecutor } from "@opencode-ai/ai/route"
|
||||
|
||||
const program = Effect.gen(function* () {
|
||||
const response = yield* Image.generate({
|
||||
@@ -54,10 +52,6 @@ const program = Effect.gen(function* () {
|
||||
|
||||
return response.images // GeneratedImage[] with owned bytes or a provider URL
|
||||
})
|
||||
|
||||
const imageLayer = ImageClient.layer.pipe(Layer.provide(RequestExecutor.fetchLayer))
|
||||
|
||||
await Effect.runPromise(program.pipe(Effect.provide(imageLayer)))
|
||||
```
|
||||
|
||||
Pass ordered image inputs to the same method for editing, composition, or image-conditioned generation:
|
||||
@@ -205,7 +199,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.system(...)` / `Message.user(...)` / `Message.assistant(...)` / `Message.tool(...)`** — message constructors from the canonical schema model. Top-level `request.system` is the initial prompt; a system message in history is a chronological operator update.
|
||||
- **`Message.user(...)` / `Message.assistant(...)` / `Message.tool(...)`** — message 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.
|
||||
|
||||
@@ -22,6 +22,7 @@ const apiKey = Config.redacted("OPENAI_API_KEY")
|
||||
const model = OpenAI.configure({
|
||||
apiKey,
|
||||
generation: { maxTokens: 160 },
|
||||
store: false,
|
||||
}).model("gpt-4o-mini")
|
||||
|
||||
// 2. Build a provider-neutral request. This is useful when reusing one request
|
||||
@@ -193,7 +194,7 @@ const FakeAdapter = Route.make({
|
||||
provider: "fake-echo",
|
||||
protocol: FakeProtocol,
|
||||
endpoint: Endpoint.path("/v1/echo", { baseURL: "https://fake.local" }),
|
||||
auth: Auth.none,
|
||||
auth: Auth.passthrough,
|
||||
framing: Framing.sse,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
import { Option, Schema } from "effect"
|
||||
import { TextVerbosity, type LLMRequest } from "../../schema/index.js"
|
||||
import type { LLMRequest } from "../../schema/index.js"
|
||||
|
||||
export const ReasoningEfforts = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] as const
|
||||
export type ReasoningEffort = (typeof ReasoningEfforts)[number] | (string & {})
|
||||
export const ReasoningEffort = Schema.declare<ReasoningEffort>(
|
||||
(value): value is ReasoningEffort => typeof value === "string",
|
||||
{ title: "ReasoningEffort" },
|
||||
)
|
||||
|
||||
export const TextVerbosities = ["low", "medium", "high"] as const
|
||||
export type TextVerbosity = (typeof TextVerbosities)[number] | (string & {})
|
||||
export const TextVerbosity = Schema.declare<TextVerbosity>(
|
||||
(value): value is TextVerbosity => typeof value === "string",
|
||||
{ title: "TextVerbosity" },
|
||||
)
|
||||
|
||||
export const ResponseIncludables = [
|
||||
"file_search_call.results",
|
||||
@@ -19,7 +33,6 @@ export type ServiceTier = (typeof ServiceTiers)[number]
|
||||
export const Truncations = ["auto", "disabled"] as const
|
||||
export type Truncation = (typeof Truncations)[number]
|
||||
|
||||
export const ReasoningEffort = Schema.String
|
||||
export const TextVerbositySchema = TextVerbosity
|
||||
export const ResponseIncludableSchema = Schema.declare<ResponseIncludable>(
|
||||
(value): value is ResponseIncludable => typeof value === "string",
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { ReasoningEfforts } from "../../schema/index.js"
|
||||
import { OpenResponsesOptions } from "./open-responses-options.js"
|
||||
|
||||
export const OpenAIReasoningEfforts = ReasoningEfforts
|
||||
export type OpenAIReasoningEffort = string
|
||||
export const OpenAIReasoningEfforts = OpenResponsesOptions.ReasoningEfforts
|
||||
export type OpenAIReasoningEffort = OpenResponsesOptions.ReasoningEffort
|
||||
export const OpenAITextVerbosities = OpenResponsesOptions.TextVerbosities
|
||||
export type OpenAITextVerbosity = OpenResponsesOptions.TextVerbosity
|
||||
|
||||
// Mirrors OpenAI's `ResponseIncludable` union from the official SDK. Keep this
|
||||
// in lockstep with `openai-node/src/resources/responses/responses.ts`.
|
||||
@@ -12,7 +13,7 @@ export const OpenAIServiceTiers = OpenResponsesOptions.ServiceTiers
|
||||
export type OpenAIServiceTier = OpenResponsesOptions.ServiceTier
|
||||
|
||||
export const OpenAIReasoningEffort = OpenResponsesOptions.ReasoningEffort
|
||||
export const OpenAITextVerbosity = OpenResponsesOptions.TextVerbositySchema
|
||||
export const OpenAITextVerbosity = OpenResponsesOptions.TextVerbosity
|
||||
export const OpenAIResponseIncludable = OpenResponsesOptions.ResponseIncludableSchema
|
||||
export const OpenAIServiceTier = OpenResponsesOptions.ServiceTierSchema
|
||||
|
||||
|
||||
@@ -40,7 +40,9 @@ export const routeDefaults = (input: Defaults) => ({
|
||||
|
||||
export const bearerCredentialValue = (input: Credential) => (input.type === "key" ? input.value : input.accessToken)
|
||||
|
||||
export const bearerAuthOption = (input: Credential): AuthOverride => ({ auth: Auth.bearer(bearerCredentialValue(input)) })
|
||||
export const bearerAuthOption = (input: Credential): AuthOverride => ({
|
||||
auth: Auth.bearer(bearerCredentialValue(input)),
|
||||
})
|
||||
|
||||
export const apiKeyOrBearerAuthOption = (
|
||||
input: Credential,
|
||||
|
||||
@@ -49,9 +49,7 @@ export const provider = {
|
||||
export const model: ProviderPackage.Definition<Settings, OpenResponsesProviderOptionsInput>["model"] = (input) =>
|
||||
configure({
|
||||
...ProviderPackage.routeDefaults(input.defaults),
|
||||
...(input.credential
|
||||
? ProviderPackage.bearerAuthOption(input.credential)
|
||||
: { apiKey: input.settings.apiKey }),
|
||||
...(input.credential ? ProviderPackage.bearerAuthOption(input.credential) : { apiKey: input.settings.apiKey }),
|
||||
baseURL: input.settings.baseURL,
|
||||
provider: input.settings.provider,
|
||||
providerOptions: input.settings.providerOptions,
|
||||
|
||||
@@ -71,9 +71,7 @@ export const provider = {
|
||||
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (input) =>
|
||||
configure({
|
||||
...ProviderPackage.routeDefaults(input.defaults),
|
||||
...(input.credential
|
||||
? ProviderPackage.bearerAuthOption(input.credential)
|
||||
: { apiKey: input.settings.apiKey }),
|
||||
...(input.credential ? ProviderPackage.bearerAuthOption(input.credential) : { apiKey: input.settings.apiKey }),
|
||||
baseURL: input.settings.baseURL,
|
||||
provider: input.settings.provider,
|
||||
providerOptions: input.settings.providerOptions,
|
||||
|
||||
@@ -156,9 +156,7 @@ const config = (input: ProviderPackage.ModelInput<Settings>): Config => {
|
||||
}
|
||||
return {
|
||||
...ProviderPackage.routeDefaults(input.defaults),
|
||||
...(input.credential
|
||||
? ProviderPackage.bearerAuthOption(input.credential)
|
||||
: { apiKey: settings.apiKey }),
|
||||
...(input.credential ? ProviderPackage.bearerAuthOption(input.credential) : { apiKey: settings.apiKey }),
|
||||
baseURL: settings.baseURL,
|
||||
headers: Object.keys(headers).length === 0 ? undefined : headers,
|
||||
queryParams: settings.queryParams === undefined ? undefined : { ...settings.queryParams },
|
||||
|
||||
@@ -194,9 +194,7 @@ export const provider = configure()
|
||||
export const model: ProviderPackage.Definition<Settings, OpenRouterProviderOptionsInput>["model"] = (input) =>
|
||||
configure({
|
||||
...ProviderPackage.routeDefaults(input.defaults),
|
||||
...(input.credential
|
||||
? ProviderPackage.bearerAuthOption(input.credential)
|
||||
: { apiKey: input.settings.apiKey }),
|
||||
...(input.credential ? ProviderPackage.bearerAuthOption(input.credential) : { apiKey: input.settings.apiKey }),
|
||||
baseURL: input.settings.baseURL,
|
||||
providerOptions: input.settings.providerOptions,
|
||||
}).model(input.id)
|
||||
|
||||
@@ -98,9 +98,7 @@ export const provider = configure()
|
||||
export const model: ProviderPackage.Definition<Settings, XAIProviderOptionsInput>["model"] = (input) =>
|
||||
configure({
|
||||
...ProviderPackage.routeDefaults(input.defaults),
|
||||
...(input.credential
|
||||
? ProviderPackage.bearerAuthOption(input.credential)
|
||||
: { apiKey: input.settings.apiKey }),
|
||||
...(input.credential ? ProviderPackage.bearerAuthOption(input.credential) : { apiKey: input.settings.apiKey }),
|
||||
baseURL: input.settings.baseURL,
|
||||
providerOptions: input.settings.providerOptions,
|
||||
}).model(input.id)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Schema } from "effect"
|
||||
import { Tool } from "@opencode-ai/schema/tool"
|
||||
import { ModelID, ProviderID, ProviderMetadata, RouteID } from "./ids.js"
|
||||
import { ModelID, ProviderID, RouteID } from "./ids.js"
|
||||
import { ProviderMetadata } from "./messages.js"
|
||||
|
||||
export const ProviderFailureClassification = Schema.Literals(["context-overflow", "payload-too-large"])
|
||||
export type ProviderFailureClassification = typeof ProviderFailureClassification.Type
|
||||
|
||||
@@ -1,8 +1,21 @@
|
||||
import { Schema } from "effect"
|
||||
import { ContentBlockID, FinishReason, ProviderMetadata, ToolCallID } from "./ids.js"
|
||||
import { Message, ToolCallPart, ToolOutput, ToolResultPart, ToolResultValue, type ContentPart } from "./messages.js"
|
||||
import { LLM } from "@opencode-ai/schema/llm"
|
||||
import { ContentBlockID, ToolCallID } from "./ids.js"
|
||||
import {
|
||||
Message,
|
||||
ProviderMetadata,
|
||||
ToolCallPart,
|
||||
ToolOutput,
|
||||
ToolResultPart,
|
||||
ToolResultValue,
|
||||
type ContentPart,
|
||||
} from "./messages.js"
|
||||
import { ProviderFailureClassification } from "./errors.js"
|
||||
|
||||
export const FinishReason = LLM.FinishReason
|
||||
export type FinishReason = Schema.Schema.Type<typeof FinishReason>
|
||||
export { ProviderMetadata } from "./messages.js"
|
||||
|
||||
/**
|
||||
* Token usage reported by an LLM provider.
|
||||
*
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import { Schema } from "effect"
|
||||
import { ProviderMetadata } from "@opencode-ai/schema/ai"
|
||||
import { LLM } from "@opencode-ai/schema/llm"
|
||||
|
||||
export { ProviderMetadata }
|
||||
|
||||
/** Stable string identifier for a protocol implementation. */
|
||||
export const ProtocolID = Schema.String
|
||||
@@ -26,19 +22,3 @@ export type ContentBlockID = Schema.Schema.Type<typeof ContentBlockID>
|
||||
|
||||
export const ToolCallID = Schema.String
|
||||
export type ToolCallID = Schema.Schema.Type<typeof ToolCallID>
|
||||
|
||||
export const ReasoningEfforts = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] as const
|
||||
export const ReasoningEffort = Schema.String
|
||||
export type ReasoningEffort = Schema.Schema.Type<typeof ReasoningEffort>
|
||||
|
||||
export const TextVerbosity = Schema.Literals(["low", "medium", "high"])
|
||||
export type TextVerbosity = Schema.Schema.Type<typeof TextVerbosity>
|
||||
|
||||
export const MessageRole = Schema.Literals(["system", "user", "assistant", "tool"])
|
||||
export type MessageRole = Schema.Schema.Type<typeof MessageRole>
|
||||
|
||||
export const FinishReason = LLM.FinishReason
|
||||
export type FinishReason = Schema.Schema.Type<typeof FinishReason>
|
||||
|
||||
export const JsonSchema = Schema.Record(Schema.String, Schema.Unknown)
|
||||
export type JsonSchema = Schema.Schema.Type<typeof JsonSchema>
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
import { Schema } from "effect"
|
||||
import { Tool } from "@opencode-ai/schema/tool"
|
||||
import { JsonSchema, MessageRole, ProviderMetadata } from "./ids.js"
|
||||
import {
|
||||
CacheHint,
|
||||
CachePolicy,
|
||||
GenerationOptions,
|
||||
HttpOptions,
|
||||
JsonSchema,
|
||||
LanguageModelSchema,
|
||||
ProviderOptions,
|
||||
} from "./options.js"
|
||||
import { isRecord } from "../utils/record.js"
|
||||
|
||||
export const MessageRole = Schema.Literals(["system", "user", "assistant", "tool"])
|
||||
export type MessageRole = Schema.Schema.Type<typeof MessageRole>
|
||||
|
||||
export const ProviderMetadata = Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Unknown)).annotate({
|
||||
identifier: "LLM.ProviderMetadata",
|
||||
})
|
||||
export type ProviderMetadata = Schema.Schema.Type<typeof ProviderMetadata>
|
||||
|
||||
const systemPartSchema = Schema.Struct({
|
||||
type: Schema.Literal("text"),
|
||||
text: Schema.String,
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Schema } from "effect"
|
||||
import { JsonSchema, ModelID, ProviderID } from "./ids.js"
|
||||
import { ModelID, ProviderID } from "./ids.js"
|
||||
import type { AnyRoute } from "../route/client.js"
|
||||
import { isRecord } from "../utils/record.js"
|
||||
|
||||
export const JsonSchema = Schema.Record(Schema.String, Schema.Unknown)
|
||||
export type JsonSchema = Schema.Schema.Type<typeof JsonSchema>
|
||||
|
||||
export const mergeJsonRecords = (
|
||||
...items: ReadonlyArray<Record<string, unknown> | undefined>
|
||||
): Record<string, unknown> | undefined => {
|
||||
|
||||
@@ -17,31 +17,25 @@ describe("request option precedence", () => {
|
||||
test("deep-merges provider option records and replaces arrays, primitives, and null", () => {
|
||||
const merged = mergeProviderOptions(
|
||||
{
|
||||
openai: {
|
||||
include: ["route"],
|
||||
metadata: { route: true, shared: "route" },
|
||||
nullable: "route",
|
||||
primitive: "route",
|
||||
},
|
||||
include: ["route"],
|
||||
metadata: { route: true, shared: "route" },
|
||||
nullable: "route",
|
||||
primitive: "route",
|
||||
},
|
||||
{
|
||||
openai: {
|
||||
include: ["model"],
|
||||
metadata: { model: true, shared: "model" },
|
||||
nullable: null,
|
||||
primitive: "model",
|
||||
},
|
||||
include: ["model"],
|
||||
metadata: { model: true, shared: "model" },
|
||||
nullable: null,
|
||||
primitive: "model",
|
||||
},
|
||||
{ openai: { metadata: { request: true }, primitive: false } },
|
||||
{ metadata: { request: true }, primitive: false },
|
||||
)
|
||||
|
||||
expect(merged).toEqual({
|
||||
openai: {
|
||||
include: ["model"],
|
||||
metadata: { route: true, model: true, request: true, shared: "model" },
|
||||
nullable: null,
|
||||
primitive: false,
|
||||
},
|
||||
include: ["model"],
|
||||
metadata: { route: true, model: true, request: true, shared: "model" },
|
||||
nullable: null,
|
||||
primitive: false,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -4,10 +4,11 @@ import { GoogleVertexResponses } from "../../src/providers.js"
|
||||
const model = GoogleVertexResponses.configure({ accessToken: "test", project: "project" }).model("gemini")
|
||||
|
||||
LLM.request({ model, prompt: "Hello", providerOptions: { textVerbosity: "high" } })
|
||||
LLM.request({ model, prompt: "Hello", providerOptions: { textVerbosity: "verbose" } })
|
||||
|
||||
LLM.request({
|
||||
model,
|
||||
prompt: "Hello",
|
||||
// @ts-expect-error Vertex Responses verbosity uses the Open Responses union.
|
||||
providerOptions: { textVerbosity: "verbose" },
|
||||
// @ts-expect-error Vertex Responses verbosity must be a string.
|
||||
providerOptions: { textVerbosity: 1 },
|
||||
})
|
||||
|
||||
@@ -4,6 +4,10 @@ import { OpenAICompatibleResponses } from "../../src/providers.js"
|
||||
const model = OpenAICompatibleResponses.configure({ baseURL: "https://example.com" }).model("model")
|
||||
|
||||
LLM.request({ model, prompt: "Hello", providerOptions: { reasoningSummary: "detailed" } })
|
||||
LLM.request({ model, prompt: "Hello", providerOptions: { reasoningEffort: "high" } })
|
||||
LLM.request({ model, prompt: "Hello", providerOptions: { reasoningEffort: "experimental" } })
|
||||
LLM.request({ model, prompt: "Hello", providerOptions: { textVerbosity: "low" } })
|
||||
LLM.request({ model, prompt: "Hello", providerOptions: { textVerbosity: "verbose" } })
|
||||
|
||||
LLM.request({
|
||||
model,
|
||||
|
||||
@@ -2,8 +2,14 @@ import { LLM } from "../../src/index.js"
|
||||
import { OpenAI } from "../../src/providers.js"
|
||||
|
||||
const selected = OpenAI.responses("gpt-5")
|
||||
const chat = OpenAI.chat("gpt-4o-mini")
|
||||
|
||||
LLM.request({ model: selected, prompt: "Hello", providerOptions: { reasoningEffort: "high" } })
|
||||
LLM.request({ model: selected, prompt: "Hello", providerOptions: { reasoningEffort: "experimental" } })
|
||||
LLM.request({ model: selected, prompt: "Hello", providerOptions: { textVerbosity: "low" } })
|
||||
LLM.request({ model: selected, prompt: "Hello", providerOptions: { textVerbosity: "verbose" } })
|
||||
LLM.request({ model: chat, prompt: "Hello", providerOptions: { reasoningEffort: "max" } })
|
||||
LLM.request({ model: chat, prompt: "Hello", providerOptions: { reasoningEffort: "experimental" } })
|
||||
|
||||
LLM.request({
|
||||
model: selected,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { XAI } from "../../src/providers.js"
|
||||
const model = XAI.provider.model("grok-4")
|
||||
|
||||
LLM.request({ model, prompt: "Hello", providerOptions: { reasoningEffort: "high" } })
|
||||
LLM.request({ model, prompt: "Hello", providerOptions: { reasoningEffort: "experimental" } })
|
||||
|
||||
LLM.request({
|
||||
model,
|
||||
|
||||
@@ -15,13 +15,15 @@ const authHeaders = (
|
||||
env: Record<string, string> = {},
|
||||
) =>
|
||||
Effect.runPromise(
|
||||
selected.route.auth.apply({
|
||||
request: LLM.request({ model: selected, prompt: "hello" }),
|
||||
method: "POST",
|
||||
url: "https://example.test/v1",
|
||||
body: "{}",
|
||||
headers: Headers.fromInput(headers),
|
||||
}).pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env })))),
|
||||
selected.route.auth
|
||||
.apply({
|
||||
request: LLM.request({ model: selected, prompt: "hello" }),
|
||||
method: "POST",
|
||||
url: "https://example.test/v1",
|
||||
body: "{}",
|
||||
headers: Headers.fromInput(headers),
|
||||
})
|
||||
.pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env })))),
|
||||
)
|
||||
|
||||
const applyAuth = (
|
||||
@@ -43,18 +45,16 @@ const applyAuth = (
|
||||
describe("provider package credential lowering", () => {
|
||||
test("intentionally renders keys and OAuth credentials as bearer auth", async () => {
|
||||
const key = await applyAuth(ProviderPackage.bearerAuthOption({ type: "key", value: "provider-key" }))
|
||||
const oauth = await applyAuth(
|
||||
ProviderPackage.bearerAuthOption({ type: "oauth", accessToken: "provider-token" }),
|
||||
)
|
||||
const oauth = await applyAuth(ProviderPackage.bearerAuthOption({ type: "oauth", accessToken: "provider-token" }))
|
||||
|
||||
expect(key.authorization).toBe("Bearer provider-key")
|
||||
expect(oauth.authorization).toBe("Bearer provider-token")
|
||||
})
|
||||
|
||||
test("keeps key-header credentials configurable and removes stale keys for OAuth", async () => {
|
||||
expect(
|
||||
ProviderPackage.apiKeyOrBearerAuthOption({ type: "key", value: "provider-key" }, "x-api-key"),
|
||||
).toEqual({ apiKey: "provider-key" })
|
||||
expect(ProviderPackage.apiKeyOrBearerAuthOption({ type: "key", value: "provider-key" }, "x-api-key")).toEqual({
|
||||
apiKey: "provider-key",
|
||||
})
|
||||
const oauth = ProviderPackage.apiKeyOrBearerAuthOption(
|
||||
{ type: "oauth", accessToken: "provider-token" },
|
||||
"x-api-key",
|
||||
|
||||
@@ -255,7 +255,7 @@ describe("OpenAI Chat route", () => {
|
||||
LLMClient.generate(
|
||||
LLMRequest.update(request, {
|
||||
model: Azure.configure({
|
||||
baseURL: "https://opencode-test.openai.azure.com/openai/v1/",
|
||||
baseURL: "https://opencode-test.openai.azure.com/openai/",
|
||||
apiKey: "azure-key",
|
||||
headers: { authorization: "Bearer stale" },
|
||||
}).chat("gpt-4o-mini"),
|
||||
|
||||
@@ -178,6 +178,16 @@ describe("OpenAI Responses route", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("passes through custom OpenAI text verbosity strings", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
LLMRequest.update(request, { providerOptions: { textVerbosity: "verbose" } }),
|
||||
)
|
||||
|
||||
expect(prepared.body.text).toEqual({ verbosity: "verbose" })
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("omits unsupported semantic service tiers", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
|
||||
@@ -94,10 +94,19 @@ it.effect("projects request settings, headers, and body overlays", () =>
|
||||
headers: { "x-test": "header" },
|
||||
body: { safety_setting: "strict" },
|
||||
})
|
||||
const prepared = yield* compileRequest(LLM.request({ model: resolved, prompt: "Hello" }))
|
||||
const prepared = yield* compileRequest(
|
||||
LLM.request({
|
||||
model: resolved,
|
||||
prompt: "Hello",
|
||||
providerOptions: { safetySettings: [{ category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE" }] },
|
||||
}),
|
||||
)
|
||||
|
||||
expect(prepared.body.providerOptions).toEqual({
|
||||
google: { thinkingConfig: { thinkingBudget: 1024 } },
|
||||
google: {
|
||||
thinkingConfig: { thinkingBudget: 1024 },
|
||||
safetySettings: [{ category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE" }],
|
||||
},
|
||||
})
|
||||
expect(prepared.body.headers).toEqual({ "x-test": "header" })
|
||||
expect(body).toEqual({ safety_setting: "strict" })
|
||||
|
||||
@@ -17,7 +17,6 @@ import { Connection } from "@opencode-ai/schema/connection"
|
||||
import { Credential } from "@opencode-ai/schema/credential"
|
||||
import { FileSystem } from "@opencode-ai/schema/filesystem"
|
||||
import { Integration } from "@opencode-ai/schema/integration"
|
||||
import { AI } from "@opencode-ai/schema/ai"
|
||||
import { LLM } from "@opencode-ai/schema/llm"
|
||||
import { Permission } from "@opencode-ai/schema/permission"
|
||||
import { Pty } from "@opencode-ai/schema/pty"
|
||||
@@ -96,7 +95,6 @@ test("Core reuses the canonical shared schemas", async () => {
|
||||
[coreIntegration.Method, Integration.Method],
|
||||
[coreIntegration.Ref, Integration.Ref],
|
||||
[coreLocation.Ref, Location.Ref],
|
||||
[coreAI.ProviderMetadata, AI.ProviderMetadata],
|
||||
[coreAI.FinishReason, LLM.FinishReason],
|
||||
[coreModel.ID, Model.ID],
|
||||
[coreModel.VariantID, Model.VariantID],
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
export * as AI from "./ai.js"
|
||||
|
||||
import { Schema } from "effect"
|
||||
|
||||
export const ProviderMetadata = Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Unknown)).annotate({
|
||||
identifier: "AI.ProviderMetadata",
|
||||
})
|
||||
export type ProviderMetadata = Schema.Schema.Type<typeof ProviderMetadata>
|
||||
@@ -8,7 +8,6 @@ export { FileSystem } from "./filesystem.js"
|
||||
export { Form } from "./form.js"
|
||||
export { Integration } from "./integration.js"
|
||||
export { LLM } from "./llm.js"
|
||||
export { AI } from "./ai.js"
|
||||
export { Location } from "./location.js"
|
||||
export { Mcp } from "./mcp.js"
|
||||
export { Model } from "./model.js"
|
||||
|
||||
@@ -39,7 +39,7 @@ describe("SessionError", () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("FinishReason is the closed browser-safe provider set", () => {
|
||||
test("FinishReason is the closed normalized provider set", () => {
|
||||
const reasons = ["stop", "length", "tool-calls", "content-filter", "error", "unknown"] as const
|
||||
expect(reasons.map((reason) => Schema.decodeUnknownSync(LLM.FinishReason)(reason))).toEqual([...reasons])
|
||||
expect(() => Schema.decodeUnknownSync(LLM.FinishReason)("other")).toThrow()
|
||||
|
||||
@@ -46,6 +46,7 @@ const ADD_TAB_WIDTH = 3
|
||||
const MARQUEE_DELAY = 600
|
||||
const MARQUEE_INTERVAL = 80
|
||||
const CONTEXT_MENU_WIDTH = 16
|
||||
const MIDDLE_MOUSE_BUTTON = 1
|
||||
const RIGHT_MOUSE_BUTTON = 2
|
||||
|
||||
type TabContextMenuState = {
|
||||
@@ -569,6 +570,14 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
|
||||
onMouseOver={() => marquee.enter(tab.sessionID, title(), hoveredTitleWidth())}
|
||||
onMouseOut={() => marquee.leave(tab.sessionID)}
|
||||
onMouseDown={(event) => {
|
||||
if (event.button === MIDDLE_MOUSE_BUTTON) {
|
||||
didDrag = false
|
||||
setDragging(undefined)
|
||||
tabs.close(tab.sessionID)
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return
|
||||
}
|
||||
if (event.button === RIGHT_MOUSE_BUTTON) {
|
||||
didDrag = false
|
||||
setDragging(undefined)
|
||||
@@ -1108,6 +1117,14 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
|
||||
onMouseOver={() => marquee.enter(tab.sessionID, title(), hoveredTitleWidth())}
|
||||
onMouseOut={() => marquee.leave(tab.sessionID)}
|
||||
onMouseDown={(event) => {
|
||||
if (event.button === MIDDLE_MOUSE_BUTTON) {
|
||||
didDrag = false
|
||||
setDragging(undefined)
|
||||
tabs.close(tab === NEW_SESSION_TAB ? undefined : tab.sessionID)
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return
|
||||
}
|
||||
if (event.button === RIGHT_MOUSE_BUTTON) {
|
||||
didDrag = false
|
||||
setDragging(undefined)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/** @jsxImportSource @opentui/solid */
|
||||
import { testRender } from "@opentui/solid"
|
||||
import { MouseButton } from "@opentui/core"
|
||||
import { expect, test } from "bun:test"
|
||||
import { createSignal } from "solid-js"
|
||||
import { ConfigProvider } from "../../src/config"
|
||||
@@ -60,3 +61,41 @@ test("releasing a transcript selection over tab controls does not activate them"
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("middle-click closes a session tab without selecting it", async () => {
|
||||
const [active, setActive] = createSignal("first")
|
||||
const closed: Array<string | undefined> = []
|
||||
const controller = {
|
||||
tabs: () => [
|
||||
{ sessionID: "first", title: "First" },
|
||||
{ sessionID: "second", title: "Second" },
|
||||
],
|
||||
current: active,
|
||||
select: setActive,
|
||||
close: (sessionID?: string) => closed.push(sessionID),
|
||||
move() {},
|
||||
status: () => EMPTY_SESSION_TAB_STATUS,
|
||||
} satisfies SessionTabsController
|
||||
const app = await testRender(
|
||||
() => (
|
||||
<TestTuiContexts>
|
||||
<ConfigProvider config={createTuiResolvedConfig({ tabs: { enabled: true } })}>
|
||||
<ThemeProvider mode="dark" source={emptyThemeSource}>
|
||||
<SessionTabs controller={controller} animations={false} />
|
||||
</ThemeProvider>
|
||||
</ConfigProvider>
|
||||
</TestTuiContexts>
|
||||
),
|
||||
{ width: 60, height: 8 },
|
||||
)
|
||||
|
||||
try {
|
||||
app.renderer.start()
|
||||
await app.waitForFrame((frame) => frame.includes("Second"))
|
||||
await app.mockMouse.click(40, 0, MouseButton.MIDDLE)
|
||||
expect(closed).toEqual(["second"])
|
||||
expect(active()).toBe("first")
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user