@openshain/agent 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ export { ASK_USER, countToolCalls, type FailureReason, type PendingQuestion, pendingQuestions, RUNTIME_PROVIDER_ID, type RunWorkOptions, runWork, } from "./loop.ts";
2
+ export { AGENT_NAMES, pickAgentName } from "./names.ts";
3
+ export { ANTHROPIC_PROVIDER_ID, AnthropicProvider, type AnthropicProviderOptions, anthropicProvider, } from "./providers/anthropic.ts";
4
+ export { OPENAI_COMPATIBLE_PROVIDER_ID, OpenAICompatibleProvider, type OpenAICompatibleProviderOptions, openaiCompatibleProvider, } from "./providers/openai-compatible.ts";
5
+ export { createSession, SESSION_TOOLS, type Session, type SessionOptions, TURN_LIMITS, type TurnResult, type TurnStop, } from "./session.ts";
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ // @openshain/agent: Tool loop and model providers (bring your own key)
2
+ export { ASK_USER, countToolCalls, pendingQuestions, RUNTIME_PROVIDER_ID, runWork, } from "./loop.js";
3
+ export { AGENT_NAMES, pickAgentName } from "./names.js";
4
+ export { ANTHROPIC_PROVIDER_ID, AnthropicProvider, anthropicProvider, } from "./providers/anthropic.js";
5
+ export { OPENAI_COMPATIBLE_PROVIDER_ID, OpenAICompatibleProvider, openaiCompatibleProvider, } from "./providers/openai-compatible.js";
6
+ export { createSession, SESSION_TOOLS, TURN_LIMITS, } from "./session.js";
package/dist/loop.d.ts ADDED
@@ -0,0 +1,37 @@
1
+ import { type AnyEvent, type ModelProvider, type Runtime, type ToolDefinition, type Work, type WorkId } from "@openshain/core";
2
+ export interface RunWorkOptions {
3
+ /** Defaults to the runtime's model. */
4
+ model?: ModelProvider;
5
+ /**
6
+ * Answers the model's questions to the person. Without it, a question leaves
7
+ * the work waiting for input. If it throws, the question stays recorded and
8
+ * unanswered, so a later run can resume from it.
9
+ */
10
+ onInput?: (question: string) => Promise<string>;
11
+ /** Called after every event is recorded, for progress reporting. A returned promise is awaited. */
12
+ onEvent?: (event: AnyEvent) => void | Promise<void>;
13
+ signal?: AbortSignal;
14
+ }
15
+ /** The provider recorded for calls the runtime handles itself. */
16
+ export declare const RUNTIME_PROVIDER_ID = "runtime";
17
+ /** The one tool the runtime itself provides: stop and ask the person. */
18
+ export declare const ASK_USER: Readonly<ToolDefinition>;
19
+ /**
20
+ * Drives one work from its current state to completion, failure or a wait:
21
+ * build the projection, ask the model, run the tool calls it made, repeat.
22
+ * Every step is recorded in the work's event log before the next one starts.
23
+ */
24
+ export declare function runWork(runtime: Runtime, workId: WorkId, options?: RunWorkOptions): Promise<Work>;
25
+ /**
26
+ * How many tool calls the model made: every call that started, plus every rejection of a call
27
+ * that never started. Counted by event, since some servers reuse call ids from turn to turn.
28
+ */
29
+ export declare function countToolCalls(events: AnyEvent[]): number;
30
+ /** Why a work failed, as recorded in `work.failed`. */
31
+ export type FailureReason = "limit_reached" | "model_refusal" | "model_error";
32
+ export interface PendingQuestion {
33
+ callId: string;
34
+ question: string;
35
+ }
36
+ /** The questions of the model's most recent turn that have no answer yet, oldest first. */
37
+ export declare function pendingQuestions(events: AnyEvent[]): PendingQuestion[];
package/dist/loop.js ADDED
@@ -0,0 +1,382 @@
1
+ import { ASK_USER_TOOL_NAME, buildProjection, compileInputValidator, isOpenshainError, isTerminal, OpenshainError, SESSION_WORK_TYPE, verifyArtifact, } from "@openshain/core";
2
+ /** The provider recorded for calls the runtime handles itself. */
3
+ export const RUNTIME_PROVIDER_ID = "runtime";
4
+ /** The one tool the runtime itself provides: stop and ask the person. */
5
+ export const ASK_USER = Object.freeze({
6
+ name: ASK_USER_TOOL_NAME,
7
+ description: "Ask the person you work for a question when you cannot proceed without their answer. Use it sparingly; prefer the workspace over guessing.",
8
+ inputSchema: {
9
+ type: "object",
10
+ properties: {
11
+ question: { type: "string", description: "The question, in the person's language." },
12
+ },
13
+ required: ["question"],
14
+ additionalProperties: false,
15
+ },
16
+ effect: "observe",
17
+ });
18
+ const validateQuestion = compileInputValidator(ASK_USER.inputSchema);
19
+ /**
20
+ * Drives one work from its current state to completion, failure or a wait:
21
+ * build the projection, ask the model, run the tool calls it made, repeat.
22
+ * Every step is recorded in the work's event log before the next one starts.
23
+ */
24
+ export async function runWork(runtime, workId, options = {}) {
25
+ const model = options.model ?? runtime.model;
26
+ const opened = await runtime.works.open(workId);
27
+ const handle = options.onEvent ? observed(opened, options.onEvent) : opened;
28
+ try {
29
+ const work = await handle.current();
30
+ if (work.type === SESSION_WORK_TYPE) {
31
+ throw new OpenshainError("invalid_transition", `work ${workId} is a conversation; it goes on in the screen that opened it, not through a run`);
32
+ }
33
+ if (isTerminal(work.status)) {
34
+ throw new OpenshainError("invalid_transition", `work ${workId} is already ${work.status}`);
35
+ }
36
+ if (work.status === "queued")
37
+ await handle.transition("in_progress", "run");
38
+ // A work still in progress was interrupted: close what the last turn left open.
39
+ if (work.status === "in_progress")
40
+ await closeInterruptedCalls(runtime, handle);
41
+ const events = await handle.events();
42
+ const pending = pendingQuestions(events);
43
+ const asked = lastTurnCallIds(events);
44
+ const ghosts = pending.filter((q) => !asked.has(q.callId));
45
+ if (ghosts.length > 0) {
46
+ throw new OpenshainError("corrupt_log", `questions that the model's last turn did not ask: ${ghosts.map((q) => q.callId).join(", ")}`);
47
+ }
48
+ if (work.status === "waiting_input" && pending.length === 0) {
49
+ throw new OpenshainError("corrupt_log", `work ${workId} waits for input but no question is pending`);
50
+ }
51
+ if (pending.length > 0) {
52
+ if (work.status !== "waiting_input") {
53
+ await handle.transition("waiting_input", "the model asked the person a question");
54
+ }
55
+ if (!options.onInput)
56
+ return handle.current();
57
+ await answerAll(handle, pending, options.onInput);
58
+ }
59
+ return await loop(runtime, handle, model, options);
60
+ }
61
+ finally {
62
+ await handle.close();
63
+ }
64
+ }
65
+ async function loop(runtime, handle, model, options) {
66
+ const { limits } = runtime.config;
67
+ const tools = [...runtime.tools.list().map((t) => t.definition), ASK_USER];
68
+ const description = model.describe();
69
+ for (;;) {
70
+ // Stopped by the person: leave the work in progress, so a later run can resume it.
71
+ if (options.signal?.aborted)
72
+ return handle.current();
73
+ const events = await handle.events();
74
+ const modelCalls = events.filter((e) => e.type === "model.requested").length;
75
+ const toolCalls = countToolCalls(events);
76
+ if (modelCalls >= limits.maxModelCalls) {
77
+ return fail(handle, "limit_reached", `model calls exhausted (${limits.maxModelCalls})`);
78
+ }
79
+ const projection = buildProjection({
80
+ events,
81
+ config: runtime.config,
82
+ tools,
83
+ providerId: model.id,
84
+ budget: {
85
+ modelCallsLeft: limits.maxModelCalls - modelCalls,
86
+ toolCallsLeft: limits.maxToolCalls - toolCalls,
87
+ },
88
+ });
89
+ await handle.append({
90
+ type: "model.requested",
91
+ payload: {
92
+ provider: model.id,
93
+ model: description.model,
94
+ messageCount: projection.messages.length,
95
+ toolNames: tools.map((t) => t.name),
96
+ },
97
+ });
98
+ let response;
99
+ try {
100
+ response = await model.generate({
101
+ system: projection.system,
102
+ messages: projection.messages,
103
+ tools: projection.tools,
104
+ maxOutputTokens: limits.maxOutputTokens,
105
+ budget: projection.budget,
106
+ stableMessages: projection.messages.length - 1,
107
+ ...(runtime.config.model.options && { providerOptions: runtime.config.model.options }),
108
+ }, options.signal);
109
+ }
110
+ catch (err) {
111
+ const message = err instanceof Error ? err.message : String(err);
112
+ await handle.append({
113
+ type: "model.failed",
114
+ payload: { code: isOpenshainError(err) ? err.code : "model_error", message },
115
+ });
116
+ if (options.signal?.aborted)
117
+ return handle.current();
118
+ return fail(handle, "model_error", message);
119
+ }
120
+ // The answer is third-party data. If it cannot be recorded, the work fails; the run does not.
121
+ try {
122
+ await handle.append({
123
+ type: "model.completed",
124
+ payload: {
125
+ stopReason: response.stopReason,
126
+ content: response.message.content,
127
+ ...(runtime.config.debug.persistRaw &&
128
+ response.raw !== undefined && { raw: response.raw }),
129
+ },
130
+ });
131
+ await handle.append({
132
+ type: "usage.recorded",
133
+ payload: {
134
+ kind: "model_inference",
135
+ provider: model.id,
136
+ model: description.model,
137
+ usage: response.usage,
138
+ },
139
+ });
140
+ }
141
+ catch (err) {
142
+ if (!isOpenshainError(err) || err.code !== "invalid_event")
143
+ throw err;
144
+ const message = `the model's answer cannot be recorded: ${err.message}`;
145
+ await handle.append({ type: "model.failed", payload: { code: "invalid_response", message } });
146
+ return fail(handle, "model_error", message);
147
+ }
148
+ switch (response.stopReason) {
149
+ case "end_turn":
150
+ return complete(runtime, handle, response.message.content);
151
+ case "tool_call": {
152
+ const calls = response.message.content.filter((p) => p.type === "tool_call");
153
+ if (calls.length === 0) {
154
+ return fail(handle, "model_error", "the model stopped for a tool call but made none");
155
+ }
156
+ const seen = new Set();
157
+ for (const call of calls) {
158
+ if (seen.has(call.id)) {
159
+ return fail(handle, "model_error", `the model used the tool call id "${call.id}" twice in one turn`);
160
+ }
161
+ seen.add(call.id);
162
+ }
163
+ // Questions go last: the other calls of the turn run before the work waits,
164
+ // and the model gets their results together with the answers.
165
+ const ordered = [
166
+ ...calls.filter((c) => c.name !== ASK_USER.name),
167
+ ...calls.filter((c) => c.name === ASK_USER.name),
168
+ ];
169
+ let used = toolCalls;
170
+ for (const call of ordered) {
171
+ if (options.signal?.aborted)
172
+ return handle.current();
173
+ if (used >= limits.maxToolCalls) {
174
+ return fail(handle, "limit_reached", `tool calls exhausted (${limits.maxToolCalls})`);
175
+ }
176
+ used += 1;
177
+ if (call.name === ASK_USER.name) {
178
+ await askQuestion(handle, call.id, call.input);
179
+ }
180
+ else {
181
+ await runtime.tools.call(handle, { id: call.id, name: call.name, input: call.input });
182
+ }
183
+ }
184
+ const pending = pendingQuestions(await handle.events());
185
+ if (pending.length > 0) {
186
+ await handle.transition("waiting_input", pending.length === 1
187
+ ? "the model asked the person a question"
188
+ : `the model asked the person ${pending.length} questions`);
189
+ if (!options.onInput)
190
+ return handle.current();
191
+ await answerAll(handle, pending, options.onInput);
192
+ }
193
+ break;
194
+ }
195
+ case "max_tokens":
196
+ return fail(handle, "limit_reached", `the answer was cut off at max_output_tokens (${limits.maxOutputTokens})`);
197
+ case "refusal":
198
+ return fail(handle, "model_refusal", textOf(response.message.content) || "the model refused to continue");
199
+ default:
200
+ return fail(handle, "model_error", `unexpected stop reason "${response.stopReason}"`);
201
+ }
202
+ }
203
+ }
204
+ /**
205
+ * How many tool calls the model made: every call that started, plus every rejection of a call
206
+ * that never started. Counted by event, since some servers reuse call ids from turn to turn.
207
+ */
208
+ export function countToolCalls(events) {
209
+ let count = 0;
210
+ let started = new Set();
211
+ for (const event of events) {
212
+ if (event.type === "model.completed")
213
+ started = new Set();
214
+ else if (event.type === "tool.called") {
215
+ started.add(event.payload.callId);
216
+ count += 1;
217
+ }
218
+ else if (event.type === "tool.rejected") {
219
+ if (!started.has(event.payload.callId))
220
+ count += 1;
221
+ }
222
+ }
223
+ return count;
224
+ }
225
+ /** The events since the model's most recent answer. Call ids are only trusted within a turn. */
226
+ function currentTurn(events) {
227
+ for (let i = events.length - 1; i >= 0; i--) {
228
+ if (events[i]?.type === "model.completed")
229
+ return events.slice(i + 1);
230
+ }
231
+ return events;
232
+ }
233
+ /** The model's most recent answer, if any. */
234
+ function lastModelTurn(events) {
235
+ for (let i = events.length - 1; i >= 0; i--) {
236
+ const event = events[i];
237
+ if (event?.type === "model.completed")
238
+ return event;
239
+ }
240
+ return undefined;
241
+ }
242
+ /** The ids of the tool calls in the model's most recent answer. */
243
+ function lastTurnCallIds(events) {
244
+ const ids = new Set();
245
+ for (const part of lastModelTurn(events)?.payload.content ?? []) {
246
+ if (part.type === "tool_call")
247
+ ids.add(part.id);
248
+ }
249
+ return ids;
250
+ }
251
+ /** The text parts of an answer, joined. */
252
+ function textOf(content) {
253
+ return content
254
+ .filter((p) => p.type === "text")
255
+ .map((p) => p.text)
256
+ .join("\n")
257
+ .trim();
258
+ }
259
+ async function fail(handle, reason, detail) {
260
+ await handle.append({ type: "work.failed", payload: { reason, detail } });
261
+ return handle.current();
262
+ }
263
+ async function complete(runtime, handle, content) {
264
+ const summary = textOf(content);
265
+ const events = await handle.events();
266
+ const writes = events.filter((e) => e.type === "tool.completed" && !e.payload.isError);
267
+ const refs = [];
268
+ const byPath = new Map();
269
+ for (const event of writes) {
270
+ if (!event.payload.after)
271
+ continue;
272
+ refs.push(event.id);
273
+ for (const { path, sha256 } of event.payload.after)
274
+ byPath.set(path, sha256);
275
+ }
276
+ const artifacts = [];
277
+ for (const [path, recorded] of byPath) {
278
+ artifacts.push(await verifyArtifact(runtime.workspaceRoot, path, recorded));
279
+ }
280
+ await handle.append({ type: "evidence.recorded", payload: { claim: summary, refs, artifacts } });
281
+ await handle.append({ type: "work.completed", payload: { summary } });
282
+ return handle.current();
283
+ }
284
+ /** A handle that reports every event it records, waiting for the report before it goes on. */
285
+ function observed(handle, onEvent) {
286
+ return {
287
+ ...handle,
288
+ async append(event) {
289
+ const recorded = await handle.append(event);
290
+ await onEvent(recorded);
291
+ return recorded;
292
+ },
293
+ async transition(to, reason) {
294
+ const recorded = await handle.transition(to, reason);
295
+ await onEvent(recorded);
296
+ return recorded;
297
+ },
298
+ };
299
+ }
300
+ const INTERRUPTED = "the run stopped before this tool call finished; call it again if it is still needed";
301
+ /**
302
+ * Gives every tool call of the last turn that has no result an error result, so the
303
+ * conversation can continue after a run was interrupted mid-call. A recorded question
304
+ * that awaits its answer is left alone.
305
+ */
306
+ async function closeInterruptedCalls(runtime, handle) {
307
+ const events = await handle.events();
308
+ const last = lastModelTurn(events);
309
+ if (!last)
310
+ return;
311
+ const answered = new Set();
312
+ const called = new Set();
313
+ for (const event of currentTurn(events)) {
314
+ if (event.type === "tool.completed" || event.type === "tool.rejected") {
315
+ answered.add(event.payload.callId);
316
+ }
317
+ if (event.type === "tool.called")
318
+ called.add(event.payload.callId);
319
+ }
320
+ const asked = new Set(pendingQuestions(events).map((q) => q.callId));
321
+ for (const part of last.payload.content) {
322
+ if (part.type !== "tool_call" || answered.has(part.id) || asked.has(part.id))
323
+ continue;
324
+ if (!called.has(part.id)) {
325
+ const provider = runtime.tools.list().find((t) => t.definition.name === part.name)?.providerId ??
326
+ RUNTIME_PROVIDER_ID;
327
+ await handle.append({
328
+ type: "tool.called",
329
+ payload: { callId: part.id, provider, name: part.name, input: part.input },
330
+ });
331
+ }
332
+ await handle.append({
333
+ type: "tool.completed",
334
+ payload: { callId: part.id, content: [{ type: "text", text: INTERRUPTED }], isError: true },
335
+ });
336
+ }
337
+ }
338
+ /** Records the model's question as a call that waits for the person, or rejects a malformed one. */
339
+ async function askQuestion(handle, callId, input) {
340
+ const validation = validateQuestion(input);
341
+ if (!validation.ok) {
342
+ await handle.append({
343
+ type: "tool.rejected",
344
+ payload: {
345
+ callId,
346
+ name: ASK_USER.name,
347
+ code: "schema_mismatch",
348
+ reason: `input does not match the schema of ${ASK_USER.name}: ${validation.reason}`,
349
+ },
350
+ });
351
+ return;
352
+ }
353
+ const { question } = input;
354
+ await handle.append({
355
+ type: "tool.called",
356
+ payload: { callId, provider: RUNTIME_PROVIDER_ID, name: ASK_USER.name, input },
357
+ });
358
+ await handle.append({ type: "human.input_requested", payload: { callId, question } });
359
+ }
360
+ /** Records each answer as the person's input and as the tool result, then continues the work. */
361
+ async function answerAll(handle, pending, onInput) {
362
+ for (const { callId, question } of pending) {
363
+ const text = await onInput(question);
364
+ await handle.append({ type: "human.input_provided", payload: { callId, answer: text } });
365
+ await handle.append({
366
+ type: "tool.completed",
367
+ payload: { callId, content: [{ type: "text", text }], isError: false },
368
+ });
369
+ }
370
+ await handle.transition("in_progress", "the person answered");
371
+ }
372
+ /** The questions of the model's most recent turn that have no answer yet, oldest first. */
373
+ export function pendingQuestions(events) {
374
+ const turn = currentTurn(events);
375
+ const answered = new Set(turn
376
+ .filter((e) => e.type === "human.input_provided")
377
+ .map((e) => e.payload.callId));
378
+ return turn
379
+ .filter((e) => e.type === "human.input_requested")
380
+ .filter((e) => !answered.has(e.payload.callId))
381
+ .map((e) => ({ callId: e.payload.callId, question: e.payload.question }));
382
+ }
@@ -0,0 +1,9 @@
1
+ import type { Language } from "@openshain/core";
2
+ /**
3
+ * Names a session's agent may go by, per language of the company. Given names that are also words
4
+ * of nature, so they read as a person to talk to without pointing at anyone real, and lean on no
5
+ * gender. Thirty each.
6
+ */
7
+ export declare const AGENT_NAMES: Readonly<Record<Language, readonly string[]>>;
8
+ /** A name for a new session's agent in the company's language, avoiding the ones open sessions use while any is free. */
9
+ export declare function pickAgentName(language: Language, taken: Iterable<string>, random?: () => number): string;
package/dist/names.js ADDED
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Names a session's agent may go by, per language of the company. Given names that are also words
3
+ * of nature, so they read as a person to talk to without pointing at anyone real, and lean on no
4
+ * gender. Thirty each.
5
+ */
6
+ export const AGENT_NAMES = Object.freeze({
7
+ ja: Object.freeze([
8
+ "あおい",
9
+ "あかね",
10
+ "あさひ",
11
+ "いずみ",
12
+ "いぶき",
13
+ "うみ",
14
+ "かえで",
15
+ "かすみ",
16
+ "こはる",
17
+ "さくら",
18
+ "しおん",
19
+ "しずく",
20
+ "すばる",
21
+ "すみれ",
22
+ "そら",
23
+ "つばき",
24
+ "つばさ",
25
+ "なぎ",
26
+ "なずな",
27
+ "はづき",
28
+ "ひかり",
29
+ "ひなた",
30
+ "ほたる",
31
+ "みお",
32
+ "みずき",
33
+ "みなと",
34
+ "みのり",
35
+ "もみじ",
36
+ "ゆずき",
37
+ "わかば",
38
+ ]),
39
+ en: Object.freeze([
40
+ "Ash",
41
+ "Aspen",
42
+ "Bay",
43
+ "Birch",
44
+ "Cedar",
45
+ "Clover",
46
+ "Coral",
47
+ "Dawn",
48
+ "Ember",
49
+ "Fern",
50
+ "Hazel",
51
+ "Holly",
52
+ "Indigo",
53
+ "Iris",
54
+ "Ivy",
55
+ "Jade",
56
+ "Juniper",
57
+ "Laurel",
58
+ "Maple",
59
+ "Moss",
60
+ "Olive",
61
+ "Rain",
62
+ "Reed",
63
+ "River",
64
+ "Robin",
65
+ "Rowan",
66
+ "Sage",
67
+ "Sky",
68
+ "Willow",
69
+ "Wren",
70
+ ]),
71
+ });
72
+ /** A name for a new session's agent in the company's language, avoiding the ones open sessions use while any is free. */
73
+ export function pickAgentName(language, taken, random = Math.random) {
74
+ const names = AGENT_NAMES[language];
75
+ const used = new Set(taken);
76
+ const free = names.filter((name) => !used.has(name));
77
+ const pool = free.length > 0 ? free : names;
78
+ return pool[Math.min(pool.length - 1, Math.floor(random() * pool.length))];
79
+ }
@@ -0,0 +1,40 @@
1
+ import Anthropic, { type ClientOptions } from "@anthropic-ai/sdk";
2
+ import { type ModelDescription, type ModelProvider, type ModelRequest, type ModelResponse, type RuntimeProviders } from "@openshain/core";
3
+ export declare const ANTHROPIC_PROVIDER_ID = "anthropic";
4
+ type ModelSection = Parameters<RuntimeProviders["models"][string]>[0];
5
+ export interface AnthropicProviderOptions {
6
+ model: string;
7
+ apiKey: string;
8
+ baseUrl?: string;
9
+ /** Replaces the global fetch. Tests answer through it with recorded responses. */
10
+ fetch?: NonNullable<ClientOptions["fetch"]>;
11
+ }
12
+ /** Builds the provider from the model section of openshain.yaml. The key comes from the environment variable the config names. */
13
+ export declare function anthropicProvider(model: ModelSection, env?: Record<string, string | undefined>): AnthropicProvider;
14
+ /** Claude through the Messages API. Thinking blocks travel as opaque parts and go back unchanged. */
15
+ export declare class AnthropicProvider implements ModelProvider {
16
+ readonly id = "anthropic";
17
+ private readonly client;
18
+ private readonly model;
19
+ constructor(options: AnthropicProviderOptions);
20
+ describe(): ModelDescription;
21
+ generate(request: ModelRequest, signal?: AbortSignal): Promise<ModelResponse>;
22
+ }
23
+ /**
24
+ * The request as the Messages API takes it. providerOptions land on the body as they are, so
25
+ * thinking, output_config and cache_control can be set or overridden from the config; `effort`
26
+ * alone is a shorthand for output_config.effort. The model, the limit, the system prompt, the
27
+ * tools, the messages and the choice not to stream come from the runtime and cannot be
28
+ * overridden. A cache breakpoint goes on the last block of the last message that will be sent
29
+ * unchanged next turn (`stableMessages`), so the next turn reads that prefix from the cache.
30
+ */
31
+ export declare function toParams(request: ModelRequest, model: string): Anthropic.MessageCreateParamsNonStreaming;
32
+ /** The SDK appends /v1/messages itself, so a base URL that ends in /v1 loses that part. */
33
+ export declare function baseUrlRoot(baseUrl: string): string;
34
+ /**
35
+ * The response in the contract's terms. Every block that is not text or a tool call is kept
36
+ * opaque. A refusal's explanation becomes text, so the log says why. A response whose shape is
37
+ * not a message is an invalid response.
38
+ */
39
+ export declare function fromMessage(message: Anthropic.Message): ModelResponse;
40
+ export {};