@springbrand/agent-runtime 0.3.0-alpha.13 → 0.3.0-alpha.15

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@springbrand/agent-runtime",
3
- "version": "0.3.0-alpha.13",
3
+ "version": "0.3.0-alpha.15",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
@@ -156,11 +156,16 @@ export abstract class AgentRuntimeKernel<
156
156
  };
157
157
 
158
158
  protected abstract ensureRuntimeReady(): Promise<void>;
159
- /** Runs only before the first pin of a newly activated Submission. */
159
+ /** Prepare per-Submission Host state before the first assembly pin. */
160
160
  protected prepareRuntimeForSubmission(_submissionId: string): Promise<void> {
161
161
  return this.ensureRuntimeReady();
162
162
  }
163
163
 
164
+ /** Runs before a new Submission records its Runtime key. */
165
+ protected prepareRuntimeForAdmission(): Promise<void> {
166
+ return this.ensureRuntimeReady();
167
+ }
168
+
164
169
  protected ensureRuntimeVersion(_runtimeKey: string): Promise<void> {
165
170
  return this.ensureRuntimeReady();
166
171
  }
@@ -513,13 +518,12 @@ export abstract class AgentRuntimeKernel<
513
518
 
514
519
  // #region Submission 准入与配置固定
515
520
 
516
- /** Pin dynamic Context and execution policy before admission. */
521
+ /** Pin dynamic Context and execution policy before the first assembly is used. */
517
522
  private async admissionPin(fresh: boolean, submissionId: string): Promise<{
518
523
  revision: string;
519
524
  descriptor: string;
520
525
  }> {
521
526
  if (fresh) await this.prepareRuntimeForSubmission(submissionId);
522
- else await this.ensureRuntimeReady();
523
527
  const submission = this.db.submissions.find(submissionId);
524
528
  if (!submission) {
525
529
  throw new Error(`Unknown SpringBrand submission: ${submissionId}`);
@@ -571,7 +575,7 @@ export abstract class AgentRuntimeKernel<
571
575
  submissions: this.db.submissions,
572
576
  transaction: (run) => this.db.transaction(run),
573
577
  prepareRuntime: async () => {
574
- await this.ensureRuntimeReady();
578
+ await this.prepareRuntimeForAdmission();
575
579
  return { runtimeKey: this.activeRuntime!.runtimeKey, admission: this.activeRuntime?.admission };
576
580
  },
577
581
  findUserMessage: (id) => this.transcript.findUserMessage(id),
@@ -777,7 +781,7 @@ export abstract class AgentRuntimeKernel<
777
781
  submission = await this.admission.pin(
778
782
  submission,
779
783
  () => this.admissionPin(mode === "fresh", submissionId),
780
- mode === "fresh" ? undefined : (key) => this.ensureRuntimeVersion(key),
784
+ (key) => this.ensureRuntimeVersion(key),
781
785
  );
782
786
  const activeRuntime = this.requireRuntime();
783
787
  const harnessInput = this.pi.harnessInputs(
@@ -17,7 +17,7 @@ import type { RuntimeState } from "../../conversation";
17
17
  import type {
18
18
  TemporaryAgentApprovalDecision,
19
19
  TemporaryAgentApprovalRequest,
20
- TemporaryAgentLaunch,
20
+ TemporaryAgentLaunchSelection,
21
21
  } from "../../tools";
22
22
  import { TEMPORARY_AGENT_LAUNCH_KEY } from "../../tools";
23
23
  import {
@@ -83,7 +83,7 @@ export interface LoadedRuntime<Config> {
83
83
  export interface RuntimeConfigUpdate<Change> {
84
84
  readonly changed: boolean;
85
85
  readonly change: Change;
86
- /** Restore any committed config state when the replacement Runtime cannot load. */
86
+ /** Restore committed state if an immediate replacement fails. */
87
87
  readonly rollback?: () => Promise<void>;
88
88
  }
89
89
 
@@ -96,11 +96,16 @@ interface RuntimeAgentDefinitionBase<
96
96
  /** 必须等于 Worker 导出名(Wrangler `class_name`)。 */
97
97
  readonly name: string;
98
98
 
99
- /** Refresh Host-owned inputs before a new Submission is pinned. Never runs on continuation/recovery. */
99
+ /** Refresh Host-owned inputs before a new Submission records its Runtime key. */
100
100
  readonly beforeSubmission?: (
101
101
  context: RuntimeAgentPlanningContext<Env, Command, Change>,
102
102
  ) => Promise<boolean>;
103
103
 
104
+ /** Prepare per-Submission Host state after admission and before its first assembly pin. */
105
+ readonly afterSubmission?: (
106
+ context: RuntimeAgentPlanningContext<Env, Command, Change>,
107
+ ) => Promise<void>;
108
+
104
109
  readonly load: (
105
110
  context: RuntimeAgentPlanningContext<Env, Command, Change>,
106
111
  requestedRuntimeKey?: string,
@@ -360,9 +365,7 @@ const TERMINAL_AGENT_TOOL_STATUSES = [
360
365
  ] as const;
361
366
  const AGENT_TOOL_RETENTION_MS = 24 * 60 * 60 * 1_000;
362
367
 
363
- function parseTemporaryAgentLaunch<Config>(
364
- input: unknown,
365
- ): TemporaryAgentLaunch<Config> {
368
+ function parseTemporaryAgentLaunch(input: unknown): TemporaryAgentLaunchSelection {
366
369
  const record = input != null && typeof input === "object"
367
370
  ? input as Record<string, unknown>
368
371
  : {};
@@ -380,17 +383,15 @@ function parseTemporaryAgentLaunch<Config>(
380
383
  ) {
381
384
  throw new Error("invalid temporary agent executionLevel");
382
385
  }
383
- if (!("config" in record)) {
384
- throw new Error("invalid temporary agent config");
385
- }
386
- return {
386
+ const selection: TemporaryAgentLaunchSelection = {
387
387
  subagentName: read("subagentName", 128),
388
388
  instructions: read("instructions", 65_536),
389
389
  task: read("task", 65_536),
390
390
  runtimeKey: read("runtimeKey", 512),
391
- config: record.config as Config,
392
391
  executionLevel: executionLevel as ExecutionLevel,
393
392
  };
393
+ if ("config" in record) throw new Error("unexpected temporary agent config");
394
+ return selection;
394
395
  }
395
396
 
396
397
  /**
@@ -428,7 +429,7 @@ export function defineRuntimeAgent<
428
429
  private loading?: Promise<void>;
429
430
  private telemetryResolved = false;
430
431
  private resolvedTelemetry?: AgentTelemetryBinding;
431
- private temporaryLaunch?: TemporaryAgentLaunch<Config>;
432
+ private temporaryLaunch?: TemporaryAgentLaunchSelection;
432
433
  private temporaryDispose?: () => Promise<void>;
433
434
  private temporaryCleanup?: Promise<void>;
434
435
 
@@ -449,10 +450,10 @@ export function defineRuntimeAgent<
449
450
  return this.ensureConfig(runtimeKey);
450
451
  }
451
452
 
452
- protected async prepareRuntimeForSubmission(submissionId: string): Promise<void> {
453
+ protected async prepareRuntimeForAdmission(): Promise<void> {
453
454
  const beforeSubmission = definition.beforeSubmission as RuntimeAgentDefinitionBase<Env, Config, Command, Change>["beforeSubmission"];
454
455
  try {
455
- const changed = await beforeSubmission?.(this.createDefinitionContext(undefined, submissionId));
456
+ const changed = await beforeSubmission?.(this.createDefinitionContext());
456
457
  await this.ensureConfig(undefined, changed === true);
457
458
  } catch (error) {
458
459
  this.hasLoadedRuntime = false;
@@ -460,6 +461,17 @@ export function defineRuntimeAgent<
460
461
  }
461
462
  }
462
463
 
464
+ protected async prepareRuntimeForSubmission(submissionId: string): Promise<void> {
465
+ const afterSubmission = definition.afterSubmission as RuntimeAgentDefinitionBase<Env, Config, Command, Change>["afterSubmission"];
466
+ try {
467
+ await this.ensureRuntimeReady();
468
+ await afterSubmission?.(this.createDefinitionContext(undefined, submissionId));
469
+ } catch (error) {
470
+ this.hasLoadedRuntime = false;
471
+ throw error;
472
+ }
473
+ }
474
+
463
475
  // 作用:加载默认 Runtime,或切换到调用方指定的 key。
464
476
  // 调用:RPC 控制面在业务配置需要刷新时调用。
465
477
  // 原因:复用 `ensureConfig`,让手动重载遵守和首次启动相同的原子规则。
@@ -551,7 +563,7 @@ export function defineRuntimeAgent<
551
563
  if (this.role !== "temporary") {
552
564
  throw new Error("Agent Tool runs require a temporary child facet");
553
565
  }
554
- const launch = parseTemporaryAgentLaunch<Config>(input);
566
+ const launch = parseTemporaryAgentLaunch(input);
555
567
  this.temporaryLaunch = launch;
556
568
  await this.ctx.storage.put(TEMPORARY_AGENT_LAUNCH_KEY, launch);
557
569
  await this.reloadRuntime(launch.runtimeKey, { force: true });
@@ -624,6 +636,11 @@ export function defineRuntimeAgent<
624
636
  if (!resolvedRuntimeKey) {
625
637
  throw new Error("Resolved Runtime key must not be empty");
626
638
  }
639
+ if (requestedRuntimeKey && resolvedRuntimeKey !== requestedRuntimeKey) {
640
+ throw new Error(
641
+ `Requested Runtime key ${requestedRuntimeKey} resolved to ${resolvedRuntimeKey}`,
642
+ );
643
+ }
627
644
  const context = this.createDefinitionContext({
628
645
  value: loaded.config,
629
646
  });
@@ -723,7 +740,7 @@ export function defineRuntimeAgent<
723
740
  resources: ResolvedResources,
724
741
  ): Promise<ResolvedResources> {
725
742
  const launch = this.temporaryLaunch ??
726
- await this.ctx.storage.get<TemporaryAgentLaunch<Config>>(
743
+ await this.ctx.storage.get<TemporaryAgentLaunchSelection>(
727
744
  TEMPORARY_AGENT_LAUNCH_KEY,
728
745
  );
729
746
  if (!launch) throw new Error("Temporary Agent is not initialized");
@@ -754,7 +771,7 @@ export function defineRuntimeAgent<
754
771
  }
755
772
 
756
773
  private async requestParentTemporaryApproval(
757
- launch: TemporaryAgentLaunch<Config>,
774
+ launch: TemporaryAgentLaunchSelection,
758
775
  request: {
759
776
  toolCallId: string;
760
777
  toolName: string;
@@ -820,7 +837,7 @@ export function defineRuntimeAgent<
820
837
  this.temporaryCleanup = (async () => {
821
838
  if (!this.temporaryDispose) {
822
839
  const launch = this.temporaryLaunch ??
823
- await this.ctx.storage.get<TemporaryAgentLaunch<Config>>(
840
+ await this.ctx.storage.get<TemporaryAgentLaunchSelection>(
824
841
  TEMPORARY_AGENT_LAUNCH_KEY,
825
842
  );
826
843
  if (launch) await this.ensureConfig(launch.runtimeKey);
@@ -890,7 +907,6 @@ export function defineRuntimeAgent<
890
907
  input: {
891
908
  ...normalized,
892
909
  runtimeKey: agent.loadedRuntimeKey,
893
- config: runtimeConfig.value,
894
910
  executionLevel: agent.executionLevel(),
895
911
  },
896
912
  runId: `temporary-agent:${runContext.toolCallId}`,
@@ -583,6 +583,9 @@ class RuntimeBuilder {
583
583
  ? { headers: Object.freeze({ ...endpoint.headers }) }
584
584
  : {}),
585
585
  models: Object.freeze([...endpoint.models]),
586
+ ...(endpoint.openRouterProviderPins
587
+ ? { openRouterProviderPins: Object.freeze({ ...endpoint.openRouterProviderPins }) }
588
+ : {}),
586
589
  ...(endpoint.modelInputModalities
587
590
  ? {
588
591
  modelInputModalities: Object.freeze(Object.fromEntries(
@@ -592,6 +595,22 @@ class RuntimeBuilder {
592
595
  )),
593
596
  }
594
597
  : {}),
598
+ ...(endpoint.modelMetadata
599
+ ? {
600
+ modelMetadata: Object.freeze(Object.fromEntries(
601
+ Object.entries(endpoint.modelMetadata).map(([modelId, metadata]) => [
602
+ modelId,
603
+ Object.freeze({
604
+ ...metadata,
605
+ cost: Object.freeze({ ...metadata.cost }),
606
+ ...(metadata.thinkingLevelMap
607
+ ? { thinkingLevelMap: Object.freeze({ ...metadata.thinkingLevelMap }) }
608
+ : {}),
609
+ }),
610
+ ]),
611
+ )),
612
+ }
613
+ : {}),
595
614
  }),
596
615
  )),
597
616
  });
@@ -283,10 +283,25 @@ function describeRuntime(
283
283
  candidates: readonly PiToolCandidate[],
284
284
  loadedExtensions: readonly PiLoadedExtension[],
285
285
  ): string {
286
+ const model = snapshot.pi.model;
287
+ const openRouterRouting = model.compat && "openRouterRouting" in model.compat
288
+ ? model.compat.openRouterRouting
289
+ : undefined;
286
290
  return JSON.stringify({
287
291
  version: 1,
288
- model: snapshot.pi.model.id,
289
- provider: snapshot.pi.model.provider,
292
+ model: model.id,
293
+ provider: model.provider,
294
+ modelConfig: {
295
+ api: model.api,
296
+ name: model.name,
297
+ reasoning: model.reasoning,
298
+ input: model.input,
299
+ contextWindow: model.contextWindow,
300
+ maxTokens: model.maxTokens,
301
+ cost: model.cost,
302
+ ...(model.thinkingLevelMap ? { thinkingLevelMap: model.thinkingLevelMap } : {}),
303
+ ...(openRouterRouting ? { openRouterRouting } : {}),
304
+ },
290
305
  thinking: snapshot.pi.thinkingLevel,
291
306
  systemPrompt: snapshot.pi.systemPrompt,
292
307
  executionLevel: snapshot.profile.executionLevel,
@@ -950,6 +950,8 @@ export interface RuntimeBrowserPort {
950
950
  export type RuntimeModelProtocol =
951
951
  | "openai-chat"
952
952
  | "openrouter-messages"
953
+ | "openrouter-chat"
954
+ | "openrouter-responses"
953
955
  | "anthropic-messages"
954
956
  | "google-generative-ai"
955
957
  | "openai-codex-responses";
@@ -963,6 +965,31 @@ export type RuntimeModelProtocol =
963
965
  *
964
966
  * 凭据只留在 Port 对象,不进入 `RuntimeProfile` 或持久状态;术语见 `../index.ts`。
965
967
  */
968
+ export type RuntimeModelThinkingLevel =
969
+ | "off"
970
+ | "minimal"
971
+ | "low"
972
+ | "medium"
973
+ | "high"
974
+ | "xhigh"
975
+ | "max";
976
+
977
+ export interface RuntimeModelMetadata {
978
+ readonly name: string;
979
+ readonly reasoning: boolean;
980
+ readonly contextWindow: number;
981
+ readonly maxTokens: number;
982
+ readonly cost: Readonly<{
983
+ input: number;
984
+ output: number;
985
+ cacheRead: number;
986
+ cacheWrite: number;
987
+ }>;
988
+ readonly thinkingLevelMap?: Readonly<
989
+ Partial<Record<RuntimeModelThinkingLevel, string | null>>
990
+ >;
991
+ }
992
+
966
993
  export interface RuntimeModelEndpoint {
967
994
  protocol: RuntimeModelProtocol;
968
995
  /** Resolved credential; never enters RuntimeProfile or persisted state. */
@@ -977,6 +1004,7 @@ export interface RuntimeModelEndpoint {
977
1004
  >;
978
1005
  /** OpenRouter upstream provider pins keyed by model ID; pinned requests never fall back. */
979
1006
  openRouterProviderPins?: Readonly<Record<string, string>>;
1007
+ modelMetadata?: Readonly<Record<string, RuntimeModelMetadata>>;
980
1008
  }
981
1009
 
982
1010
  /**
@@ -373,6 +373,23 @@ export class PiChunkEncoder {
373
373
  const chunks = this.toolInputs.has(event.toolCallId)
374
374
  ? []
375
375
  : this.toolInput(event.toolCallId, event.toolName, event.args);
376
+ const partialResult = isRecord(event.partialResult)
377
+ ? event.partialResult
378
+ : undefined;
379
+ const details = isRecord(partialResult?.details)
380
+ ? partialResult.details
381
+ : undefined;
382
+ const actionToolMeta = isRecord(details?.actionToolMeta)
383
+ ? details.actionToolMeta
384
+ : undefined;
385
+ if (actionToolMeta) {
386
+ chunks.push({
387
+ type: "data-action-tool-meta",
388
+ id: event.toolCallId,
389
+ data: { toolCallId: event.toolCallId, ...actionToolMeta },
390
+ });
391
+ return chunks;
392
+ }
376
393
  chunks.push({
377
394
  type: "tool-output-available",
378
395
  toolCallId: event.toolCallId,
package/src/index.ts CHANGED
@@ -127,7 +127,7 @@ export {
127
127
  export type {
128
128
  TemporaryAgentApprovalDecision,
129
129
  TemporaryAgentApprovalRequest,
130
- TemporaryAgentLaunch,
130
+ TemporaryAgentLaunchSelection,
131
131
  TemporaryAgentRequest,
132
132
  TemporaryAgentRunContext,
133
133
  } from "./tools/subagent/temporary/core";
@@ -144,3 +144,12 @@ export {
144
144
  modelRequestUrl,
145
145
  resolvePiModel,
146
146
  } from "./models";
147
+ export {
148
+ openRouterRequestUrl,
149
+ requestOpenRouter,
150
+ } from "./model-protocol/openrouter";
151
+ export type { OpenRouterRequestOptions } from "./model-protocol/openrouter";
152
+ export type {
153
+ ModelProtocolResponse,
154
+ OpenRouterProtocol,
155
+ } from "./models/openrouter/protocol-response";
@@ -0,0 +1,41 @@
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
+ }
@@ -48,12 +48,15 @@ import {
48
48
  } from "agents/chat";
49
49
  import type {
50
50
  RuntimeModelEndpoint,
51
+ RuntimeModelMetadata,
51
52
  RuntimeModelInputModality,
52
53
  RuntimeModelProtocol,
53
54
  RuntimeProviderPort,
54
55
  } from "../config/ports";
55
56
  import { modelInputMessages } from "../conversation/message/model-input";
56
57
  import { openRouterMessagesApi } from "./openrouter/messages";
58
+ import { openRouterChatApi, openRouterResponsesApi } from "./openrouter/openai";
59
+ import { withModelProtocolResponse } from "./openrouter/protocol-response";
57
60
  import {
58
61
  overlayProviderStreams,
59
62
  remapProviderStreamModels,
@@ -62,6 +65,8 @@ import {
62
65
  const CATALOGS = {
63
66
  "openai-chat": openaiProvider().getModels(),
64
67
  "openrouter-messages": openrouterProvider().getModels(),
68
+ "openrouter-chat": openrouterProvider().getModels(),
69
+ "openrouter-responses": openrouterProvider().getModels(),
65
70
  "anthropic-messages": anthropicProvider().getModels(),
66
71
  "google-generative-ai": googleProvider().getModels(),
67
72
  "openai-codex-responses": openaiCodexProvider().getModels(),
@@ -73,7 +78,7 @@ const NO_HOSTED_INPUT_MODALITIES: readonly RuntimeModelInputModality[] = [];
73
78
  function adapterHostedInputModalities(
74
79
  protocol: RuntimeModelProtocol,
75
80
  ): readonly RuntimeModelInputModality[] {
76
- return protocol === "openrouter-messages" || protocol === "anthropic-messages"
81
+ return protocol.startsWith("openrouter-") || protocol === "anthropic-messages"
77
82
  ? MESSAGES_HOSTED_INPUT_MODALITIES
78
83
  : NO_HOSTED_INPUT_MODALITIES;
79
84
  }
@@ -148,10 +153,13 @@ export function structuredOutputStream(
148
153
  model: Model<Api>,
149
154
  schema: Record<string, unknown>,
150
155
  ): StreamFn | undefined {
151
- if (
152
- model.api !== "anthropic-messages" ||
153
- !model.provider.startsWith("model-api-openrouter-messages-")
154
- ) return undefined;
156
+ const messages = model.api === "anthropic-messages" &&
157
+ model.provider.startsWith("model-api-openrouter-messages-");
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;
155
163
 
156
164
  return (activeModel, context, options) =>
157
165
  streamFn(activeModel, context, {
@@ -161,14 +169,36 @@ export function structuredOutputStream(
161
169
  if (!value || typeof value !== "object" || Array.isArray(value)) {
162
170
  throw new Error("Structured model payload must be an object");
163
171
  }
164
- const current = (value as Record<string, unknown>).output_config;
172
+ const body = value as Record<string, unknown>;
173
+ if (messages) {
174
+ const current = body.output_config;
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;
165
195
  return {
166
- ...value,
167
- output_config: {
196
+ ...body,
197
+ text: {
168
198
  ...(current && typeof current === "object" && !Array.isArray(current)
169
199
  ? current
170
200
  : {}),
171
- format: { type: "json_schema", schema },
201
+ format: { type: "json_schema", name: "agent_output", strict: true, schema },
172
202
  },
173
203
  };
174
204
  },
@@ -264,7 +294,8 @@ function pdfPayload(payload: unknown, api: Api): unknown {
264
294
  };
265
295
  }
266
296
  }
267
- if (api === "openai-codex-responses" && value.type === "input_image") {
297
+ if ((api === "openai-codex-responses" || api === "openai-responses") &&
298
+ value.type === "input_image") {
268
299
  if (
269
300
  typeof value.image_url === "string" &&
270
301
  value.image_url.startsWith("data:application/pdf;base64,")
@@ -483,6 +514,8 @@ export function modelRequestUrl(model: Model<Api>): string {
483
514
  return `${base}/v1/messages`;
484
515
  case "google-generative-ai":
485
516
  return `${base}/models/${model.id}:streamGenerateContent`;
517
+ case "openai-responses":
518
+ return `${base}/responses`;
486
519
  case "openai-codex-responses":
487
520
  return base.endsWith("/codex/responses") || base.endsWith("/responses")
488
521
  ? base
@@ -697,7 +730,10 @@ function providerId(endpoint: RuntimeModelEndpoint, index: number): string {
697
730
  function apiFor(protocol: RuntimeModelProtocol): Api {
698
731
  switch (protocol) {
699
732
  case "openai-chat":
733
+ case "openrouter-chat":
700
734
  return "openai-completions";
735
+ case "openrouter-responses":
736
+ return "openai-responses";
701
737
  case "openrouter-messages":
702
738
  case "anthropic-messages":
703
739
  return "anthropic-messages";
@@ -710,12 +746,16 @@ function apiFor(protocol: RuntimeModelProtocol): Api {
710
746
 
711
747
  function piApiFor(endpoint: RuntimeModelEndpoint): ProviderStreams {
712
748
  const protocol = endpoint.protocol;
713
- const api = (() => {
749
+ const implementation = (() => {
714
750
  switch (protocol) {
715
751
  case "openai-chat":
716
752
  return openAICompletionsApi();
717
753
  case "openrouter-messages":
718
754
  return openRouterMessagesApi();
755
+ case "openrouter-chat":
756
+ return openRouterChatApi();
757
+ case "openrouter-responses":
758
+ return openRouterResponsesApi();
719
759
  case "anthropic-messages":
720
760
  return anthropicMessagesApi();
721
761
  case "google-generative-ai":
@@ -724,6 +764,9 @@ function piApiFor(endpoint: RuntimeModelEndpoint): ProviderStreams {
724
764
  return openAICodexResponsesApi();
725
765
  }
726
766
  })();
767
+ const api = protocol.startsWith("openrouter-")
768
+ ? withModelProtocolResponse(implementation, protocol as "openrouter-messages" | "openrouter-chat" | "openrouter-responses")
769
+ : implementation;
727
770
  const contextFor = (
728
771
  model: Model<Api>,
729
772
  context: TranscriptContext,
@@ -760,6 +803,28 @@ function anthropicMessagesModelForOpenRouter(
760
803
  // 把部署声明的模型改造成可由对应自定义端点执行的 Pi 模型。
761
804
  // resolvePiModel 为单个运行快照调用它,configurePiModels 则在注册端点时为端点中的每个 modelId 调用它。
762
805
  // Pi 的内置 OpenAI、Anthropic provider 提供上下文窗口、费用和推理能力等元数据;这里保留这些元数据,但以部署协议、provider 标识和 baseURL 覆盖内置路由,未知模型会在初始化阶段直接报错,而不是拖到请求阶段。
806
+ function assertModelMetadata(
807
+ metadata: RuntimeModelMetadata,
808
+ modelId: string,
809
+ ): void {
810
+ if (
811
+ typeof metadata.name !== "string" || !metadata.name.trim() ||
812
+ typeof metadata.reasoning !== "boolean" ||
813
+ !Number.isSafeInteger(metadata.contextWindow) || metadata.contextWindow <= 0 ||
814
+ !Number.isSafeInteger(metadata.maxTokens) || metadata.maxTokens <= 0 ||
815
+ metadata.maxTokens > metadata.contextWindow ||
816
+ !metadata.cost ||
817
+ [metadata.cost.input, metadata.cost.output, metadata.cost.cacheRead, metadata.cost.cacheWrite]
818
+ .some((rate) => !Number.isFinite(rate) || rate < 0) ||
819
+ Object.entries(metadata.thinkingLevelMap ?? {}).some(([level, value]) =>
820
+ !["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(level) ||
821
+ (value !== null && (typeof value !== "string" || !value.trim()))
822
+ )
823
+ ) {
824
+ throw new Error(`Invalid deployment model metadata: ${modelId}`);
825
+ }
826
+ }
827
+
763
828
  function configuredModel(
764
829
  endpoint: RuntimeModelEndpoint,
765
830
  index: number,
@@ -768,11 +833,28 @@ function configuredModel(
768
833
  const catalogModel = CATALOGS[endpoint.protocol].find(
769
834
  (model) => model.id === modelId,
770
835
  );
771
- if (!catalogModel) {
836
+ const explicitMetadata = endpoint.modelMetadata?.[modelId];
837
+ if (!catalogModel && !explicitMetadata) {
772
838
  throw new Error(
773
- `Model is not in SpringBrand's built-in catalog for ${endpoint.protocol}: ${modelId}`,
839
+ `Model is not in SpringBrand's built-in catalog and has no deployment metadata for ${endpoint.protocol}: ${modelId}`,
774
840
  );
775
841
  }
842
+ if (explicitMetadata) assertModelMetadata(explicitMetadata, modelId);
843
+ if (!catalogModel && !endpoint.modelInputModalities?.[modelId]?.includes("text")) {
844
+ throw new Error(`Deployment model requires explicit text input capability: ${modelId}`);
845
+ }
846
+ const sourceModel: Model<Api> = catalogModel ?? {
847
+ id: modelId,
848
+ name: explicitMetadata!.name,
849
+ api: apiFor(endpoint.protocol),
850
+ provider: "openrouter",
851
+ baseUrl: endpoint.baseURL,
852
+ reasoning: explicitMetadata!.reasoning,
853
+ input: ["text"],
854
+ cost: explicitMetadata!.cost,
855
+ contextWindow: explicitMetadata!.contextWindow,
856
+ maxTokens: explicitMetadata!.maxTokens,
857
+ };
776
858
  const {
777
859
  api: catalogApi,
778
860
  provider: _provider,
@@ -781,7 +863,7 @@ function configuredModel(
781
863
  headers: catalogHeaders,
782
864
  input: catalogInput,
783
865
  ...metadata
784
- } = catalogModel;
866
+ } = sourceModel;
785
867
  const headers = {
786
868
  ...catalogHeaders,
787
869
  ...endpoint.headers,
@@ -794,27 +876,70 @@ function configuredModel(
794
876
  (modality): modality is "text" | "image" =>
795
877
  modality === "text" || modality === "image",
796
878
  );
797
- // OpenRouter uses dotted versions and optional transport suffixes while the
798
- // Anthropic catalog uses dashed base model ids.
879
+ const isOpenRouter = endpoint.protocol.startsWith("openrouter-");
799
880
  const anthropicMessagesCompat = endpoint.protocol === "openrouter-messages"
800
881
  ? anthropicMessagesModelForOpenRouter(modelId)?.compat
801
882
  : undefined;
883
+ const openAIResponsesCompat = endpoint.protocol === "openrouter-responses" &&
884
+ modelId.startsWith("openai/")
885
+ ? CATALOGS["openai-chat"].find((model) =>
886
+ model.id === modelId.slice("openai/".length) && model.api === "openai-responses"
887
+ )?.compat
888
+ : undefined;
802
889
  return {
803
890
  ...metadata,
891
+ ...(explicitMetadata
892
+ ? {
893
+ name: explicitMetadata.name,
894
+ reasoning: explicitMetadata.reasoning,
895
+ contextWindow: explicitMetadata.contextWindow,
896
+ maxTokens: explicitMetadata.maxTokens,
897
+ cost: { ...explicitMetadata.cost },
898
+ ...(explicitMetadata.thinkingLevelMap
899
+ ? { thinkingLevelMap: { ...explicitMetadata.thinkingLevelMap } }
900
+ : {}),
901
+ }
902
+ : {}),
804
903
  input,
805
904
  api: configuredApi,
806
905
  provider: providerId(endpoint, index),
807
906
  baseUrl: endpoint.baseURL,
808
- ...(endpoint.protocol === "openrouter-messages"
907
+ ...(isOpenRouter
809
908
  ? {
810
909
  compat: {
811
- ...anthropicMessagesCompat,
812
- // OpenRouter rejects Anthropic's mid-conversation effort messages and block binding.
813
- supportsMidConvoEffort: false,
814
- supportsEagerToolInputStreaming: false,
815
- supportsStrictTools:
816
- anthropicMessagesCompat?.supportsStrictTools ?? false,
817
- supportsToolReferences: true,
910
+ ...(endpoint.protocol === "openrouter-messages"
911
+ ? {
912
+ ...anthropicMessagesCompat,
913
+ supportsMidConvoEffort: false,
914
+ supportsEagerToolInputStreaming: false,
915
+ supportsStrictTools:
916
+ anthropicMessagesCompat?.supportsStrictTools ?? false,
917
+ supportsToolReferences: true,
918
+ ...(modelId === "anthropic/claude-sonnet-5.5" ||
919
+ modelId === "anthropic/claude-opus-5.5"
920
+ ? { forceAdaptiveThinking: true }
921
+ : {}),
922
+ }
923
+ : endpoint.protocol === "openrouter-chat"
924
+ ? {
925
+ ...catalogCompat,
926
+ thinkingFormat: "openrouter" as const,
927
+ maxTokensField: "max_tokens" as const,
928
+ supportsStore: false,
929
+ sessionAffinityFormat: "openrouter" as const,
930
+ sendSessionAffinityHeaders: true,
931
+ ...(modelId.startsWith("anthropic/")
932
+ ? { cacheControlFormat: "anthropic" as const }
933
+ : {}),
934
+ }
935
+ : {
936
+ sessionAffinityFormat: "openrouter" as const,
937
+ supportsDeveloperRole: modelId.startsWith("openai/") ||
938
+ modelId.startsWith("anthropic/"),
939
+ supportsStrictMode: openAIResponsesCompat?.supportsStrictMode ?? false,
940
+ supportsOpenAIGrammarTools:
941
+ openAIResponsesCompat?.supportsOpenAIGrammarTools ?? false,
942
+ }),
818
943
  openRouterRouting: {
819
944
  require_parameters: true,
820
945
  ...(openRouterProviderPin
@@ -868,6 +993,13 @@ export function configurePiModels(
868
993
  models: MutableModels,
869
994
  provider: RuntimeProviderPort,
870
995
  ): MutableModels {
996
+ for (const endpoint of provider.endpoints) {
997
+ for (const modelId of Object.keys(endpoint.modelMetadata ?? {})) {
998
+ if (!endpoint.models.includes(modelId)) {
999
+ throw new Error(`Deployment metadata is not configured for this endpoint: ${modelId}`);
1000
+ }
1001
+ }
1002
+ }
871
1003
  const configuredProviders = provider.endpoints.map((endpoint, index) =>
872
1004
  createProvider({
873
1005
  id: providerId(endpoint, index),
@@ -0,0 +1,200 @@
1
+ import type {
2
+ Context,
3
+ Model,
4
+ ProviderStreams,
5
+ StreamOptions,
6
+ TranscriptContext,
7
+ } from "@earendil-works/pi-ai";
8
+ import { getCurrentTools } from "@earendil-works/pi-ai";
9
+ import {
10
+ openAICompletionsApi,
11
+ } from "@earendil-works/pi-ai/api/openai-completions.lazy";
12
+ import {
13
+ openAIResponsesApi,
14
+ } from "@earendil-works/pi-ai/api/openai-responses.lazy";
15
+ import { overlayProviderStreams } from "../provider-streams";
16
+
17
+ function record(value: unknown): Record<string, unknown> | undefined {
18
+ return value !== null && typeof value === "object" && !Array.isArray(value)
19
+ ? value as Record<string, unknown>
20
+ : undefined;
21
+ }
22
+
23
+ function openRouterOptions(
24
+ model: Model<"openai-completions" | "openai-responses">,
25
+ context: TranscriptContext,
26
+ options?: StreamOptions,
27
+ ): StreamOptions {
28
+ return {
29
+ ...options,
30
+ onPayload: async (payload, activeModel) => {
31
+ const callerPayload = await options?.onPayload?.(payload, activeModel) ?? payload;
32
+ const body = record(callerPayload);
33
+ if (!body) throw new Error("OpenRouter payload must be an object");
34
+ const replayed = model.api === "openai-responses"
35
+ ? responsesToolSearch(replayResponsesMessages(body, model, context), context)
36
+ : body;
37
+ if (model.api === "openai-responses") {
38
+ if (replayed.store !== false || replayed.previous_response_id != null) {
39
+ throw new Error("OpenRouter Responses requires store: false and full input history");
40
+ }
41
+ if (replayed.background === true) {
42
+ throw new Error("Background Responses cannot run in a streaming Agent turn");
43
+ }
44
+ }
45
+ const compat = model.compat as {
46
+ openRouterRouting?: Readonly<Record<string, unknown>>;
47
+ } | undefined;
48
+ const reasoning = record(replayed.reasoning);
49
+ const include = Array.isArray(replayed.include) ? replayed.include : [];
50
+ return {
51
+ ...replayed,
52
+ ...(model.api === "openai-responses" && reasoning?.effort !== undefined &&
53
+ reasoning.effort !== null && reasoning.effort !== "none" &&
54
+ !include.includes("reasoning.encrypted_content")
55
+ ? { include: [...include, "reasoning.encrypted_content"] }
56
+ : {}),
57
+ ...(options?.sessionId ? { session_id: options.sessionId } : {}),
58
+ provider: {
59
+ ...record(replayed.provider),
60
+ ...compat?.openRouterRouting,
61
+ require_parameters: true,
62
+ },
63
+ };
64
+ },
65
+ };
66
+ }
67
+
68
+ function responsesToolSearch(
69
+ body: Record<string, unknown>,
70
+ context: TranscriptContext,
71
+ ): Record<string, unknown> {
72
+ const deferredNames = new Set(
73
+ getCurrentTools(context.messages)
74
+ .filter((tool) => tool.deferLoading).map((tool) => tool.name),
75
+ );
76
+ if (deferredNames.size === 0) return body;
77
+ const tools = Array.isArray(body.tools) ? body.tools : [];
78
+ const toolName = (tool: unknown) => {
79
+ const name = record(tool)?.name;
80
+ return typeof name === "string" ? name : "";
81
+ };
82
+ const deferred = tools.filter((tool) => deferredNames.has(toolName(tool)));
83
+ if (deferred.length !== deferredNames.size) {
84
+ throw new Error("Deferred Runtime tools are missing from the Responses payload");
85
+ }
86
+ const choice = record(body.tool_choice);
87
+ if (body.tool_choice !== undefined && body.tool_choice !== "auto" &&
88
+ choice?.type !== "allowed_tools") {
89
+ throw new Error("tool_choice conflicts with openrouter:tool_search");
90
+ }
91
+ const hasSearch = tools.some((tool) =>
92
+ ["openrouter:tool_search", "tool_search"].includes(record(tool)?.type as string)
93
+ );
94
+ return {
95
+ ...body,
96
+ tools: [
97
+ ...(hasSearch ? [] : [{ type: "openrouter:tool_search", parameters: { max_results: 5 } }]),
98
+ ...tools.map((tool) => deferredNames.has(toolName(tool))
99
+ ? { ...record(tool), defer_loading: true }
100
+ : tool),
101
+ ],
102
+ };
103
+ }
104
+
105
+ function responsesOutput(message: Context["messages"][number]): Record<string, unknown>[] {
106
+ if (message.role !== "assistant") return [];
107
+ if (!message.protocolResponse?.output) return [];
108
+ return message.protocolResponse.output.map((item) => ({ ...item }));
109
+ }
110
+
111
+ function sameResponseItem(
112
+ generated: Record<string, unknown> | undefined,
113
+ original: Record<string, unknown>,
114
+ ): boolean {
115
+ if (!generated || generated.type !== original.type ||
116
+ generated.id !== original.id) return false;
117
+ if (original.type === "message") {
118
+ const text = (content: unknown) => Array.isArray(content)
119
+ ? content.map((block) => {
120
+ const part = record(block);
121
+ return part?.type === "output_text" ? part.text : part?.refusal;
122
+ }).join("")
123
+ : content;
124
+ return text(generated.content) === text(original.content);
125
+ }
126
+ if (original.type === "function_call" || original.type === "custom_tool_call") {
127
+ return generated.call_id === original.call_id &&
128
+ generated.name === original.name;
129
+ }
130
+ return original.type === "reasoning";
131
+ }
132
+
133
+ function replayResponsesMessages(
134
+ body: Record<string, unknown>,
135
+ model: Model<"openai-completions" | "openai-responses">,
136
+ context: TranscriptContext,
137
+ ): Record<string, unknown> {
138
+ if (!Array.isArray(body.input)) return body;
139
+ const input = [...body.input];
140
+ for (const message of context.messages) {
141
+ if (
142
+ message.role !== "assistant" ||
143
+ message.provider !== model.provider ||
144
+ message.model !== model.id ||
145
+ message.api !== "openai-responses" ||
146
+ !message.protocolResponse?.output
147
+ ) continue;
148
+ const output = responsesOutput(message);
149
+ const anchors = output.filter((item) => [
150
+ "message", "reasoning", "function_call", "custom_tool_call",
151
+ ].includes(item.type as string));
152
+ if (anchors.length === 0) continue;
153
+ const start = input.findIndex((item) =>
154
+ sameResponseItem(record(item), anchors[0]!)
155
+ );
156
+ if (start < 0 || !anchors.every((item, index) =>
157
+ sameResponseItem(record(input[start + index]), item)
158
+ )) {
159
+ for (const item of output) {
160
+ if (item.type !== "message") continue;
161
+ const index = input.findIndex((value) => sameResponseItem(record(value), item));
162
+ if (index >= 0) input[index] = item;
163
+ }
164
+ continue;
165
+ }
166
+ input.splice(start, anchors.length, ...output.map((item) => {
167
+ if (item.type !== "reasoning" || item.encrypted_content) return item;
168
+ const original = record(input[start + anchors.indexOf(item)]);
169
+ return original?.encrypted_content
170
+ ? { ...item, encrypted_content: original.encrypted_content }
171
+ : item;
172
+ }));
173
+ }
174
+ return { ...body, input };
175
+ }
176
+
177
+ function routedApi(api: ProviderStreams): ProviderStreams {
178
+ return overlayProviderStreams(api, {
179
+ stream: (model, context, options) =>
180
+ api.stream(model, context, openRouterOptions(
181
+ model as Model<"openai-completions" | "openai-responses">,
182
+ context,
183
+ options,
184
+ )),
185
+ streamSimple: (model, context, options) =>
186
+ api.streamSimple(model, context, openRouterOptions(
187
+ model as Model<"openai-completions" | "openai-responses">,
188
+ context,
189
+ options,
190
+ )),
191
+ });
192
+ }
193
+
194
+ export function openRouterChatApi(): ProviderStreams {
195
+ return routedApi(openAICompletionsApi());
196
+ }
197
+
198
+ export function openRouterResponsesApi(): ProviderStreams {
199
+ return routedApi(openAIResponsesApi());
200
+ }
@@ -0,0 +1,161 @@
1
+ import {
2
+ createAssistantMessageEventStream,
3
+ fauxAssistantMessage,
4
+ type Api,
5
+ type AssistantMessage,
6
+ type AssistantMessageEventStream,
7
+ type Model,
8
+ type ProviderStreams,
9
+ type StreamOptions,
10
+ } from "@earendil-works/pi-ai";
11
+ import type { RuntimeModelProtocol } from "../../config/ports";
12
+ import { overlayProviderStreams } from "../provider-streams";
13
+
14
+ export type OpenRouterProtocol = Extract<RuntimeModelProtocol, `openrouter-${string}`>;
15
+
16
+ /** Minimal native output retained for the next OpenRouter Responses request. */
17
+ export interface ModelProtocolResponse {
18
+ readonly output: readonly Readonly<Record<string, unknown>>[];
19
+ }
20
+
21
+ interface ProtocolEvent {
22
+ readonly data: unknown;
23
+ }
24
+
25
+ declare module "@earendil-works/pi-ai" {
26
+ interface AssistantMessage {
27
+ protocolResponse?: ModelProtocolResponse;
28
+ }
29
+ }
30
+
31
+ function record(value: unknown): Record<string, unknown> | undefined {
32
+ return value !== null && typeof value === "object" && !Array.isArray(value)
33
+ ? value as Record<string, unknown>
34
+ : undefined;
35
+ }
36
+
37
+ function parseProtocolEvents(body: string): ProtocolEvent[] {
38
+ return body.split(/\r?\n\r?\n/).flatMap((frame) => {
39
+ const lines = frame.split(/\r?\n/);
40
+ const dataLines = lines.filter((line) => line.startsWith("data:"))
41
+ .map((line) => line.slice(5).replace(/^ /, ""));
42
+ if (dataLines.length === 0) return [];
43
+ const dataText = dataLines.join("\n");
44
+ let data: unknown = dataText;
45
+ try {
46
+ data = JSON.parse(dataText);
47
+ } catch {
48
+ data = dataText;
49
+ }
50
+ return [{ data }];
51
+ });
52
+ }
53
+
54
+ function compactResponsesBody(body: string): ModelProtocolResponse | undefined {
55
+ const completedItems = new Map<number, Record<string, unknown>>();
56
+ let terminalItems: Record<string, unknown>[] | undefined;
57
+ let terminal = false;
58
+ for (const frame of parseProtocolEvents(body)) {
59
+ const event = record(frame.data);
60
+ if (
61
+ event?.type === "response.output_item.done" &&
62
+ typeof event.output_index === "number"
63
+ ) {
64
+ const item = record(event.item);
65
+ if (item) completedItems.set(event.output_index, item);
66
+ }
67
+ if (event?.type === "response.completed" || event?.type === "response.incomplete") {
68
+ terminal = true;
69
+ const response = record(event.response);
70
+ if (Array.isArray(response?.output)) {
71
+ terminalItems = response.output.flatMap((value) => {
72
+ const output = record(value);
73
+ return output ? [output] : [];
74
+ });
75
+ }
76
+ }
77
+ }
78
+ if (!terminal) return undefined;
79
+ const output = terminalItems?.length
80
+ ? terminalItems.map((item, index) => ({ ...completedItems.get(index), ...item }))
81
+ : [...completedItems.entries()]
82
+ .sort(([left], [right]) => left - right)
83
+ .map(([, item]) => item);
84
+ return output.length > 0 ? { output } : undefined;
85
+ }
86
+
87
+ function streamWithProtocolResponse<T extends StreamOptions>(
88
+ model: Model<Api>,
89
+ options: T | undefined,
90
+ start: (options: T) => AssistantMessageEventStream,
91
+ ): AssistantMessageEventStream {
92
+ const output = createAssistantMessageEventStream();
93
+ const fetcher = options?.fetch ?? globalThis.fetch;
94
+ let responseOutput: Promise<ModelProtocolResponse | undefined> | undefined;
95
+ let terminal = false;
96
+ const requestOptions = {
97
+ ...options,
98
+ fetch: (async (...args: Parameters<typeof fetch>) => {
99
+ const response = await fetcher(...args);
100
+ responseOutput = response.clone().text()
101
+ .then((body) => compactResponsesBody(body))
102
+ .catch(() => undefined);
103
+ return response;
104
+ }) satisfies typeof fetch,
105
+ } as T;
106
+
107
+ void (async () => {
108
+ const source = start(requestOptions);
109
+ for await (const event of source) {
110
+ if (event.type === "done" || event.type === "error") {
111
+ terminal = true;
112
+ if (responseOutput) {
113
+ const message = event.type === "done" ? event.message : event.error;
114
+ const compact = await responseOutput;
115
+ if (compact) message.protocolResponse = compact;
116
+ }
117
+ }
118
+ output.push(event);
119
+ }
120
+ if (!terminal) throw new Error("Provider stream ended without a terminal event");
121
+ output.end();
122
+ })().catch(async (error: unknown) => {
123
+ if (terminal) {
124
+ output.end();
125
+ return;
126
+ }
127
+ const reason = options?.signal?.aborted ? "aborted" : "error";
128
+ const failure = fauxAssistantMessage("", {
129
+ stopReason: reason,
130
+ errorMessage: error instanceof Error ? error.message : String(error),
131
+ });
132
+ if (responseOutput) {
133
+ const compact = await responseOutput;
134
+ if (compact) failure.protocolResponse = compact;
135
+ }
136
+ output.push({
137
+ type: "error",
138
+ reason,
139
+ error: { ...failure, api: model.api, provider: model.provider, model: model.id },
140
+ });
141
+ output.end();
142
+ });
143
+ return output;
144
+ }
145
+
146
+ export function withModelProtocolResponse(
147
+ api: ProviderStreams,
148
+ protocol: OpenRouterProtocol,
149
+ ): ProviderStreams {
150
+ if (protocol !== "openrouter-responses") return api;
151
+ return overlayProviderStreams(api, {
152
+ stream: (model, context, options) =>
153
+ streamWithProtocolResponse(model, options, (requestOptions) =>
154
+ api.stream(model, context, requestOptions)
155
+ ),
156
+ streamSimple: (model, context, options) =>
157
+ streamWithProtocolResponse(model, options, (requestOptions) =>
158
+ api.streamSimple(model, context, requestOptions)
159
+ ),
160
+ });
161
+ }
@@ -9,10 +9,9 @@ export interface TemporaryAgentRequest {
9
9
  task: string;
10
10
  }
11
11
 
12
- export interface TemporaryAgentLaunch<Config = unknown>
12
+ export interface TemporaryAgentLaunchSelection
13
13
  extends TemporaryAgentRequest {
14
14
  runtimeKey: string;
15
- config: Config;
16
15
  executionLevel: ExecutionLevel;
17
16
  }
18
17
 
@@ -1128,6 +1128,8 @@ export interface WebSearchOptions {
1128
1128
  function apiFor(endpoint: RuntimeModelEndpoint): NativeApi {
1129
1129
  switch (endpoint.protocol) {
1130
1130
  case "openrouter-messages":
1131
+ case "openrouter-chat":
1132
+ case "openrouter-responses":
1131
1133
  return "openrouter-responses";
1132
1134
  case "google-generative-ai":
1133
1135
  return "google-generative-ai";