@springbrand/agent-runtime 0.3.0-alpha.23 → 0.3.0-alpha.24
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/package.json +3 -2
- package/src/config/ports.ts +2 -3
- package/src/index.ts +0 -8
- package/src/models/index.ts +23 -40
- package/src/models/openrouter/messages.ts +5 -8
- package/src/models/openrouter/openai.ts +3 -7
- package/src/models/openrouter/protocol-response.ts +2 -4
- package/src/tools/web/search/api.ts +6 -1
- package/src/model-protocol/openrouter.ts +0 -41
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@springbrand/agent-runtime",
|
|
3
|
-
"version": "0.3.0-alpha.
|
|
3
|
+
"version": "0.3.0-alpha.24",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"src",
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
"lodash-es": "^4.18.1",
|
|
36
36
|
"zod": "^4.4.3",
|
|
37
37
|
"@earendil-works/pi-durable": "1.1.0",
|
|
38
|
-
"@earendil-works/chord": "1.1.0"
|
|
38
|
+
"@earendil-works/chord": "1.1.0",
|
|
39
|
+
"@springbrand/model-client": "0.1.0-alpha.1"
|
|
39
40
|
},
|
|
40
41
|
"peerDependencies": {
|
|
41
42
|
"agents": ">=0.28.1 <0.29.0",
|
package/src/config/ports.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export type { RuntimeSchedulePreparation } from "../submission/contracts";
|
|
2
2
|
import type { RuntimeModelInputModality } from "./profile";
|
|
3
|
+
import type { OpenRouterGenerationProtocol } from "@springbrand/model-client";
|
|
3
4
|
import type { SkillSource } from "agents/skills";
|
|
4
5
|
import type { ScheduleSpec, RuntimeSchedulePreparation } from "../submission/contracts";
|
|
5
6
|
import type { RuntimeActivityProjection } from "../conversation/state";
|
|
@@ -949,9 +950,7 @@ export interface RuntimeBrowserPort {
|
|
|
949
950
|
*/
|
|
950
951
|
export type RuntimeModelProtocol =
|
|
951
952
|
| "openai-chat"
|
|
952
|
-
|
|
|
953
|
-
| "openrouter-chat"
|
|
954
|
-
| "openrouter-responses"
|
|
953
|
+
| OpenRouterGenerationProtocol
|
|
955
954
|
| "anthropic-messages"
|
|
956
955
|
| "google-generative-ai"
|
|
957
956
|
| "openai-codex-responses";
|
package/src/index.ts
CHANGED
|
@@ -145,11 +145,3 @@ export {
|
|
|
145
145
|
modelRequestUrl,
|
|
146
146
|
resolvePiModel,
|
|
147
147
|
} from "./models";
|
|
148
|
-
export {
|
|
149
|
-
openRouterRequestUrl,
|
|
150
|
-
requestOpenRouter,
|
|
151
|
-
} from "./model-protocol/openrouter";
|
|
152
|
-
export type { OpenRouterRequestOptions } from "./model-protocol/openrouter";
|
|
153
|
-
export type {
|
|
154
|
-
OpenRouterProtocol,
|
|
155
|
-
} from "./models/openrouter/protocol-response";
|
package/src/models/index.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { StreamFn } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import {
|
|
3
|
+
openRouterRequestUrl,
|
|
4
|
+
withOpenRouterStructuredOutput,
|
|
5
|
+
type OpenRouterGenerationProtocol,
|
|
6
|
+
} from "@springbrand/model-client";
|
|
2
7
|
import {
|
|
3
8
|
createModels,
|
|
4
9
|
createProvider,
|
|
@@ -148,18 +153,23 @@ type ProviderBoundStream = (
|
|
|
148
153
|
options?: SimpleStreamOptions,
|
|
149
154
|
) => ReturnType<StreamFn>;
|
|
150
155
|
|
|
156
|
+
function openRouterProtocolForModel(model: Model<Api>): OpenRouterGenerationProtocol | undefined {
|
|
157
|
+
if (model.api === "anthropic-messages" &&
|
|
158
|
+
model.provider.startsWith("model-api-openrouter-messages-")) return "openrouter-messages";
|
|
159
|
+
if (model.api === "openai-completions" &&
|
|
160
|
+
model.provider.startsWith("model-api-openrouter-chat-")) return "openrouter-chat";
|
|
161
|
+
if (model.api === "openai-responses" &&
|
|
162
|
+
model.provider.startsWith("model-api-openrouter-responses-")) return "openrouter-responses";
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
|
|
151
166
|
export function structuredOutputStream(
|
|
152
167
|
streamFn: StreamFn,
|
|
153
168
|
model: Model<Api>,
|
|
154
169
|
schema: Record<string, unknown>,
|
|
155
170
|
): StreamFn | undefined {
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
const chat = model.api === "openai-completions" &&
|
|
159
|
-
model.provider.startsWith("model-api-openrouter-chat-");
|
|
160
|
-
const responses = model.api === "openai-responses" &&
|
|
161
|
-
model.provider.startsWith("model-api-openrouter-responses-");
|
|
162
|
-
if (!messages && !chat && !responses) return undefined;
|
|
171
|
+
const protocol = openRouterProtocolForModel(model);
|
|
172
|
+
if (!protocol) return undefined;
|
|
163
173
|
|
|
164
174
|
return (activeModel, context, options) =>
|
|
165
175
|
streamFn(activeModel, context, {
|
|
@@ -169,38 +179,9 @@ export function structuredOutputStream(
|
|
|
169
179
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
170
180
|
throw new Error("Structured model payload must be an object");
|
|
171
181
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
return {
|
|
176
|
-
...body,
|
|
177
|
-
output_config: {
|
|
178
|
-
...(current && typeof current === "object" && !Array.isArray(current)
|
|
179
|
-
? current
|
|
180
|
-
: {}),
|
|
181
|
-
format: { type: "json_schema", schema },
|
|
182
|
-
},
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
if (chat) {
|
|
186
|
-
return {
|
|
187
|
-
...body,
|
|
188
|
-
response_format: {
|
|
189
|
-
type: "json_schema",
|
|
190
|
-
json_schema: { name: "agent_output", strict: true, schema },
|
|
191
|
-
},
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
const current = body.text;
|
|
195
|
-
return {
|
|
196
|
-
...body,
|
|
197
|
-
text: {
|
|
198
|
-
...(current && typeof current === "object" && !Array.isArray(current)
|
|
199
|
-
? current
|
|
200
|
-
: {}),
|
|
201
|
-
format: { type: "json_schema", name: "agent_output", strict: true, schema },
|
|
202
|
-
},
|
|
203
|
-
};
|
|
182
|
+
return withOpenRouterStructuredOutput(
|
|
183
|
+
value as Record<string, unknown>, protocol, schema, "agent_output",
|
|
184
|
+
);
|
|
204
185
|
},
|
|
205
186
|
});
|
|
206
187
|
}
|
|
@@ -506,6 +487,8 @@ function trimTrailingSlash(value: string): string {
|
|
|
506
487
|
* Codex 走 `resolveCodexUrl`。不要在这里"顺手规范化"路径,否则日志会与真实请求脱节,反而掩盖配置错误。
|
|
507
488
|
*/
|
|
508
489
|
export function modelRequestUrl(model: Model<Api>): string {
|
|
490
|
+
const protocol = openRouterProtocolForModel(model);
|
|
491
|
+
if (protocol) return openRouterRequestUrl({ protocol, baseURL: model.baseUrl ?? "" });
|
|
509
492
|
const base = trimTrailingSlash(model.baseUrl ?? "");
|
|
510
493
|
switch (model.api) {
|
|
511
494
|
case "openai-completions":
|
|
@@ -766,7 +749,7 @@ function piApiFor(endpoint: RuntimeModelEndpoint): ProviderStreams {
|
|
|
766
749
|
}
|
|
767
750
|
})();
|
|
768
751
|
const api = protocol.startsWith("openrouter-")
|
|
769
|
-
? withModelProtocolResponse(implementation, protocol as
|
|
752
|
+
? withModelProtocolResponse(implementation, protocol as OpenRouterGenerationProtocol)
|
|
770
753
|
: implementation;
|
|
771
754
|
const contextFor = (
|
|
772
755
|
model: Model<Api>,
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
TranscriptContext,
|
|
6
6
|
} from "@earendil-works/pi-ai";
|
|
7
7
|
import { getCurrentTools } from "@earendil-works/pi-ai";
|
|
8
|
+
import { createOpenRouterFetch, openRouterHeaders, withOpenRouterRouting } from "@springbrand/model-client";
|
|
8
9
|
import {
|
|
9
10
|
anthropicMessagesApi,
|
|
10
11
|
} from "@earendil-works/pi-ai/api/anthropic-messages.lazy";
|
|
@@ -60,13 +61,7 @@ function decoratePayload(
|
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
return {
|
|
63
|
-
...body,
|
|
64
|
-
...(sessionId ? { session_id: sessionId } : {}),
|
|
65
|
-
provider: {
|
|
66
|
-
...record(body.provider),
|
|
67
|
-
...model.compat?.openRouterRouting,
|
|
68
|
-
require_parameters: true,
|
|
69
|
-
},
|
|
64
|
+
...withOpenRouterRouting(body, { ...model.compat?.openRouterRouting }, sessionId),
|
|
70
65
|
...(deferred.length > 0
|
|
71
66
|
? {
|
|
72
67
|
tools: [
|
|
@@ -96,12 +91,14 @@ function openRouterOptions(
|
|
|
96
91
|
return {
|
|
97
92
|
...options,
|
|
98
93
|
apiKey: undefined,
|
|
94
|
+
fetch: createOpenRouterFetch({ apiKey, fetch: options?.fetch }),
|
|
99
95
|
headers: {
|
|
96
|
+
// Pi uses null to suppress SDK/model defaults before building native headers.
|
|
100
97
|
...headers,
|
|
98
|
+
...Object.fromEntries(openRouterHeaders(apiKey)),
|
|
101
99
|
...(model.compat?.supportsStrictTools === true
|
|
102
100
|
? { "x-anthropic-beta": STRUCTURED_OUTPUTS_BETA }
|
|
103
101
|
: {}),
|
|
104
|
-
...(apiKey ? { authorization: `Bearer ${apiKey}` } : {}),
|
|
105
102
|
"x-api-key": null,
|
|
106
103
|
},
|
|
107
104
|
onPayload: async (payload, activeModel) => {
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
TranscriptContext,
|
|
7
7
|
} from "@earendil-works/pi-ai";
|
|
8
8
|
import { getCurrentTools } from "@earendil-works/pi-ai";
|
|
9
|
+
import { createOpenRouterFetch, withOpenRouterRouting } from "@springbrand/model-client";
|
|
9
10
|
import {
|
|
10
11
|
openAICompletionsApi,
|
|
11
12
|
} from "@earendil-works/pi-ai/api/openai-completions.lazy";
|
|
@@ -27,6 +28,7 @@ function openRouterOptions(
|
|
|
27
28
|
): StreamOptions {
|
|
28
29
|
return {
|
|
29
30
|
...options,
|
|
31
|
+
fetch: createOpenRouterFetch({ apiKey: options?.apiKey, fetch: options?.fetch }),
|
|
30
32
|
onPayload: async (payload, activeModel) => {
|
|
31
33
|
const callerPayload = await options?.onPayload?.(payload, activeModel) ?? payload;
|
|
32
34
|
const body = record(callerPayload);
|
|
@@ -48,18 +50,12 @@ function openRouterOptions(
|
|
|
48
50
|
const reasoning = record(replayed.reasoning);
|
|
49
51
|
const include = Array.isArray(replayed.include) ? replayed.include : [];
|
|
50
52
|
return {
|
|
51
|
-
...replayed,
|
|
53
|
+
...withOpenRouterRouting(replayed, compat?.openRouterRouting, options?.sessionId),
|
|
52
54
|
...(model.api === "openai-responses" && reasoning?.effort !== undefined &&
|
|
53
55
|
reasoning.effort !== null && reasoning.effort !== "none" &&
|
|
54
56
|
!include.includes("reasoning.encrypted_content")
|
|
55
57
|
? { include: [...include, "reasoning.encrypted_content"] }
|
|
56
58
|
: {}),
|
|
57
|
-
...(options?.sessionId ? { session_id: options.sessionId } : {}),
|
|
58
|
-
provider: {
|
|
59
|
-
...record(replayed.provider),
|
|
60
|
-
...compat?.openRouterRouting,
|
|
61
|
-
require_parameters: true,
|
|
62
|
-
},
|
|
63
59
|
};
|
|
64
60
|
},
|
|
65
61
|
};
|
|
@@ -7,12 +7,10 @@ import {
|
|
|
7
7
|
type ProviderStreams,
|
|
8
8
|
type StreamOptions,
|
|
9
9
|
} from "@earendil-works/pi-ai";
|
|
10
|
-
import type {
|
|
10
|
+
import type { OpenRouterGenerationProtocol } from "@springbrand/model-client";
|
|
11
11
|
import { overlayProviderStreams } from "../provider-streams";
|
|
12
12
|
import type { ModelProtocolResponse } from "../../conversation/canonical-context";
|
|
13
13
|
|
|
14
|
-
export type OpenRouterProtocol = Extract<RuntimeModelProtocol, `openrouter-${string}`>;
|
|
15
|
-
|
|
16
14
|
function record(value: unknown): Record<string, unknown> | undefined {
|
|
17
15
|
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
18
16
|
? value as Record<string, unknown>
|
|
@@ -63,7 +61,7 @@ function streamWithProtocolResponse<T extends StreamOptions>(
|
|
|
63
61
|
|
|
64
62
|
export function withModelProtocolResponse(
|
|
65
63
|
api: ProviderStreams,
|
|
66
|
-
protocol:
|
|
64
|
+
protocol: OpenRouterGenerationProtocol,
|
|
67
65
|
): ProviderStreams {
|
|
68
66
|
if (protocol !== "openrouter-responses") return api;
|
|
69
67
|
return overlayProviderStreams(api, {
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { RuntimeModelEndpoint } from "../../../config/ports";
|
|
9
|
+
import { createOpenRouterFetch, openRouterRequestUrl } from "@springbrand/model-client";
|
|
9
10
|
|
|
10
11
|
export interface WebSearchSource {
|
|
11
12
|
title: string;
|
|
@@ -295,6 +296,9 @@ function isOpenRouterModel(model: NativeWebSearchModel): boolean {
|
|
|
295
296
|
}
|
|
296
297
|
|
|
297
298
|
function resolveOpenAIResponsesUrl(model: NativeWebSearchModel): string {
|
|
299
|
+
if (isOpenRouterModel(model)) {
|
|
300
|
+
return openRouterRequestUrl({ protocol: "openrouter-responses", baseURL: model.baseUrl });
|
|
301
|
+
}
|
|
298
302
|
const base = trimTrailingSlash(model.baseUrl);
|
|
299
303
|
if (!isOpenAICodexModel(model)) return `${base}/responses`;
|
|
300
304
|
if (base.endsWith("/codex/responses")) return base;
|
|
@@ -733,7 +737,8 @@ async function callOpenAIStream(
|
|
|
733
737
|
requestBody.parallel_tool_calls = true;
|
|
734
738
|
}
|
|
735
739
|
|
|
736
|
-
const
|
|
740
|
+
const fetcher = isOpenRouter ? createOpenRouterFetch({ apiKey: auth.apiKey }) : fetch;
|
|
741
|
+
const response = await fetcher(resolveOpenAIResponsesUrl(model), {
|
|
737
742
|
method: "POST",
|
|
738
743
|
headers: requestHeaders,
|
|
739
744
|
body: JSON.stringify(requestBody),
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import type { RuntimeModelEndpoint } from "../config/ports";
|
|
2
|
-
|
|
3
|
-
export interface OpenRouterRequestOptions {
|
|
4
|
-
readonly headers?: HeadersInit;
|
|
5
|
-
readonly signal?: AbortSignal;
|
|
6
|
-
readonly fetch?: typeof fetch;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function openRouterRequestUrl(
|
|
10
|
-
endpoint: Pick<RuntimeModelEndpoint, "protocol" | "baseURL">,
|
|
11
|
-
): string {
|
|
12
|
-
const baseURL = endpoint.baseURL.replace(/\/+$/, "");
|
|
13
|
-
switch (endpoint.protocol) {
|
|
14
|
-
case "openrouter-messages":
|
|
15
|
-
return `${baseURL}/v1/messages`;
|
|
16
|
-
case "openrouter-chat":
|
|
17
|
-
return `${baseURL}/chat/completions`;
|
|
18
|
-
case "openrouter-responses":
|
|
19
|
-
return `${baseURL}/responses`;
|
|
20
|
-
default:
|
|
21
|
-
throw new Error(`Not an OpenRouter protocol: ${endpoint.protocol}`);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function requestOpenRouter(
|
|
26
|
-
endpoint: RuntimeModelEndpoint,
|
|
27
|
-
body: Readonly<Record<string, unknown>>,
|
|
28
|
-
options: OpenRouterRequestOptions = {},
|
|
29
|
-
): Promise<Response> {
|
|
30
|
-
const url = openRouterRequestUrl(endpoint);
|
|
31
|
-
const headers = new Headers(endpoint.headers);
|
|
32
|
-
new Headers(options.headers).forEach((value, name) => headers.set(name, value));
|
|
33
|
-
headers.set("content-type", "application/json");
|
|
34
|
-
headers.set("authorization", `Bearer ${endpoint.apiKey}`);
|
|
35
|
-
return (options.fetch ?? globalThis.fetch)(url, {
|
|
36
|
-
method: "POST",
|
|
37
|
-
headers,
|
|
38
|
-
body: JSON.stringify(body),
|
|
39
|
-
...(options.signal ? { signal: options.signal } : {}),
|
|
40
|
-
});
|
|
41
|
-
}
|