mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-23 22:23:18 -04:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5514024655 | |||
| 3ed523888c | |||
| 07c2597558 |
@@ -0,0 +1,184 @@
|
||||
# Anthropic Messages Protocol Support
|
||||
|
||||
This checklist tracks correctness and parity for the native Anthropic Messages
|
||||
protocol in `src/protocols/anthropic-messages.ts`, including direct Anthropic,
|
||||
Anthropic-compatible deployments, and Claude on Google Vertex.
|
||||
|
||||
The scope is protocol behavior: request lowering, streamed lifecycle events,
|
||||
reasoning state, tool calls and results, terminal outcomes, persistence, replay,
|
||||
and usage. Optional provider capabilities are tracked separately from baseline
|
||||
flow correctness.
|
||||
|
||||
## Status Legend
|
||||
|
||||
- `[x]` Implemented and covered by focused tests.
|
||||
- `[ ]` Missing, incomplete, or awaiting an unmerged fix.
|
||||
|
||||
## Reference Baseline
|
||||
|
||||
- [Anthropic TypeScript SDK Messages types](https://github.com/anthropics/anthropic-sdk-typescript/blob/bfa9197f0182084941052be9752c948638421601/src/resources/messages/messages.ts)
|
||||
- [Anthropic TypeScript SDK stream accumulator](https://github.com/anthropics/anthropic-sdk-typescript/blob/bfa9197f0182084941052be9752c948638421601/src/lib/MessageStream.ts)
|
||||
- [Anthropic TypeScript Vertex client](https://github.com/anthropics/anthropic-sdk-typescript/blob/bfa9197f0182084941052be9752c948638421601/packages/vertex-sdk/src/client.ts)
|
||||
- [Anthropic streaming protocol](https://platform.claude.com/docs/en/build-with-claude/streaming)
|
||||
- [Anthropic stop reasons](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons)
|
||||
- [Anthropic mid-conversation system messages](https://platform.claude.com/docs/en/build-with-claude/mid-conversation-system-messages)
|
||||
- [Claude on Google Cloud](https://platform.claude.com/docs/en/build-with-claude/claude-on-vertex-ai)
|
||||
- OpenCode tracker: [#41932](https://github.com/anomalyco/opencode/issues/41932)
|
||||
|
||||
## Change Log
|
||||
|
||||
| Status | Change | Tracking |
|
||||
| --- | --- | --- |
|
||||
| Merged | Accept nullable Anthropic input usage without losing the `message_start` count | [#43759](https://github.com/anomalyco/opencode/issues/43759), [#43761](https://github.com/anomalyco/opencode/pull/43761) |
|
||||
| In progress | Ignore unknown named SSE events before JSON decoding | [#43765](https://github.com/anomalyco/opencode/issues/43765), [#43767](https://github.com/anomalyco/opencode/pull/43767) |
|
||||
|
||||
Anthropic's `AnthropicVertex` client reuses the Messages API implementation. It
|
||||
rewrites Google authentication, the endpoint URL, `model`, and
|
||||
`anthropic_version`; it does not perform agent-history normalization. OpenCode's
|
||||
basic Vertex projection in `src/providers/google-vertex-messages.ts` follows the
|
||||
same model.
|
||||
|
||||
## Request Lowering
|
||||
|
||||
- [x] Lower initial system prompts through the top-level `system` field.
|
||||
- [x] Lower supported chronological system updates as native `system` messages.
|
||||
- [x] Reject system updates that split a local `tool_use` from its `tool_result`.
|
||||
- [x] Fall back to escaped user text when native chronological system updates are unsupported.
|
||||
- [x] Batch parallel local tool results into one immediately following user turn.
|
||||
- [x] Lower signed thinking blocks without changing their text or signature.
|
||||
- [x] Round-trip opaque `redacted_thinking` blocks.
|
||||
- [x] Lower text, supported base64 images, and PDF documents.
|
||||
- [x] Enforce Anthropic's four explicit cache-breakpoint limit.
|
||||
- [ ] Reject or safely lower visible reasoning that has no Anthropic signature. Current lowering emits an invalid unsigned `thinking` block.
|
||||
- [ ] Enforce manual thinking budget constraints: `budget_tokens >= 1024` and, outside interleaved-thinking exceptions, `budget_tokens < max_tokens`.
|
||||
- [ ] Reconcile canonical answer-token limits with Anthropic's `max_tokens`, which includes thinking output.
|
||||
- [ ] Select manual, adaptive, disabled, and always-on thinking according to model capability.
|
||||
- [ ] Suppress unsupported `temperature`, `top_p`, and `top_k` values for newer models.
|
||||
- [ ] Reject unsupported thinking and effort combinations before network I/O.
|
||||
- [ ] Normalize or reject final assistant prefills according to model capability and whitespace rules.
|
||||
- [ ] Normalize Anthropic tool-call IDs to the provider's accepted character and length constraints.
|
||||
- [ ] Filter empty request text blocks at the AI-package boundary rather than relying on Core history conversion.
|
||||
- [ ] Restrict base64 image MIME types to JPEG, PNG, GIF, and WebP.
|
||||
- [ ] Support URL image/document sources, plain-text/content documents, search-result blocks, and tool references where available.
|
||||
- [ ] Model typed request metadata and current request controls instead of requiring raw HTTP body overlays.
|
||||
|
||||
## Stream Lifecycle
|
||||
|
||||
- [x] Parse text, thinking, signature, and tool-input deltas.
|
||||
- [x] Emit canonical text and reasoning start, delta, and end events.
|
||||
- [x] Assemble streamed tool JSON and emit one terminal local tool call.
|
||||
- [x] Settle pending streamed tool calls at `message_stop`.
|
||||
- [x] Require a canonical terminal event before transport EOF.
|
||||
- [x] Reject canonical events emitted after a terminal event.
|
||||
- [ ] Require exactly one `message_start` before message or content events.
|
||||
- [ ] Reject duplicate `message_start` events.
|
||||
- [ ] Track open content-block indexes and reject deltas or stops without matching starts.
|
||||
- [ ] Reject duplicate block starts and stops.
|
||||
- [ ] Distinguish a normal `content_block_stop` from cleanup at `message_stop` or transport interruption.
|
||||
- [ ] Require a valid terminal message delta and stop reason before accepting `message_stop`.
|
||||
- [ ] Reject a standalone `message_stop` instead of manufacturing an unknown successful finish.
|
||||
- [ ] Preserve abnormal closure state for interrupted text, reasoning, and tool input.
|
||||
- [x] Ignore unknown named SSE events before decoding their data payload while keeping recognized non-empty event payloads strict. Tracked by [#43765](https://github.com/anomalyco/opencode/issues/43765) and [#43767](https://github.com/anomalyco/opencode/pull/43767).
|
||||
- [ ] Preserve citation deltas and attach them to their text blocks.
|
||||
- [ ] Fail safely on unknown replay-critical content blocks instead of silently dropping them.
|
||||
|
||||
## Terminal Outcomes
|
||||
|
||||
- [x] Preserve raw stop reasons alongside normalized finish reasons.
|
||||
- [x] Map `end_turn` and `stop_sequence` to normal stop.
|
||||
- [x] Map `max_tokens` and `model_context_window_exceeded` to length.
|
||||
- [x] Map `tool_use` to tool continuation.
|
||||
- [x] Map `refusal` to content-filter outcome.
|
||||
- [ ] Treat `pause_turn` as a continuation requirement by replaying the assistant content immediately. It is currently normalized as an ordinary stop.
|
||||
- [ ] Preserve structured refusal `stop_details`, including category and explanation.
|
||||
- [ ] Discard or quarantine partial visible output when a streamed refusal requires it.
|
||||
- [ ] Retain response ID, actual served model, container state, initial message metadata, and complete stop diagnostics.
|
||||
- [ ] Apply a runner-level policy for valid empty `end_turn` responses when the product requires usable output. Empty `end_turn` is valid Anthropic protocol and must not be rejected by the adapter itself.
|
||||
|
||||
## Compaction And Context Management
|
||||
|
||||
- [ ] Model native context-management request options.
|
||||
- [ ] Parse `compaction` content blocks and `compaction_delta` events.
|
||||
- [ ] Preserve compaction `content` and opaque `encrypted_content` exactly.
|
||||
- [ ] Persist and replay compaction blocks across stateless requests.
|
||||
- [ ] Map the compaction stop reason to a continuation outcome.
|
||||
- [ ] Include compaction, advisor, fallback, and message iterations in usage accounting.
|
||||
- [ ] Fail safely when raw HTTP overlays enable unsupported context management. Current behavior can silently drop compaction output and report an empty unknown completion.
|
||||
|
||||
## Hosted Tools
|
||||
|
||||
- [x] Parse and replay `server_tool_use` blocks.
|
||||
- [x] Parse and replay web-search, web-fetch, and code-execution result payloads.
|
||||
- [x] Mark hosted calls and results as provider-executed so local dispatch is skipped.
|
||||
- [ ] Lower `ToolDefinition.native` into Anthropic hosted-tool definitions.
|
||||
- [ ] Support current web search, web fetch, code execution, bash, editor, memory, and tool-search request definitions where deployed.
|
||||
- [ ] Preserve bash, editor, tool-search, and container-upload response blocks.
|
||||
- [ ] Preserve `caller` provenance and container continuation state.
|
||||
- [ ] Support strict schemas, deferred loading, tool references, and eager input streaming.
|
||||
- [ ] Gate hosted-tool definitions and result families by deployment capability, especially on Vertex.
|
||||
|
||||
## Usage And Accounting
|
||||
|
||||
- [x] Merge usage split between `message_start` and `message_delta`.
|
||||
- [x] Preserve unknown Anthropic usage fields in provider metadata.
|
||||
- [x] Normalize cache-read, cache-write, non-cached input, output, and thinking-token counts.
|
||||
- [x] Accept nullable `message_delta.usage.input_tokens` while retaining the earlier start count for normalized usage. Implemented by [PR #43761](https://github.com/anomalyco/opencode/pull/43761).
|
||||
- [ ] Normalize the five-minute and one-hour cache-write split for accurate costing.
|
||||
- [ ] Aggregate iteration usage once at terminal completion without double counting.
|
||||
|
||||
## Google Vertex
|
||||
|
||||
- [x] Remove `model` from the body and place it in the Vertex endpoint URL.
|
||||
- [x] Add `anthropic_version: "vertex-2023-10-16"` to the request body.
|
||||
- [x] Use `streamRawPredict` for streaming requests.
|
||||
- [x] Support global, `us`, `eu`, and regional endpoint hosts.
|
||||
- [x] Support explicit access tokens and lazy Application Default Credentials.
|
||||
- [ ] Resolve project identity from the Google auth client or quota-project header when not configured explicitly.
|
||||
- [ ] Preserve all Google auth headers, including quota-project headers, rather than reducing authentication to a bearer token.
|
||||
- [ ] Classify transient ADC acquisition failures as retryable transport failures instead of missing credentials.
|
||||
- [ ] Handle the Vertex terminal-system-after-tool-result divergence without treating the workaround as the canonical Messages representation. Tracked by [#43478](https://github.com/anomalyco/opencode/issues/43478) and [PR #43498](https://github.com/anomalyco/opencode/pull/43498).
|
||||
|
||||
Anthropic documents a terminal system message immediately after a `tool_result`
|
||||
as valid and supported on Google Cloud. Vertex nevertheless returns 404 for the
|
||||
reported continuation shape. Folding the update into the user tool-result turn
|
||||
avoids the deployment error, but lowers system authority and adds text after a
|
||||
tool result, which can cause an empty `end_turn`. Treat this as deployment
|
||||
capability policy rather than a change to the canonical Messages protocol.
|
||||
|
||||
## Comparison With Pi
|
||||
|
||||
| Area | OpenCode | pi |
|
||||
| --- | --- | --- |
|
||||
| Unknown named SSE events | Filters before JSON parsing | Filters before JSON parsing |
|
||||
| Response ID and refusal details | Missing | Preserved |
|
||||
| Unsigned thinking replay | Emits invalid thinking | Safely lowers to text by default |
|
||||
| Empty request content | Core filters some paths | Adapter filters directly |
|
||||
| Tool-call ID normalization | Missing | Implemented |
|
||||
| Temperature compatibility | Missing | Model compatibility driven |
|
||||
| One-hour cache-write accounting | Raw metadata only | Normalized for costing |
|
||||
| Vertex Anthropic | First-class route | Available through custom `AnthropicVertex` client injection |
|
||||
| Required terminal EOF | Enforced | Less strict |
|
||||
| Parallel tool-result batching | Implemented | Implemented |
|
||||
| Pending tool settlement | Implemented | Implemented |
|
||||
| Redacted-thinking replay | Implemented | Implemented |
|
||||
| PDF and media tool results | Implemented | Narrower support |
|
||||
| Native compaction | Missing | Missing |
|
||||
| `pause_turn` continuation | Missing | Missing |
|
||||
|
||||
## Confirmed Non-Gaps
|
||||
|
||||
- [x] Treat `signature_delta.signature` as the complete signature value. Anthropic documents one signature delta before block stop, and the official `MessageStream` also replaces rather than appends it. Appending is optional proxy tolerance.
|
||||
- [x] Accept empty `end_turn` as a valid provider response. Product-level recovery may request continuation, but the protocol must preserve the valid outcome.
|
||||
- [x] Require `message_stop` before clean completion. OpenCode is stricter than Anthropic's raw SDK stream here.
|
||||
|
||||
## Suggested Work Order
|
||||
|
||||
- [x] Merge nullable input usage support in [PR #43761](https://github.com/anomalyco/opencode/pull/43761).
|
||||
- [ ] Handle unsigned reasoning replay.
|
||||
- [ ] Restrict image MIME types.
|
||||
- [ ] Validate manual thinking budgets.
|
||||
- [ ] Preserve response identity and refusal details.
|
||||
- [x] Filter unknown named SSE events while preserving strict recognized-event decoding.
|
||||
- [ ] Enforce the complete stream lifecycle grammar.
|
||||
- [ ] Add native compaction representation, persistence, and replay.
|
||||
- [ ] Complete hosted-tool request and response support.
|
||||
@@ -33,6 +33,18 @@ export const DEFAULT_BASE_URL = "https://api.anthropic.com/v1"
|
||||
export const PATH = "/messages"
|
||||
export const DEFAULT_MAX_TOKENS = 32_000
|
||||
|
||||
const SSE_EVENTS = new Set([
|
||||
"message",
|
||||
"message_start",
|
||||
"message_delta",
|
||||
"message_stop",
|
||||
"content_block_start",
|
||||
"content_block_delta",
|
||||
"content_block_stop",
|
||||
"error",
|
||||
])
|
||||
export const framing = Framing.sseEvents(SSE_EVENTS)
|
||||
|
||||
export type ThinkingInput =
|
||||
| {
|
||||
readonly type: "adaptive"
|
||||
@@ -1039,7 +1051,7 @@ export const route = Route.make({
|
||||
protocol,
|
||||
endpoint: Endpoint.path(PATH, { baseURL: DEFAULT_BASE_URL }),
|
||||
auth: Auth.none,
|
||||
framing: Framing.sse,
|
||||
framing,
|
||||
headers: () => ({ "anthropic-version": "2023-06-01" }),
|
||||
})
|
||||
|
||||
|
||||
@@ -197,19 +197,28 @@ export const errorText = (error: unknown) => {
|
||||
|
||||
/**
|
||||
* `framing` step for Server-Sent Events. Decodes UTF-8, runs the SSE channel
|
||||
* decoder, and drops empty / `[DONE]` keep-alive events so the protocol event
|
||||
* schema sees one JSON string per element. The SSE channel emits a
|
||||
* decoder, optionally filters named events, and drops empty / `[DONE]`
|
||||
* keep-alive events so the protocol event schema 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). Decoder failures become provider output
|
||||
* errors so the public error channel stays `AIError`.
|
||||
*/
|
||||
export const sseFraming = (bytes: Stream.Stream<Uint8Array, AIError>): Stream.Stream<string, AIError> =>
|
||||
export const sseFraming = (
|
||||
bytes: Stream.Stream<Uint8Array, AIError>,
|
||||
events?: ReadonlySet<string>,
|
||||
): Stream.Stream<string, AIError> =>
|
||||
bytes.pipe(
|
||||
Stream.decodeText(),
|
||||
Stream.pipeThroughChannel(Sse.decode()),
|
||||
Stream.catchTag("Retry", () => Stream.empty),
|
||||
Stream.catchTag("SseError", (error) => Stream.fail(eventError("sse", error.message))),
|
||||
Stream.filter((event) => event.data.length > 0 && event.data !== "[DONE]"),
|
||||
Stream.filter(
|
||||
(event) =>
|
||||
(events === undefined || events.has(event.event)) &&
|
||||
event.data.length > 0 &&
|
||||
(event.data !== "[DONE]" || (events !== undefined && event.event !== "message")),
|
||||
),
|
||||
Stream.map((event) => event.data),
|
||||
)
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import { AnthropicMessages } from "../protocols/anthropic-messages.js"
|
||||
import { Auth } from "../route/auth.js"
|
||||
import { Route, type RouteDefaultsInput } from "../route/client.js"
|
||||
import { Endpoint } from "../route/endpoint.js"
|
||||
import { Framing } from "../route/framing.js"
|
||||
import { Protocol } from "../route/protocol.js"
|
||||
import { ProviderID, type ModelID } from "../schema/index.js"
|
||||
import { GoogleVertexShared } from "./google-vertex-shared.js"
|
||||
@@ -57,7 +56,7 @@ const route = Route.make({
|
||||
}),
|
||||
endpoint: Endpoint.path(({ request }) => `/${request.model.id}:streamRawPredict`),
|
||||
auth: Auth.none,
|
||||
framing: Framing.sse,
|
||||
framing: AnthropicMessages.framing,
|
||||
})
|
||||
|
||||
export const routes = [route]
|
||||
|
||||
@@ -24,4 +24,10 @@ export interface Definition<Frame> {
|
||||
/** Server-Sent Events framing. Used by every JSON-streaming HTTP provider. */
|
||||
export const sse: Definition<string> = { id: "sse", frame: ProviderShared.sseFraming }
|
||||
|
||||
/** SSE framing restricted to protocol-recognized event names. */
|
||||
export const sseEvents = (events: ReadonlySet<string>): Definition<string> => ({
|
||||
id: "sse",
|
||||
frame: (bytes) => ProviderShared.sseFraming(bytes, events),
|
||||
})
|
||||
|
||||
export * as Framing from "./framing.js"
|
||||
|
||||
@@ -10,6 +10,9 @@ export const sseEvents = (...chunks: ReadonlyArray<unknown>): string =>
|
||||
|
||||
const formatChunk = (chunk: unknown) => `data: ${typeof chunk === "string" ? chunk : JSON.stringify(chunk)}\n\n`
|
||||
|
||||
export const sseNamedEvent = (event: string, data: unknown): string =>
|
||||
`event: ${event}\ndata: ${typeof data === "string" ? data : JSON.stringify(data)}`
|
||||
|
||||
/**
|
||||
* Build an SSE body from already-serialized strings (used when the chunk shape
|
||||
* itself is part of what's being tested, e.g. malformed chunks).
|
||||
|
||||
@@ -8,7 +8,7 @@ import * as AnthropicMessages from "../../src/protocols/anthropic-messages.js"
|
||||
import { continuationRequest, nativeAnthropicMessagesContinuation } from "../continuation-scenarios.js"
|
||||
import { it } from "../lib/effect.js"
|
||||
import { dynamicResponse, fixedResponse } from "../lib/http.js"
|
||||
import { sseEvents } from "../lib/sse.js"
|
||||
import { sseEvents, sseNamedEvent, sseRaw } from "../lib/sse.js"
|
||||
|
||||
const model = AnthropicMessages.route
|
||||
.with({ endpoint: { baseURL: "https://api.anthropic.test/v1/" }, auth: Auth.header("x-api-key", "test") })
|
||||
@@ -640,6 +640,59 @@ describe("Anthropic Messages route", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("ignores unknown named SSE events", () =>
|
||||
Effect.gen(function* () {
|
||||
const response = yield* LLMClient.generate(request).pipe(
|
||||
Effect.provide(
|
||||
fixedResponse(
|
||||
sseRaw(
|
||||
sseNamedEvent("message_start", {
|
||||
type: "message_start",
|
||||
message: { usage: { input_tokens: 5 } },
|
||||
}),
|
||||
sseNamedEvent("proxy.stats", "not json"),
|
||||
sseNamedEvent("content_block_start", {
|
||||
type: "content_block_start",
|
||||
index: 0,
|
||||
content_block: { type: "text", text: "" },
|
||||
}),
|
||||
sseNamedEvent("content_block_delta", {
|
||||
type: "content_block_delta",
|
||||
index: 0,
|
||||
delta: { type: "text_delta", text: "Hello" },
|
||||
}),
|
||||
sseNamedEvent("content_block_stop", { type: "content_block_stop", index: 0 }),
|
||||
sseNamedEvent("message_delta", {
|
||||
type: "message_delta",
|
||||
delta: { stop_reason: "end_turn" },
|
||||
usage: { output_tokens: 1 },
|
||||
}),
|
||||
sseNamedEvent("message_stop", { type: "message_stop" }),
|
||||
sseNamedEvent("proxy.done", "still not json"),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
expect(response.message.content).toEqual([{ type: "text", text: "Hello" }])
|
||||
expect(response.finishReason).toEqual({ normalized: "stop", raw: "end_turn" })
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("rejects malformed recognized SSE events", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* LLMClient.generate(request).pipe(
|
||||
Effect.provide(fixedResponse(sseRaw(sseNamedEvent("message_start", "[DONE]")))),
|
||||
Effect.flip,
|
||||
)
|
||||
|
||||
expect(error.reason).toMatchObject({
|
||||
_tag: "InvalidProviderOutput",
|
||||
message: "Invalid anthropic/anthropic-messages stream event",
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("maps nullable input tokens and preserves unknown Anthropic usage fields", () =>
|
||||
Effect.gen(function* () {
|
||||
const response = yield* LLMClient.generate(request).pipe(
|
||||
|
||||
@@ -181,20 +181,23 @@ export function createComposerModel(adapter: ComposerAdapter): ComposerModel {
|
||||
const skills = createMemo(() => data.location.skill.list({ directory: sdk().directory }) ?? [])
|
||||
const context = createMemo<ComposerSuggestion[]>(() => [
|
||||
...references(),
|
||||
...skills().map((skill) => ({
|
||||
id: `skill:${skill.id}`,
|
||||
kind: "skill" as const,
|
||||
label: `@${skill.id}`,
|
||||
description: skill.description,
|
||||
mention: {
|
||||
type: "skill" as const,
|
||||
id: Skill.ID.make(skill.id),
|
||||
name: Skill.Name.make(skill.name),
|
||||
content: `@${skill.id}`,
|
||||
start: 0,
|
||||
end: 0,
|
||||
},
|
||||
})),
|
||||
...skills().map((skill) => {
|
||||
const id = skill.id ?? skill.name
|
||||
return {
|
||||
id: `skill:${id}`,
|
||||
kind: "skill" as const,
|
||||
label: `@${id}`,
|
||||
description: skill.description,
|
||||
mention: {
|
||||
type: "skill" as const,
|
||||
id: Skill.ID.make(id),
|
||||
name: Skill.Name.make(skill.name),
|
||||
content: `@${id}`,
|
||||
start: 0,
|
||||
end: 0,
|
||||
},
|
||||
}
|
||||
}),
|
||||
...adapter
|
||||
.controls()
|
||||
.agents.available.filter((agent) => !agent.hidden && agent.mode !== "primary")
|
||||
|
||||
@@ -36,6 +36,26 @@ describe("normalizeAgentList", () => {
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test("adapts agents returned by servers without request settings", () => {
|
||||
const result = normalizeAgentList([
|
||||
{
|
||||
id: "build",
|
||||
name: "Build",
|
||||
mode: "primary",
|
||||
hidden: false,
|
||||
request: { headers: {}, body: {} },
|
||||
permissions: [],
|
||||
},
|
||||
] as unknown as AgentListOutput["data"])
|
||||
|
||||
expect(result[0]).toMatchObject({
|
||||
name: "build",
|
||||
temperature: undefined,
|
||||
topP: undefined,
|
||||
options: {},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("normalizeProviderList", () => {
|
||||
|
||||
@@ -7,26 +7,28 @@ export const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
|
||||
|
||||
export function normalizeAgentList(input: AgentListOutput["data"] | Agent[]): Agent[] {
|
||||
if (input.every((agent) => !("request" in agent))) return input as Agent[]
|
||||
return (input as AgentListOutput["data"]).map((agent) => ({
|
||||
name: agent.id,
|
||||
description: agent.description,
|
||||
mode: agent.mode,
|
||||
hidden: agent.hidden,
|
||||
temperature:
|
||||
typeof agent.request.settings.temperature === "number" ? agent.request.settings.temperature : undefined,
|
||||
topP: typeof agent.request.settings.topP === "number" ? agent.request.settings.topP : undefined,
|
||||
color: agent.color,
|
||||
permission: agent.permissions.map((rule) => ({
|
||||
permission: rule.action,
|
||||
pattern: rule.resource,
|
||||
action: rule.effect,
|
||||
})),
|
||||
model: agent.model && { providerID: agent.model.providerID, modelID: agent.model.id },
|
||||
variant: agent.model?.variant,
|
||||
prompt: agent.system,
|
||||
options: agent.request.settings,
|
||||
steps: agent.steps,
|
||||
}))
|
||||
return (input as AgentListOutput["data"]).map((agent) => {
|
||||
const settings = agent.request.settings ?? {}
|
||||
return {
|
||||
name: agent.id,
|
||||
description: agent.description,
|
||||
mode: agent.mode,
|
||||
hidden: agent.hidden,
|
||||
temperature: typeof settings.temperature === "number" ? settings.temperature : undefined,
|
||||
topP: typeof settings.topP === "number" ? settings.topP : undefined,
|
||||
color: agent.color,
|
||||
permission: agent.permissions.map((rule) => ({
|
||||
permission: rule.action,
|
||||
pattern: rule.resource,
|
||||
action: rule.effect,
|
||||
})),
|
||||
model: agent.model && { providerID: agent.model.providerID, modelID: agent.model.id },
|
||||
variant: agent.model?.variant,
|
||||
prompt: agent.system,
|
||||
options: settings,
|
||||
steps: agent.steps,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function normalizeProviderList(
|
||||
|
||||
Reference in New Issue
Block a user