@openshain/agent 0.1.1 → 0.3.1
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.
- package/dist/client.d.ts +24 -0
- package/dist/client.js +48 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +6 -0
- package/dist/names.d.ts +9 -0
- package/dist/names.js +79 -0
- package/dist/providers/anthropic.d.ts +40 -0
- package/dist/providers/anthropic.js +214 -0
- package/dist/providers/openai-compatible.d.ts +47 -0
- package/dist/providers/openai-compatible.js +251 -0
- package/dist/session.d.ts +50 -0
- package/dist/session.js +428 -0
- package/dist/testing/fake-model.d.ts +29 -0
- package/dist/testing/fake-model.js +39 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +1 -0
- package/package.json +24 -7
- package/src/client.ts +73 -0
- package/src/index.ts +2 -12
- package/src/session.ts +429 -399
- package/src/loop.ts +0 -479
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import type { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import type { ToolContent, ToolDefinition } from "@openshain/core";
|
|
4
|
+
/** What a tool call returned, as the client sees it: MCP content, and the same as text. */
|
|
5
|
+
export interface ClientResult {
|
|
6
|
+
content: ToolContent[];
|
|
7
|
+
isError: boolean;
|
|
8
|
+
text: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The runtime as a client sees it: the tools it offers and a way to call them. The interactive
|
|
12
|
+
* CLI's loop talks to the runtime through this and nothing else, the way Claude Code does over MCP.
|
|
13
|
+
*/
|
|
14
|
+
export interface RuntimeClient {
|
|
15
|
+
listTools(): Promise<ToolDefinition[]>;
|
|
16
|
+
call(name: string, input: unknown, signal?: AbortSignal): Promise<ClientResult>;
|
|
17
|
+
close(): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
/** Connects an MCP client to a server in the same process, over the SDK's in-memory transport. */
|
|
20
|
+
export declare function connectInMemory(server: Server): Promise<RuntimeClient>;
|
|
21
|
+
/** Adapts any connected MCP client to the runtime client the loop uses. */
|
|
22
|
+
export declare function wrap(client: Client): RuntimeClient;
|
|
23
|
+
/** Parses a JSON result. Returns undefined when the text is not JSON. */
|
|
24
|
+
export declare function jsonOf(result: ClientResult): unknown;
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
3
|
+
import pkg from "../package.json" with { type: "json" };
|
|
4
|
+
/** Connects an MCP client to a server in the same process, over the SDK's in-memory transport. */
|
|
5
|
+
export async function connectInMemory(server) {
|
|
6
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
7
|
+
await server.connect(serverTransport);
|
|
8
|
+
const client = new Client({ name: "openshain", version: pkg.version });
|
|
9
|
+
await client.connect(clientTransport);
|
|
10
|
+
return wrap(client);
|
|
11
|
+
}
|
|
12
|
+
/** Adapts any connected MCP client to the runtime client the loop uses. */
|
|
13
|
+
export function wrap(client) {
|
|
14
|
+
return {
|
|
15
|
+
async listTools() {
|
|
16
|
+
const { tools } = await client.listTools();
|
|
17
|
+
return tools.map((tool) => ({
|
|
18
|
+
name: tool.name,
|
|
19
|
+
description: tool.description ?? "",
|
|
20
|
+
inputSchema: tool.inputSchema,
|
|
21
|
+
effect: tool.annotations?.readOnlyHint === true ? "observe" : "mutate",
|
|
22
|
+
}));
|
|
23
|
+
},
|
|
24
|
+
async call(name, input, signal) {
|
|
25
|
+
const result = await client.callTool({ name, arguments: (input ?? {}) }, undefined, signal ? { signal } : undefined);
|
|
26
|
+
const parts = (result.content ?? []);
|
|
27
|
+
const content = parts.map((part) => ({
|
|
28
|
+
type: "text",
|
|
29
|
+
text: part.type === "text" ? (part.text ?? "") : JSON.stringify(part),
|
|
30
|
+
}));
|
|
31
|
+
return {
|
|
32
|
+
content,
|
|
33
|
+
isError: result.isError === true,
|
|
34
|
+
text: content.map((c) => (c.type === "text" ? c.text : "")).join(""),
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
close: () => client.close(),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** Parses a JSON result. Returns undefined when the text is not JSON. */
|
|
41
|
+
export function jsonOf(result) {
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(result.text);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { type ClientResult, connectInMemory, jsonOf, type RuntimeClient, wrap } from "./client.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, 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: The conversation loop that drives the runtime as an MCP client, and the model providers (bring your own key)
|
|
2
|
+
export { connectInMemory, jsonOf, wrap } from "./client.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, TURN_LIMITS, } from "./session.js";
|
package/dist/names.d.ts
ADDED
|
@@ -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 {};
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import Anthropic, {} from "@anthropic-ai/sdk";
|
|
2
|
+
import { OpenshainError, } from "@openshain/core";
|
|
3
|
+
export const ANTHROPIC_PROVIDER_ID = "anthropic";
|
|
4
|
+
/** Used when the request names no output limit. The config's limit normally does. */
|
|
5
|
+
const DEFAULT_MAX_TOKENS = 16_000;
|
|
6
|
+
/** Builds the provider from the model section of openshain.yaml. The key comes from the environment variable the config names. */
|
|
7
|
+
export function anthropicProvider(model, env = process.env) {
|
|
8
|
+
const apiKey = env[model.apiKeyEnv];
|
|
9
|
+
if (!apiKey) {
|
|
10
|
+
throw new OpenshainError("config", `environment variable ${model.apiKeyEnv} is not set; it should hold the Anthropic API key`);
|
|
11
|
+
}
|
|
12
|
+
return new AnthropicProvider({
|
|
13
|
+
model: model.model,
|
|
14
|
+
apiKey,
|
|
15
|
+
...(model.baseUrl && { baseUrl: model.baseUrl }),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
/** Claude through the Messages API. Thinking blocks travel as opaque parts and go back unchanged. */
|
|
19
|
+
export class AnthropicProvider {
|
|
20
|
+
id = ANTHROPIC_PROVIDER_ID;
|
|
21
|
+
client;
|
|
22
|
+
model;
|
|
23
|
+
constructor(options) {
|
|
24
|
+
this.model = options.model;
|
|
25
|
+
this.client = new Anthropic({
|
|
26
|
+
apiKey: options.apiKey.trim(),
|
|
27
|
+
...(options.baseUrl && { baseURL: baseUrlRoot(options.baseUrl) }),
|
|
28
|
+
...(options.fetch && { fetch: options.fetch }),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
describe() {
|
|
32
|
+
return { provider: ANTHROPIC_PROVIDER_ID, model: this.model, capabilities: { tools: true } };
|
|
33
|
+
}
|
|
34
|
+
async generate(request, signal) {
|
|
35
|
+
const params = toParams(request, this.model);
|
|
36
|
+
try {
|
|
37
|
+
const message = await this.client.messages.create(params, { ...(signal && { signal }) });
|
|
38
|
+
return fromMessage(message);
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
throw toError(err);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The request as the Messages API takes it. providerOptions land on the body as they are, so
|
|
47
|
+
* thinking, output_config and cache_control can be set or overridden from the config; `effort`
|
|
48
|
+
* alone is a shorthand for output_config.effort. The model, the limit, the system prompt, the
|
|
49
|
+
* tools, the messages and the choice not to stream come from the runtime and cannot be
|
|
50
|
+
* overridden. A cache breakpoint goes on the last block of the last message that will be sent
|
|
51
|
+
* unchanged next turn (`stableMessages`), so the next turn reads that prefix from the cache.
|
|
52
|
+
*/
|
|
53
|
+
export function toParams(request, model) {
|
|
54
|
+
const { effort, output_config, model: _model, max_tokens: _maxTokens, system: _system, tools: _tools, messages: _messages, stream: _stream, ...extra } = request.providerOptions ?? {};
|
|
55
|
+
const outputConfig = {
|
|
56
|
+
...output_config,
|
|
57
|
+
...(effort !== undefined && { effort }),
|
|
58
|
+
};
|
|
59
|
+
const messages = request.messages.map(toMessage);
|
|
60
|
+
anchorCache(messages, request.stableMessages ?? 0);
|
|
61
|
+
return {
|
|
62
|
+
...extra,
|
|
63
|
+
...(Object.keys(outputConfig).length > 0 && { output_config: outputConfig }),
|
|
64
|
+
model,
|
|
65
|
+
stream: false,
|
|
66
|
+
max_tokens: request.maxOutputTokens ?? DEFAULT_MAX_TOKENS,
|
|
67
|
+
...(request.system && { system: request.system }),
|
|
68
|
+
...(request.tools && request.tools.length > 0 && { tools: request.tools.map(toTool) }),
|
|
69
|
+
messages,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** Marks the last block of the last stable message, the point up to which the next turn is identical. */
|
|
73
|
+
function anchorCache(messages, stable) {
|
|
74
|
+
const anchor = messages[stable - 1];
|
|
75
|
+
if (!anchor || !Array.isArray(anchor.content))
|
|
76
|
+
return;
|
|
77
|
+
const last = anchor.content.at(-1);
|
|
78
|
+
if (last && (last.type === "text" || last.type === "tool_result" || last.type === "tool_use")) {
|
|
79
|
+
last.cache_control = { type: "ephemeral" };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** The SDK appends /v1/messages itself, so a base URL that ends in /v1 loses that part. */
|
|
83
|
+
export function baseUrlRoot(baseUrl) {
|
|
84
|
+
return baseUrl.replace(/\/v1\/?$/, "");
|
|
85
|
+
}
|
|
86
|
+
function toTool(tool) {
|
|
87
|
+
return {
|
|
88
|
+
name: tool.name,
|
|
89
|
+
description: tool.description,
|
|
90
|
+
input_schema: tool.inputSchema,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function toMessage(message) {
|
|
94
|
+
const content = [];
|
|
95
|
+
if (message.role === "user") {
|
|
96
|
+
for (const part of message.content) {
|
|
97
|
+
if (part.type === "text") {
|
|
98
|
+
if (part.text !== "")
|
|
99
|
+
content.push({ type: "text", text: part.text });
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
content.push({
|
|
103
|
+
type: "tool_result",
|
|
104
|
+
tool_use_id: part.callId,
|
|
105
|
+
content: part.content,
|
|
106
|
+
...(part.isError && { is_error: true }),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { role: "user", content };
|
|
111
|
+
}
|
|
112
|
+
for (const part of message.content) {
|
|
113
|
+
if (part.type === "text") {
|
|
114
|
+
if (part.text !== "")
|
|
115
|
+
content.push({ type: "text", text: part.text });
|
|
116
|
+
}
|
|
117
|
+
else if (part.type === "tool_call") {
|
|
118
|
+
content.push({ type: "tool_use", id: part.id, name: part.name, input: part.input });
|
|
119
|
+
}
|
|
120
|
+
else if (part.provider === ANTHROPIC_PROVIDER_ID) {
|
|
121
|
+
content.push(part.data);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (content.length === 0) {
|
|
125
|
+
throw new OpenshainError("invalid_response", "an assistant message has nothing this provider can send; it may belong to another provider");
|
|
126
|
+
}
|
|
127
|
+
return { role: "assistant", content };
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The response in the contract's terms. Every block that is not text or a tool call is kept
|
|
131
|
+
* opaque. A refusal's explanation becomes text, so the log says why. A response whose shape is
|
|
132
|
+
* not a message is an invalid response.
|
|
133
|
+
*/
|
|
134
|
+
export function fromMessage(message) {
|
|
135
|
+
if (!message || !Array.isArray(message.content)) {
|
|
136
|
+
throw new OpenshainError("invalid_response", "the response is not a message");
|
|
137
|
+
}
|
|
138
|
+
const content = [];
|
|
139
|
+
for (const block of message.content) {
|
|
140
|
+
if (block.type === "text")
|
|
141
|
+
content.push({ type: "text", text: block.text });
|
|
142
|
+
else if (block.type === "tool_use") {
|
|
143
|
+
content.push({ type: "tool_call", id: block.id, name: block.name, input: block.input });
|
|
144
|
+
}
|
|
145
|
+
else
|
|
146
|
+
content.push({ type: "opaque", provider: ANTHROPIC_PROVIDER_ID, data: block });
|
|
147
|
+
}
|
|
148
|
+
const explanation = message.stop_details?.explanation;
|
|
149
|
+
if (message.stop_reason === "refusal" && explanation) {
|
|
150
|
+
content.push({ type: "text", text: explanation });
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
message: { role: "assistant", content },
|
|
154
|
+
stopReason: toStopReason(message.stop_reason, content.some((part) => part.type === "tool_call")),
|
|
155
|
+
usage: toUsage(message.usage),
|
|
156
|
+
raw: message,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const STOP_REASONS = {
|
|
160
|
+
end_turn: "end_turn",
|
|
161
|
+
stop_sequence: "end_turn",
|
|
162
|
+
tool_use: "tool_call",
|
|
163
|
+
max_tokens: "max_tokens",
|
|
164
|
+
refusal: "refusal",
|
|
165
|
+
};
|
|
166
|
+
/** Some gateways say end_turn with tool_use blocks present; the blocks decide. */
|
|
167
|
+
function toStopReason(reason, hasToolUse) {
|
|
168
|
+
if (hasToolUse && reason !== "max_tokens")
|
|
169
|
+
return "tool_call";
|
|
170
|
+
return (reason && STOP_REASONS[reason]) || "other";
|
|
171
|
+
}
|
|
172
|
+
function toUsage(usage) {
|
|
173
|
+
const thinking = usage?.output_tokens_details?.thinking_tokens;
|
|
174
|
+
const read = usage?.cache_read_input_tokens ?? 0;
|
|
175
|
+
const written = usage?.cache_creation_input_tokens ?? 0;
|
|
176
|
+
return {
|
|
177
|
+
inputTokens: (usage?.input_tokens ?? 0) + read + written,
|
|
178
|
+
outputTokens: usage?.output_tokens ?? 0,
|
|
179
|
+
...(usage?.cache_read_input_tokens != null && {
|
|
180
|
+
cachedInputTokens: usage.cache_read_input_tokens,
|
|
181
|
+
}),
|
|
182
|
+
...(usage?.cache_creation_input_tokens != null && {
|
|
183
|
+
cacheWriteTokens: usage.cache_creation_input_tokens,
|
|
184
|
+
}),
|
|
185
|
+
...(thinking != null && { reasoningTokens: thinking }),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/** The SDK's typed errors as the codes the runtime records. Anything else means the response could not be read. */
|
|
189
|
+
function toError(err) {
|
|
190
|
+
if (err instanceof OpenshainError)
|
|
191
|
+
return err;
|
|
192
|
+
if (err instanceof Anthropic.APIUserAbortError)
|
|
193
|
+
return wrap("network", err);
|
|
194
|
+
if (err instanceof Anthropic.AuthenticationError)
|
|
195
|
+
return wrap("auth", err);
|
|
196
|
+
if (err instanceof Anthropic.PermissionDeniedError)
|
|
197
|
+
return wrap("auth", err);
|
|
198
|
+
if (err instanceof Anthropic.RateLimitError)
|
|
199
|
+
return wrap("rate_limit", err);
|
|
200
|
+
if (err instanceof Anthropic.BadRequestError)
|
|
201
|
+
return wrap("config", err);
|
|
202
|
+
if (err instanceof Anthropic.NotFoundError)
|
|
203
|
+
return wrap("config", err);
|
|
204
|
+
if (err instanceof Anthropic.APIConnectionError)
|
|
205
|
+
return wrap("network", err);
|
|
206
|
+
if (err instanceof Anthropic.InternalServerError)
|
|
207
|
+
return wrap("network", err);
|
|
208
|
+
if (err instanceof Anthropic.APIError)
|
|
209
|
+
return wrap("invalid_response", err);
|
|
210
|
+
return wrap("invalid_response", err instanceof Error ? err : new Error(String(err)));
|
|
211
|
+
}
|
|
212
|
+
function wrap(code, err) {
|
|
213
|
+
return new OpenshainError(code, `Anthropic: ${err.message}`, { cause: err });
|
|
214
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type ModelDescription, type ModelProvider, type ModelRequest, type ModelResponse, type RuntimeProviders } from "@openshain/core";
|
|
2
|
+
import { type ClientOptions } from "openai";
|
|
3
|
+
import type { ChatCompletion, ChatCompletionCreateParamsNonStreaming } from "openai/resources/chat/completions";
|
|
4
|
+
export declare const OPENAI_COMPATIBLE_PROVIDER_ID = "openai-compatible";
|
|
5
|
+
type ModelSection = Parameters<RuntimeProviders["models"][string]>[0];
|
|
6
|
+
export interface OpenAICompatibleProviderOptions {
|
|
7
|
+
model: string;
|
|
8
|
+
apiKey: string;
|
|
9
|
+
/** The API root including its version segment, for example http://localhost:11434/v1. */
|
|
10
|
+
baseUrl?: string;
|
|
11
|
+
/** False for an endpoint that cannot call tools. The runtime then refuses to start. */
|
|
12
|
+
tools?: boolean;
|
|
13
|
+
/** Replaces the global fetch. Tests answer through it with recorded responses. */
|
|
14
|
+
fetch?: NonNullable<ClientOptions["fetch"]>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Builds the provider from the model section of openshain.yaml. The key comes from the environment
|
|
18
|
+
* variable the config names; `options: { tools: false }` declares an endpoint without tool support.
|
|
19
|
+
*/
|
|
20
|
+
export declare function openaiCompatibleProvider(model: ModelSection, env?: Record<string, string | undefined>): OpenAICompatibleProvider;
|
|
21
|
+
/** Any chat completions endpoint with function calling: OpenAI, a local server, or another vendor's compatible API. */
|
|
22
|
+
export declare class OpenAICompatibleProvider implements ModelProvider {
|
|
23
|
+
readonly id = "openai-compatible";
|
|
24
|
+
private readonly client;
|
|
25
|
+
private readonly model;
|
|
26
|
+
private readonly tools;
|
|
27
|
+
constructor(options: OpenAICompatibleProviderOptions);
|
|
28
|
+
describe(): ModelDescription;
|
|
29
|
+
generate(request: ModelRequest, signal?: AbortSignal): Promise<ModelResponse>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The request as chat completions take it. providerOptions land on the body as they are
|
|
33
|
+
* (reasoning_effort, temperature, and so on); the model, the limit, the messages, the tools and
|
|
34
|
+
* the choice not to stream come from the runtime. The limit is sent as max_completion_tokens; a
|
|
35
|
+
* max_tokens in the options only asks for that older name, which some servers still expect,
|
|
36
|
+
* and its value is ignored. A `tools` flag in the options is the provider's, not the request's.
|
|
37
|
+
*/
|
|
38
|
+
export declare function toParams(request: ModelRequest, model: string): ChatCompletionCreateParamsNonStreaming;
|
|
39
|
+
/**
|
|
40
|
+
* The completion in the contract's terms. Fields of the assistant message beyond content and
|
|
41
|
+
* tool calls, such as a server's reasoning, are kept opaque and go back with the message. Tool
|
|
42
|
+
* calls that are not function calls are kept opaque too, but never sent back. A refusal's text
|
|
43
|
+
* becomes text, so the log says why. A response whose shape is not a completion is an invalid
|
|
44
|
+
* response.
|
|
45
|
+
*/
|
|
46
|
+
export declare function fromCompletion(completion: ChatCompletion): ModelResponse;
|
|
47
|
+
export {};
|