File size: 6,007 Bytes
e249c6d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import type { PluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.types.js";
import { FailoverError } from "../failover/error.js";
import { resolveBuiltInModelSuppressionFromManifest } from "../model-suppression.js";
import {
  resolveProviderModelMaterializationAuthMode,
  resolveProviderModelRouteMaterializationAuthMode,
  type ProviderModelRouteMaterializationAuthMode,
} from "../provider-model-route-auth.js";
import {
  canonicalizeProviderModelId,
  modelMatchesProviderModelRoute,
  projectProviderModelRouteConfig,
} from "../provider-model-route.js";
import type { AgentRuntimeAuthPlan } from "./types.js";

type RuntimeRouteModel = {
  provider?: string;
  id?: string;
  api?: string | null;
  baseUrl?: string;
};

function modelMatchesPreparedTarget(params: {
  model: RuntimeRouteModel;
  provider: string;
  modelId: string;
  route: NonNullable<AgentRuntimeAuthPlan["modelRoute"]>;
}): boolean {
  const modelId = canonicalizeProviderModelId(params.provider, params.model.id ?? "");
  const targetModelId = canonicalizeProviderModelId(params.provider, params.modelId);
  return (
    normalizeProviderId(params.model.provider ?? "") === normalizeProviderId(params.provider) &&
    modelId === targetModelId &&
    modelMatchesProviderModelRoute({
      provider: params.provider,
      api: params.model.api,
      baseUrl: params.model.baseUrl,
      route: params.route,
    })
  );
}

type PreparedRuntimeModelRequest = {
  config: OpenClawConfig;
  authProfileId?: string;
  authProfileMode?: ProviderModelRouteMaterializationAuthMode;
};

/** Resolves the exact model tuple selected by a prepared runtime auth plan. */
export async function materializePreparedRuntimeModel<Model extends RuntimeRouteModel>(params: {
  plan: AgentRuntimeAuthPlan;
  provider: string;
  modelId: string;
  config?: OpenClawConfig;
  workspaceDir?: string;
  metadataSnapshot?: PluginMetadataSnapshot;
  model?: Model;
  /** Re-resolve when a later auth candidate changes credential-scoped model metadata. */
  forceResolve?: boolean;
  rejectMismatchedModel?: boolean;
  resolveModel(
    request: PreparedRuntimeModelRequest,
  ): Promise<{ model?: Model | null; error?: string }>;
}): Promise<Model | undefined> {
  const route = params.plan.modelRoute;
  const config = route
    ? projectProviderModelRouteConfig({ provider: params.provider, config: params.config, route })
    : params.config;
  const validateFinalModel = (model: Model | undefined): Model | undefined => {
    if (!model) {
      return undefined;
    }
    const suppression = resolveBuiltInModelSuppressionFromManifest({
      provider: model.provider ?? params.provider,
      id: model.id ?? params.modelId,
      baseUrl: model.baseUrl,
      config,
      workspaceDir: params.workspaceDir,
      metadataSnapshot: params.metadataSnapshot,
    });
    if (suppression?.retirement) {
      throw new FailoverError(suppression.errorMessage, {
        reason: "model_not_found",
        provider: model.provider ?? params.provider,
        model: model.id ?? params.modelId,
      });
    }
    return model;
  };
  if (!route && !params.forceResolve) {
    return validateFinalModel(params.model);
  }
  if (
    route &&
    (normalizeProviderId(route.provider) !== normalizeProviderId(params.provider) ||
      canonicalizeProviderModelId(route.provider, route.modelId) !==
        canonicalizeProviderModelId(params.provider, params.modelId))
  ) {
    throw new Error(
      `Prepared runtime auth route ${route.provider}/${route.modelId} does not match target ${params.provider}/${params.modelId}.`,
    );
  }
  const callerModelMatches =
    params.model !== undefined &&
    normalizeProviderId(params.model.provider ?? "") === normalizeProviderId(params.provider) &&
    canonicalizeProviderModelId(params.provider, params.model.id ?? "") ===
      canonicalizeProviderModelId(params.provider, params.modelId) &&
    (!route ||
      modelMatchesPreparedTarget({
        model: params.model,
        provider: params.provider,
        modelId: params.modelId,
        route,
      }));
  if (callerModelMatches && !params.forceResolve) {
    return validateFinalModel(params.model);
  }
  if (params.model && !callerModelMatches && params.rejectMismatchedModel) {
    throw new Error(
      route
        ? `Caller-provided ${params.provider}/${params.modelId} metadata does not match its prepared ${route.authRequirement} route.`
        : `Caller-provided model metadata does not match ${params.provider}/${params.modelId}.`,
    );
  }

  const resolved = await params.resolveModel({
    config: config ?? {},
    authProfileId: params.plan.forwardedAuthProfileId,
    authProfileMode: route
      ? resolveProviderModelRouteMaterializationAuthMode({
          mode: params.plan.selectedAuthMode,
          requirement: route.authRequirement,
        })
      : resolveProviderModelMaterializationAuthMode(params.plan.selectedAuthMode),
  });
  if (
    !resolved.model ||
    normalizeProviderId(resolved.model.provider ?? "") !== normalizeProviderId(params.provider) ||
    canonicalizeProviderModelId(params.provider, resolved.model.id ?? "") !==
      canonicalizeProviderModelId(params.provider, params.modelId) ||
    (route &&
      !modelMatchesPreparedTarget({
        model: resolved.model,
        provider: params.provider,
        modelId: params.modelId,
        route,
      }))
  ) {
    throw new Error(
      resolved.error ??
        (route
          ? `Unable to materialize ${params.provider}/${params.modelId} for its prepared ${route.authRequirement} route.`
          : `Unable to rematerialize ${params.provider}/${params.modelId} for its resolved auth profile.`),
    );
  }
  // Validate only the final route: an API credential may replace retired subscription metadata.
  return validateFinalModel(resolved.model);
}