@jeffusion/bungee-core 5.18.0 → 5.19.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.
Files changed (35) hide show
  1. package/dist/gateway/contracts.d.ts +29 -2
  2. package/dist/gateway/dispatch-plugin.d.ts +7 -0
  3. package/dist/gateway/dispatch-targets.d.ts +4 -0
  4. package/dist/gateway/plugin.js +2 -0
  5. package/dist/gateway/routing-plugin.d.ts +3 -1
  6. package/dist/hooks/plugin-hooks.d.ts +3 -1
  7. package/dist/main.js +146 -34
  8. package/dist/plugin-extensions.d.ts +2 -0
  9. package/dist/plugins/ai-transformer/index.js.map +1 -1
  10. package/dist/plugins/anthropic-request-sanitizer/index.js.map +1 -1
  11. package/dist/plugins/anthropic-tool-name-transformer/index.js.map +1 -1
  12. package/dist/plugins/chatgpt-oauth/control.js.map +1 -1
  13. package/dist/plugins/chatgpt-oauth/index.js.map +1 -1
  14. package/dist/plugins/codex-router/index.js +179 -2
  15. package/dist/plugins/codex-router/index.js.map +6 -5
  16. package/dist/plugins/deepseek-reasoning-fix/index.js.map +1 -1
  17. package/dist/plugins/key-access/control.js.map +2 -2
  18. package/dist/plugins/key-access/index.js +14 -10
  19. package/dist/plugins/key-access/index.js.map +4 -4
  20. package/dist/plugins/key-access/ingress.js +14 -10
  21. package/dist/plugins/key-access/ingress.js.map +4 -4
  22. package/dist/plugins/key-rate-limit/index.js.map +1 -1
  23. package/dist/plugins/local-accounts/control.js.map +1 -1
  24. package/dist/plugins/local-accounts/index.js.map +1 -1
  25. package/dist/plugins/model-mapping/index.js.map +1 -1
  26. package/dist/plugins/models-dev/index.js.map +1 -1
  27. package/dist/plugins/openai-messages-to-chat/index.js.map +1 -1
  28. package/dist/plugins/signature-repair/index.js.map +1 -1
  29. package/dist/plugins/token-budget/control.js.map +1 -1
  30. package/dist/plugins/token-budget/index.js.map +1 -1
  31. package/dist/plugins/token-metering/index.js.map +1 -1
  32. package/dist/plugins/token-stats/control.js.map +1 -1
  33. package/dist/plugins/token-stats/index.js.map +1 -1
  34. package/dist/scoped-plugin-registry.d.ts +1 -0
  35. package/package.json +2 -2
@@ -1,5 +1,5 @@
1
1
  /** Typed extension stages shared by built-in and project plugins. */
2
- import type { AppConfig, RouteConfig, CorsConfig } from '@jeffusion/bungee-types';
2
+ import type { AppConfig, RouteConfig, CorsConfig, LLMProtocol } from '@jeffusion/bungee-types';
3
3
  import type { ExpressionContext } from '../expression-engine';
4
4
  import type { EffectiveRouteConfig, RuntimeUpstream, RequestSnapshot } from '../worker/types';
5
5
  import type { HandleRequestRuntimeContext } from './request-plugin';
@@ -24,6 +24,33 @@ export type GatewayBodyRuleArguments = [body: Record<string, any>, rules: Modifi
24
24
  export type GatewayHeaderRuleArguments = [headers: Headers, rules: ModificationRules['headers'], context: ExpressionContext];
25
25
  export type GatewayCorsArguments = [response: Response, cors: CorsConfig | undefined, request: Request];
26
26
  export type GatewayQueryRuleArguments = [params: URLSearchParams, rules: ModificationRules['query'], context: ExpressionContext, requestLog: any];
27
+ export interface GatewayDispatchTarget {
28
+ readonly type: 'route' | 'service';
29
+ readonly id: string;
30
+ }
31
+ export interface DispatchRequestInput {
32
+ readonly context: import('../hooks').MutableRequestContext;
33
+ readonly targets: readonly (GatewayDispatchTarget & {
34
+ readonly protocol?: LLMProtocol;
35
+ })[];
36
+ readonly principal?: import('../plugin-extensions').DataPrincipal;
37
+ readonly signal: AbortSignal;
38
+ readonly servingRevision?: number;
39
+ }
40
+ export interface DispatchRequestDecision {
41
+ readonly target: GatewayDispatchTarget;
42
+ }
43
+ export interface GatewayDispatchInput extends Omit<DispatchRequestInput, 'targets'> {
44
+ readonly config: AppConfig;
45
+ readonly entry: RouteConfig;
46
+ }
47
+ export interface GatewayDispatchDecision {
48
+ readonly route: RouteConfig;
49
+ readonly effective: EffectiveRouteConfig;
50
+ readonly context: import('../hooks').MutableRequestContext;
51
+ readonly entryRouteId?: string;
52
+ readonly target?: GatewayDispatchTarget;
53
+ }
27
54
  export interface GatewayRouteInput {
28
55
  readonly request: Request;
29
56
  readonly config: AppConfig;
@@ -107,4 +134,4 @@ export interface GatewayLogResult {
107
134
  readonly logger?: RequestLogger;
108
135
  }
109
136
  /** Provider stages are fixed, required, and registered exactly once at startup. */
110
- export declare const GATEWAY_PROVIDER_STAGES: readonly ["onGatewayWebSocket", "onGatewayBody", "onGatewayRequest", "onGatewayRoute", "onGatewayAdmission", "onGatewayAdmissionSession", "onGatewayAdmissionPrepare", "onGatewaySelect", "onGatewayFailover", "onGatewayRetry", "onGatewayForward", "onGatewayHeaderRules", "onGatewayCors", "onGatewayBodyRules", "onGatewayQueryRules", "onGatewayResponseRules", "onGatewayLog"];
137
+ export declare const GATEWAY_PROVIDER_STAGES: readonly ["onGatewayWebSocket", "onGatewayBody", "onGatewayRequest", "onGatewayRoute", "onGatewayDispatch", "onGatewayAdmission", "onGatewayAdmissionSession", "onGatewayAdmissionPrepare", "onGatewaySelect", "onGatewayFailover", "onGatewayRetry", "onGatewayForward", "onGatewayHeaderRules", "onGatewayCors", "onGatewayBodyRules", "onGatewayQueryRules", "onGatewayResponseRules", "onGatewayLog"];
@@ -0,0 +1,7 @@
1
+ import type { Plugin, PluginHooks } from './plugin';
2
+ export declare class DispatchPlugin implements Plugin {
3
+ bodyRequirements(): {
4
+ request: "none";
5
+ };
6
+ register(hooks: PluginHooks): void;
7
+ }
@@ -0,0 +1,4 @@
1
+ import type { PluginConfigField } from '../plugin.types';
2
+ import type { GatewayDispatchTarget } from './contracts';
3
+ /** Extract references only from the trusted schema, never from arbitrary request JSON. */
4
+ export declare function declaredDispatchTargets(options: Record<string, unknown>, fields: readonly PluginConfigField[]): GatewayDispatchTarget[];
@@ -325,6 +325,8 @@ function createPluginHooks() {
325
325
  onGatewayBody: new SyncBailHook("onGatewayBody"),
326
326
  onGatewayRequest: new AsyncSeriesBailHook("onGatewayRequest"),
327
327
  onGatewayRoute: new AsyncSeriesBailHook("onGatewayRoute"),
328
+ onGatewayDispatch: new AsyncSeriesBailHook("onGatewayDispatch"),
329
+ onDispatchRequest: new AsyncSeriesBailHook("onDispatchRequest"),
328
330
  onGatewayAdmission: new AsyncSeriesBailHook("onGatewayAdmission"),
329
331
  onGatewayAdmissionSession: new AsyncSeriesBailHook("onGatewayAdmissionSession"),
330
332
  onGatewayAdmissionPrepare: new AsyncSeriesBailHook("onGatewayAdmissionPrepare"),
@@ -1,5 +1,7 @@
1
- import type { CorsConfig } from '@jeffusion/bungee-types';
1
+ import type { AppConfig, RouteConfig, CorsConfig } from '@jeffusion/bungee-types';
2
+ import type { EffectiveRouteConfig } from '../worker/types';
2
3
  import type { Plugin, PluginHooks } from '@jeffusion/bungee-core/plugin';
4
+ export declare function resolveEffectiveRoute(config: AppConfig, route: RouteConfig): EffectiveRouteConfig;
3
5
  export declare function applyCorsHeaders(response: Response, cors: CorsConfig | undefined, request: Request): Response;
4
6
  export declare class RoutingPlugin implements Plugin {
5
7
  bodyRequirements(): {
@@ -11,7 +11,7 @@ import type { PluginServices } from '../plugin-services';
11
11
  import type { PluginStorage, SSEEnvelope } from '../plugin.types';
12
12
  import type { InterceptResult, PluginPhase } from '@jeffusion/bungee-types';
13
13
  import type { RawResponseResult } from '../plugin-control/contracts';
14
- import type { GatewayBodyArguments, GatewayRequestArguments, GatewayForwardArguments, GatewayResponseRuleArguments, GatewayBodyRuleArguments, GatewayQueryRuleArguments, GatewayHeaderRuleArguments, GatewayCorsArguments, GatewayRouteInput, GatewayRouteDecision, GatewayAdmissionInput, GatewayAdmissionDecision, GatewayAdmissionSessionInput, GatewayAdmissionPrepareInput, GatewaySelectInput, GatewaySelectDecision, GatewayFailoverInput, GatewayRetryInput, GatewayLogInput, GatewayLogResult } from '../gateway/contracts';
14
+ import type { GatewayBodyArguments, GatewayRequestArguments, GatewayForwardArguments, GatewayResponseRuleArguments, GatewayBodyRuleArguments, GatewayQueryRuleArguments, GatewayHeaderRuleArguments, GatewayCorsArguments, GatewayRouteInput, GatewayRouteDecision, GatewayDispatchInput, GatewayDispatchDecision, DispatchRequestInput, DispatchRequestDecision, GatewayAdmissionInput, GatewayAdmissionDecision, GatewayAdmissionSessionInput, GatewayAdmissionPrepareInput, GatewaySelectInput, GatewaySelectDecision, GatewayFailoverInput, GatewayRetryInput, GatewayLogInput, GatewayLogResult } from '../gateway/contracts';
15
15
  import type { WorkerRequestAdmission, PreparedAdmissionAttempt } from '../data-admission/worker';
16
16
  import type { FailoverCoordinator } from '../worker/upstream/failover-coordinator';
17
17
  import type { ProxyRequestResult } from '../gateway/forward-plugin';
@@ -233,6 +233,8 @@ export interface PluginHooks {
233
233
  onGatewayCors: AsyncSeriesBailHook<GatewayCorsArguments, Response>;
234
234
  onGatewayRequest: AsyncSeriesBailHook<GatewayRequestArguments, Response>;
235
235
  onGatewayRoute: AsyncSeriesBailHook<[GatewayRouteInput], GatewayRouteDecision>;
236
+ onGatewayDispatch: AsyncSeriesBailHook<[GatewayDispatchInput], GatewayDispatchDecision>;
237
+ onDispatchRequest: AsyncSeriesBailHook<[DispatchRequestInput], DispatchRequestDecision>;
236
238
  onGatewayAdmission: AsyncSeriesBailHook<[GatewayAdmissionInput], GatewayAdmissionDecision>;
237
239
  onGatewayAdmissionSession: AsyncSeriesBailHook<[GatewayAdmissionSessionInput], WorkerRequestAdmission>;
238
240
  onGatewayAdmissionPrepare: AsyncSeriesBailHook<[GatewayAdmissionPrepareInput], PreparedAdmissionAttempt[]>;
package/dist/main.js CHANGED
@@ -455,14 +455,17 @@ var require_main = __commonJS(function(exports, module) {
455
455
  function validateGatewayTargets(config, schemas, context) {
456
456
  if (!schemas)
457
457
  return;
458
- const routes = new Set(config.routes.map((route) => route.id));
459
- const services = new Set(config.services.map((service) => service.id));
458
+ const routes = new Map(config.routes.map((route) => [route.id, route]));
459
+ const services = new Map(config.services.map((service) => [service.id, service]));
460
460
  const edges = new Map;
461
461
  const visit = (value, field, owner, path) => {
462
462
  if (field.type === "gateway_target" && value && typeof value === "object") {
463
463
  const target = value;
464
- if (!(target.type === "route" ? routes : services).has(target.id))
464
+ const entity = (target.type === "route" ? routes : services).get(target.id);
465
+ if (!entity)
465
466
  context.add("invalid_value", path, "Dispatch target does not exist");
467
+ else if (!entity.llm_protocol)
468
+ context.add("invalid_value", path, "Dispatch target requires an explicit receiving protocol");
466
469
  const refs = edges.get(owner) ?? new Set;
467
470
  refs.add(`${target.type}:${target.id}`);
468
471
  edges.set(owner, refs);
@@ -30765,7 +30768,7 @@ var package_default;
30765
30768
  var init_package = __esm(() => {
30766
30769
  package_default = {
30767
30770
  name: "@jeffusion/bungee-core",
30768
- version: "5.18.0",
30771
+ version: "5.19.0",
30769
30772
  description: "Bungee reverse proxy core engine",
30770
30773
  main: "./dist/main.js",
30771
30774
  types: "./dist/main.d.ts",
@@ -202919,6 +202922,8 @@ function createPluginHooks() {
202919
202922
  onGatewayBody: new SyncBailHook("onGatewayBody"),
202920
202923
  onGatewayRequest: new AsyncSeriesBailHook("onGatewayRequest"),
202921
202924
  onGatewayRoute: new AsyncSeriesBailHook("onGatewayRoute"),
202925
+ onGatewayDispatch: new AsyncSeriesBailHook("onGatewayDispatch"),
202926
+ onDispatchRequest: new AsyncSeriesBailHook("onDispatchRequest"),
202922
202927
  onGatewayAdmission: new AsyncSeriesBailHook("onGatewayAdmission"),
202923
202928
  onGatewayAdmissionSession: new AsyncSeriesBailHook("onGatewayAdmissionSession"),
202924
202929
  onGatewayAdmissionPrepare: new AsyncSeriesBailHook("onGatewayAdmissionPrepare"),
@@ -202990,6 +202995,23 @@ var init_hooks = __esm(() => {
202990
202995
  init_plugin_hooks();
202991
202996
  });
202992
202997
 
202998
+ // src/gateway/dispatch-targets.ts
202999
+ function declaredDispatchTargets(options, fields) {
203000
+ const result = [];
203001
+ const walk = (value, field) => {
203002
+ if (field.type === "gateway_target" && value && typeof value === "object") {
203003
+ const target = value;
203004
+ if (["route", "service"].includes(target.type) && typeof target.id === "string")
203005
+ result.push({ type: target.type, id: target.id });
203006
+ } else if (field.type === "array" && field.items && Array.isArray(value))
203007
+ value.forEach((item) => walk(item, field.items));
203008
+ else if (field.type === "object" && field.properties && value && typeof value === "object")
203009
+ field.properties.forEach((property) => walk(value[property.name], property));
203010
+ };
203011
+ fields.forEach((field) => walk(options[field.name], field));
203012
+ return result;
203013
+ }
203014
+
202993
203015
  // src/plugin-services/rpc-runtime.ts
202994
203016
  import { isProxy as isProxy4 } from "util/types";
202995
203017
  function positiveLimit2(value, fallback, label) {
@@ -213763,6 +213785,9 @@ ${errorMessages}`);
213763
213785
  getGlobalAdmissionHandlers() {
213764
213786
  return this.globalInstances.map((instance) => instance.handler);
213765
213787
  }
213788
+ getDeclaredDispatchTargets(routeId) {
213789
+ return (this.routeInstances.get(routeId) ?? []).flatMap((instance) => declaredDispatchTargets(instance.config.options ?? {}, this.pluginClasses.get(instance.handler.pluginName)?.configSchema ?? []));
213790
+ }
213766
213791
  async initializeFromConfig(config, dependencies) {
213767
213792
  this.dependencies = dependencies;
213768
213793
  if (dependencies) {
@@ -213835,8 +213860,9 @@ ${errorMessages}`);
213835
213860
  logger2.error({ error: result.error, pluginConfig, routeId }, "Failed to create route plugin instance after retries");
213836
213861
  }
213837
213862
  }
213838
- if (route.service) {
213839
- const service = config.services?.find((candidate) => candidate.name === route.service);
213863
+ const dispatchServices = this.getDeclaredDispatchTargets(routeId).filter((target) => target.type === "service").map((target) => config.services?.find((service) => service.id === target.id)).filter((service) => !!service);
213864
+ const relatedServices = [...new Map([...config.services?.filter((service) => service.name === route.service) ?? [], ...dispatchServices].map((service) => [service.name, service])).values()];
213865
+ for (const service of relatedServices) {
213840
213866
  if (service?.plugins?.length) {
213841
213867
  const serviceName = service.name;
213842
213868
  for (const pluginConfig of orderBindings(service.plugins)) {
@@ -213853,7 +213879,15 @@ ${errorMessages}`);
213853
213879
  }
213854
213880
  }
213855
213881
  }
213856
- const endpoints = resolveEffectiveRouteEndpoints(route, config.services);
213882
+ const endpoints = [...resolveEffectiveRouteEndpoints(route, config.services)];
213883
+ const endpointIds = new Set(endpoints.flatMap((endpoint) => endpoint.id ? [endpoint.id] : []));
213884
+ for (const endpoint of dispatchServices.flatMap((service) => service.endpoints)) {
213885
+ if (endpoint.id && endpointIds.has(endpoint.id))
213886
+ continue;
213887
+ if (endpoint.id)
213888
+ endpointIds.add(endpoint.id);
213889
+ endpoints.push(endpoint);
213890
+ }
213857
213891
  for (const [upstreamIndex, upstream] of endpoints.entries()) {
213858
213892
  const upstreamId = upstream.id || String(upstreamIndex);
213859
213893
  for (const pluginConfig of orderBindings(upstream.plugins || [])) {
@@ -214333,6 +214367,7 @@ var init_contracts = __esm(() => {
214333
214367
  "onGatewayBody",
214334
214368
  "onGatewayRequest",
214335
214369
  "onGatewayRoute",
214370
+ "onGatewayDispatch",
214336
214371
  "onGatewayAdmission",
214337
214372
  "onGatewayAdmissionSession",
214338
214373
  "onGatewayAdmissionPrepare",
@@ -214609,7 +214644,7 @@ class DataAdmissionHost {
214609
214644
  if (this.options.authorizeWorker(worker) !== "active")
214610
214645
  throw new DataAdmissionError(503, "admission_worker_not_active");
214611
214646
  for (const requirement of this.routeRequirements)
214612
- if (requirement.routeIds.includes(target.routeId) && !this.plugins.some((runtime) => runtime.publication.name === requirement.plugin && runtime.plugin.resolveIdentity))
214647
+ if ((requirement.routeIds.includes(target.routeId) || !!target.entryRouteId && requirement.routeIds.includes(target.entryRouteId)) && !this.plugins.some((runtime) => runtime.publication.name === requirement.plugin && runtime.plugin.resolveIdentity))
214613
214648
  throw new DataAdmissionError(503, "route_protection_unavailable");
214614
214649
  let principal = ANONYMOUS_PRINCIPAL;
214615
214650
  for (const runtime of this.plugins)
@@ -214620,7 +214655,7 @@ class DataAdmissionHost {
214620
214655
  throw normalizeAdmissionError(error) ?? new DataAdmissionError(503, "route_protection_unavailable");
214621
214656
  }
214622
214657
  }
214623
- if (principal.domain === "anonymous" && this.routeRequirements.some((requirement) => requirement.routeIds.includes(target.routeId)))
214658
+ if (principal.domain === "anonymous" && this.routeRequirements.some((requirement) => requirement.routeIds.includes(target.routeId) || !!target.entryRouteId && requirement.routeIds.includes(target.entryRouteId)))
214624
214659
  throw new DataAdmissionError(503, "route_protection_unavailable");
214625
214660
  const requirements = {};
214626
214661
  for (const runtime of this.plugins) {
@@ -214651,7 +214686,7 @@ class DataAdmissionHost {
214651
214686
  if (this.options.authorizeWorker(worker) !== "active")
214652
214687
  throw new DataAdmissionError(503, "admission_worker_not_active");
214653
214688
  for (const requirement of this.routeRequirements) {
214654
- if (requirement.routeIds.includes(target.routeId) && !this.plugins.some((p) => p.publication.name === requirement.plugin && p.plugin.resolveIdentity)) {
214689
+ if ((requirement.routeIds.includes(target.routeId) || !!target.entryRouteId && requirement.routeIds.includes(target.entryRouteId)) && !this.plugins.some((p) => p.publication.name === requirement.plugin && p.plugin.resolveIdentity)) {
214655
214690
  throw new DataAdmissionError(503, "route_protection_unavailable");
214656
214691
  }
214657
214692
  }
@@ -214667,10 +214702,10 @@ class DataAdmissionHost {
214667
214702
  throw new DataAdmissionError(503, "route_protection_unavailable");
214668
214703
  }
214669
214704
  }
214670
- if (principal.domain === "anonymous" && this.routeRequirements.some((r) => r.routeIds.includes(target.routeId)))
214705
+ if (principal.domain === "anonymous" && this.routeRequirements.some((r) => r.routeIds.includes(target.routeId) || !!target.entryRouteId && r.routeIds.includes(target.entryRouteId)))
214671
214706
  throw new DataAdmissionError(503, "route_protection_unavailable");
214672
214707
  const ticket = this.previews.get(target.requestId);
214673
- if (!preview && expectedVersion !== undefined && (!ticket || ticket.version !== this.version || ticket.expires <= this.now() || ticket.worker !== workerKey(worker) || ticket.target.attemptId !== target.attemptId || ticket.target.url !== target.url || ticket.target.model !== target.model || ticket.target.routeId !== target.routeId || JSON.stringify(ticket.target.principal) !== JSON.stringify(principal) || ticket.target.serviceId !== target.serviceId || ticket.target.upstreamId !== target.upstreamId)) {
214708
+ if (!preview && expectedVersion !== undefined && (!ticket || ticket.version !== this.version || ticket.expires <= this.now() || ticket.worker !== workerKey(worker) || ticket.target.attemptId !== target.attemptId || ticket.target.url !== target.url || ticket.target.model !== target.model || ticket.target.routeId !== target.routeId || ticket.target.entryRouteId !== target.entryRouteId || JSON.stringify(ticket.target.principal) !== JSON.stringify(principal) || ticket.target.serviceId !== target.serviceId || ticket.target.upstreamId !== target.upstreamId)) {
214674
214709
  throw new DataAdmissionError(409, "admission_preview_changed");
214675
214710
  }
214676
214711
  const nowTarget = Object.freeze({ ...target, principal, now: !preview && ticket ? ticket.target.now : this.now() });
@@ -214716,7 +214751,7 @@ class DataAdmissionHost {
214716
214751
  plan.runtime.state = plan.state;
214717
214752
  }
214718
214753
  this.previews.delete(target.requestId);
214719
- this.grants.set(target.requestId, { routeId: target.routeId, sourcePrincipal: structuredClone(target.principal), worker: workerKey(worker), grant, plugins: this.plugins.map((runtime) => ({ publication: runtime.publication, plugin: runtime.plugin, state: null })), expires: null });
214754
+ this.grants.set(target.requestId, { entryRouteId: target.entryRouteId, routeId: target.routeId, sourcePrincipal: structuredClone(target.principal), worker: workerKey(worker), grant, plugins: this.plugins.map((runtime) => ({ publication: runtime.publication, plugin: runtime.plugin, state: null })), expires: null });
214720
214755
  return grant;
214721
214756
  }
214722
214757
  beforeAttempt(target, worker) {
@@ -214739,7 +214774,7 @@ class DataAdmissionHost {
214739
214774
  stored.expires = this.now() + 15000;
214740
214775
  }
214741
214776
  assertGrant(stored, target, worker) {
214742
- if (stored.worker !== workerKey(worker) || stored.routeId !== target.routeId || JSON.stringify(stored.sourcePrincipal) !== JSON.stringify(target.principal) || !["active", "retired"].includes(this.options.authorizeWorker(worker)))
214777
+ if (stored.worker !== workerKey(worker) || stored.routeId !== target.routeId || stored.entryRouteId !== target.entryRouteId || JSON.stringify(stored.sourcePrincipal) !== JSON.stringify(target.principal) || !["active", "retired"].includes(this.options.authorizeWorker(worker)))
214743
214778
  throw new DataAdmissionError(403, "admission_identity_mismatch");
214744
214779
  }
214745
214780
  }
@@ -221778,7 +221813,7 @@ async function executeHttpRequestInternal(req, config, runtimeContextOrSelector,
221778
221813
  === Incoming Request ===`);
221779
221814
  const routeMatchStart = performance.now();
221780
221815
  const routeDecision = requireGatewayResult(await gatewayHooks().onGatewayRoute.promise({ request: req, config }), "onGatewayRoute");
221781
- const route = routeDecision.route;
221816
+ let route = routeDecision.route;
221782
221817
  if (!route) {
221783
221818
  logger2.error({ request: requestLog }, `No route found for path: ${url.pathname}`);
221784
221819
  success = false;
@@ -221793,39 +221828,62 @@ async function executeHttpRequestInternal(req, config, runtimeContextOrSelector,
221793
221828
  originalHeaders[key] = value;
221794
221829
  });
221795
221830
  reqLogger.setOriginalRequestHeaders(redactRequestHeaders(originalHeaders));
221796
- routeId = route.path;
221797
- routeServiceName = route.service;
221798
- const currentRouteId = route.path;
221799
- const effectiveRoute = requireGatewayResult(routeDecision.effective, "onGatewayRoute");
221800
- const endpoints = effectiveRoute.endpoints;
221801
- const runtimeStateKey = route.service ?? route.path;
221802
221831
  if (hasWorkerAdmissionSession() && !trustedIdentity) {
221803
221832
  success = false;
221804
221833
  responseStatus = 401;
221805
221834
  return finalizeLocalResponse(Response.json({ error: "unauthorized" }, { status: 401, headers: { "www-authenticate": "Bearer" } }));
221806
221835
  }
221836
+ if (routeDecision.response) {
221837
+ responseStatus = routeDecision.response.status;
221838
+ success = responseStatus < 400;
221839
+ return routeDecision.responseKind === "rule" ? finalizeRootStreamingResponse(routeDecision.response) : finalizeLocalResponse(routeDecision.response);
221840
+ }
221841
+ const snapshotStart = performance.now();
221842
+ const processingDeadline = AbortSignal.timeout(route.timeouts?.request_ms ?? 30000);
221843
+ const processingSignal = AbortSignal.any([req.signal, processingDeadline]);
221844
+ const requestSnapshot = await createRequestSnapshot(req, config.body_parser_limit, processingSignal, originalBody, { requestId, attemptId: requestId, direction: "request", stage: "original-request", version: 0, contentType: req.headers.get("content-type") ?? "", contentEncoding: req.headers.get("content-encoding") ?? "" });
221845
+ requestBodySource = requestSnapshot.bodySource;
221846
+ const entryRoute = route;
221847
+ const dispatchContext = createPhaseContext(requestSnapshot, requestId, entryRoute.path, entryRoute.service);
221848
+ const dispatchHooks = requestRegistry?.getRoutePrecompiledHooks?.(entryRoute.path);
221849
+ if (dispatchHooks?.hooks.onDispatchRequest.hasCallbacks()) {
221850
+ await readSnapshotJson(requestSnapshot, "internal-dispatch", true);
221851
+ dispatchContext.body = structuredClone(requestSnapshot.body);
221852
+ }
221853
+ const dispatch = requireGatewayResult(await gatewayHooks().onGatewayDispatch.promise({
221854
+ config,
221855
+ entry: entryRoute,
221856
+ context: dispatchContext,
221857
+ signal: processingSignal,
221858
+ principal: trustedIdentity?.principal,
221859
+ servingRevision: runtimeContext?.servingRevision
221860
+ }), "onGatewayDispatch");
221861
+ route = dispatch.route;
221862
+ if (dispatch.target) {
221863
+ requestSnapshot.body = dispatch.context.body;
221864
+ reqLogger.addStep("internal_dispatch", { entry: entryRoute.id ?? entryRoute.path, target: dispatch.target, model: dispatch.context.body?.model });
221865
+ }
221866
+ routeId = route.path;
221867
+ routeServiceName = route.service;
221868
+ const currentRouteId = route.path;
221869
+ const effectiveRoute = dispatch.effective;
221870
+ const endpoints = effectiveRoute.endpoints;
221871
+ const runtimeStateKey = route.service ?? route.path;
221807
221872
  if (trustedIdentity) {
221808
221873
  for (const handler of admissionHandlers)
221809
221874
  retainOwner(handler.pluginName);
221810
221875
  dataAdmission = requireGatewayResult(await gatewayHooks().onGatewayAdmissionSession.promise({ handlers: admissionHandlers, identity: {
221811
221876
  requestId,
221812
221877
  principal: trustedIdentity.principal,
221878
+ ...dispatch.entryRouteId ? { entryRouteId: dispatch.entryRouteId } : {},
221813
221879
  routeId: route.id ?? currentRouteId,
221814
221880
  serviceId: route.service_id ?? config.services?.find((service) => service.name === route.service)?.id ?? null
221815
221881
  }, invoke: (plugin, method, payload, target) => requestRegistry.invokeAdmissionRpc(plugin, method, payload, target, ownerLeases.get(`${plugin}\x00global`)) }), "onGatewayAdmissionSession");
221816
221882
  }
221817
- if (routeDecision.response) {
221818
- responseStatus = routeDecision.response.status;
221819
- success = responseStatus < 400;
221820
- return routeDecision.responseKind === "rule" ? finalizeRootStreamingResponse(routeDecision.response) : finalizeLocalResponse(routeDecision.response);
221821
- }
221822
- const snapshotStart = performance.now();
221823
- const processingDeadline = AbortSignal.timeout(effectiveRoute.timeouts?.request_ms ?? 30000);
221824
- const processingSignal = AbortSignal.any([req.signal, processingDeadline]);
221825
- const requestSnapshot = await createRequestSnapshot(req, config.body_parser_limit, processingSignal, originalBody, { requestId, attemptId: requestId, direction: "request", stage: "original-request", version: 0, contentType: req.headers.get("content-type") ?? "", contentEncoding: req.headers.get("content-encoding") ?? "" });
221826
- requestBodySource = requestSnapshot.bodySource;
221827
221883
  const demandHooks = requestRegistry?.getPrecompiledHooks(currentRouteId, undefined, routeServiceName);
221828
221884
  const demandContext = createPhaseContext(requestSnapshot, requestId, currentRouteId, routeServiceName);
221885
+ if (dispatch.target)
221886
+ demandContext.url = new URL(dispatch.context.url);
221829
221887
  applyRoutePathRewriteToContext(demandContext, route, requestLog);
221830
221888
  const routeDemand = collectPluginBodyRequirements([demandHooks?.routePhase, demandHooks?.servicePhase], {
221831
221889
  requestId,
@@ -221844,7 +221902,7 @@ async function executeHttpRequestInternal(req, config, runtimeContextOrSelector,
221844
221902
  const routeNeedsBody = analyzeExpressionDependencies(routeRequestDemand, "request").requestBody || analyzeExpressionDependencies(route.response, "response").requestBody;
221845
221903
  if (routeDemand.replay || route.retry?.enabled || effectiveRoute.failover?.enabled)
221846
221904
  await requestBodySource?.buffer("configured-replay");
221847
- if (selectionNeedsBody || routeNeedsBody || routeDemand.request !== "none")
221905
+ if (!requestSnapshot.is_json_body && (selectionNeedsBody || routeNeedsBody || routeDemand.request !== "none"))
221848
221906
  await readSnapshotJson(requestSnapshot, selectionNeedsBody ? "selection-expression" : routeDemand.request !== "none" ? "plugin-body-demand" : "route-expression", routeDemand.request === "json-write" && !["GET", "HEAD"].includes(req.method));
221849
221907
  reqLogger.addStep("request_body_plan", { mode: requestBodySource?.mode, reasons: requestBodySource?.reasons ?? [], replay: requestBodySource?.replayable ?? false, source: "wire" });
221850
221908
  reqLogger.addStepWithDuration("request_snapshot_created", performance.now() - snapshotStart, {
@@ -221863,6 +221921,11 @@ async function executeHttpRequestInternal(req, config, runtimeContextOrSelector,
221863
221921
  method: req.method,
221864
221922
  env: process.env
221865
221923
  };
221924
+ if (dispatch.target && entryRoute.id !== route.id) {
221925
+ const entryAdmission = requireGatewayResult(await gatewayHooks().onGatewayAdmission.promise({ route: entryRoute, trustedPeer: getTrustedWorkerPeer(req), context: expressionContext, servingRevision: runtimeContext?.servingRevision, signal: processingSignal }), "onGatewayAdmission");
221926
+ if (!entryAdmission.allowed)
221927
+ throw new DataAdmissionError(entryAdmission.retryAfterMs === undefined ? 503 : 429, "entry_route_rate_limited");
221928
+ }
221866
221929
  const rateLimit = requireGatewayResult(await gatewayHooks().onGatewayAdmission.promise({
221867
221930
  route,
221868
221931
  trustedPeer: getTrustedWorkerPeer(req),
@@ -221891,7 +221954,9 @@ async function executeHttpRequestInternal(req, config, runtimeContextOrSelector,
221891
221954
  const scopedRegistry = requestRegistry;
221892
221955
  const requestPhaseHooks = scopedRegistry?.getPrecompiledHooks(currentRouteId, undefined, routeServiceName) ?? null;
221893
221956
  const phaseContext = createPhaseContext(requestSnapshot, requestId, currentRouteId, routeServiceName);
221894
- phaseContext.bodyWrite = routeDemand.request === "json-write";
221957
+ phaseContext.bodyWrite = !!dispatch.target || routeDemand.request === "json-write";
221958
+ if (dispatch.target)
221959
+ phaseContext.url = new URL(dispatch.context.url);
221895
221960
  phaseContext.bodyRequirements = routeDemand;
221896
221961
  applyRoutePathRewriteToContext(phaseContext, route, requestLog);
221897
221962
  const routePhaseResponse = await executePreFailoverPhase(requestPhaseHooks?.routePhase, phaseContext, {
@@ -222942,6 +223007,49 @@ var init_routing_plugin = __esm(() => {
222942
223007
  init_endpoint_resolver();
222943
223008
  });
222944
223009
 
223010
+ // src/gateway/dispatch-plugin.ts
223011
+ class DispatchPlugin {
223012
+ bodyRequirements() {
223013
+ return { request: "none" };
223014
+ }
223015
+ register(hooks) {
223016
+ hooks.onGatewayDispatch.tapPromise("builtin.dispatch", async ({ config, entry, context, signal, principal, servingRevision }) => {
223017
+ const registry = getScopedPluginRegistry();
223018
+ const references = registry?.getDeclaredDispatchTargets?.(entry.path) ?? [];
223019
+ const targets = references.map((target) => {
223020
+ const entity = (target.type === "route" ? config.routes : config.services ?? []).find((entity) => entity.id === target.id);
223021
+ return { ...target, protocol: entity?.llm_protocol };
223022
+ });
223023
+ const dispatch = await registry?.getRoutePrecompiledHooks?.(entry.path)?.hooks.onDispatchRequest.promise({ context, signal, principal, servingRevision, targets });
223024
+ if (!dispatch)
223025
+ return { route: entry, effective: resolveEffectiveRoute(config, entry), context };
223026
+ if (!targets.some((target) => target.type === dispatch.target.type && target.id === dispatch.target.id))
223027
+ throw new DataAdmissionError(403, "dispatch_target_not_declared");
223028
+ signal.throwIfAborted();
223029
+ let route;
223030
+ if (dispatch.target.type === "route") {
223031
+ const target = config.routes.find((route) => route.id === dispatch.target.id);
223032
+ if (!target || target.id === entry.id || target.direct_response?.enabled || target.redirect?.enabled || target.response_rules?.some((rule) => rule.enabled))
223033
+ throw new DataAdmissionError(422, "dispatch_target_unavailable");
223034
+ route = target;
223035
+ const suffix = context.url.pathname.startsWith(entry.path) ? context.url.pathname.slice(entry.path.length) : "";
223036
+ context.url.pathname = target.path.replace(/\/$/, "") + suffix;
223037
+ } else {
223038
+ const service = config.services?.find((service) => service.id === dispatch.target.id);
223039
+ if (!service)
223040
+ throw new DataAdmissionError(422, "dispatch_target_unavailable");
223041
+ route = { ...entry, service: service.name, service_id: service.id, endpoints: undefined };
223042
+ }
223043
+ return { route, effective: resolveEffectiveRoute(config, route), context, entryRouteId: entry.id ?? entry.path, target: dispatch.target };
223044
+ });
223045
+ }
223046
+ }
223047
+ var init_dispatch_plugin = __esm(() => {
223048
+ init_errors();
223049
+ init_scoped_plugin_registry();
223050
+ init_routing_plugin();
223051
+ });
223052
+
222945
223053
  // src/rate-limit/json.ts
222946
223054
  class RateLimitJsonScanner {
222947
223055
  content;
@@ -224575,7 +224683,9 @@ function exact3(value, keys) {
224575
224683
  }
224576
224684
  function parseAdmissionTarget(value) {
224577
224685
  const target = object(value);
224578
- exact3(target, ["requestId", "attemptId", "principal", "routeId", "serviceId", "upstreamId", "url", "model", "now"]);
224686
+ exact3(target, ["requestId", "attemptId", "principal", "routeId", "serviceId", "upstreamId", "url", "model", "now", ...Object.hasOwn(target, "entryRouteId") ? ["entryRouteId"] : []]);
224687
+ if (Object.hasOwn(target, "entryRouteId") && (typeof target.entryRouteId !== "string" || !target.entryRouteId || target.entryRouteId.length > 256))
224688
+ throw new Error("invalid RPC entry route");
224579
224689
  for (const name of ["requestId", "attemptId", "routeId", "upstreamId", "url"])
224580
224690
  if (typeof target[name] !== "string" || !target[name].length || target[name].length > (name === "url" ? 65536 : 256))
224581
224691
  throw new Error("invalid RPC target");
@@ -230573,6 +230683,7 @@ function gatewayBuiltins() {
230573
230683
  new BodyServicePlugin,
230574
230684
  new HttpRequestPlugin,
230575
230685
  new RoutingPlugin,
230686
+ new DispatchPlugin,
230576
230687
  new AdmissionPlugin,
230577
230688
  new SelectionPlugin,
230578
230689
  new RetryPlugin,
@@ -230633,6 +230744,7 @@ var init_runtime = __esm(() => {
230633
230744
  init_contracts();
230634
230745
  init_request_plugin();
230635
230746
  init_forward_plugin();
230747
+ init_dispatch_plugin();
230636
230748
  init_routing_plugin();
230637
230749
  init_admission_plugin();
230638
230750
  init_selection_plugin();
@@ -31,6 +31,8 @@ export interface ManagementProvider {
31
31
  sessionCookie?(request: Request): string | undefined;
32
32
  }
33
33
  export interface AdmissionTarget {
34
+ /** Host-pinned entry scope for one internal handoff. */
35
+ readonly entryRouteId?: string;
34
36
  readonly requestId: string;
35
37
  readonly attemptId: string;
36
38
  readonly principal: DataPrincipal;