File size: 5,133 Bytes
eb3f11e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
/**
 * Provider/backend/credential eligibility for CLI-backend dispatch of
 * embedded runs. Shared by the dispatch itself and by callers that must
 * budget for CLI latency before starting the run (active-memory recall's
 * timeout default); both sides seeing one decision keeps timeouts and
 * routing from drifting apart. Kept separate from the dispatch module so
 * the plugin runtime surface does not eagerly load the run machinery.
 */
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { resolveRuntimeCliBackends } from "../../plugins/cli-backends.runtime.js";
import {
  ensureAuthProfileStore,
  resolveAuthProfileOrder,
  resolveModelAuthMode,
} from "../model-auth.js";
import { resolveCliRuntimeExecutionProvider } from "../model-runtime-aliases.js";

type EmbeddedCliBackendDispatchEligibilityParams = {
  provider?: string;
  model?: string;
  agentId?: string;
  /** Explicitly pinned auth profile for the run; decisive when it resolves. */
  authProfileId?: string;
  config?: OpenClawConfig;
  agentDir?: string;
  workspaceDir?: string;
};

/**
 * Decides whether an opted-in embedded run would execute through the CLI
 * backend. Resolution stays on stored credential metadata — no credential
 * materialization, refresh locks, or network calls on this per-turn path.
 */
export function resolveEmbeddedCliBackendDispatchEligibility(
  params: EmbeddedCliBackendDispatchEligibilityParams,
): { provider: string } | undefined {
  // Canonical refs (anthropic/<model> routed to claude-cli via agentRuntime
  // config) must dispatch the same as runs that arrive with the CLI runtime
  // provider id directly, so resolve the configured execution runtime before
  // gating.
  const backends = new Map(
    resolveRuntimeCliBackends("metadata").map((backend) => [
      normalizeProviderId(backend.id),
      backend,
    ]),
  );
  const requestedProvider = normalizeProviderId(params.provider ?? "");
  const provider = backends.has(requestedProvider)
    ? requestedProvider
    : normalizeProviderId(
        resolveCliRuntimeExecutionProvider({
          provider: params.provider ?? "",
          cfg: params.config,
          agentId: params.agentId,
          modelId: params.model,
          // A pinned profile can be what maps a canonical ref onto its CLI
          // runtime (e.g. multiple compatible profiles, no configured
          // agentRuntime); omitting it here would strand the run on the
          // passthrough before the credential-type check ever sees the pin.
          authProfileId: params.authProfileId,
        }) ?? "",
      );
  // The backend plugin owns the claim that its provider's direct-API
  // passthrough cannot run on subscription credentials. Providers whose
  // registered backend does not declare it — and config-only backends with
  // no plugin descriptor — keep the passthrough unchanged.
  if (!backends.get(provider)?.subscriptionAuthDispatch) {
    return undefined;
  }
  const authMode = resolveAuthModeSafe(params, provider);
  // An API key (also mixed/aws-sdk stores that include one) keeps the faster
  // direct passthrough working; only subscription (oauth/token) or
  // unresolvable credentials route through the CLI backend.
  if (authMode === "api-key" || authMode === "mixed" || authMode === "aws-sdk") {
    return undefined;
  }
  return { provider };
}

function resolveAuthModeSafe(
  params: {
    authProfileId?: string;
    config?: OpenClawConfig;
    agentDir?: string;
    workspaceDir?: string;
  },
  provider: string,
): ReturnType<typeof resolveModelAuthMode> {
  try {
    const store = ensureAuthProfileStore(params.agentDir, { config: params.config });
    // A run pinned to an explicit profile executes on that credential, so its
    // type decides regardless of the general order; an unresolvable pinned id
    // falls back to ordered selection like the passthrough does.
    const pinnedType = params.authProfileId
      ? store.profiles[params.authProfileId.trim()]?.type
      : undefined;
    // Mixed stores must follow the same ordered selection the passthrough
    // uses: the first ordered profile decides the mode. A store-wide
    // aggregate would read oauth+api_key as "mixed" and keep the passthrough
    // even when auth.order selects the subscription profile first.
    const [selectedProfileId] = resolveAuthProfileOrder({
      cfg: params.config,
      store,
      provider,
    });
    const selectedType =
      pinnedType ?? (selectedProfileId ? store.profiles[selectedProfileId]?.type : undefined);
    if (selectedType === "api_key") {
      return "api-key";
    }
    if (selectedType === "oauth" || selectedType === "token") {
      return selectedType;
    }
    return resolveModelAuthMode(provider, params.config, store, {
      workspaceDir: params.workspaceDir,
    });
  } catch {
    // Unreadable credential stores behave like missing credentials: the
    // passthrough cannot work either, so the CLI backend is still the
    // best-effort path.
    return undefined;
  }
}