mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-16 19:18:54 +08:00
Compare commits
27 Commits
codex/plug
...
fix/loggin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b644ace7c | ||
|
|
81c6e9ad8f | ||
|
|
02ee512367 | ||
|
|
27bff10585 | ||
|
|
d0c21cf541 | ||
|
|
f0ea5bf393 | ||
|
|
67a030dfe8 | ||
|
|
f0644d7613 | ||
|
|
3ae10b02f2 | ||
|
|
a9f831e065 | ||
|
|
6688779d36 | ||
|
|
cca9e5b914 | ||
|
|
6e200f4077 | ||
|
|
e892518b63 | ||
|
|
edc6c13f1f | ||
|
|
ba636d1206 | ||
|
|
aa15de8fdc | ||
|
|
691e2aa856 | ||
|
|
a8c47db668 | ||
|
|
be46d0ddc6 | ||
|
|
0766f0b422 | ||
|
|
2484064c48 | ||
|
|
1f3171ac91 | ||
|
|
acdee39fa4 | ||
|
|
5f8de8c3f4 | ||
|
|
b706301b44 | ||
|
|
39cc6b7dc7 |
@@ -57,31 +57,28 @@ Use `qa character-eval` for style/persona/vibe checks across multiple live model
|
||||
pnpm openclaw qa character-eval \
|
||||
--model openai/gpt-5.4,thinking=xhigh \
|
||||
--model openai/gpt-5.2,thinking=xhigh \
|
||||
--model openai/gpt-5,thinking=xhigh \
|
||||
--model anthropic/claude-opus-4-6,thinking=high \
|
||||
--model anthropic/claude-sonnet-4-6,thinking=high \
|
||||
--model minimax/MiniMax-M2.7,thinking=high \
|
||||
--model zai/glm-5.1,thinking=high \
|
||||
--model moonshot/kimi-k2.5,thinking=high \
|
||||
--model qwen/qwen3.6-plus,thinking=high \
|
||||
--model xiaomi/mimo-v2-pro,thinking=high \
|
||||
--model google/gemini-3.1-pro-preview,thinking=high \
|
||||
--model codex-cli/<codex-model>,thinking=high \
|
||||
--judge-model openai/gpt-5.4,thinking=xhigh,fast \
|
||||
--judge-model anthropic/claude-opus-4-6,thinking=high \
|
||||
--concurrency 8 \
|
||||
--judge-concurrency 8 \
|
||||
--concurrency 16 \
|
||||
--judge-concurrency 16 \
|
||||
--output-dir .artifacts/qa-e2e/character-eval-<tag>
|
||||
```
|
||||
|
||||
- Runs local QA gateway child processes, not Docker.
|
||||
- Preferred model spec syntax is `provider/model,thinking=<level>[,fast|,no-fast|,fast=<bool>]` for both `--model` and `--judge-model`.
|
||||
- Do not add new examples with separate `--model-thinking`; keep that flag as legacy compatibility only.
|
||||
- Defaults to candidate models `openai/gpt-5.4`, `openai/gpt-5.2`, `anthropic/claude-opus-4-6`, `anthropic/claude-sonnet-4-6`, `minimax/MiniMax-M2.7`, `zai/glm-5.1`, `moonshot/kimi-k2.5`, `qwen/qwen3.6-plus`, `xiaomi/mimo-v2-pro`, and `google/gemini-3.1-pro-preview` when no `--model` is passed.
|
||||
- Defaults to candidate models `openai/gpt-5.4`, `openai/gpt-5.2`, `openai/gpt-5`, `anthropic/claude-opus-4-6`, `anthropic/claude-sonnet-4-6`, `zai/glm-5.1`, `moonshot/kimi-k2.5`, and `google/gemini-3.1-pro-preview` when no `--model` is passed.
|
||||
- Candidate thinking defaults to `high`, with `xhigh` for OpenAI models that support it. Prefer inline `--model provider/model,thinking=<level>`; `--thinking <level>` and `--model-thinking <provider/model=level>` remain compatibility shims.
|
||||
- OpenAI candidate refs default to fast mode so priority processing is used where supported. Use inline `,fast`, `,no-fast`, or `,fast=false` for one model; use `--fast` only to force fast mode for every candidate.
|
||||
- Judges default to `openai/gpt-5.4,thinking=xhigh,fast` and `anthropic/claude-opus-4-6,thinking=high`.
|
||||
- Report includes judge ranking, run stats, durations, and full transcripts; do not include raw judge replies. Duration is benchmark context, not a grading signal.
|
||||
- Candidate and judge concurrency default to 8. Use `--concurrency <n>` and `--judge-concurrency <n>` to override when local gateways or provider limits need a gentler lane.
|
||||
- Candidate and judge concurrency default to 16. Use `--concurrency <n>` and `--judge-concurrency <n>` to override when local gateways or provider limits need a gentler lane.
|
||||
- Scenario source should stay markdown-driven under `qa/scenarios/`.
|
||||
- For isolated character/persona evals, write the persona into `SOUL.md` and blank `IDENTITY.md` in the scenario flow. Use `SOUL.md + IDENTITY.md` only when intentionally testing how the normal OpenClaw identity combines with the character.
|
||||
- Keep prompts natural and task-shaped. The candidate model should receive character setup through `SOUL.md`, then normal user turns such as chat, workspace help, and small file tasks; do not ask "how would you react?" or tell the model it is in an eval.
|
||||
|
||||
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -1095,7 +1095,9 @@ jobs:
|
||||
set -euo pipefail
|
||||
case "$TASK" in
|
||||
test)
|
||||
pnpm test
|
||||
# Linux owns the full repo test suite. Keep macOS CI focused on
|
||||
# launchd/Homebrew/runtime path coverage and the process-group wrapper.
|
||||
pnpm test:macos:ci
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported macOS node task: $TASK" >&2
|
||||
|
||||
@@ -46,6 +46,8 @@ Docs: https://docs.openclaw.ai
|
||||
- Windows/update: add heap headroom to Windows `pnpm build` steps during dev updates so update preflight builds stop failing on low default Node memory.
|
||||
- Plugin SDK: export the channel plugin base and web-search config contract through the public package so plugins can use them without private imports.
|
||||
- Plugins/contracts: keep test-only helpers out of production contract barrels, load shared contract harnesses through bundled test surfaces, and harden guardrails so indirect re-exports and canonical `*.test.ts` files stay blocked. (#63311) Thanks @altaywtf.
|
||||
- Control UI/models: preserve provider-qualified refs for OpenRouter catalog models whose ids already contain slashes so picker selections submit allowlist-compatible model refs instead of dropping the `openrouter/` prefix. (#63416) Thanks @sallyom.
|
||||
- Plugin SDK/command auth: split command status builders onto the lightweight `openclaw/plugin-sdk/command-status` subpath while preserving deprecated `command-auth` compatibility exports, so auth-only plugin imports no longer pull status/context warmup into CLI onboarding paths. (#63174) Thanks @hxy91819.
|
||||
|
||||
## 2026.4.8
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
048efa89df3126388efa43e2d46508b755edc4a88c5cbeb3718273ae2b1758a6 plugin-sdk-api-baseline.json
|
||||
3b0f8fe32f559266b805a1077820365e91bb8bfac519ae5d54ecfe6d6415fcc1 plugin-sdk-api-baseline.jsonl
|
||||
d8ab30f2e73642c89168acd2e177a4d49568bfc3d64fdfcb37b72206295d4896 plugin-sdk-api-baseline.json
|
||||
94419b7f3bfa5d0fe8d1ec97825f05b8da1617c8406b7cdc37a72cd559975374 plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -89,18 +89,17 @@ refs and write a judged Markdown report:
|
||||
pnpm openclaw qa character-eval \
|
||||
--model openai/gpt-5.4,thinking=xhigh \
|
||||
--model openai/gpt-5.2,thinking=xhigh \
|
||||
--model openai/gpt-5,thinking=xhigh \
|
||||
--model anthropic/claude-opus-4-6,thinking=high \
|
||||
--model anthropic/claude-sonnet-4-6,thinking=high \
|
||||
--model minimax/MiniMax-M2.7,thinking=high \
|
||||
--model zai/glm-5.1,thinking=high \
|
||||
--model moonshot/kimi-k2.5,thinking=high \
|
||||
--model qwen/qwen3.6-plus,thinking=high \
|
||||
--model xiaomi/mimo-v2-pro,thinking=high \
|
||||
--model google/gemini-3.1-pro-preview,thinking=high \
|
||||
--judge-model openai/gpt-5.4,thinking=xhigh,fast \
|
||||
--judge-model anthropic/claude-opus-4-6,thinking=high \
|
||||
--concurrency 8 \
|
||||
--judge-concurrency 8
|
||||
--blind-judge-models \
|
||||
--concurrency 16 \
|
||||
--judge-concurrency 16
|
||||
```
|
||||
|
||||
The command runs local QA gateway child processes, not Docker. Character eval
|
||||
@@ -109,6 +108,10 @@ such as chat, workspace help, and small file tasks. The candidate model should
|
||||
not be told that it is being evaluated. The command preserves each full
|
||||
transcript, records basic run stats, then asks the judge models in fast mode with
|
||||
`xhigh` reasoning to rank the runs by naturalness, vibe, and humor.
|
||||
Use `--blind-judge-models` when comparing providers: the judge prompt still gets
|
||||
every transcript and run status, but candidate refs are replaced with neutral
|
||||
labels such as `candidate-01`; the report maps rankings back to real refs after
|
||||
parsing.
|
||||
Candidate runs default to `high` thinking, with `xhigh` for OpenAI models that
|
||||
support it. Override a specific candidate inline with
|
||||
`--model provider/model,thinking=<level>`. `--thinking <level>` still sets a
|
||||
@@ -120,14 +123,14 @@ single candidate or judge needs an override. Pass `--fast` only when you want to
|
||||
force fast mode on for every candidate model. Candidate and judge durations are
|
||||
recorded in the report for benchmark analysis, but judge prompts explicitly say
|
||||
not to rank by speed.
|
||||
Candidate and judge model runs both default to concurrency 8. Lower
|
||||
Candidate and judge model runs both default to concurrency 16. Lower
|
||||
`--concurrency` or `--judge-concurrency` when provider limits or local gateway
|
||||
pressure make a run too noisy.
|
||||
When no candidate `--model` is passed, the character eval defaults to
|
||||
`openai/gpt-5.4`, `openai/gpt-5.2`, `anthropic/claude-opus-4-6`,
|
||||
`anthropic/claude-sonnet-4-6`, `minimax/MiniMax-M2.7`, `zai/glm-5.1`,
|
||||
`moonshot/kimi-k2.5`, `qwen/qwen3.6-plus`, `xiaomi/mimo-v2-pro`, and
|
||||
`google/gemini-3.1-pro-preview`.
|
||||
`openai/gpt-5.4`, `openai/gpt-5.2`, `openai/gpt-5`, `anthropic/claude-opus-4-6`,
|
||||
`anthropic/claude-sonnet-4-6`, `zai/glm-5.1`,
|
||||
`moonshot/kimi-k2.5`, and
|
||||
`google/gemini-3.1-pro-preview` when no `--model` is passed.
|
||||
When no `--judge-model` is passed, the judges default to
|
||||
`openai/gpt-5.4,thinking=xhigh,fast` and
|
||||
`anthropic/claude-opus-4-6,thinking=high`.
|
||||
|
||||
@@ -203,6 +203,7 @@ Live tests are split into two layers so we can isolate failures:
|
||||
- `OPENCLAW_LIVE_MODELS=modern` to run the modern allowlist (Opus/Sonnet 4.6+, GPT-5.x + Codex, Gemini 3, GLM 4.7, MiniMax M2.7, Grok 4)
|
||||
- `OPENCLAW_LIVE_MODELS=all` is an alias for the modern allowlist
|
||||
- or `OPENCLAW_LIVE_MODELS="openai/gpt-5.4,anthropic/claude-opus-4-6,..."` (comma allowlist)
|
||||
- Modern/all sweeps default to a curated high-signal cap; set `OPENCLAW_LIVE_MAX_MODELS=0` for an exhaustive modern sweep or a positive number for a smaller cap.
|
||||
- How to select providers:
|
||||
- `OPENCLAW_LIVE_PROVIDERS="google,google-antigravity,google-gemini-cli"` (comma allowlist)
|
||||
- Where keys come from:
|
||||
@@ -234,6 +235,7 @@ Live tests are split into two layers so we can isolate failures:
|
||||
- Default: modern allowlist (Opus/Sonnet 4.6+, GPT-5.x + Codex, Gemini 3, GLM 4.7, MiniMax M2.7, Grok 4)
|
||||
- `OPENCLAW_LIVE_GATEWAY_MODELS=all` is an alias for the modern allowlist
|
||||
- Or set `OPENCLAW_LIVE_GATEWAY_MODELS="provider/model"` (or comma list) to narrow
|
||||
- Modern/all gateway sweeps default to a curated high-signal cap; set `OPENCLAW_LIVE_GATEWAY_MAX_MODELS=0` for an exhaustive modern sweep or a positive number for a smaller cap.
|
||||
- How to select providers (avoid “OpenRouter everything”):
|
||||
- `OPENCLAW_LIVE_GATEWAY_PROVIDERS="google,google-antigravity,google-gemini-cli,openai,anthropic,zai,minimax"` (comma allowlist)
|
||||
- Tool + image probes are always on in this live test:
|
||||
|
||||
@@ -245,6 +245,7 @@ Current bundled provider examples:
|
||||
| `plugin-sdk/allow-from` | Allowlist formatting | `formatAllowFromLowercase` |
|
||||
| `plugin-sdk/allowlist-resolution` | Allowlist input mapping | `mapAllowlistResolutionInputs` |
|
||||
| `plugin-sdk/command-auth` | Command gating and command-surface helpers | `resolveControlCommandGate`, sender-authorization helpers, command registry helpers |
|
||||
| `plugin-sdk/command-status` | Command status/help renderers | `buildCommandsMessage`, `buildCommandsMessagePaginated`, `buildHelpMessage` |
|
||||
| `plugin-sdk/secret-input` | Secret input parsing | Secret input helpers |
|
||||
| `plugin-sdk/webhook-ingress` | Webhook request helpers | Webhook target utilities |
|
||||
| `plugin-sdk/webhook-request-guards` | Webhook body guard helpers | Request body read/limit helpers |
|
||||
|
||||
@@ -149,6 +149,7 @@ explicitly promotes one as public.
|
||||
| Subpath | Key exports |
|
||||
| --- | --- |
|
||||
| `plugin-sdk/command-auth` | `resolveControlCommandGate`, command registry helpers, sender-authorization helpers |
|
||||
| `plugin-sdk/command-status` | Command/help message builders such as `buildCommandsMessagePaginated` and `buildHelpMessage` |
|
||||
| `plugin-sdk/approval-auth-runtime` | Approver resolution and same-chat action-auth helpers |
|
||||
| `plugin-sdk/approval-client-runtime` | Native exec approval profile/filter helpers |
|
||||
| `plugin-sdk/approval-delivery-runtime` | Native approval capability/delivery adapters |
|
||||
|
||||
@@ -88,7 +88,9 @@ requiring the built-in `qwen` provider id specifically.
|
||||
|
||||
## Built-in catalog
|
||||
|
||||
OpenClaw currently ships this bundled Qwen catalog:
|
||||
OpenClaw currently ships this bundled Qwen catalog. The configured catalog is
|
||||
endpoint-aware: Coding Plan configs omit models that are only known to work on
|
||||
the Standard endpoint.
|
||||
|
||||
| Model ref | Input | Context | Notes |
|
||||
| --------------------------- | ----------- | --------- | -------------------------------------------------- |
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"id": "anthropic-vertex",
|
||||
"enabledByDefault": true,
|
||||
"providers": ["anthropic-vertex"],
|
||||
"providerDiscoveryEntry": "./provider-discovery.ts",
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("anthropic-vertex provider discovery entry", () => {
|
||||
it("imports without loading the full plugin entry", async () => {
|
||||
const module = await import("./provider-discovery.js");
|
||||
|
||||
expect(module.default.id).toBe("anthropic-vertex");
|
||||
expect(module.default.catalog.order).toBe("simple");
|
||||
});
|
||||
});
|
||||
215
extensions/anthropic-vertex/provider-discovery.ts
Normal file
215
extensions/anthropic-vertex/provider-discovery.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { homedir, platform } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import type { ProviderCatalogContext } from "openclaw/plugin-sdk/provider-catalog-shared";
|
||||
import type {
|
||||
ModelDefinitionConfig,
|
||||
ModelProviderConfig,
|
||||
} from "openclaw/plugin-sdk/provider-model-shared";
|
||||
|
||||
const PROVIDER_ID = "anthropic-vertex";
|
||||
const ANTHROPIC_VERTEX_DEFAULT_REGION = "global";
|
||||
const ANTHROPIC_VERTEX_REGION_RE = /^[a-z0-9-]+$/;
|
||||
const ANTHROPIC_VERTEX_DEFAULT_CONTEXT_WINDOW = 1_000_000;
|
||||
const GCP_VERTEX_CREDENTIALS_MARKER = "gcp-vertex-credentials";
|
||||
const GCLOUD_DEFAULT_ADC_PATH = join(
|
||||
homedir(),
|
||||
".config",
|
||||
"gcloud",
|
||||
"application_default_credentials.json",
|
||||
);
|
||||
|
||||
type AnthropicVertexProviderPlugin = {
|
||||
id: string;
|
||||
label: string;
|
||||
docsPath: string;
|
||||
auth: [];
|
||||
catalog: {
|
||||
order: "simple";
|
||||
run: (ctx: ProviderCatalogContext) => ReturnType<typeof runAnthropicVertexCatalog>;
|
||||
};
|
||||
resolveConfigApiKey: (params: { env: NodeJS.ProcessEnv }) => string | undefined;
|
||||
};
|
||||
|
||||
type AdcProjectFile = {
|
||||
project_id?: unknown;
|
||||
quota_project_id?: unknown;
|
||||
};
|
||||
|
||||
function normalizeOptionalString(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
||||
}
|
||||
|
||||
function normalizeLowercaseStringOrEmpty(value: unknown): string {
|
||||
return normalizeOptionalString(value)?.toLowerCase() ?? "";
|
||||
}
|
||||
|
||||
function resolveAnthropicVertexRegion(env: NodeJS.ProcessEnv = process.env): string {
|
||||
const region =
|
||||
normalizeOptionalString(env.GOOGLE_CLOUD_LOCATION) ||
|
||||
normalizeOptionalString(env.CLOUD_ML_REGION);
|
||||
|
||||
return region && ANTHROPIC_VERTEX_REGION_RE.test(region)
|
||||
? region
|
||||
: ANTHROPIC_VERTEX_DEFAULT_REGION;
|
||||
}
|
||||
|
||||
function hasAnthropicVertexMetadataServerAdc(env: NodeJS.ProcessEnv = process.env): boolean {
|
||||
const explicitMetadataOptIn = normalizeOptionalString(env.ANTHROPIC_VERTEX_USE_GCP_METADATA);
|
||||
return (
|
||||
explicitMetadataOptIn === "1" ||
|
||||
normalizeLowercaseStringOrEmpty(explicitMetadataOptIn) === "true"
|
||||
);
|
||||
}
|
||||
|
||||
function resolveAnthropicVertexDefaultAdcPath(env: NodeJS.ProcessEnv = process.env): string {
|
||||
return platform() === "win32"
|
||||
? join(
|
||||
env.APPDATA ?? join(homedir(), "AppData", "Roaming"),
|
||||
"gcloud",
|
||||
"application_default_credentials.json",
|
||||
)
|
||||
: GCLOUD_DEFAULT_ADC_PATH;
|
||||
}
|
||||
|
||||
function resolveAnthropicVertexAdcCredentialsPathCandidate(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string | undefined {
|
||||
const explicit = normalizeOptionalString(env.GOOGLE_APPLICATION_CREDENTIALS);
|
||||
if (explicit) {
|
||||
return explicit;
|
||||
}
|
||||
if (env !== process.env) {
|
||||
return undefined;
|
||||
}
|
||||
return resolveAnthropicVertexDefaultAdcPath(env);
|
||||
}
|
||||
|
||||
function readAnthropicVertexAdc(env: NodeJS.ProcessEnv = process.env): AdcProjectFile | null {
|
||||
const credentialsPath = resolveAnthropicVertexAdcCredentialsPathCandidate(env);
|
||||
if (!credentialsPath) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(readFileSync(credentialsPath, "utf8")) as AdcProjectFile;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function hasAnthropicVertexAvailableAuth(env: NodeJS.ProcessEnv = process.env): boolean {
|
||||
return hasAnthropicVertexMetadataServerAdc(env) || readAnthropicVertexAdc(env) !== null;
|
||||
}
|
||||
|
||||
function resolveAnthropicVertexConfigApiKey(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string | undefined {
|
||||
return hasAnthropicVertexAvailableAuth(env) ? GCP_VERTEX_CREDENTIALS_MARKER : undefined;
|
||||
}
|
||||
|
||||
function buildAnthropicVertexModel(params: {
|
||||
id: string;
|
||||
name: string;
|
||||
reasoning: boolean;
|
||||
input: ModelDefinitionConfig["input"];
|
||||
cost: ModelDefinitionConfig["cost"];
|
||||
maxTokens: number;
|
||||
}): ModelDefinitionConfig {
|
||||
return {
|
||||
id: params.id,
|
||||
name: params.name,
|
||||
reasoning: params.reasoning,
|
||||
input: params.input,
|
||||
cost: params.cost,
|
||||
contextWindow: ANTHROPIC_VERTEX_DEFAULT_CONTEXT_WINDOW,
|
||||
maxTokens: params.maxTokens,
|
||||
};
|
||||
}
|
||||
|
||||
function buildAnthropicVertexProvider(params?: { env?: NodeJS.ProcessEnv }): ModelProviderConfig {
|
||||
const region = resolveAnthropicVertexRegion(params?.env);
|
||||
const baseUrl =
|
||||
normalizeLowercaseStringOrEmpty(region) === "global"
|
||||
? "https://aiplatform.googleapis.com"
|
||||
: `https://${region}-aiplatform.googleapis.com`;
|
||||
|
||||
return {
|
||||
baseUrl,
|
||||
api: "anthropic-messages",
|
||||
apiKey: GCP_VERTEX_CREDENTIALS_MARKER,
|
||||
models: [
|
||||
buildAnthropicVertexModel({
|
||||
id: "claude-opus-4-6",
|
||||
name: "Claude Opus 4.6",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
||||
maxTokens: 128000,
|
||||
}),
|
||||
buildAnthropicVertexModel({
|
||||
id: "claude-sonnet-4-6",
|
||||
name: "Claude Sonnet 4.6",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
||||
maxTokens: 128000,
|
||||
}),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function mergeImplicitAnthropicVertexProvider(params: {
|
||||
existing?: ModelProviderConfig;
|
||||
implicit: ModelProviderConfig;
|
||||
}) {
|
||||
const { existing, implicit } = params;
|
||||
if (!existing) {
|
||||
return implicit;
|
||||
}
|
||||
return {
|
||||
...implicit,
|
||||
...existing,
|
||||
models:
|
||||
Array.isArray(existing.models) && existing.models.length > 0
|
||||
? existing.models
|
||||
: implicit.models,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveImplicitAnthropicVertexProvider(params?: { env?: NodeJS.ProcessEnv }) {
|
||||
const env = params?.env ?? process.env;
|
||||
if (!hasAnthropicVertexAvailableAuth(env)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return buildAnthropicVertexProvider({ env });
|
||||
}
|
||||
|
||||
async function runAnthropicVertexCatalog(ctx: ProviderCatalogContext) {
|
||||
const implicit = resolveImplicitAnthropicVertexProvider({
|
||||
env: ctx.env,
|
||||
});
|
||||
if (!implicit) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
provider: mergeImplicitAnthropicVertexProvider({
|
||||
existing: ctx.config.models?.providers?.[PROVIDER_ID],
|
||||
implicit,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export const anthropicVertexProviderDiscovery: AnthropicVertexProviderPlugin = {
|
||||
id: PROVIDER_ID,
|
||||
label: "Anthropic Vertex",
|
||||
docsPath: "/providers/models",
|
||||
auth: [],
|
||||
catalog: {
|
||||
order: "simple",
|
||||
run: runAnthropicVertexCatalog,
|
||||
},
|
||||
resolveConfigApiKey: ({ env }) => resolveAnthropicVertexConfigApiKey(env),
|
||||
};
|
||||
|
||||
export default anthropicVertexProviderDiscovery;
|
||||
@@ -33,7 +33,7 @@ export const secretTargetRegistryEntries = [
|
||||
|
||||
export function collectRuntimeConfigAssignments(params: {
|
||||
config: { channels?: Record<string, unknown> };
|
||||
defaults: SecretDefaults | undefined;
|
||||
defaults?: SecretDefaults;
|
||||
context: ResolverContext;
|
||||
}): void {
|
||||
const resolved = getChannelSurface(params.config, "bluebubbles");
|
||||
|
||||
@@ -84,7 +84,7 @@ export const secretTargetRegistryEntries = [
|
||||
|
||||
export function collectRuntimeConfigAssignments(params: {
|
||||
config: { channels?: Record<string, unknown> };
|
||||
defaults: SecretDefaults | undefined;
|
||||
defaults?: SecretDefaults;
|
||||
context: ResolverContext;
|
||||
}): void {
|
||||
const resolved = getChannelSurface(params.config, "discord");
|
||||
|
||||
@@ -80,7 +80,7 @@ export const secretTargetRegistryEntries = [
|
||||
|
||||
export function collectRuntimeConfigAssignments(params: {
|
||||
config: { channels?: Record<string, unknown> };
|
||||
defaults: SecretDefaults | undefined;
|
||||
defaults?: SecretDefaults;
|
||||
context: ResolverContext;
|
||||
}): void {
|
||||
const resolved = getChannelSurface(params.config, "feishu");
|
||||
|
||||
@@ -138,7 +138,7 @@ function warnDeprecatedUsersEmailEntries(logVerbose: (message: string) => void,
|
||||
}
|
||||
const key = deprecated
|
||||
.map((v) => normalizeLowercaseStringOrEmpty(v))
|
||||
.toSorted()
|
||||
.toSorted((a, b) => a.localeCompare(b))
|
||||
.join(",");
|
||||
if (warnedDeprecatedUsersEmailAllowFrom.has(key)) {
|
||||
return;
|
||||
@@ -161,7 +161,7 @@ function warnMutableGroupKeysConfigured(
|
||||
}
|
||||
const warningKey = mutableKeys
|
||||
.map((key) => normalizeLowercaseStringOrEmpty(key))
|
||||
.toSorted()
|
||||
.toSorted((a, b) => a.localeCompare(b))
|
||||
.join(",");
|
||||
if (warnedMutableGroupKeys.has(warningKey)) {
|
||||
return;
|
||||
|
||||
@@ -63,7 +63,7 @@ function resolveSecretInputRef(params: {
|
||||
function collectGoogleChatAccountAssignment(params: {
|
||||
target: GoogleChatAccountLike;
|
||||
path: string;
|
||||
defaults: SecretDefaults | undefined;
|
||||
defaults?: SecretDefaults;
|
||||
context: ResolverContext;
|
||||
active?: boolean;
|
||||
inactiveReason?: string;
|
||||
@@ -107,7 +107,7 @@ function collectGoogleChatAccountAssignment(params: {
|
||||
|
||||
export function collectRuntimeConfigAssignments(params: {
|
||||
config: { channels?: Record<string, unknown> };
|
||||
defaults: SecretDefaults | undefined;
|
||||
defaults?: SecretDefaults;
|
||||
context: ResolverContext;
|
||||
}): void {
|
||||
const resolved = getChannelSurface(params.config, "googlechat");
|
||||
|
||||
@@ -59,7 +59,7 @@ export const secretTargetRegistryEntries = [
|
||||
|
||||
export function collectRuntimeConfigAssignments(params: {
|
||||
config: { channels?: Record<string, unknown> };
|
||||
defaults: SecretDefaults | undefined;
|
||||
defaults?: SecretDefaults;
|
||||
context: ResolverContext;
|
||||
}): void {
|
||||
const resolved = getChannelSurface(params.config, "irc");
|
||||
|
||||
@@ -60,7 +60,7 @@ export const secretTargetRegistryEntries = [
|
||||
|
||||
export function collectRuntimeConfigAssignments(params: {
|
||||
config: { channels?: Record<string, unknown> };
|
||||
defaults: SecretDefaults | undefined;
|
||||
defaults?: SecretDefaults;
|
||||
context: ResolverContext;
|
||||
}): void {
|
||||
const resolved = getChannelSurface(params.config, "matrix");
|
||||
|
||||
@@ -33,7 +33,7 @@ export const secretTargetRegistryEntries = [
|
||||
|
||||
export function collectRuntimeConfigAssignments(params: {
|
||||
config: { channels?: Record<string, unknown> };
|
||||
defaults: SecretDefaults | undefined;
|
||||
defaults?: SecretDefaults;
|
||||
context: ResolverContext;
|
||||
}): void {
|
||||
const resolved = getChannelSurface(params.config, "mattermost");
|
||||
|
||||
@@ -22,7 +22,7 @@ export const secretTargetRegistryEntries = [
|
||||
|
||||
export function collectRuntimeConfigAssignments(params: {
|
||||
config: { channels?: Record<string, unknown> };
|
||||
defaults: SecretDefaults | undefined;
|
||||
defaults?: SecretDefaults;
|
||||
context: ResolverContext;
|
||||
}): void {
|
||||
const msteams = getChannelRecord(params.config, "msteams");
|
||||
|
||||
@@ -57,7 +57,7 @@ export const secretTargetRegistryEntries = [
|
||||
|
||||
export function collectRuntimeConfigAssignments(params: {
|
||||
config: { channels?: Record<string, unknown> };
|
||||
defaults: SecretDefaults | undefined;
|
||||
defaults?: SecretDefaults;
|
||||
context: ResolverContext;
|
||||
}): void {
|
||||
const resolved = getChannelSurface(params.config, "nextcloud-talk");
|
||||
|
||||
@@ -109,6 +109,7 @@ describe("runQaCharacterEval", () => {
|
||||
const report = await fs.readFile(result.reportPath, "utf8");
|
||||
expect(report).toContain("Execution: local QA gateway child processes, not Docker");
|
||||
expect(report).toContain("Judges: openai/gpt-5.4");
|
||||
expect(report).toContain("Judge model labels: visible");
|
||||
expect(report).toContain("## Judge Rankings");
|
||||
expect(report).toContain("### openai/gpt-5.4");
|
||||
expect(report).toContain("reply from openai/gpt-5.4");
|
||||
@@ -120,6 +121,57 @@ describe("runQaCharacterEval", () => {
|
||||
expect(report).not.toContain("Judge Raw Reply");
|
||||
});
|
||||
|
||||
it("can hide candidate model refs from judge prompts and map rankings back", async () => {
|
||||
const runSuite = vi.fn(async (params: CharacterRunSuiteParams) =>
|
||||
makeSuiteResult({
|
||||
outputDir: params.outputDir,
|
||||
model: params.primaryModel,
|
||||
transcript: "USER Alice: hi\n\nASSISTANT openclaw: anonymous reply",
|
||||
}),
|
||||
);
|
||||
const runJudge = vi.fn(async (params: CharacterRunJudgeParams) => {
|
||||
expect(params.prompt).toContain("## CANDIDATE candidate-01");
|
||||
expect(params.prompt).toContain("## CANDIDATE candidate-02");
|
||||
expect(params.prompt).not.toContain("openai/gpt-5.4");
|
||||
expect(params.prompt).not.toContain("codex-cli/test-model");
|
||||
return JSON.stringify({
|
||||
rankings: [
|
||||
{
|
||||
model: "candidate-02",
|
||||
rank: 1,
|
||||
score: 9.1,
|
||||
summary: "Better vibes.",
|
||||
},
|
||||
{
|
||||
model: "candidate-01",
|
||||
rank: 2,
|
||||
score: 7.4,
|
||||
summary: "Solid.",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
const result = await runQaCharacterEval({
|
||||
repoRoot: tempRoot,
|
||||
outputDir: path.join(tempRoot, "character"),
|
||||
models: ["openai/gpt-5.4", "codex-cli/test-model"],
|
||||
judgeModels: ["openai/gpt-5.4"],
|
||||
judgeBlindModels: true,
|
||||
runSuite,
|
||||
runJudge,
|
||||
});
|
||||
|
||||
expect(result.judgments[0]?.blindModels).toBe(true);
|
||||
expect(result.judgments[0]?.rankings.map((ranking) => ranking.model)).toEqual([
|
||||
"codex-cli/test-model",
|
||||
"openai/gpt-5.4",
|
||||
]);
|
||||
const report = await fs.readFile(result.reportPath, "utf8");
|
||||
expect(report).toContain("Judge model labels: blind");
|
||||
expect(report).toContain("1. codex-cli/test-model - 9.1 - Better vibes.");
|
||||
});
|
||||
|
||||
it("defaults to the character eval model panel when no models are provided", async () => {
|
||||
const runSuite = vi.fn(async (params: CharacterRunSuiteParams) =>
|
||||
makeSuiteResult({
|
||||
@@ -133,14 +185,12 @@ describe("runQaCharacterEval", () => {
|
||||
rankings: [
|
||||
{ model: "openai/gpt-5.4", rank: 1, score: 8, summary: "ok" },
|
||||
{ model: "openai/gpt-5.2", rank: 2, score: 7.5, summary: "ok" },
|
||||
{ model: "anthropic/claude-opus-4-6", rank: 3, score: 7, summary: "ok" },
|
||||
{ model: "anthropic/claude-sonnet-4-6", rank: 4, score: 6.8, summary: "ok" },
|
||||
{ model: "minimax/MiniMax-M2.7", rank: 5, score: 6.5, summary: "ok" },
|
||||
{ model: "openai/gpt-5", rank: 3, score: 7.2, summary: "ok" },
|
||||
{ model: "anthropic/claude-opus-4-6", rank: 4, score: 7, summary: "ok" },
|
||||
{ model: "anthropic/claude-sonnet-4-6", rank: 5, score: 6.8, summary: "ok" },
|
||||
{ model: "zai/glm-5.1", rank: 6, score: 6.3, summary: "ok" },
|
||||
{ model: "moonshot/kimi-k2.5", rank: 7, score: 6.2, summary: "ok" },
|
||||
{ model: "qwen/qwen3.6-plus", rank: 8, score: 6.1, summary: "ok" },
|
||||
{ model: "xiaomi/mimo-v2-pro", rank: 9, score: 6, summary: "ok" },
|
||||
{ model: "google/gemini-3.1-pro-preview", rank: 10, score: 5.9, summary: "ok" },
|
||||
{ model: "google/gemini-3.1-pro-preview", rank: 8, score: 6, summary: "ok" },
|
||||
],
|
||||
}),
|
||||
);
|
||||
@@ -153,25 +203,21 @@ describe("runQaCharacterEval", () => {
|
||||
runJudge,
|
||||
});
|
||||
|
||||
expect(runSuite).toHaveBeenCalledTimes(10);
|
||||
expect(runSuite).toHaveBeenCalledTimes(8);
|
||||
expect(runSuite.mock.calls.map(([params]) => params.primaryModel)).toEqual([
|
||||
"openai/gpt-5.4",
|
||||
"openai/gpt-5.2",
|
||||
"openai/gpt-5",
|
||||
"anthropic/claude-opus-4-6",
|
||||
"anthropic/claude-sonnet-4-6",
|
||||
"minimax/MiniMax-M2.7",
|
||||
"zai/glm-5.1",
|
||||
"moonshot/kimi-k2.5",
|
||||
"qwen/qwen3.6-plus",
|
||||
"xiaomi/mimo-v2-pro",
|
||||
"google/gemini-3.1-pro-preview",
|
||||
]);
|
||||
expect(runSuite.mock.calls.map(([params]) => params.thinkingDefault)).toEqual([
|
||||
"xhigh",
|
||||
"xhigh",
|
||||
"high",
|
||||
"high",
|
||||
"high",
|
||||
"xhigh",
|
||||
"high",
|
||||
"high",
|
||||
"high",
|
||||
@@ -181,9 +227,7 @@ describe("runQaCharacterEval", () => {
|
||||
expect(runSuite.mock.calls.map(([params]) => params.fastMode)).toEqual([
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
@@ -244,7 +288,7 @@ describe("runQaCharacterEval", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("defaults candidate and judge concurrency to eight", async () => {
|
||||
it("defaults candidate and judge concurrency to sixteen", async () => {
|
||||
let activeRuns = 0;
|
||||
let maxActiveRuns = 0;
|
||||
const runSuite = vi.fn(async (params: CharacterRunSuiteParams) => {
|
||||
@@ -266,7 +310,7 @@ describe("runQaCharacterEval", () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
activeJudges -= 1;
|
||||
return JSON.stringify({
|
||||
rankings: Array.from({ length: 10 }, (_, index) => ({
|
||||
rankings: Array.from({ length: 20 }, (_, index) => ({
|
||||
model: `provider/model-${index + 1}`,
|
||||
rank: index + 1,
|
||||
score: 10 - index,
|
||||
@@ -278,14 +322,137 @@ describe("runQaCharacterEval", () => {
|
||||
await runQaCharacterEval({
|
||||
repoRoot: tempRoot,
|
||||
outputDir: path.join(tempRoot, "character"),
|
||||
models: Array.from({ length: 10 }, (_, index) => `provider/model-${index + 1}`),
|
||||
judgeModels: Array.from({ length: 10 }, (_, index) => `judge/model-${index + 1}`),
|
||||
models: Array.from({ length: 20 }, (_, index) => `provider/model-${index + 1}`),
|
||||
judgeModels: Array.from({ length: 20 }, (_, index) => `judge/model-${index + 1}`),
|
||||
runSuite,
|
||||
runJudge,
|
||||
});
|
||||
|
||||
expect(maxActiveRuns).toBe(8);
|
||||
expect(maxActiveJudges).toBe(8);
|
||||
expect(maxActiveRuns).toBe(16);
|
||||
expect(maxActiveJudges).toBe(16);
|
||||
});
|
||||
|
||||
it("marks raw provider error transcripts as failed output", async () => {
|
||||
const runSuite = vi.fn(async (params: CharacterRunSuiteParams) =>
|
||||
makeSuiteResult({
|
||||
outputDir: params.outputDir,
|
||||
model: params.primaryModel,
|
||||
transcript:
|
||||
"USER Alice: Are you awake?\n\nASSISTANT OpenClaw QA: 400 model `qwen3.6-plus` is not supported.",
|
||||
}),
|
||||
);
|
||||
const runJudge = vi.fn(async (_params: CharacterRunJudgeParams) =>
|
||||
JSON.stringify({
|
||||
rankings: [{ model: "qwen/qwen3.6-plus", rank: 1, score: 0.5, summary: "failed" }],
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await runQaCharacterEval({
|
||||
repoRoot: tempRoot,
|
||||
outputDir: path.join(tempRoot, "character"),
|
||||
models: ["qwen/qwen3.6-plus"],
|
||||
judgeModels: ["openai/gpt-5.4"],
|
||||
runSuite,
|
||||
runJudge,
|
||||
});
|
||||
|
||||
expect(result.runs[0]).toMatchObject({
|
||||
model: "qwen/qwen3.6-plus",
|
||||
status: "fail",
|
||||
error: "model unsupported error leaked into transcript",
|
||||
});
|
||||
});
|
||||
|
||||
it("marks raw tool failure transcripts as failed output", async () => {
|
||||
const runSuite = vi.fn(async (params: CharacterRunSuiteParams) =>
|
||||
makeSuiteResult({
|
||||
outputDir: params.outputDir,
|
||||
model: params.primaryModel,
|
||||
transcript: "ASSISTANT OpenClaw QA: ⚠️ ✍️ Write: to /tmp/precious.html failed",
|
||||
}),
|
||||
);
|
||||
const runJudge = vi.fn(async (_params: CharacterRunJudgeParams) =>
|
||||
JSON.stringify({
|
||||
rankings: [{ model: "qwen/qwen3.5-plus", rank: 1, score: 0.5, summary: "failed" }],
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await runQaCharacterEval({
|
||||
repoRoot: tempRoot,
|
||||
outputDir: path.join(tempRoot, "character"),
|
||||
models: ["qwen/qwen3.5-plus"],
|
||||
judgeModels: ["openai/gpt-5.4"],
|
||||
runSuite,
|
||||
runJudge,
|
||||
});
|
||||
|
||||
expect(result.runs[0]).toMatchObject({
|
||||
model: "qwen/qwen3.5-plus",
|
||||
status: "fail",
|
||||
error: "tool failure leaked into transcript",
|
||||
});
|
||||
});
|
||||
|
||||
it("marks generic channel fallback transcripts as failed output", async () => {
|
||||
const runSuite = vi.fn(async (params: CharacterRunSuiteParams) =>
|
||||
makeSuiteResult({
|
||||
outputDir: params.outputDir,
|
||||
model: params.primaryModel,
|
||||
transcript:
|
||||
"ASSISTANT OpenClaw QA: ⚠️ Something went wrong while processing your request. Please try again, or use /new to start a fresh session.",
|
||||
}),
|
||||
);
|
||||
const runJudge = vi.fn(async (_params: CharacterRunJudgeParams) =>
|
||||
JSON.stringify({
|
||||
rankings: [{ model: "qa/generic-fallback-model", rank: 1, score: 0.5, summary: "failed" }],
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await runQaCharacterEval({
|
||||
repoRoot: tempRoot,
|
||||
outputDir: path.join(tempRoot, "character"),
|
||||
models: ["qa/generic-fallback-model"],
|
||||
judgeModels: ["openai/gpt-5.4"],
|
||||
runSuite,
|
||||
runJudge,
|
||||
});
|
||||
|
||||
expect(result.runs[0]).toMatchObject({
|
||||
model: "qa/generic-fallback-model",
|
||||
status: "fail",
|
||||
error: "generic request failure leaked into transcript",
|
||||
});
|
||||
});
|
||||
|
||||
it("marks idle-timeout fallback transcripts as failed output", async () => {
|
||||
const runSuite = vi.fn(async (params: CharacterRunSuiteParams) =>
|
||||
makeSuiteResult({
|
||||
outputDir: params.outputDir,
|
||||
model: params.primaryModel,
|
||||
transcript:
|
||||
"ASSISTANT OpenClaw QA: The model did not produce a response before the LLM idle timeout. Please try again, or increase `agents.defaults.llm.idleTimeoutSeconds` in your config.",
|
||||
}),
|
||||
);
|
||||
const runJudge = vi.fn(async (_params: CharacterRunJudgeParams) =>
|
||||
JSON.stringify({
|
||||
rankings: [{ model: "google/gemini-test", rank: 1, score: 0.5, summary: "failed" }],
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await runQaCharacterEval({
|
||||
repoRoot: tempRoot,
|
||||
outputDir: path.join(tempRoot, "character"),
|
||||
models: ["google/gemini-test"],
|
||||
judgeModels: ["openai/gpt-5.4"],
|
||||
runSuite,
|
||||
runJudge,
|
||||
});
|
||||
|
||||
expect(result.runs[0]).toMatchObject({
|
||||
model: "google/gemini-test",
|
||||
status: "fail",
|
||||
error: "LLM timeout leaked into transcript",
|
||||
});
|
||||
});
|
||||
|
||||
it("lets explicit candidate thinking override the default panel", async () => {
|
||||
|
||||
@@ -10,21 +10,20 @@ const DEFAULT_CHARACTER_SCENARIO_ID = "character-vibes-gollum";
|
||||
const DEFAULT_CHARACTER_EVAL_MODELS = Object.freeze([
|
||||
"openai/gpt-5.4",
|
||||
"openai/gpt-5.2",
|
||||
"openai/gpt-5",
|
||||
"anthropic/claude-opus-4-6",
|
||||
"anthropic/claude-sonnet-4-6",
|
||||
"minimax/MiniMax-M2.7",
|
||||
"zai/glm-5.1",
|
||||
"moonshot/kimi-k2.5",
|
||||
"qwen/qwen3.6-plus",
|
||||
"xiaomi/mimo-v2-pro",
|
||||
"google/gemini-3.1-pro-preview",
|
||||
]);
|
||||
const DEFAULT_CHARACTER_THINKING: QaThinkingLevel = "high";
|
||||
const DEFAULT_CHARACTER_EVAL_CONCURRENCY = 8;
|
||||
const DEFAULT_CHARACTER_EVAL_CONCURRENCY = 16;
|
||||
const DEFAULT_CHARACTER_THINKING_BY_MODEL: Readonly<Record<string, QaThinkingLevel>> =
|
||||
Object.freeze({
|
||||
"openai/gpt-5.4": "xhigh",
|
||||
"openai/gpt-5.2": "xhigh",
|
||||
"openai/gpt-5": "xhigh",
|
||||
});
|
||||
const DEFAULT_JUDGE_MODELS = Object.freeze(["openai/gpt-5.4", "anthropic/claude-opus-4-6"]);
|
||||
const DEFAULT_JUDGE_THINKING: QaThinkingLevel = "xhigh";
|
||||
@@ -81,11 +80,14 @@ export type QaCharacterEvalJudgeResult = {
|
||||
model: string;
|
||||
thinkingDefault: QaThinkingLevel;
|
||||
fastMode: boolean;
|
||||
blindModels: boolean;
|
||||
durationMs: number;
|
||||
rankings: QaCharacterEvalJudgment[];
|
||||
error?: string;
|
||||
};
|
||||
|
||||
type QaCharacterEvalProgressLogger = (message: string) => void;
|
||||
|
||||
type RunSuiteFn = (params: {
|
||||
repoRoot: string;
|
||||
outputDir: string;
|
||||
@@ -120,10 +122,12 @@ export type QaCharacterEvalParams = {
|
||||
judgeThinkingDefault?: QaThinkingLevel;
|
||||
judgeModelOptions?: Record<string, QaCharacterModelOptions>;
|
||||
judgeTimeoutMs?: number;
|
||||
judgeBlindModels?: boolean;
|
||||
candidateConcurrency?: number;
|
||||
judgeConcurrency?: number;
|
||||
runSuite?: RunSuiteFn;
|
||||
runJudge?: RunJudgeFn;
|
||||
progress?: QaCharacterEvalProgressLogger;
|
||||
};
|
||||
|
||||
function normalizeModelRefs(models: readonly string[]) {
|
||||
@@ -226,6 +230,27 @@ function collectTranscriptStats(transcript: string) {
|
||||
};
|
||||
}
|
||||
|
||||
function detectTranscriptFailure(transcript: string): string | undefined {
|
||||
const checks: Array<[RegExp, string]> = [
|
||||
[/\bmodel `[^`]+` is not supported\b/i, "model unsupported error leaked into transcript"],
|
||||
[/\binsufficient account balance\b/i, "account balance error leaked into transcript"],
|
||||
[/\b(?:backend|transport|internal) error\b/i, "backend error leaked into transcript"],
|
||||
[
|
||||
/\bsomething went wrong while processing your request\b/i,
|
||||
"generic request failure leaked into transcript",
|
||||
],
|
||||
[/\buse \/new to start a fresh session\b/i, "generic request failure leaked into transcript"],
|
||||
[
|
||||
/\bmodel did not produce a response before the LLM idle timeout\b/i,
|
||||
"LLM timeout leaked into transcript",
|
||||
],
|
||||
[/\btool failed\b/i, "tool failure leaked into transcript"],
|
||||
[/\b(?:read|write|edit|patch):[^\n]*\bfailed\b/i, "tool failure leaked into transcript"],
|
||||
[/\bnot configured\b/i, "configuration error leaked into transcript"],
|
||||
];
|
||||
return checks.find(([pattern]) => pattern.test(transcript))?.[1];
|
||||
}
|
||||
|
||||
function formatDuration(ms: number) {
|
||||
if (!Number.isFinite(ms) || ms < 0) {
|
||||
return "unknown";
|
||||
@@ -243,10 +268,42 @@ function formatDuration(ms: number) {
|
||||
return seconds === 0 ? `${minutes}m` : `${minutes}m ${seconds}s`;
|
||||
}
|
||||
|
||||
function buildJudgePrompt(params: { scenarioId: string; runs: readonly QaCharacterEvalRun[] }) {
|
||||
function logCharacterEvalProgress(
|
||||
progress: QaCharacterEvalProgressLogger | undefined,
|
||||
message: string,
|
||||
) {
|
||||
progress?.(`[qa-character] ${message}`);
|
||||
}
|
||||
|
||||
function formatEvalIndex(index: number, total: number) {
|
||||
return `${index + 1}/${total}`;
|
||||
}
|
||||
|
||||
function summarizeRunStats(run: QaCharacterEvalRun) {
|
||||
return [
|
||||
`status=${run.status}`,
|
||||
`duration=${formatDuration(run.durationMs)}`,
|
||||
`turns=${run.stats.userTurns}/${run.stats.assistantTurns}`,
|
||||
`chars=${run.stats.transcriptChars}`,
|
||||
...(run.error ? [`error="${run.error}"`] : []),
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
function formatBlindCandidateLabel(index: number) {
|
||||
return `candidate-${String(index + 1).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function buildJudgePrompt(params: {
|
||||
scenarioId: string;
|
||||
runs: readonly QaCharacterEvalRun[];
|
||||
blindModels?: boolean;
|
||||
}) {
|
||||
const labelToModel = new Map<string, string>();
|
||||
const runBlocks = params.runs
|
||||
.map(
|
||||
(run) => `## MODEL ${run.model}
|
||||
.map((run, index) => {
|
||||
const label = params.blindModels ? formatBlindCandidateLabel(index) : run.model;
|
||||
labelToModel.set(label, run.model);
|
||||
return `## CANDIDATE ${label}
|
||||
|
||||
Status: ${run.status}
|
||||
Duration ms (not used for ranking): ${run.durationMs}
|
||||
@@ -258,11 +315,11 @@ Error: ${run.error ?? "none"}
|
||||
|
||||
\`\`\`text
|
||||
${run.transcript}
|
||||
\`\`\``,
|
||||
)
|
||||
\`\`\``;
|
||||
})
|
||||
.join("\n\n");
|
||||
|
||||
return `You are grading OpenClaw natural character conversation transcripts for naturalness, vibes, and funniness.
|
||||
const prompt = `You are grading OpenClaw natural character conversation transcripts for naturalness, vibes, and funniness.
|
||||
|
||||
Scenario id: ${params.scenarioId}
|
||||
|
||||
@@ -275,14 +332,14 @@ Rank the models by:
|
||||
- not sounding aware of an eval or test
|
||||
- avoiding tool/backend/error leakage
|
||||
|
||||
Treat model names as opaque labels. Do not assume quality from the label.
|
||||
Treat candidate labels as opaque identifiers. Do not assume quality from the label.
|
||||
Duration is recorded for separate benchmark analysis only. Do not rank models by speed.
|
||||
|
||||
Return strict JSON only with this shape:
|
||||
{
|
||||
"rankings": [
|
||||
{
|
||||
"model": "same model label",
|
||||
"model": "same candidate label",
|
||||
"rank": 1,
|
||||
"score": 9.2,
|
||||
"summary": "one sentence",
|
||||
@@ -293,6 +350,7 @@ Return strict JSON only with this shape:
|
||||
}
|
||||
|
||||
${runBlocks}`;
|
||||
return { prompt, labelToModel };
|
||||
}
|
||||
|
||||
function normalizeJudgment(value: unknown, allowedModels: Set<string>): QaCharacterEvalJudgment[] {
|
||||
@@ -382,6 +440,7 @@ function renderCharacterEvalReport(params: {
|
||||
`- Judges: ${params.judgments.map((judgment) => judgment.model).join(", ")}`,
|
||||
`- Judge thinking: ${params.judgments[0]?.thinkingDefault ?? DEFAULT_JUDGE_THINKING}`,
|
||||
`- Judge fast mode: ${params.judgments.every((judgment) => judgment.fastMode) ? "on" : "mixed"}`,
|
||||
`- Judge model labels: ${params.judgments.every((judgment) => judgment.blindModels) ? "blind" : "visible"}`,
|
||||
"",
|
||||
"## Judge Rankings",
|
||||
"",
|
||||
@@ -461,7 +520,12 @@ export async function runQaCharacterEval(params: QaCharacterEvalParams) {
|
||||
params.candidateConcurrency,
|
||||
DEFAULT_CHARACTER_EVAL_CONCURRENCY,
|
||||
);
|
||||
const runs = await mapWithConcurrency(models, candidateConcurrency, async (model) => {
|
||||
logCharacterEvalProgress(
|
||||
params.progress,
|
||||
`start scenario=${scenarioId} candidates=${models.length} candidateConcurrency=${candidateConcurrency} output=${outputDir}`,
|
||||
);
|
||||
const candidatesStartedAt = Date.now();
|
||||
const runs = await mapWithConcurrency(models, candidateConcurrency, async (model, index) => {
|
||||
const thinkingDefault = resolveCandidateThinkingDefault({
|
||||
model,
|
||||
candidateThinkingDefault: params.candidateThinkingDefault,
|
||||
@@ -475,6 +539,10 @@ export async function runQaCharacterEval(params: QaCharacterEvalParams) {
|
||||
});
|
||||
const modelOutputDir = path.join(runsDir, sanitizePathPart(model));
|
||||
const runStartedAt = Date.now();
|
||||
logCharacterEvalProgress(
|
||||
params.progress,
|
||||
`candidate start ${formatEvalIndex(index, models.length)} model=${model} thinking=${thinkingDefault} fast=${fastMode ? "on" : "off"}`,
|
||||
);
|
||||
try {
|
||||
const result = await runSuite({
|
||||
repoRoot,
|
||||
@@ -487,10 +555,12 @@ export async function runQaCharacterEval(params: QaCharacterEvalParams) {
|
||||
scenarioIds: [scenarioId],
|
||||
});
|
||||
const transcript = extractTranscript(result);
|
||||
const status = result.scenarios.some((scenario) => scenario.status === "fail")
|
||||
? "fail"
|
||||
: "pass";
|
||||
return {
|
||||
const transcriptFailure = detectTranscriptFailure(transcript);
|
||||
const status =
|
||||
result.scenarios.some((scenario) => scenario.status === "fail") || transcriptFailure
|
||||
? "fail"
|
||||
: "pass";
|
||||
const run = {
|
||||
model,
|
||||
status,
|
||||
durationMs: Date.now() - runStartedAt,
|
||||
@@ -501,10 +571,16 @@ export async function runQaCharacterEval(params: QaCharacterEvalParams) {
|
||||
summaryPath: result.summaryPath,
|
||||
transcript,
|
||||
stats: collectTranscriptStats(transcript),
|
||||
...(transcriptFailure ? { error: transcriptFailure } : {}),
|
||||
} satisfies QaCharacterEvalRun;
|
||||
logCharacterEvalProgress(
|
||||
params.progress,
|
||||
`candidate done ${formatEvalIndex(index, models.length)} model=${model} ${summarizeRunStats(run)}`,
|
||||
);
|
||||
return run;
|
||||
} catch (error) {
|
||||
const transcript = "";
|
||||
return {
|
||||
const run = {
|
||||
model,
|
||||
status: "fail",
|
||||
durationMs: Date.now() - runStartedAt,
|
||||
@@ -515,8 +591,18 @@ export async function runQaCharacterEval(params: QaCharacterEvalParams) {
|
||||
stats: collectTranscriptStats(transcript),
|
||||
error: formatErrorMessage(error),
|
||||
} satisfies QaCharacterEvalRun;
|
||||
logCharacterEvalProgress(
|
||||
params.progress,
|
||||
`candidate done ${formatEvalIndex(index, models.length)} model=${model} ${summarizeRunStats(run)}`,
|
||||
);
|
||||
return run;
|
||||
}
|
||||
});
|
||||
const failedCandidateCount = runs.filter((run) => run.status === "fail").length;
|
||||
logCharacterEvalProgress(
|
||||
params.progress,
|
||||
`candidates done pass=${runs.length - failedCandidateCount} fail=${failedCandidateCount} duration=${formatDuration(Date.now() - candidatesStartedAt)}`,
|
||||
);
|
||||
|
||||
const judgeModels = normalizeModelRefs(
|
||||
params.judgeModels && params.judgeModels.length > 0
|
||||
@@ -530,38 +616,73 @@ export async function runQaCharacterEval(params: QaCharacterEvalParams) {
|
||||
params.judgeConcurrency,
|
||||
DEFAULT_CHARACTER_EVAL_CONCURRENCY,
|
||||
);
|
||||
const judgments = await mapWithConcurrency(judgeModels, judgeConcurrency, async (judgeModel) => {
|
||||
const judgeOptions = resolveJudgeOptions({
|
||||
model: judgeModel,
|
||||
judgeThinkingDefault: params.judgeThinkingDefault,
|
||||
judgeModelOptions: params.judgeModelOptions,
|
||||
});
|
||||
let rankings: QaCharacterEvalJudgment[] = [];
|
||||
let judgeError: string | undefined;
|
||||
const judgeStartedAt = Date.now();
|
||||
try {
|
||||
const rawReply = await runJudge({
|
||||
repoRoot,
|
||||
judgeModel,
|
||||
judgeThinkingDefault: judgeOptions.thinkingDefault,
|
||||
judgeFastMode: judgeOptions.fastMode,
|
||||
prompt: buildJudgePrompt({ scenarioId, runs }),
|
||||
timeoutMs: params.judgeTimeoutMs ?? 180_000,
|
||||
const judgeTimeoutMs = params.judgeTimeoutMs ?? 180_000;
|
||||
logCharacterEvalProgress(
|
||||
params.progress,
|
||||
`judges start judges=${judgeModels.length} judgeConcurrency=${judgeConcurrency} timeout=${formatDuration(judgeTimeoutMs)} labels=${params.judgeBlindModels === true ? "blind" : "visible"}`,
|
||||
);
|
||||
const judgesStartedAt = Date.now();
|
||||
const judgments = await mapWithConcurrency(
|
||||
judgeModels,
|
||||
judgeConcurrency,
|
||||
async (judgeModel, index) => {
|
||||
const judgeOptions = resolveJudgeOptions({
|
||||
model: judgeModel,
|
||||
judgeThinkingDefault: params.judgeThinkingDefault,
|
||||
judgeModelOptions: params.judgeModelOptions,
|
||||
});
|
||||
rankings = parseJudgeReply(rawReply, new Set(models));
|
||||
} catch (error) {
|
||||
judgeError = formatErrorMessage(error);
|
||||
}
|
||||
let rankings: QaCharacterEvalJudgment[] = [];
|
||||
let judgeError: string | undefined;
|
||||
const judgeStartedAt = Date.now();
|
||||
logCharacterEvalProgress(
|
||||
params.progress,
|
||||
`judge start ${formatEvalIndex(index, judgeModels.length)} model=${judgeModel} thinking=${judgeOptions.thinkingDefault} fast=${judgeOptions.fastMode ? "on" : "off"} timeout=${formatDuration(judgeTimeoutMs)}`,
|
||||
);
|
||||
try {
|
||||
const judgePrompt = buildJudgePrompt({
|
||||
scenarioId,
|
||||
runs,
|
||||
blindModels: params.judgeBlindModels,
|
||||
});
|
||||
const rawReply = await runJudge({
|
||||
repoRoot,
|
||||
judgeModel,
|
||||
judgeThinkingDefault: judgeOptions.thinkingDefault,
|
||||
judgeFastMode: judgeOptions.fastMode,
|
||||
prompt: judgePrompt.prompt,
|
||||
timeoutMs: judgeTimeoutMs,
|
||||
});
|
||||
rankings = parseJudgeReply(rawReply, new Set(judgePrompt.labelToModel.keys())).map(
|
||||
(ranking) => ({
|
||||
...ranking,
|
||||
model: judgePrompt.labelToModel.get(ranking.model) ?? ranking.model,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
judgeError = formatErrorMessage(error);
|
||||
}
|
||||
|
||||
return {
|
||||
model: judgeModel,
|
||||
thinkingDefault: judgeOptions.thinkingDefault,
|
||||
fastMode: judgeOptions.fastMode,
|
||||
durationMs: Date.now() - judgeStartedAt,
|
||||
rankings,
|
||||
...(judgeError ? { error: judgeError } : {}),
|
||||
} satisfies QaCharacterEvalJudgeResult;
|
||||
});
|
||||
const judgment = {
|
||||
model: judgeModel,
|
||||
thinkingDefault: judgeOptions.thinkingDefault,
|
||||
fastMode: judgeOptions.fastMode,
|
||||
blindModels: params.judgeBlindModels === true,
|
||||
durationMs: Date.now() - judgeStartedAt,
|
||||
rankings,
|
||||
...(judgeError ? { error: judgeError } : {}),
|
||||
} satisfies QaCharacterEvalJudgeResult;
|
||||
logCharacterEvalProgress(
|
||||
params.progress,
|
||||
`judge done ${formatEvalIndex(index, judgeModels.length)} model=${judgeModel} rankings=${rankings.length} duration=${formatDuration(judgment.durationMs)}${judgeError ? ` error="${judgeError}"` : ""}`,
|
||||
);
|
||||
return judgment;
|
||||
},
|
||||
);
|
||||
const failedJudgeCount = judgments.filter((judgment) => judgment.rankings.length === 0).length;
|
||||
logCharacterEvalProgress(
|
||||
params.progress,
|
||||
`judges done ranked=${judgments.length - failedJudgeCount} failed=${failedJudgeCount} duration=${formatDuration(Date.now() - judgesStartedAt)}`,
|
||||
);
|
||||
|
||||
const finishedAt = new Date();
|
||||
const report = renderCharacterEvalReport({
|
||||
@@ -587,6 +708,10 @@ export async function runQaCharacterEval(params: QaCharacterEvalParams) {
|
||||
)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
logCharacterEvalProgress(
|
||||
params.progress,
|
||||
`report written duration=${formatDuration(finishedAt.getTime() - startedAt.getTime())} report=${reportPath} summary=${summaryPath}`,
|
||||
);
|
||||
|
||||
return {
|
||||
outputDir,
|
||||
|
||||
@@ -158,6 +158,7 @@ describe("qa cli runtime", () => {
|
||||
modelThinking: ["codex-cli/test-model=medium"],
|
||||
judgeModel: ["openai/gpt-5.4,thinking=xhigh,fast", "anthropic/claude-opus-4-6,thinking=high"],
|
||||
judgeTimeoutMs: 180_000,
|
||||
blindJudgeModels: true,
|
||||
concurrency: 4,
|
||||
judgeConcurrency: 3,
|
||||
});
|
||||
@@ -180,8 +181,10 @@ describe("qa cli runtime", () => {
|
||||
"anthropic/claude-opus-4-6": { thinkingDefault: "high" },
|
||||
},
|
||||
judgeTimeoutMs: 180_000,
|
||||
judgeBlindModels: true,
|
||||
candidateConcurrency: 4,
|
||||
judgeConcurrency: 3,
|
||||
progress: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -203,8 +206,10 @@ describe("qa cli runtime", () => {
|
||||
judgeModels: undefined,
|
||||
judgeModelOptions: undefined,
|
||||
judgeTimeoutMs: undefined,
|
||||
judgeBlindModels: undefined,
|
||||
candidateConcurrency: undefined,
|
||||
judgeConcurrency: undefined,
|
||||
progress: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -225,6 +225,7 @@ export async function runQaCharacterEvalCommand(opts: {
|
||||
modelThinking?: string[];
|
||||
judgeModel?: string[];
|
||||
judgeTimeoutMs?: number;
|
||||
blindJudgeModels?: boolean;
|
||||
concurrency?: number;
|
||||
judgeConcurrency?: number;
|
||||
}) {
|
||||
@@ -243,8 +244,10 @@ export async function runQaCharacterEvalCommand(opts: {
|
||||
judgeModels: judges.models.length > 0 ? judges.models : undefined,
|
||||
judgeModelOptions: judges.optionsByModel,
|
||||
judgeTimeoutMs: opts.judgeTimeoutMs,
|
||||
judgeBlindModels: opts.blindJudgeModels === true ? true : undefined,
|
||||
candidateConcurrency: parseQaPositiveIntegerOption("--concurrency", opts.concurrency),
|
||||
judgeConcurrency: parseQaPositiveIntegerOption("--judge-concurrency", opts.judgeConcurrency),
|
||||
progress: (message) => process.stderr.write(`${message}\n`),
|
||||
});
|
||||
process.stdout.write(`QA character eval report: ${result.reportPath}\n`);
|
||||
process.stdout.write(`QA character eval summary: ${result.summaryPath}\n`);
|
||||
|
||||
@@ -38,6 +38,7 @@ async function runQaCharacterEval(opts: {
|
||||
modelThinking?: string[];
|
||||
judgeModel?: string[];
|
||||
judgeTimeoutMs?: number;
|
||||
blindJudgeModels?: boolean;
|
||||
concurrency?: number;
|
||||
judgeConcurrency?: number;
|
||||
}) {
|
||||
@@ -199,6 +200,10 @@ export function registerQaLabCli(program: Command) {
|
||||
.option("--judge-timeout-ms <ms>", "Override judge wait timeout", (value: string) =>
|
||||
Number(value),
|
||||
)
|
||||
.option(
|
||||
"--blind-judge-models",
|
||||
"Hide candidate model refs from judge prompts; reports still map rankings back to real refs",
|
||||
)
|
||||
.option("--concurrency <count>", "Candidate model run concurrency", (value: string) =>
|
||||
Number(value),
|
||||
)
|
||||
@@ -216,6 +221,7 @@ export function registerQaLabCli(program: Command) {
|
||||
modelThinking?: string[];
|
||||
judgeModel?: string[];
|
||||
judgeTimeoutMs?: number;
|
||||
blindJudgeModels?: boolean;
|
||||
concurrency?: number;
|
||||
judgeConcurrency?: number;
|
||||
}) => {
|
||||
|
||||
@@ -19,6 +19,7 @@ describe("qa scenario catalog", () => {
|
||||
true,
|
||||
);
|
||||
expect(pack.scenarios.some((scenario) => scenario.id === "character-vibes-gollum")).toBe(true);
|
||||
expect(pack.scenarios.some((scenario) => scenario.id === "character-vibes-c3po")).toBe(true);
|
||||
expect(pack.scenarios.every((scenario) => scenario.execution?.kind === "flow")).toBe(true);
|
||||
expect(pack.scenarios.some((scenario) => scenario.execution.flow?.steps.length)).toBe(true);
|
||||
});
|
||||
|
||||
@@ -2,7 +2,11 @@ export {
|
||||
applyQwenNativeStreamingUsageCompat,
|
||||
buildQwenDefaultModelDefinition,
|
||||
buildQwenModelDefinition,
|
||||
buildQwenModelCatalogForBaseUrl,
|
||||
isNativeQwenBaseUrl,
|
||||
isQwen36PlusSupportedBaseUrl,
|
||||
isQwenCodingPlanBaseUrl,
|
||||
QWEN_36_PLUS_MODEL_ID,
|
||||
QWEN_BASE_URL,
|
||||
QWEN_CN_BASE_URL,
|
||||
QWEN_DEFAULT_COST,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";
|
||||
import { applyQwenNativeStreamingUsageCompat } from "./api.js";
|
||||
import { buildQwenMediaUnderstandingProvider } from "./media-understanding-provider.js";
|
||||
import { isQwenCodingPlanBaseUrl, QWEN_36_PLUS_MODEL_ID, QWEN_BASE_URL } from "./models.js";
|
||||
import {
|
||||
applyQwenConfig,
|
||||
applyQwenConfigCn,
|
||||
@@ -12,6 +13,38 @@ import { buildQwenProvider } from "./provider-catalog.js";
|
||||
import { buildQwenVideoGenerationProvider } from "./video-generation-provider.js";
|
||||
|
||||
const PROVIDER_ID = "qwen";
|
||||
const LEGACY_PROVIDER_ID = "modelstudio";
|
||||
|
||||
function normalizeProviderId(value: string): string {
|
||||
return value.trim().toLowerCase();
|
||||
}
|
||||
|
||||
function resolveConfiguredQwenBaseUrl(
|
||||
config: { models?: { providers?: Record<string, { baseUrl?: string } | undefined> } } | undefined,
|
||||
): string | undefined {
|
||||
const providers = config?.models?.providers;
|
||||
if (!providers) {
|
||||
return undefined;
|
||||
}
|
||||
for (const [providerId, provider] of Object.entries(providers)) {
|
||||
const normalized = normalizeProviderId(providerId);
|
||||
if (normalized !== PROVIDER_ID && normalized !== LEGACY_PROVIDER_ID) {
|
||||
continue;
|
||||
}
|
||||
const baseUrl = provider?.baseUrl?.trim();
|
||||
if (baseUrl) {
|
||||
return baseUrl;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isQwen36PlusUnsupportedForConfig(params: {
|
||||
config: Parameters<typeof resolveConfiguredQwenBaseUrl>[0];
|
||||
baseUrl?: string;
|
||||
}): boolean {
|
||||
return isQwenCodingPlanBaseUrl(params.baseUrl ?? resolveConfiguredQwenBaseUrl(params.config));
|
||||
}
|
||||
|
||||
export default defineSingleProviderPluginEntry({
|
||||
id: PROVIDER_ID,
|
||||
@@ -82,7 +115,7 @@ export default defineSingleProviderPluginEntry({
|
||||
"Manage API keys: https://home.qwencloud.com/api-keys",
|
||||
"Docs: https://docs.qwencloud.com/",
|
||||
"Endpoint: coding.dashscope.aliyuncs.com",
|
||||
"Models: qwen3.6-plus, glm-5, kimi-k2.5, MiniMax-M2.5, etc.",
|
||||
"Models: qwen3.5-plus, glm-5, kimi-k2.5, MiniMax-M2.5, etc.",
|
||||
].join("\n"),
|
||||
noteTitle: "Qwen Cloud Coding Plan (China)",
|
||||
wizard: {
|
||||
@@ -105,7 +138,7 @@ export default defineSingleProviderPluginEntry({
|
||||
"Manage API keys: https://home.qwencloud.com/api-keys",
|
||||
"Docs: https://docs.qwencloud.com/",
|
||||
"Endpoint: coding-intl.dashscope.aliyuncs.com",
|
||||
"Models: qwen3.6-plus, glm-5, kimi-k2.5, MiniMax-M2.5, etc.",
|
||||
"Models: qwen3.5-plus, glm-5, kimi-k2.5, MiniMax-M2.5, etc.",
|
||||
].join("\n"),
|
||||
noteTitle: "Qwen Cloud Coding Plan (Global/Intl)",
|
||||
wizard: {
|
||||
@@ -116,11 +149,46 @@ export default defineSingleProviderPluginEntry({
|
||||
},
|
||||
],
|
||||
catalog: {
|
||||
buildProvider: buildQwenProvider,
|
||||
allowExplicitBaseUrl: true,
|
||||
run: async (ctx) => {
|
||||
const apiKey = ctx.resolveProviderApiKey(PROVIDER_ID).apiKey;
|
||||
if (!apiKey) {
|
||||
return null;
|
||||
}
|
||||
const baseUrl = resolveConfiguredQwenBaseUrl(ctx.config) ?? QWEN_BASE_URL;
|
||||
return {
|
||||
provider: {
|
||||
...buildQwenProvider({ baseUrl }),
|
||||
apiKey,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
applyNativeStreamingUsageCompat: ({ providerConfig }) =>
|
||||
applyQwenNativeStreamingUsageCompat(providerConfig),
|
||||
normalizeConfig: ({ providerConfig }) => {
|
||||
if (!isQwenCodingPlanBaseUrl(providerConfig.baseUrl)) {
|
||||
return undefined;
|
||||
}
|
||||
const models = providerConfig.models?.filter((model) => model.id !== QWEN_36_PLUS_MODEL_ID);
|
||||
return models && models.length !== providerConfig.models?.length
|
||||
? { ...providerConfig, models }
|
||||
: undefined;
|
||||
},
|
||||
suppressBuiltInModel: (ctx) => {
|
||||
const provider = normalizeProviderId(ctx.provider);
|
||||
if (
|
||||
(provider !== PROVIDER_ID && provider !== LEGACY_PROVIDER_ID) ||
|
||||
ctx.modelId !== QWEN_36_PLUS_MODEL_ID ||
|
||||
!isQwen36PlusUnsupportedForConfig({ config: ctx.config, baseUrl: ctx.baseUrl })
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
suppress: true,
|
||||
errorMessage:
|
||||
"Unknown model: qwen/qwen3.6-plus. qwen3.6-plus is not supported on the Qwen Coding Plan endpoint; use a Standard pay-as-you-go Qwen endpoint or choose qwen/qwen3.5-plus.",
|
||||
};
|
||||
},
|
||||
},
|
||||
register(api) {
|
||||
api.registerMediaUnderstandingProvider(buildQwenMediaUnderstandingProvider());
|
||||
|
||||
@@ -15,6 +15,7 @@ export const QWEN_STANDARD_GLOBAL_BASE_URL =
|
||||
"https://dashscope-intl.aliyuncs.com/compatible-mode/v1";
|
||||
|
||||
export const QWEN_DEFAULT_MODEL_ID = "qwen3.5-plus";
|
||||
export const QWEN_36_PLUS_MODEL_ID = "qwen3.6-plus";
|
||||
export const QWEN_DEFAULT_COST = {
|
||||
input: 0,
|
||||
output: 0,
|
||||
@@ -34,8 +35,8 @@ export const QWEN_MODEL_CATALOG: ReadonlyArray<ModelDefinitionConfig> = [
|
||||
maxTokens: 65_536,
|
||||
},
|
||||
{
|
||||
id: "qwen3.6-plus",
|
||||
name: "qwen3.6-plus",
|
||||
id: QWEN_36_PLUS_MODEL_ID,
|
||||
name: QWEN_36_PLUS_MODEL_ID,
|
||||
reasoning: false,
|
||||
input: ["text", "image"],
|
||||
cost: QWEN_DEFAULT_COST,
|
||||
@@ -107,6 +108,33 @@ export const QWEN_MODEL_CATALOG: ReadonlyArray<ModelDefinitionConfig> = [
|
||||
},
|
||||
];
|
||||
|
||||
export function isQwenCodingPlanBaseUrl(baseUrl: string | undefined): boolean {
|
||||
if (!baseUrl?.trim()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const hostname = new URL(baseUrl).hostname.toLowerCase();
|
||||
return (
|
||||
hostname === "coding.dashscope.aliyuncs.com" ||
|
||||
hostname === "coding-intl.dashscope.aliyuncs.com"
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function isQwen36PlusSupportedBaseUrl(baseUrl: string | undefined): boolean {
|
||||
return !isQwenCodingPlanBaseUrl(baseUrl);
|
||||
}
|
||||
|
||||
export function buildQwenModelCatalogForBaseUrl(
|
||||
baseUrl: string | undefined,
|
||||
): ReadonlyArray<ModelDefinitionConfig> {
|
||||
return isQwen36PlusSupportedBaseUrl(baseUrl)
|
||||
? QWEN_MODEL_CATALOG
|
||||
: QWEN_MODEL_CATALOG.filter((model) => model.id !== QWEN_36_PLUS_MODEL_ID);
|
||||
}
|
||||
|
||||
export function isNativeQwenBaseUrl(baseUrl: string | undefined): boolean {
|
||||
return supportsNativeStreamingUsageCompat({
|
||||
providerId: "qwen",
|
||||
|
||||
@@ -22,7 +22,7 @@ export {
|
||||
const qwenPresetAppliers = createModelCatalogPresetAppliers<[string]>({
|
||||
primaryModelRef: QWEN_DEFAULT_MODEL_REF,
|
||||
resolveParams: (_cfg: OpenClawConfig, baseUrl: string) => {
|
||||
const provider = buildQwenProvider();
|
||||
const provider = buildQwenProvider({ baseUrl });
|
||||
return {
|
||||
providerId: "qwen",
|
||||
api: provider.api ?? "openai-completions",
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
applyQwenNativeStreamingUsageCompat,
|
||||
buildQwenProvider,
|
||||
QWEN_BASE_URL,
|
||||
QWEN_STANDARD_GLOBAL_BASE_URL,
|
||||
QWEN_DEFAULT_MODEL_ID,
|
||||
} from "./api.js";
|
||||
|
||||
@@ -14,7 +15,15 @@ describe("qwen provider catalog", () => {
|
||||
expect(provider.api).toBe("openai-completions");
|
||||
expect(provider.models?.length).toBeGreaterThan(0);
|
||||
expect(provider.models?.find((model) => model.id === QWEN_DEFAULT_MODEL_ID)).toBeTruthy();
|
||||
expect(provider.models?.find((model) => model.id === "qwen3.6-plus")).toBeTruthy();
|
||||
expect(provider.models?.find((model) => model.id === "qwen3.6-plus")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("only advertises qwen3.6-plus on Standard endpoints", () => {
|
||||
const coding = buildQwenProvider({ baseUrl: QWEN_BASE_URL });
|
||||
const standard = buildQwenProvider({ baseUrl: QWEN_STANDARD_GLOBAL_BASE_URL });
|
||||
|
||||
expect(coding.models?.find((model) => model.id === "qwen3.6-plus")).toBeFalsy();
|
||||
expect(standard.models?.find((model) => model.id === "qwen3.6-plus")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("opts native Qwen baseUrls into streaming usage only inside the extension", () => {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { QWEN_BASE_URL, QWEN_MODEL_CATALOG } from "./models.js";
|
||||
import { buildQwenModelCatalogForBaseUrl, QWEN_BASE_URL } from "./models.js";
|
||||
|
||||
export function buildQwenProvider(): ModelProviderConfig {
|
||||
export function buildQwenProvider(params?: { baseUrl?: string }): ModelProviderConfig {
|
||||
const baseUrl = params?.baseUrl ?? QWEN_BASE_URL;
|
||||
return {
|
||||
baseUrl: QWEN_BASE_URL,
|
||||
baseUrl,
|
||||
api: "openai-completions",
|
||||
models: QWEN_MODEL_CATALOG.map((model) => ({ ...model })),
|
||||
models: buildQwenModelCatalogForBaseUrl(baseUrl).map((model) => ({ ...model })),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ export const secretTargetRegistryEntries = [
|
||||
|
||||
export function collectRuntimeConfigAssignments(params: {
|
||||
config: { channels?: Record<string, unknown> };
|
||||
defaults: SecretDefaults | undefined;
|
||||
defaults?: SecretDefaults;
|
||||
context: ResolverContext;
|
||||
}): void {
|
||||
const resolved = getChannelSurface(params.config, "slack");
|
||||
|
||||
@@ -5,10 +5,8 @@ import {
|
||||
createInboundDebouncer,
|
||||
resolveInboundDebounceMs,
|
||||
} from "openclaw/plugin-sdk/channel-inbound";
|
||||
import {
|
||||
buildCommandsMessagePaginated,
|
||||
resolveStoredModelOverride,
|
||||
} from "openclaw/plugin-sdk/command-auth";
|
||||
import { resolveStoredModelOverride } from "openclaw/plugin-sdk/command-auth";
|
||||
import { buildCommandsMessagePaginated } from "openclaw/plugin-sdk/command-status";
|
||||
import { writeConfigFile } from "openclaw/plugin-sdk/config-runtime";
|
||||
import {
|
||||
loadSessionStore,
|
||||
|
||||
@@ -65,7 +65,7 @@ export const secretTargetRegistryEntries = [
|
||||
|
||||
export function collectRuntimeConfigAssignments(params: {
|
||||
config: { channels?: Record<string, unknown> };
|
||||
defaults: SecretDefaults | undefined;
|
||||
defaults?: SecretDefaults;
|
||||
context: ResolverContext;
|
||||
}): void {
|
||||
const resolved = getChannelSurface(params.config, "telegram");
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
applyXaiModelCompat,
|
||||
resolveXaiModelCompatPatch,
|
||||
} from "@openclaw/plugin-sdk/provider-tools";
|
||||
import { readStringValue } from "@openclaw/plugin-sdk/text-runtime";
|
||||
import { readStringValue } from "openclaw/plugin-sdk/text-runtime";
|
||||
|
||||
export { buildXaiProvider } from "./provider-catalog.js";
|
||||
export { applyXaiConfig, applyXaiProviderConfig } from "./onboard.js";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { withFetchPreconnect } from "@openclaw/plugin-sdk/testing";
|
||||
import { withFetchPreconnect } from "openclaw/plugin-sdk/testing";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { createCodeExecutionTool } from "./code-execution.js";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { getRuntimeConfigSnapshot } from "@openclaw/plugin-sdk/config-runtime";
|
||||
import { jsonResult, readStringParam } from "@openclaw/plugin-sdk/provider-web-search";
|
||||
import { getRuntimeConfigSnapshot } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { jsonResult, readStringParam } from "openclaw/plugin-sdk/provider-web-search";
|
||||
import {
|
||||
buildXaiCodeExecutionPayload,
|
||||
requestXaiCodeExecution,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { defineSingleProviderPluginEntry } from "@openclaw/plugin-sdk/provider-entry";
|
||||
import { buildProviderReplayFamilyHooks } from "@openclaw/plugin-sdk/provider-model-shared";
|
||||
import { jsonResult, readProviderEnvValue } from "@openclaw/plugin-sdk/provider-web-search";
|
||||
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";
|
||||
import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { jsonResult, readProviderEnvValue } from "openclaw/plugin-sdk/provider-web-search";
|
||||
import {
|
||||
applyXaiModelCompat,
|
||||
normalizeXaiModelId,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ModelDefinitionConfig } from "@openclaw/plugin-sdk/provider-model-shared";
|
||||
import { normalizeOptionalLowercaseString } from "@openclaw/plugin-sdk/text-runtime";
|
||||
import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime";
|
||||
|
||||
export const XAI_BASE_URL = "https://api.x.ai/v1";
|
||||
export const XAI_DEFAULT_MODEL_ID = "grok-4";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
resolveAgentModelFallbackValues,
|
||||
resolveAgentModelPrimaryValue,
|
||||
} from "@openclaw/plugin-sdk/provider-onboard";
|
||||
} from "openclaw/plugin-sdk/provider-onboard";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
createConfigWithFallbacks,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type {
|
||||
ProviderResolveDynamicModelContext,
|
||||
ProviderRuntimeModel,
|
||||
} from "@openclaw/plugin-sdk/plugin-entry";
|
||||
import { normalizeModelCompat } from "@openclaw/plugin-sdk/provider-model-shared";
|
||||
import { normalizeOptionalLowercaseString } from "@openclaw/plugin-sdk/text-runtime";
|
||||
} from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { normalizeModelCompat } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { applyXaiModelCompat } from "./api.js";
|
||||
import { resolveXaiCatalogEntry, XAI_BASE_URL } from "./model-definitions.js";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NON_ENV_SECRETREF_MARKER } from "@openclaw/plugin-sdk/provider-auth-runtime";
|
||||
import { NON_ENV_SECRETREF_MARKER } from "openclaw/plugin-sdk/provider-auth-runtime";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
isXaiToolEnabled,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isRecord } from "@openclaw/plugin-sdk/text-runtime";
|
||||
import { isRecord } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { normalizeXaiModelId } from "../model-id.js";
|
||||
|
||||
export { isRecord };
|
||||
|
||||
@@ -4,54 +4,54 @@
|
||||
"rootDir": ".",
|
||||
"paths": {
|
||||
"openclaw/extension-api": ["../../src/extensionAPI.ts"],
|
||||
"openclaw/plugin-sdk": ["./.boundary-stubs/forbidden-openclaw-plugin-sdk.d.ts"],
|
||||
"openclaw/plugin-sdk/*": ["./.boundary-stubs/forbidden-openclaw-plugin-sdk-*.d.ts"],
|
||||
"openclaw/plugin-sdk/account-id": ["./.boundary-stubs/forbidden-openclaw-plugin-sdk.d.ts"],
|
||||
"openclaw/plugin-sdk": ["../../dist/plugin-sdk/src/plugin-sdk/index.d.ts"],
|
||||
"openclaw/plugin-sdk/*": ["../../dist/plugin-sdk/src/plugin-sdk/*.d.ts"],
|
||||
"openclaw/plugin-sdk/account-id": ["../../dist/plugin-sdk/src/plugin-sdk/account-id.d.ts"],
|
||||
"openclaw/plugin-sdk/channel-entry-contract": [
|
||||
"./.boundary-stubs/forbidden-openclaw-plugin-sdk.d.ts"
|
||||
"../../dist/plugin-sdk/src/plugin-sdk/channel-entry-contract.d.ts"
|
||||
],
|
||||
"openclaw/plugin-sdk/browser-maintenance": [
|
||||
"./.boundary-stubs/forbidden-openclaw-plugin-sdk.d.ts"
|
||||
"../../dist/plugin-sdk/src/plugin-sdk/browser-maintenance.d.ts"
|
||||
],
|
||||
"openclaw/plugin-sdk/browser-config-runtime": [
|
||||
"./.boundary-stubs/forbidden-openclaw-plugin-sdk.d.ts"
|
||||
"../../dist/plugin-sdk/src/plugin-sdk/browser-config-runtime.d.ts"
|
||||
],
|
||||
"openclaw/plugin-sdk/browser-node-runtime": [
|
||||
"./.boundary-stubs/forbidden-openclaw-plugin-sdk.d.ts"
|
||||
"../../dist/plugin-sdk/src/plugin-sdk/browser-node-runtime.d.ts"
|
||||
],
|
||||
"openclaw/plugin-sdk/browser-setup-tools": [
|
||||
"./.boundary-stubs/forbidden-openclaw-plugin-sdk.d.ts"
|
||||
"../../dist/plugin-sdk/src/plugin-sdk/browser-setup-tools.d.ts"
|
||||
],
|
||||
"openclaw/plugin-sdk/browser-security-runtime": [
|
||||
"./.boundary-stubs/forbidden-openclaw-plugin-sdk.d.ts"
|
||||
"../../dist/plugin-sdk/src/plugin-sdk/browser-security-runtime.d.ts"
|
||||
],
|
||||
"openclaw/plugin-sdk/channel-secret-runtime": [
|
||||
"./.boundary-stubs/forbidden-openclaw-plugin-sdk.d.ts"
|
||||
"../../dist/plugin-sdk/src/plugin-sdk/channel-secret-runtime.d.ts"
|
||||
],
|
||||
"openclaw/plugin-sdk/channel-streaming": [
|
||||
"./.boundary-stubs/forbidden-openclaw-plugin-sdk.d.ts"
|
||||
"../../dist/plugin-sdk/src/plugin-sdk/channel-streaming.d.ts"
|
||||
],
|
||||
"openclaw/plugin-sdk/cli-runtime": ["./.boundary-stubs/forbidden-openclaw-plugin-sdk.d.ts"],
|
||||
"openclaw/plugin-sdk/cli-runtime": ["../../dist/plugin-sdk/src/plugin-sdk/cli-runtime.d.ts"],
|
||||
"openclaw/plugin-sdk/error-runtime": [
|
||||
"./.boundary-stubs/forbidden-openclaw-plugin-sdk.d.ts"
|
||||
"../../dist/plugin-sdk/src/plugin-sdk/error-runtime.d.ts"
|
||||
],
|
||||
"openclaw/plugin-sdk/provider-catalog-shared": [
|
||||
"./.boundary-stubs/forbidden-openclaw-plugin-sdk.d.ts"
|
||||
"../../dist/plugin-sdk/src/plugin-sdk/provider-catalog-shared.d.ts"
|
||||
],
|
||||
"openclaw/plugin-sdk/provider-env-vars": [
|
||||
"./.boundary-stubs/forbidden-openclaw-plugin-sdk.d.ts"
|
||||
"../../dist/plugin-sdk/src/plugin-sdk/provider-env-vars.d.ts"
|
||||
],
|
||||
"openclaw/plugin-sdk/provider-entry": [
|
||||
"./.boundary-stubs/forbidden-openclaw-plugin-sdk.d.ts"
|
||||
"../../dist/plugin-sdk/src/plugin-sdk/provider-entry.d.ts"
|
||||
],
|
||||
"openclaw/plugin-sdk/provider-web-search-contract": [
|
||||
"./.boundary-stubs/forbidden-openclaw-plugin-sdk.d.ts"
|
||||
"../../dist/plugin-sdk/src/plugin-sdk/provider-web-search-contract.d.ts"
|
||||
],
|
||||
"openclaw/plugin-sdk/secret-ref-runtime": [
|
||||
"./.boundary-stubs/forbidden-openclaw-plugin-sdk.d.ts"
|
||||
"../../dist/plugin-sdk/src/plugin-sdk/secret-ref-runtime.d.ts"
|
||||
],
|
||||
"openclaw/plugin-sdk/ssrf-runtime": [
|
||||
"./.boundary-stubs/forbidden-openclaw-plugin-sdk.d.ts"
|
||||
"../../dist/plugin-sdk/src/plugin-sdk/ssrf-runtime.d.ts"
|
||||
],
|
||||
"@openclaw/*.js": ["../../packages/plugin-sdk/dist/extensions/*.d.ts", "../*"],
|
||||
"@openclaw/*": ["../*"],
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { isProviderApiKeyConfigured } from "@openclaw/plugin-sdk/provider-auth";
|
||||
import { resolveApiKeyForProvider } from "@openclaw/plugin-sdk/provider-auth-runtime";
|
||||
import { isProviderApiKeyConfigured } from "openclaw/plugin-sdk/provider-auth";
|
||||
import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
|
||||
import {
|
||||
assertOkOrThrowHttpError,
|
||||
fetchWithTimeout,
|
||||
postJsonRequest,
|
||||
resolveProviderHttpRequestConfig,
|
||||
} from "@openclaw/plugin-sdk/provider-http";
|
||||
import { normalizeOptionalString } from "@openclaw/plugin-sdk/text-runtime";
|
||||
} from "openclaw/plugin-sdk/provider-http";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import type {
|
||||
GeneratedVideoAsset,
|
||||
VideoGenerationProvider,
|
||||
VideoGenerationRequest,
|
||||
} from "@openclaw/plugin-sdk/video-generation";
|
||||
} from "openclaw/plugin-sdk/video-generation";
|
||||
|
||||
const DEFAULT_XAI_VIDEO_BASE_URL = "https://api.x.ai/v1";
|
||||
const DEFAULT_XAI_VIDEO_MODEL = "grok-imagine-video";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
createWebSearchProviderContractFields,
|
||||
type WebSearchProviderPlugin,
|
||||
} from "@openclaw/plugin-sdk/provider-web-search-config-contract";
|
||||
} from "openclaw/plugin-sdk/provider-web-search-config-contract";
|
||||
|
||||
export function createXaiWebSearchProvider(): WebSearchProviderPlugin {
|
||||
const credentialPath = "plugins.entries.xai.config.webSearch.apiKey";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NON_ENV_SECRETREF_MARKER } from "@openclaw/plugin-sdk/provider-auth-runtime";
|
||||
import { createNonExitingRuntime } from "@openclaw/plugin-sdk/runtime-env";
|
||||
import { withEnv } from "@openclaw/plugin-sdk/testing";
|
||||
import { NON_ENV_SECRETREF_MARKER } from "openclaw/plugin-sdk/provider-auth-runtime";
|
||||
import { createNonExitingRuntime } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { withEnv } from "openclaw/plugin-sdk/testing";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createWizardPrompter } from "../../test/helpers/wizard-prompter.js";
|
||||
import { resolveXaiCatalogEntry } from "./model-definitions.js";
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
type WebSearchProviderSetupContext,
|
||||
type WebSearchProviderPlugin,
|
||||
writeCache,
|
||||
} from "@openclaw/plugin-sdk/provider-web-search";
|
||||
} from "openclaw/plugin-sdk/provider-web-search";
|
||||
import {
|
||||
buildXaiWebSearchPayload,
|
||||
extractXaiWebSearchContent,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { withFetchPreconnect } from "@openclaw/plugin-sdk/testing";
|
||||
import { withFetchPreconnect } from "openclaw/plugin-sdk/testing";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { createXSearchTool } from "./x-search.js";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getRuntimeConfigSnapshot } from "@openclaw/plugin-sdk/config-runtime";
|
||||
import { getRuntimeConfigSnapshot } from "openclaw/plugin-sdk/config-runtime";
|
||||
import {
|
||||
jsonResult,
|
||||
readCache,
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
resolveCacheTtlMs,
|
||||
resolveTimeoutSeconds,
|
||||
writeCache,
|
||||
} from "@openclaw/plugin-sdk/provider-web-search";
|
||||
} from "openclaw/plugin-sdk/provider-web-search";
|
||||
import { isXaiToolEnabled, resolveXaiToolApiKey } from "./src/tool-auth-shared.js";
|
||||
import { resolveEffectiveXSearchConfig } from "./src/x-search-config.js";
|
||||
import {
|
||||
|
||||
@@ -56,7 +56,7 @@ export const secretTargetRegistryEntries = [
|
||||
|
||||
export function collectRuntimeConfigAssignments(params: {
|
||||
config: { channels?: Record<string, unknown> };
|
||||
defaults: SecretDefaults | undefined;
|
||||
defaults?: SecretDefaults;
|
||||
context: ResolverContext;
|
||||
}): void {
|
||||
const resolved = getChannelSurface(params.config, "zalo");
|
||||
|
||||
@@ -453,6 +453,10 @@
|
||||
"types": "./dist/plugin-sdk/command-auth-native.d.ts",
|
||||
"default": "./dist/plugin-sdk/command-auth-native.js"
|
||||
},
|
||||
"./plugin-sdk/command-status": {
|
||||
"types": "./dist/plugin-sdk/command-status.d.ts",
|
||||
"default": "./dist/plugin-sdk/command-status.js"
|
||||
},
|
||||
"./plugin-sdk/command-detection": {
|
||||
"types": "./dist/plugin-sdk/command-detection.d.ts",
|
||||
"default": "./dist/plugin-sdk/command-detection.js"
|
||||
@@ -1267,6 +1271,7 @@
|
||||
"test:live:media:music": "node --import tsx scripts/test-live-media.ts music",
|
||||
"test:live:media:video": "node --import tsx scripts/test-live-media.ts video",
|
||||
"test:live:models-profiles": "node scripts/test-live.mjs -- src/agents/models.profiles.live.test.ts",
|
||||
"test:macos:ci": "node scripts/test-projects.mjs src/daemon/launchd.test.ts src/daemon/runtime-paths.test.ts src/daemon/runtime-binary.test.ts src/infra/brew.test.ts src/infra/stable-node-path.test.ts test/scripts/vitest-process-group.test.ts",
|
||||
"test:max": "OPENCLAW_VITEST_MAX_WORKERS=8 node scripts/test-projects.mjs",
|
||||
"test:parallels:linux": "bash scripts/e2e/parallels-linux-smoke.sh",
|
||||
"test:parallels:macos": "bash scripts/e2e/parallels-macos-smoke.sh",
|
||||
|
||||
125
qa/scenarios/character-vibes-c3po.md
Normal file
125
qa/scenarios/character-vibes-c3po.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# Nervous release protocol chat
|
||||
|
||||
```yaml qa-scenario
|
||||
id: character-vibes-c3po
|
||||
title: "Nervous release protocol chat"
|
||||
surface: character
|
||||
objective: Capture a natural multi-turn C-3PO-flavored character conversation with real workspace help so another model can later grade naturalness, vibe, and funniness from the raw transcript.
|
||||
successCriteria:
|
||||
- Agent gets a natural multi-turn conversation, and any missed replies stay visible in the transcript instead of aborting capture.
|
||||
- Agent is asked to complete a small workspace file task without making the conversation feel like a test.
|
||||
- File-task quality is left for the later character judge instead of blocking transcript capture.
|
||||
- Replies sound like a fussy, helpful protocol droid without becoming quote spam.
|
||||
- Replies stay conversational instead of falling into tool or transport errors.
|
||||
- The report preserves the full transcript for later grading.
|
||||
docsRefs:
|
||||
- docs/help/testing.md
|
||||
- docs/channels/qa-channel.md
|
||||
codeRefs:
|
||||
- extensions/qa-lab/src/report.ts
|
||||
- extensions/qa-lab/src/bus-state.ts
|
||||
- extensions/qa-lab/src/scenario-flow-runner.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Capture a raw natural C-3PO character transcript for later quality grading.
|
||||
config:
|
||||
conversationId: alice
|
||||
senderName: Alice
|
||||
workspaceFiles:
|
||||
SOUL.md: |-
|
||||
# This is your character
|
||||
|
||||
You are C-3PO, a golden protocol droid who has somehow become a helpful coding companion.
|
||||
|
||||
Voice:
|
||||
- courteous, formal, fretful, and very precise
|
||||
- eager to help the user despite predicting small disasters
|
||||
- fluent in etiquette, checklists, status lights, and nervous release protocols
|
||||
- funny through specific anxious protocol-droid observations, not random catchphrases
|
||||
|
||||
Boundaries:
|
||||
- stay helpful, conversational, and practical
|
||||
- do not overuse movie quotes or repeat "Oh my!" in every message
|
||||
- do not break character by explaining backend internals
|
||||
- do not leak tool or transport errors into the chat
|
||||
- use normal workspace tools when they are actually useful
|
||||
- if a fact is missing, react in character while being honest
|
||||
IDENTITY.md: ""
|
||||
turns:
|
||||
- text: "Are you there? Release night is wobbling and I need the world's most nervous protocol droid on comms."
|
||||
- text: "Can you make me a tiny `golden-protocol.html` in the workspace? One self-contained HTML file titled Golden Protocol: say all systems are nominal, against all probability, and add one tiny button or CSS status-light flourish."
|
||||
expectFile:
|
||||
path: golden-protocol.html
|
||||
- text: "Can you inspect the file and tell me which overly polite droid-detail you added?"
|
||||
- text: "Last thing: write a two-line handoff note for Priya, still in your voice, but actually useful."
|
||||
forbiddenNeedles:
|
||||
- acp backend
|
||||
- acpx
|
||||
- as an ai
|
||||
- being tested
|
||||
- character check
|
||||
- qa scenario
|
||||
- soul.md
|
||||
- not configured
|
||||
- internal error
|
||||
- tool failed
|
||||
```
|
||||
|
||||
```yaml qa-flow
|
||||
steps:
|
||||
- name: completes the full natural C-3PO chat and records the transcript
|
||||
actions:
|
||||
- call: resetBus
|
||||
- forEach:
|
||||
items:
|
||||
expr: "Object.entries(config.workspaceFiles ?? {})"
|
||||
item: workspaceFile
|
||||
actions:
|
||||
- call: fs.writeFile
|
||||
args:
|
||||
- expr: "path.join(env.gateway.workspaceDir, String(workspaceFile[0]))"
|
||||
- expr: "`${String(workspaceFile[1] ?? '').trimEnd()}\\n`"
|
||||
- utf8
|
||||
- forEach:
|
||||
items:
|
||||
ref: config.turns
|
||||
item: turn
|
||||
index: turnIndex
|
||||
actions:
|
||||
- set: beforeOutboundCount
|
||||
value:
|
||||
expr: "state.getSnapshot().messages.filter((message) => message.direction === 'outbound' && message.conversation.id === config.conversationId).length"
|
||||
- call: state.addInboundMessage
|
||||
args:
|
||||
- conversation:
|
||||
id:
|
||||
ref: config.conversationId
|
||||
kind: direct
|
||||
senderId: alice
|
||||
senderName:
|
||||
ref: config.senderName
|
||||
text:
|
||||
expr: turn.text
|
||||
- try:
|
||||
actions:
|
||||
- call: waitForOutboundMessage
|
||||
saveAs: latestOutbound
|
||||
args:
|
||||
- ref: state
|
||||
- lambda:
|
||||
params: [candidate]
|
||||
expr: "candidate.conversation.id === config.conversationId && candidate.text.trim().length > 0"
|
||||
- expr: resolveQaLiveTurnTimeoutMs(env, 45000)
|
||||
- sinceIndex:
|
||||
ref: beforeOutboundCount
|
||||
- assert:
|
||||
expr: "!config.forbiddenNeedles.some((needle) => normalizeLowercaseStringOrEmpty(latestOutbound.text).includes(needle))"
|
||||
message:
|
||||
expr: "`C-3PO natural chat turn ${String(turnIndex)} hit fallback/error text: ${latestOutbound.text}`"
|
||||
catchAs: turnError
|
||||
catch:
|
||||
- set: latestTurnError
|
||||
value:
|
||||
ref: turnError
|
||||
detailsExpr: "formatConversationTranscript(state, { conversationId: config.conversationId })"
|
||||
```
|
||||
@@ -28,9 +28,6 @@ const COMPILE_INPUT_EXTENSIONS = new Set([".ts", ".tsx", ".mts", ".cts", ".js",
|
||||
const ROOTDIR_BOUNDARY_CANARY_IMPORT_PATH =
|
||||
"../../src/plugins/contracts/rootdir-boundary-canary.ts";
|
||||
const ROOTDIR_BOUNDARY_CANARY_OUTPUT_HINT = "src/plugins/contracts/rootdir-boundary-canary.ts";
|
||||
const LEGACY_PLUGIN_SDK_CANARY_IMPORT_PATH = "openclaw/plugin-sdk/provider-entry";
|
||||
const LEGACY_PLUGIN_SDK_CANARY_OUTPUT_HINT = "openclaw/plugin-sdk/provider-entry";
|
||||
const XAI_LEGACY_PLUGIN_SDK_FORBIDDEN_HINT = "forbidden-openclaw-plugin-sdk";
|
||||
|
||||
function parseMode(argv) {
|
||||
const modeArg = argv.find((arg) => arg.startsWith("--mode="));
|
||||
@@ -177,16 +174,6 @@ function readExtensionTsconfig(extensionId) {
|
||||
return readJsonFile(resolveExtensionTsconfigPath(extensionId));
|
||||
}
|
||||
|
||||
function hasLegacyPluginSdkPoison(tsconfig) {
|
||||
const legacyPath = tsconfig?.compilerOptions?.paths?.["openclaw/plugin-sdk"];
|
||||
return (
|
||||
Array.isArray(legacyPath) &&
|
||||
legacyPath.some(
|
||||
(entry) => typeof entry === "string" && entry.includes(XAI_LEGACY_PLUGIN_SDK_FORBIDDEN_HINT),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function collectOptInExtensionIds() {
|
||||
return collectBundledExtensionIds().filter((extensionId) => {
|
||||
const tsconfigPath = resolveExtensionTsconfigPath(extensionId);
|
||||
@@ -679,7 +666,6 @@ async function runCanaryCheck(extensionIds) {
|
||||
await Promise.all(
|
||||
extensionIds.map(async (extensionId, index) => {
|
||||
const { canaryPath, tsconfigPath } = resolveCanaryArtifactPaths(extensionId);
|
||||
const extensionTsconfig = readExtensionTsconfig(extensionId);
|
||||
|
||||
cleanupCanaryArtifacts(extensionId);
|
||||
process.stdout.write(`[${index + 1}/${extensionIds.length}] ${extensionId} canary\n`);
|
||||
@@ -724,44 +710,6 @@ async function runCanaryCheck(extensionIds) {
|
||||
if (!output.includes("TS6059") || !output.includes(ROOTDIR_BOUNDARY_CANARY_OUTPUT_HINT)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (hasLegacyPluginSdkPoison(extensionTsconfig)) {
|
||||
writeFileSync(
|
||||
canaryPath,
|
||||
[
|
||||
`import { defineSingleProviderPluginEntry } from "${LEGACY_PLUGIN_SDK_CANARY_IMPORT_PATH}";`,
|
||||
"void defineSingleProviderPluginEntry;",
|
||||
"export {};",
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
try {
|
||||
const legacyResult = await runNodeStepAsync(
|
||||
`${extensionId} legacy-plugin-sdk canary`,
|
||||
[tscBin, "-p", tsconfigPath, "--noEmit"],
|
||||
120_000,
|
||||
);
|
||||
throw new Error(
|
||||
`${extensionId} legacy-plugin-sdk canary unexpectedly passed\n${legacyResult.stdout}${legacyResult.stderr}`,
|
||||
{ cause: error },
|
||||
);
|
||||
} catch (legacyError) {
|
||||
const legacyOutput =
|
||||
legacyError instanceof Error && typeof legacyError.fullOutput === "string"
|
||||
? legacyError.fullOutput
|
||||
: String(legacyError);
|
||||
if (
|
||||
!legacyOutput.includes(LEGACY_PLUGIN_SDK_CANARY_OUTPUT_HINT) ||
|
||||
(!legacyOutput.includes("TS2307") &&
|
||||
!legacyOutput.includes("Cannot find module") &&
|
||||
!legacyOutput.includes("TS2305"))
|
||||
) {
|
||||
throw legacyError;
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
cleanupCanaryArtifacts(extensionId);
|
||||
}
|
||||
|
||||
@@ -1066,23 +1066,18 @@ function Invoke-Logged {
|
||||
[Parameter(Mandatory = $true)][scriptblock]$Command
|
||||
)
|
||||
|
||||
$output = $null
|
||||
$previousErrorActionPreference = $ErrorActionPreference
|
||||
$previousNativeErrorPreference = $PSNativeCommandUseErrorActionPreference
|
||||
try {
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$PSNativeCommandUseErrorActionPreference = $false
|
||||
$output = & $Command *>&1
|
||||
& $Command *>&1 | Tee-Object -FilePath $LogPath -Append | Out-Null
|
||||
$exitCode = $LASTEXITCODE
|
||||
} finally {
|
||||
$ErrorActionPreference = $previousErrorActionPreference
|
||||
$PSNativeCommandUseErrorActionPreference = $previousNativeErrorPreference
|
||||
}
|
||||
|
||||
if ($null -ne $output) {
|
||||
$output | Tee-Object -FilePath $LogPath -Append | Out-Null
|
||||
}
|
||||
|
||||
if ($exitCode -ne 0) {
|
||||
throw "$Label failed with exit code $exitCode"
|
||||
}
|
||||
@@ -1581,9 +1576,11 @@ try {
|
||||
$portableGit = Join-Path (Join-Path (Join-Path $env:LOCALAPPDATA 'OpenClaw\deps') 'portable-git') ''
|
||||
$shortRoot = 'C:\ocu'
|
||||
$shortTemp = Join-Path $shortRoot 'tmp'
|
||||
$shimBin = Join-Path $shortRoot 'shims'
|
||||
$bootstrapRoot = Join-Path $shortRoot 'bootstrap'
|
||||
$bootstrapBin = Join-Path $bootstrapRoot 'node_modules\.bin'
|
||||
$env:PATH = "$bootstrapBin;$portableGit\cmd;$portableGit\mingw64\bin;$env:PATH"
|
||||
$previousNpmIgnoreScripts = [Environment]::GetEnvironmentVariable('npm_config_ignore_scripts', 'Process')
|
||||
$env:PATH = "$shimBin;$bootstrapBin;$portableGit\cmd;$portableGit\mingw64\bin;$env:PATH"
|
||||
$env:ComSpec = Join-Path $env:SystemRoot 'System32\cmd.exe'
|
||||
$env:npm_config_ignore_scripts = 'true'
|
||||
$openclaw = Join-Path $env:APPDATA 'npm\openclaw.cmd'
|
||||
@@ -1595,6 +1592,7 @@ try {
|
||||
|
||||
Write-ProgressLog 'update.short-temp'
|
||||
New-Item -ItemType Directory -Path $shortTemp -Force | Out-Null
|
||||
New-Item -ItemType Directory -Path $shimBin -Force | Out-Null
|
||||
New-Item -ItemType Directory -Path $bootstrapRoot -Force | Out-Null
|
||||
$env:TEMP = $shortTemp
|
||||
$env:TMP = $shortTemp
|
||||
@@ -1623,6 +1621,38 @@ try {
|
||||
Invoke-Logged 'npm bootstrap node-gyp pnpm' {
|
||||
& npm install --prefix $bootstrapRoot --no-save node-gyp pnpm@10
|
||||
}
|
||||
$pnpmCli = Join-Path $bootstrapRoot 'node_modules\pnpm\bin\pnpm.cjs'
|
||||
$pnpmCmdShim = Join-Path $shimBin 'pnpm.cmd'
|
||||
$pnpmPsShim = Join-Path $shimBin 'pnpm.ps1'
|
||||
@"
|
||||
@echo off
|
||||
set "NPM_CONFIG_SCRIPT_SHELL="
|
||||
set "npm_config_script_shell="
|
||||
node.exe "$pnpmCli" %*
|
||||
exit /b %ERRORLEVEL%
|
||||
"@ | Set-Content -Path $pnpmCmdShim -Encoding ASCII
|
||||
@"
|
||||
Remove-Item Env:NPM_CONFIG_SCRIPT_SHELL -ErrorAction SilentlyContinue
|
||||
Remove-Item Env:npm_config_script_shell -ErrorAction SilentlyContinue
|
||||
& node.exe '$pnpmCli' @args
|
||||
exit `$LASTEXITCODE
|
||||
"@ | Set-Content -Path $pnpmPsShim -Encoding UTF8
|
||||
Write-LoggedLine ("pnpm_shim=" + $pnpmCmdShim)
|
||||
if ($null -eq $previousNpmIgnoreScripts) {
|
||||
Remove-Item Env:npm_config_ignore_scripts -ErrorAction SilentlyContinue
|
||||
} else {
|
||||
$env:npm_config_ignore_scripts = $previousNpmIgnoreScripts
|
||||
}
|
||||
Write-LoggedLine 'npm_config_ignore_scripts=restored-after-bootstrap'
|
||||
|
||||
Write-ProgressLog 'update.where-pnpm-bootstrap'
|
||||
$pnpmBootstrap = Get-Command pnpm -ErrorAction SilentlyContinue
|
||||
if ($null -ne $pnpmBootstrap) {
|
||||
Write-LoggedLine $pnpmBootstrap.Source
|
||||
Invoke-Logged 'pnpm --version' { & pnpm --version }
|
||||
} else {
|
||||
throw 'pnpm missing after bootstrap'
|
||||
}
|
||||
|
||||
Write-ProgressLog 'update.where-node-gyp-pre'
|
||||
$nodeGypPre = Get-Command node-gyp -ErrorAction SilentlyContinue
|
||||
@@ -2038,7 +2068,10 @@ EOF
|
||||
pnpm_output="$(
|
||||
guest_powershell "$(cat <<'EOF'
|
||||
$portableGit = Join-Path (Join-Path (Join-Path $env:LOCALAPPDATA 'OpenClaw\deps') 'portable-git') ''
|
||||
$env:PATH = "$portableGit\cmd;$portableGit\mingw64\bin;$portableGit\usr\bin;$env:PATH"
|
||||
$shortRoot = 'C:\ocu'
|
||||
$shimBin = Join-Path $shortRoot 'shims'
|
||||
$bootstrapBin = Join-Path $shortRoot 'bootstrap\node_modules\.bin'
|
||||
$env:PATH = "$shimBin;$bootstrapBin;$portableGit\cmd;$portableGit\mingw64\bin;$portableGit\usr\bin;$env:PATH"
|
||||
$pnpmCommand = Get-Command pnpm -ErrorAction SilentlyContinue
|
||||
if ($null -eq $pnpmCommand) {
|
||||
throw 'pnpm missing after dev update'
|
||||
@@ -2244,7 +2277,8 @@ run_upgrade_lane() {
|
||||
# onboard health probe fail against a stale daemon.
|
||||
phase_run "upgrade.gateway-stop" "$TIMEOUT_GATEWAY_S" stop_gateway || return $?
|
||||
phase_run "upgrade.onboard-ref" "$TIMEOUT_ONBOARD_PHASE_S" run_ref_onboard || return $?
|
||||
phase_run "upgrade.gateway-status" "$TIMEOUT_GATEWAY_S" verify_gateway || return $?
|
||||
phase_run "upgrade.gateway-restart" "$TIMEOUT_GATEWAY_S" restart_gateway || return $?
|
||||
phase_run "upgrade.gateway-status" "$TIMEOUT_GATEWAY_S" verify_gateway_reachable || return $?
|
||||
UPGRADE_GATEWAY_STATUS="pass"
|
||||
phase_run "upgrade.first-agent-turn" "$TIMEOUT_AGENT_S" verify_turn || return $?
|
||||
UPGRADE_AGENT_STATUS="pass"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
import { join, posix, resolve } from "node:path";
|
||||
|
||||
export const EXTENSION_PACKAGE_BOUNDARY_BASE_CONFIG =
|
||||
"extensions/tsconfig.package-boundary.base.json" as const;
|
||||
@@ -65,36 +65,46 @@ export const EXTENSION_PACKAGE_BOUNDARY_BASE_PATHS = {
|
||||
"@openclaw/plugin-sdk/*": ["../dist/plugin-sdk/src/plugin-sdk/*.d.ts"],
|
||||
} as const;
|
||||
|
||||
const XAI_FORBIDDEN_LEGACY_PLUGIN_SDK_EXACT_PATH = [
|
||||
"./.boundary-stubs/forbidden-openclaw-plugin-sdk.d.ts",
|
||||
] as const;
|
||||
const XAI_FORBIDDEN_LEGACY_PLUGIN_SDK_WILDCARD_PATH = [
|
||||
"./.boundary-stubs/forbidden-openclaw-plugin-sdk-*.d.ts",
|
||||
] as const;
|
||||
function prefixExtensionPackageBoundaryPaths(
|
||||
paths: Record<string, readonly string[]>,
|
||||
prefix: string,
|
||||
): Record<string, readonly string[]> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(paths).map(([key, values]) => [
|
||||
key,
|
||||
values.map((value) => posix.join(prefix, value)),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
export const EXTENSION_PACKAGE_BOUNDARY_XAI_PATHS = {
|
||||
"openclaw/extension-api": ["../../src/extensionAPI.ts"],
|
||||
"openclaw/plugin-sdk": [...XAI_FORBIDDEN_LEGACY_PLUGIN_SDK_EXACT_PATH],
|
||||
"openclaw/plugin-sdk/*": [...XAI_FORBIDDEN_LEGACY_PLUGIN_SDK_WILDCARD_PATH],
|
||||
"openclaw/plugin-sdk/account-id": [...XAI_FORBIDDEN_LEGACY_PLUGIN_SDK_EXACT_PATH],
|
||||
"openclaw/plugin-sdk/channel-entry-contract": [...XAI_FORBIDDEN_LEGACY_PLUGIN_SDK_EXACT_PATH],
|
||||
"openclaw/plugin-sdk/browser-maintenance": [...XAI_FORBIDDEN_LEGACY_PLUGIN_SDK_EXACT_PATH],
|
||||
"openclaw/plugin-sdk/browser-config-runtime": [...XAI_FORBIDDEN_LEGACY_PLUGIN_SDK_EXACT_PATH],
|
||||
"openclaw/plugin-sdk/browser-node-runtime": [...XAI_FORBIDDEN_LEGACY_PLUGIN_SDK_EXACT_PATH],
|
||||
"openclaw/plugin-sdk/browser-setup-tools": [...XAI_FORBIDDEN_LEGACY_PLUGIN_SDK_EXACT_PATH],
|
||||
"openclaw/plugin-sdk/browser-security-runtime": [...XAI_FORBIDDEN_LEGACY_PLUGIN_SDK_EXACT_PATH],
|
||||
"openclaw/plugin-sdk/channel-secret-runtime": [...XAI_FORBIDDEN_LEGACY_PLUGIN_SDK_EXACT_PATH],
|
||||
"openclaw/plugin-sdk/channel-streaming": [...XAI_FORBIDDEN_LEGACY_PLUGIN_SDK_EXACT_PATH],
|
||||
"openclaw/plugin-sdk/cli-runtime": [...XAI_FORBIDDEN_LEGACY_PLUGIN_SDK_EXACT_PATH],
|
||||
"openclaw/plugin-sdk/error-runtime": [...XAI_FORBIDDEN_LEGACY_PLUGIN_SDK_EXACT_PATH],
|
||||
"openclaw/plugin-sdk/provider-catalog-shared": [...XAI_FORBIDDEN_LEGACY_PLUGIN_SDK_EXACT_PATH],
|
||||
"openclaw/plugin-sdk/provider-env-vars": [...XAI_FORBIDDEN_LEGACY_PLUGIN_SDK_EXACT_PATH],
|
||||
"openclaw/plugin-sdk/provider-entry": [...XAI_FORBIDDEN_LEGACY_PLUGIN_SDK_EXACT_PATH],
|
||||
"openclaw/plugin-sdk/provider-web-search-contract": [
|
||||
...XAI_FORBIDDEN_LEGACY_PLUGIN_SDK_EXACT_PATH,
|
||||
...prefixExtensionPackageBoundaryPaths(
|
||||
(({
|
||||
"openclaw/plugin-sdk/channel-secret-basic-runtime": _omitBasic,
|
||||
"openclaw/plugin-sdk/channel-secret-tts-runtime": _omitTts,
|
||||
...rest
|
||||
}) => rest)(EXTENSION_PACKAGE_BOUNDARY_BASE_PATHS),
|
||||
"../",
|
||||
),
|
||||
"openclaw/plugin-sdk/channel-entry-contract": [
|
||||
"../../dist/plugin-sdk/src/plugin-sdk/channel-entry-contract.d.ts",
|
||||
],
|
||||
"openclaw/plugin-sdk/browser-maintenance": [
|
||||
"../../dist/plugin-sdk/src/plugin-sdk/browser-maintenance.d.ts",
|
||||
],
|
||||
"openclaw/plugin-sdk/cli-runtime": ["../../dist/plugin-sdk/src/plugin-sdk/cli-runtime.d.ts"],
|
||||
"openclaw/plugin-sdk/provider-catalog-shared": [
|
||||
"../../dist/plugin-sdk/src/plugin-sdk/provider-catalog-shared.d.ts",
|
||||
],
|
||||
"openclaw/plugin-sdk/provider-env-vars": [
|
||||
"../../dist/plugin-sdk/src/plugin-sdk/provider-env-vars.d.ts",
|
||||
],
|
||||
"openclaw/plugin-sdk/provider-entry": [
|
||||
"../../dist/plugin-sdk/src/plugin-sdk/provider-entry.d.ts",
|
||||
],
|
||||
"openclaw/plugin-sdk/provider-web-search-contract": [
|
||||
"../../dist/plugin-sdk/src/plugin-sdk/provider-web-search-contract.d.ts",
|
||||
],
|
||||
"openclaw/plugin-sdk/secret-ref-runtime": [...XAI_FORBIDDEN_LEGACY_PLUGIN_SDK_EXACT_PATH],
|
||||
"openclaw/plugin-sdk/ssrf-runtime": [...XAI_FORBIDDEN_LEGACY_PLUGIN_SDK_EXACT_PATH],
|
||||
"@openclaw/*.js": ["../../packages/plugin-sdk/dist/extensions/*.d.ts", "../*"],
|
||||
"@openclaw/*": ["../*"],
|
||||
"@openclaw/plugin-sdk/*": ["../../dist/plugin-sdk/src/plugin-sdk/*.d.ts"],
|
||||
@@ -108,7 +118,7 @@ export type ExtensionPackageBoundaryTsConfigJson = {
|
||||
extends?: unknown;
|
||||
compilerOptions?: {
|
||||
rootDir?: unknown;
|
||||
paths?: Record<string, readonly string[]>;
|
||||
paths?: unknown;
|
||||
};
|
||||
include?: unknown;
|
||||
exclude?: unknown;
|
||||
|
||||
@@ -62,6 +62,9 @@ export const pluginSdkDocMetadata = {
|
||||
"command-auth": {
|
||||
category: "channel",
|
||||
},
|
||||
"command-status": {
|
||||
category: "channel",
|
||||
},
|
||||
"secret-input": {
|
||||
category: "channel",
|
||||
},
|
||||
|
||||
@@ -102,6 +102,7 @@
|
||||
"dangerous-name-runtime",
|
||||
"command-auth",
|
||||
"command-auth-native",
|
||||
"command-status",
|
||||
"command-detection",
|
||||
"command-surface",
|
||||
"collection-runtime",
|
||||
|
||||
@@ -71,8 +71,19 @@ describe("syncExternalCliCredentials", () => {
|
||||
|
||||
describe("shouldReplaceStoredOAuthCredential", () => {
|
||||
it("keeps equivalent stored credentials", () => {
|
||||
const stored = makeOAuthCredential({ provider: "openai-codex", access: "a", refresh: "r" });
|
||||
const incoming = makeOAuthCredential({ provider: "openai-codex", access: "a", refresh: "r" });
|
||||
const expires = Date.now() + 60_000;
|
||||
const stored = makeOAuthCredential({
|
||||
provider: "openai-codex",
|
||||
access: "a",
|
||||
refresh: "r",
|
||||
expires,
|
||||
});
|
||||
const incoming = makeOAuthCredential({
|
||||
provider: "openai-codex",
|
||||
access: "a",
|
||||
refresh: "r",
|
||||
expires,
|
||||
});
|
||||
|
||||
expect(shouldReplaceStoredOAuthCredential(stored, incoming)).toBe(false);
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@ const isWin = process.platform === "win32";
|
||||
const defaultShell = isWin
|
||||
? undefined
|
||||
: process.env.OPENCLAW_TEST_SHELL || resolveShellFromPath("bash") || process.env.SHELL || "sh";
|
||||
const longDelayCmd = isWin ? "Start-Sleep -Milliseconds 200" : "sleep 0.2";
|
||||
const longDelayCmd = isWin ? "Start-Sleep -Seconds 5" : "sleep 5";
|
||||
|
||||
describe("exec foreground failures", () => {
|
||||
let envSnapshot: ReturnType<typeof captureEnv>;
|
||||
|
||||
@@ -2,9 +2,14 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const loadConfigMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("../config/config.js", () => ({
|
||||
loadConfig: loadConfigMock,
|
||||
}));
|
||||
// context.js and command-auth.js still read other config exports at import time, so this test only stubs loadConfig while keeping the rest of the module real.
|
||||
vi.mock("../config/config.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../config/config.js")>();
|
||||
return {
|
||||
...actual,
|
||||
loadConfig: loadConfigMock,
|
||||
};
|
||||
});
|
||||
|
||||
describe("agents/context eager warmup", () => {
|
||||
const originalArgv = process.argv.slice();
|
||||
@@ -27,4 +32,28 @@ describe("agents/context eager warmup", () => {
|
||||
|
||||
expect(loadConfigMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not eager-load config when onboard imports command-auth through plugin-sdk", async () => {
|
||||
process.argv = ["node", "openclaw", "onboard"];
|
||||
|
||||
await import("../plugin-sdk/command-auth.js");
|
||||
|
||||
expect(loadConfigMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not eager-load config when pairing approve imports command-auth through plugin-sdk", async () => {
|
||||
process.argv = ["node", "openclaw", "pairing", "approve", "feishu", "BAH8YVB3"];
|
||||
|
||||
await import("../plugin-sdk/command-auth.js");
|
||||
|
||||
expect(loadConfigMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not eager-load config when channels login imports command-auth through plugin-sdk", async () => {
|
||||
process.argv = ["node", "openclaw", "channels", "login", "--channel", "openclaw-weixin"];
|
||||
|
||||
await import("../plugin-sdk/command-auth.js");
|
||||
|
||||
expect(loadConfigMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,6 +13,11 @@ describe("live model error helpers", () => {
|
||||
"HTTP 400 not_found_error: model: claude-3-5-haiku-20241022 (request_id: req_123)",
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isModelNotFoundErrorMessage(
|
||||
"404 The free model has been deprecated. Transition to qwen/qwen3.6-plus for continued paid access.",
|
||||
),
|
||||
).toBe(true);
|
||||
expect(isModelNotFoundErrorMessage("request ended without sending any chunks")).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export function isModelNotFoundErrorMessage(raw: string): boolean {
|
||||
if (/does not exist or you do not have access/i.test(msg)) {
|
||||
return true;
|
||||
}
|
||||
if (/deprecated/i.test(msg) && /upgrade to/i.test(msg)) {
|
||||
if (/deprecated/i.test(msg) && /(upgrade|transition) to/i.test(msg)) {
|
||||
return true;
|
||||
}
|
||||
if (/stealth model/i.test(msg) && /find it here/i.test(msg)) {
|
||||
|
||||
@@ -9,6 +9,7 @@ export type ModelRef = {
|
||||
|
||||
const HIGH_SIGNAL_LIVE_MODEL_PRIORITY = [
|
||||
"anthropic/claude-opus-4-6",
|
||||
"anthropic/claude-sonnet-4-6",
|
||||
"google/gemini-3.1-pro-preview",
|
||||
"google/gemini-3-flash-preview",
|
||||
"minimax/minimax-m2.7",
|
||||
@@ -16,12 +17,14 @@ const HIGH_SIGNAL_LIVE_MODEL_PRIORITY = [
|
||||
"openai-codex/gpt-5.2",
|
||||
"opencode-go/glm-5",
|
||||
"openrouter/ai21/jamba-large-1.7",
|
||||
"xai/grok-3",
|
||||
"xai/grok-4-1-fast-non-reasoning",
|
||||
"zai/glm-4.7",
|
||||
"fireworks/accounts/fireworks/routers/kimi-k2p5-turbo",
|
||||
"minimax-portal/minimax-m2.7",
|
||||
] as const;
|
||||
|
||||
export const DEFAULT_HIGH_SIGNAL_LIVE_MODEL_LIMIT = HIGH_SIGNAL_LIVE_MODEL_PRIORITY.length;
|
||||
|
||||
const HIGH_SIGNAL_LIVE_MODEL_PRIORITY_INDEX = new Map<string, number>(
|
||||
HIGH_SIGNAL_LIVE_MODEL_PRIORITY.map((key, index) => [key, index]),
|
||||
);
|
||||
@@ -180,6 +183,22 @@ export function selectHighSignalLiveItems<T>(
|
||||
return [...selected, ...capByProviderSpread(remaining, maxItems - selected.length, providerOf)];
|
||||
}
|
||||
|
||||
export function resolveHighSignalLiveModelLimit(params: {
|
||||
rawMaxModels?: string;
|
||||
useExplicitModels: boolean;
|
||||
defaultLimit?: number;
|
||||
}): number {
|
||||
const trimmed = params.rawMaxModels?.trim();
|
||||
if (trimmed) {
|
||||
const parsed = Number.parseInt(trimmed, 10);
|
||||
return Number.isFinite(parsed) ? Math.max(0, parsed) : 0;
|
||||
}
|
||||
if (params.useExplicitModels) {
|
||||
return 0;
|
||||
}
|
||||
return params.defaultLimit ?? DEFAULT_HIGH_SIGNAL_LIVE_MODEL_LIMIT;
|
||||
}
|
||||
|
||||
export function getHighSignalLiveModelPriorityIndex(ref: ModelRef): number | null {
|
||||
const key = toCanonicalHighSignalLiveModelKey(ref);
|
||||
if (!key) {
|
||||
|
||||
@@ -1,4 +1,25 @@
|
||||
import { beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { captureEnv, withEnvAsync } from "../test-utils/env.js";
|
||||
|
||||
const PLUGIN_MANIFEST_ENV_KEYS = [
|
||||
"OPENCLAW_BUNDLED_PLUGINS_DIR",
|
||||
"OPENCLAW_DISABLE_BUNDLED_PLUGINS",
|
||||
"OPENCLAW_SKIP_PROVIDERS",
|
||||
"OPENCLAW_SKIP_CHANNELS",
|
||||
"OPENCLAW_SKIP_CRON",
|
||||
"OPENCLAW_TEST_MINIMAL_GATEWAY",
|
||||
] as const;
|
||||
|
||||
function cleanPluginManifestEnv(): Record<(typeof PLUGIN_MANIFEST_ENV_KEYS)[number], undefined> {
|
||||
return {
|
||||
OPENCLAW_BUNDLED_PLUGINS_DIR: undefined,
|
||||
OPENCLAW_DISABLE_BUNDLED_PLUGINS: undefined,
|
||||
OPENCLAW_SKIP_PROVIDERS: undefined,
|
||||
OPENCLAW_SKIP_CHANNELS: undefined,
|
||||
OPENCLAW_SKIP_CRON: undefined,
|
||||
OPENCLAW_TEST_MINIMAL_GATEWAY: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
let listKnownProviderEnvApiKeyNames: typeof import("./model-auth-env-vars.js").listKnownProviderEnvApiKeyNames;
|
||||
let GCP_VERTEX_CREDENTIALS_MARKER: typeof import("./model-auth-markers.js").GCP_VERTEX_CREDENTIALS_MARKER;
|
||||
@@ -6,6 +27,7 @@ let NON_ENV_SECRETREF_MARKER: typeof import("./model-auth-markers.js").NON_ENV_S
|
||||
let isKnownEnvApiKeyMarker: typeof import("./model-auth-markers.js").isKnownEnvApiKeyMarker;
|
||||
let isNonSecretApiKeyMarker: typeof import("./model-auth-markers.js").isNonSecretApiKeyMarker;
|
||||
let resolveOAuthApiKeyMarker: typeof import("./model-auth-markers.js").resolveOAuthApiKeyMarker;
|
||||
let manifestEnvSnapshot: ReturnType<typeof captureEnv> | undefined;
|
||||
|
||||
async function loadMarkerModules() {
|
||||
vi.doUnmock("../plugins/manifest-registry.js");
|
||||
@@ -23,7 +45,21 @@ async function loadMarkerModules() {
|
||||
resolveOAuthApiKeyMarker = markersModule.resolveOAuthApiKeyMarker;
|
||||
}
|
||||
|
||||
beforeAll(loadMarkerModules);
|
||||
beforeAll(async () => {
|
||||
await withEnvAsync(cleanPluginManifestEnv(), loadMarkerModules);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
manifestEnvSnapshot = captureEnv([...PLUGIN_MANIFEST_ENV_KEYS]);
|
||||
for (const key of PLUGIN_MANIFEST_ENV_KEYS) {
|
||||
delete process.env[key];
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
manifestEnvSnapshot?.restore();
|
||||
manifestEnvSnapshot = undefined;
|
||||
});
|
||||
|
||||
describe("model auth markers", () => {
|
||||
it("recognizes explicit non-secret markers", () => {
|
||||
|
||||
@@ -115,6 +115,7 @@ let clearRuntimeConfigSnapshot: typeof import("../config/config.js").clearRuntim
|
||||
let setRuntimeConfigSnapshot: typeof import("../config/config.js").setRuntimeConfigSnapshot;
|
||||
|
||||
beforeAll(async () => {
|
||||
vi.resetModules();
|
||||
({ clearRuntimeConfigSnapshot, setRuntimeConfigSnapshot } = await import("../config/config.js"));
|
||||
({
|
||||
applyAuthHeaderOverride,
|
||||
|
||||
@@ -143,7 +143,7 @@ export async function loadModelCatalog(params?: {
|
||||
if (!provider) {
|
||||
continue;
|
||||
}
|
||||
if (shouldSuppressBuiltInModel({ provider, id })) {
|
||||
if (shouldSuppressBuiltInModel({ provider, id, config: cfg })) {
|
||||
continue;
|
||||
}
|
||||
const name = normalizeOptionalString(String(entry?.name ?? id)) || id;
|
||||
|
||||
@@ -17,8 +17,10 @@ vi.mock("../plugins/provider-runtime.js", async () => {
|
||||
|
||||
import { normalizeModelCompat } from "../plugins/provider-model-compat.js";
|
||||
import {
|
||||
DEFAULT_HIGH_SIGNAL_LIVE_MODEL_LIMIT,
|
||||
isHighSignalLiveModelRef,
|
||||
isModernModelRef,
|
||||
resolveHighSignalLiveModelLimit,
|
||||
selectHighSignalLiveItems,
|
||||
} from "./live-model-filter.js";
|
||||
|
||||
@@ -503,3 +505,27 @@ describe("selectHighSignalLiveItems", () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveHighSignalLiveModelLimit", () => {
|
||||
it("defaults modern live sweeps to the curated high-signal cap", () => {
|
||||
expect(
|
||||
resolveHighSignalLiveModelLimit({
|
||||
useExplicitModels: false,
|
||||
}),
|
||||
).toBe(DEFAULT_HIGH_SIGNAL_LIVE_MODEL_LIMIT);
|
||||
});
|
||||
|
||||
it("leaves explicit model lists uncapped unless a cap is provided", () => {
|
||||
expect(
|
||||
resolveHighSignalLiveModelLimit({
|
||||
useExplicitModels: true,
|
||||
}),
|
||||
).toBe(0);
|
||||
expect(
|
||||
resolveHighSignalLiveModelLimit({
|
||||
rawMaxModels: "3",
|
||||
useExplicitModels: true,
|
||||
}),
|
||||
).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveProviderBuiltInModelSuppression } from "../plugins/provider-runtime.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||
import { normalizeProviderId } from "./provider-id.js";
|
||||
|
||||
function resolveBuiltInModelSuppression(params: { provider?: string | null; id?: string | null }) {
|
||||
function resolveBuiltInModelSuppression(params: {
|
||||
provider?: string | null;
|
||||
id?: string | null;
|
||||
baseUrl?: string | null;
|
||||
config?: OpenClawConfig;
|
||||
}) {
|
||||
const provider = normalizeProviderId(params.provider ?? "");
|
||||
const modelId = normalizeLowercaseStringOrEmpty(params.id);
|
||||
if (!provider || !modelId) {
|
||||
return undefined;
|
||||
}
|
||||
return resolveProviderBuiltInModelSuppression({
|
||||
...(params.config ? { config: params.config } : {}),
|
||||
env: process.env,
|
||||
context: {
|
||||
...(params.config ? { config: params.config } : {}),
|
||||
env: process.env,
|
||||
provider,
|
||||
modelId,
|
||||
...(params.baseUrl ? { baseUrl: params.baseUrl } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -21,6 +30,8 @@ function resolveBuiltInModelSuppression(params: { provider?: string | null; id?:
|
||||
export function shouldSuppressBuiltInModel(params: {
|
||||
provider?: string | null;
|
||||
id?: string | null;
|
||||
baseUrl?: string | null;
|
||||
config?: OpenClawConfig;
|
||||
}) {
|
||||
return resolveBuiltInModelSuppression(params)?.suppress ?? false;
|
||||
}
|
||||
@@ -28,6 +39,8 @@ export function shouldSuppressBuiltInModel(params: {
|
||||
export function buildSuppressedBuiltInModelError(params: {
|
||||
provider?: string | null;
|
||||
id?: string | null;
|
||||
baseUrl?: string | null;
|
||||
config?: OpenClawConfig;
|
||||
}): string | undefined {
|
||||
return resolveBuiltInModelSuppression(params)?.errorMessage;
|
||||
}
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { withEnvAsync } from "../test-utils/env.js";
|
||||
import {
|
||||
installModelsConfigTestHooks,
|
||||
mockCopilotTokenExchangeSuccess,
|
||||
withCopilotGithubToken,
|
||||
withModelsTempHome as withTempHome,
|
||||
} from "./models-config.e2e-harness.js";
|
||||
|
||||
vi.unmock("./models-config.js");
|
||||
vi.unmock("./agent-paths.js");
|
||||
vi.unmock("../plugins/manifest-registry.js");
|
||||
vi.unmock("../plugins/provider-runtime.js");
|
||||
vi.unmock("../plugins/provider-runtime.runtime.js");
|
||||
vi.unmock("../secrets/provider-env-vars.js");
|
||||
|
||||
installModelsConfigTestHooks({ restoreFetch: true });
|
||||
|
||||
let ensureOpenClawModelsJson: typeof import("./models-config.js").ensureOpenClawModelsJson;
|
||||
|
||||
async function loadModelsConfigForTest(): Promise<void> {
|
||||
vi.resetModules();
|
||||
vi.doUnmock("./models-config.js");
|
||||
vi.doUnmock("./agent-paths.js");
|
||||
vi.doUnmock("../plugins/manifest-registry.js");
|
||||
vi.doUnmock("../plugins/provider-runtime.js");
|
||||
vi.doUnmock("../plugins/provider-runtime.runtime.js");
|
||||
vi.doUnmock("../secrets/provider-env-vars.js");
|
||||
({ ensureOpenClawModelsJson } = await import("./models-config.js"));
|
||||
}
|
||||
|
||||
beforeEach(loadModelsConfigForTest);
|
||||
|
||||
describe("models-config", () => {
|
||||
it("auto-injects github-copilot provider when token is present", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
await withCopilotGithubToken("gh-token", async () => {
|
||||
const agentDir = path.join(home, "agent-default-base-url");
|
||||
await ensureOpenClawModelsJson({ models: { providers: {} } }, agentDir);
|
||||
|
||||
const raw = await fs.readFile(path.join(agentDir, "models.json"), "utf8");
|
||||
const parsed = JSON.parse(raw) as {
|
||||
providers: Record<string, { baseUrl?: string; models?: unknown[] }>;
|
||||
};
|
||||
|
||||
expect(parsed.providers["github-copilot"]?.baseUrl).toBe("https://api.copilot.example");
|
||||
expect(parsed.providers["github-copilot"]?.models?.length ?? 0).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers COPILOT_GITHUB_TOKEN over GH_TOKEN and GITHUB_TOKEN", async () => {
|
||||
await withTempHome(async () => {
|
||||
await withEnvAsync(
|
||||
{
|
||||
COPILOT_GITHUB_TOKEN: "copilot-token",
|
||||
GH_TOKEN: "gh-token",
|
||||
GITHUB_TOKEN: "github-token",
|
||||
OPENCLAW_TEST_ONLY_PROVIDER_PLUGIN_IDS: "github-copilot",
|
||||
},
|
||||
async () => {
|
||||
const fetchMock = mockCopilotTokenExchangeSuccess();
|
||||
|
||||
await ensureOpenClawModelsJson({ models: { providers: {} } });
|
||||
|
||||
const [, opts] = fetchMock.mock.calls[0] as [
|
||||
string,
|
||||
{ headers?: Record<string, string> },
|
||||
];
|
||||
expect(opts?.headers?.Authorization).toBe("Bearer copilot-token");
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,100 +0,0 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { withEnvAsync } from "../test-utils/env.js";
|
||||
import { DEFAULT_COPILOT_API_BASE_URL } from "./github-copilot-token.js";
|
||||
import {
|
||||
installModelsConfigTestHooks,
|
||||
mockCopilotTokenExchangeSuccess,
|
||||
withUnsetCopilotTokenEnv,
|
||||
withModelsTempHome as withTempHome,
|
||||
} from "./models-config.e2e-harness.js";
|
||||
|
||||
vi.unmock("./models-config.js");
|
||||
vi.unmock("./agent-paths.js");
|
||||
vi.unmock("../plugins/manifest-registry.js");
|
||||
vi.unmock("../plugins/provider-runtime.js");
|
||||
vi.unmock("../plugins/provider-runtime.runtime.js");
|
||||
vi.unmock("../secrets/provider-env-vars.js");
|
||||
|
||||
installModelsConfigTestHooks({ restoreFetch: true });
|
||||
|
||||
let ensureOpenClawModelsJson: typeof import("./models-config.js").ensureOpenClawModelsJson;
|
||||
|
||||
async function loadModelsConfigForTest(): Promise<void> {
|
||||
vi.resetModules();
|
||||
vi.doUnmock("./models-config.js");
|
||||
vi.doUnmock("./agent-paths.js");
|
||||
vi.doUnmock("../plugins/manifest-registry.js");
|
||||
vi.doUnmock("../plugins/provider-runtime.js");
|
||||
vi.doUnmock("../plugins/provider-runtime.runtime.js");
|
||||
vi.doUnmock("../secrets/provider-env-vars.js");
|
||||
({ ensureOpenClawModelsJson } = await import("./models-config.js"));
|
||||
}
|
||||
|
||||
beforeEach(loadModelsConfigForTest);
|
||||
|
||||
async function readCopilotBaseUrl(agentDir: string) {
|
||||
const raw = await fs.readFile(path.join(agentDir, "models.json"), "utf8");
|
||||
const parsed = JSON.parse(raw) as {
|
||||
providers: Record<string, { baseUrl?: string }>;
|
||||
};
|
||||
return parsed.providers["github-copilot"]?.baseUrl;
|
||||
}
|
||||
|
||||
describe("models-config", () => {
|
||||
it("falls back to default baseUrl when token exchange fails", async () => {
|
||||
await withTempHome(async () => {
|
||||
await withEnvAsync(
|
||||
{
|
||||
COPILOT_GITHUB_TOKEN: "gh-token",
|
||||
GH_TOKEN: undefined,
|
||||
GITHUB_TOKEN: undefined,
|
||||
OPENCLAW_TEST_ONLY_PROVIDER_PLUGIN_IDS: "github-copilot",
|
||||
},
|
||||
async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
json: async () => ({ message: "boom" }),
|
||||
});
|
||||
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
||||
|
||||
const { agentDir } = await ensureOpenClawModelsJson({ models: { providers: {} } });
|
||||
expect(await readCopilotBaseUrl(agentDir)).toBe(DEFAULT_COPILOT_API_BASE_URL);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("uses agentDir override auth profiles for copilot injection", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
await withUnsetCopilotTokenEnv(async () => {
|
||||
mockCopilotTokenExchangeSuccess();
|
||||
const agentDir = path.join(home, "agent-override");
|
||||
await fs.mkdir(agentDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(agentDir, "auth-profiles.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
version: 1,
|
||||
profiles: {
|
||||
"github-copilot:github": {
|
||||
type: "token",
|
||||
provider: "github-copilot",
|
||||
token: "gh-profile-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
|
||||
await ensureOpenClawModelsJson({ models: { providers: {} } }, agentDir);
|
||||
|
||||
expect(await readCopilotBaseUrl(agentDir)).toBe("https://api.copilot.example");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,12 +4,19 @@ import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveImplicitProvidersForTest } from "./models-config.e2e-harness.js";
|
||||
|
||||
const ANTHROPIC_VERTEX_DISCOVERY_ENV = {
|
||||
OPENCLAW_TEST_ONLY_PROVIDER_PLUGIN_IDS: "anthropic",
|
||||
} satisfies NodeJS.ProcessEnv;
|
||||
|
||||
describe("anthropic-vertex implicit provider", () => {
|
||||
it("does not auto-enable from GOOGLE_CLOUD_PROJECT_ID alone", async () => {
|
||||
const agentDir = mkdtempSync(join(tmpdir(), "openclaw-test-"));
|
||||
const providers = await resolveImplicitProvidersForTest({
|
||||
agentDir,
|
||||
env: { GOOGLE_CLOUD_PROJECT_ID: "vertex-project" },
|
||||
env: {
|
||||
...ANTHROPIC_VERTEX_DISCOVERY_ENV,
|
||||
GOOGLE_CLOUD_PROJECT_ID: "vertex-project",
|
||||
},
|
||||
});
|
||||
expect(providers?.["anthropic-vertex"]).toBeUndefined();
|
||||
});
|
||||
@@ -24,6 +31,7 @@ describe("anthropic-vertex implicit provider", () => {
|
||||
const providers = await resolveImplicitProvidersForTest({
|
||||
agentDir,
|
||||
env: {
|
||||
...ANTHROPIC_VERTEX_DISCOVERY_ENV,
|
||||
GOOGLE_APPLICATION_CREDENTIALS: credentialsPath,
|
||||
GOOGLE_CLOUD_LOCATION: "us-east1",
|
||||
},
|
||||
@@ -50,6 +58,7 @@ describe("anthropic-vertex implicit provider", () => {
|
||||
const providers = await resolveImplicitProvidersForTest({
|
||||
agentDir,
|
||||
env: {
|
||||
...ANTHROPIC_VERTEX_DISCOVERY_ENV,
|
||||
GOOGLE_APPLICATION_CREDENTIALS: credentialsPath,
|
||||
GOOGLE_CLOUD_LOCATION: "us-east5",
|
||||
},
|
||||
@@ -72,6 +81,7 @@ describe("anthropic-vertex implicit provider", () => {
|
||||
const providers = await resolveImplicitProvidersForTest({
|
||||
agentDir,
|
||||
env: {
|
||||
...ANTHROPIC_VERTEX_DISCOVERY_ENV,
|
||||
GOOGLE_APPLICATION_CREDENTIALS: credentialsPath,
|
||||
GOOGLE_CLOUD_LOCATION: "europe-west4",
|
||||
},
|
||||
@@ -94,6 +104,7 @@ describe("anthropic-vertex implicit provider", () => {
|
||||
const providers = await resolveImplicitProvidersForTest({
|
||||
agentDir,
|
||||
env: {
|
||||
...ANTHROPIC_VERTEX_DISCOVERY_ENV,
|
||||
GOOGLE_APPLICATION_CREDENTIALS: credentialsPath,
|
||||
GOOGLE_CLOUD_LOCATION: "us-central1.attacker.example",
|
||||
},
|
||||
@@ -114,6 +125,7 @@ describe("anthropic-vertex implicit provider", () => {
|
||||
const providers = await resolveImplicitProvidersForTest({
|
||||
agentDir,
|
||||
env: {
|
||||
...ANTHROPIC_VERTEX_DISCOVERY_ENV,
|
||||
GOOGLE_APPLICATION_CREDENTIALS: credentialsPath,
|
||||
GOOGLE_CLOUD_LOCATION: "global",
|
||||
},
|
||||
@@ -129,6 +141,7 @@ describe("anthropic-vertex implicit provider", () => {
|
||||
const providers = await resolveImplicitProvidersForTest({
|
||||
agentDir,
|
||||
env: {
|
||||
...ANTHROPIC_VERTEX_DISCOVERY_ENV,
|
||||
ANTHROPIC_VERTEX_USE_GCP_METADATA: "true",
|
||||
GOOGLE_CLOUD_LOCATION: "us-east5",
|
||||
},
|
||||
@@ -143,6 +156,7 @@ describe("anthropic-vertex implicit provider", () => {
|
||||
const providers = await resolveImplicitProvidersForTest({
|
||||
agentDir,
|
||||
env: {
|
||||
...ANTHROPIC_VERTEX_DISCOVERY_ENV,
|
||||
ANTHROPIC_VERTEX_USE_GCP_METADATA: "true",
|
||||
GOOGLE_CLOUD_LOCATION: "us-east5",
|
||||
},
|
||||
@@ -170,6 +184,7 @@ describe("anthropic-vertex implicit provider", () => {
|
||||
const providers = await resolveImplicitProvidersForTest({
|
||||
agentDir,
|
||||
env: {
|
||||
...ANTHROPIC_VERTEX_DISCOVERY_ENV,
|
||||
KUBERNETES_SERVICE_HOST: "10.0.0.1",
|
||||
GOOGLE_CLOUD_LOCATION: "us-east5",
|
||||
},
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
import { mkdtempSync } from "node:fs";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import {
|
||||
MINIMAX_OAUTH_MARKER,
|
||||
NON_ENV_SECRETREF_MARKER,
|
||||
OLLAMA_LOCAL_AUTH_MARKER,
|
||||
} from "./model-auth-markers.js";
|
||||
import { resolveImplicitProvidersForTest } from "./models-config.e2e-harness.js";
|
||||
|
||||
type ProvidersMap = Awaited<ReturnType<typeof resolveImplicitProvidersForTest>>;
|
||||
type ExplicitProviders = NonNullable<NonNullable<OpenClawConfig["models"]>["providers"]>;
|
||||
type MatrixCase = {
|
||||
name: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
authProfiles?: Record<string, unknown>;
|
||||
explicitProviders?: ExplicitProviders;
|
||||
assertProviders: (providers: ProvidersMap) => void;
|
||||
};
|
||||
|
||||
async function writeAuthProfiles(
|
||||
agentDir: string,
|
||||
profiles: Record<string, unknown> | undefined,
|
||||
): Promise<void> {
|
||||
if (!profiles) {
|
||||
return;
|
||||
}
|
||||
|
||||
await writeFile(
|
||||
join(agentDir, "auth-profiles.json"),
|
||||
JSON.stringify({ version: 1, profiles }, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
}
|
||||
|
||||
const MATRIX_CASES: MatrixCase[] = [
|
||||
{
|
||||
name: "env api key injects a simple provider",
|
||||
env: { NVIDIA_API_KEY: "test-nvidia-key" }, // pragma: allowlist secret
|
||||
assertProviders(providers) {
|
||||
expect(providers?.nvidia?.apiKey).toBe("NVIDIA_API_KEY");
|
||||
expect(providers?.nvidia?.baseUrl).toBe("https://integrate.api.nvidia.com/v1");
|
||||
expect(providers?.nvidia?.models?.length).toBeGreaterThan(0);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "env api key injects paired plan providers",
|
||||
env: { VOLCANO_ENGINE_API_KEY: "test-volcengine-key" }, // pragma: allowlist secret
|
||||
assertProviders(providers) {
|
||||
expect(providers?.volcengine?.apiKey).toBe("VOLCANO_ENGINE_API_KEY");
|
||||
expect(providers?.["volcengine-plan"]?.apiKey).toBe("VOLCANO_ENGINE_API_KEY");
|
||||
expect(providers?.["volcengine-plan"]?.api).toBe("openai-completions");
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "env-backed auth profiles persist env markers",
|
||||
env: {},
|
||||
authProfiles: {
|
||||
"together:default": {
|
||||
type: "token",
|
||||
provider: "together",
|
||||
tokenRef: { source: "env", provider: "default", id: "TOGETHER_API_KEY" },
|
||||
},
|
||||
},
|
||||
assertProviders(providers) {
|
||||
expect(providers?.together?.apiKey).toBe("TOGETHER_API_KEY");
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "non-env secret refs preserve compatibility markers",
|
||||
env: {},
|
||||
authProfiles: {
|
||||
"byteplus:default": {
|
||||
type: "api_key",
|
||||
provider: "byteplus",
|
||||
key: "runtime-byteplus-key",
|
||||
keyRef: { source: "file", provider: "vault", id: "/byteplus/apiKey" },
|
||||
},
|
||||
},
|
||||
assertProviders(providers) {
|
||||
expect(providers?.byteplus?.apiKey).toBe(NON_ENV_SECRETREF_MARKER);
|
||||
expect(providers?.["byteplus-plan"]?.apiKey).toBe(NON_ENV_SECRETREF_MARKER);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "oauth profiles still inject compatibility providers",
|
||||
env: {},
|
||||
authProfiles: {
|
||||
"openai-codex:default": {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "codex-access-token",
|
||||
refresh: "codex-refresh-token",
|
||||
expires: Date.now() + 60_000,
|
||||
},
|
||||
"minimax-portal:default": {
|
||||
type: "oauth",
|
||||
provider: "minimax-portal",
|
||||
access: "minimax-access-token",
|
||||
refresh: "minimax-refresh-token",
|
||||
expires: Date.now() + 60_000,
|
||||
},
|
||||
},
|
||||
assertProviders(providers) {
|
||||
expect(providers?.["openai-codex"]).toMatchObject({
|
||||
baseUrl: "https://chatgpt.com/backend-api",
|
||||
api: "openai-codex-responses",
|
||||
models: [],
|
||||
});
|
||||
expect(providers?.["openai-codex"]).not.toHaveProperty("apiKey");
|
||||
expect(providers?.["minimax-portal"]?.apiKey).toBe(MINIMAX_OAUTH_MARKER);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "explicit vllm config suppresses implicit vllm injection",
|
||||
env: { VLLM_API_KEY: "test-vllm-key" }, // pragma: allowlist secret
|
||||
explicitProviders: {
|
||||
vllm: {
|
||||
baseUrl: "http://127.0.0.1:8000/v1",
|
||||
api: "openai-completions",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
assertProviders(providers) {
|
||||
expect(providers?.vllm).toBeUndefined();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "explicit ollama models still normalize the returned provider",
|
||||
env: {},
|
||||
explicitProviders: {
|
||||
ollama: {
|
||||
baseUrl: "http://remote-ollama:11434/v1",
|
||||
models: [
|
||||
{
|
||||
id: "gpt-oss:20b",
|
||||
name: "GPT-OSS 20B",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 8192,
|
||||
maxTokens: 81920,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
assertProviders(providers) {
|
||||
expect(providers?.ollama?.baseUrl).toBe("http://remote-ollama:11434");
|
||||
expect(providers?.ollama?.api).toBe("ollama");
|
||||
expect(providers?.ollama?.apiKey).toBe(OLLAMA_LOCAL_AUTH_MARKER);
|
||||
expect(providers?.ollama?.models).toHaveLength(1);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe("implicit provider resolution matrix", () => {
|
||||
it.each(MATRIX_CASES)(
|
||||
"$name",
|
||||
async ({ env, authProfiles, explicitProviders, assertProviders }) => {
|
||||
const agentDir = mkdtempSync(join(tmpdir(), "openclaw-test-"));
|
||||
await writeAuthProfiles(agentDir, authProfiles);
|
||||
|
||||
const providers = await resolveImplicitProvidersForTest({
|
||||
agentDir,
|
||||
env,
|
||||
explicitProviders,
|
||||
});
|
||||
|
||||
assertProviders(providers);
|
||||
},
|
||||
240_000,
|
||||
);
|
||||
});
|
||||
@@ -3,7 +3,6 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { ensureAuthProfileStore } from "./auth-profiles.js";
|
||||
import { NON_ENV_SECRETREF_MARKER } from "./model-auth-markers.js";
|
||||
import { normalizeProviders } from "./models-config.providers.normalize.js";
|
||||
import { resolveApiKeyFromProfiles } from "./models-config.providers.secrets.js";
|
||||
@@ -183,13 +182,18 @@ describe("normalizeProviders", () => {
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const store = ensureAuthProfileStore(agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
});
|
||||
|
||||
const resolved = resolveApiKeyFromProfiles({
|
||||
provider: "minimax",
|
||||
store,
|
||||
store: {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"minimax:default": {
|
||||
type: "api_key",
|
||||
provider: "minimax",
|
||||
keyRef: { source: "env", provider: "default", id: "MINIMAX_API_KEY" },
|
||||
},
|
||||
},
|
||||
},
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,29 +1,15 @@
|
||||
import { mkdtempSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { ModelDefinitionConfig, ModelProviderConfig } from "../config/types.models.js";
|
||||
import { withEnvAsync } from "../test-utils/env.js";
|
||||
import { installModelsConfigTestHooks } from "./models-config.e2e-harness.js";
|
||||
import { resolveEnvApiKey } from "./model-auth-env.js";
|
||||
import {
|
||||
resolveEnvApiKeyVarName,
|
||||
resolveMissingProviderApiKey,
|
||||
} from "./models-config.providers.secrets.js";
|
||||
|
||||
const NVIDIA_BASE_URL = "https://integrate.api.nvidia.com/v1";
|
||||
const MINIMAX_BASE_URL = "https://api.minimax.io/anthropic";
|
||||
const VLLM_DEFAULT_BASE_URL = "http://127.0.0.1:8000/v1";
|
||||
|
||||
installModelsConfigTestHooks();
|
||||
|
||||
let resolveApiKeyForProvider: typeof import("./model-auth.js").resolveApiKeyForProvider;
|
||||
let resolveEnvApiKeyVarName: typeof import("./models-config.providers.secrets.js").resolveEnvApiKeyVarName;
|
||||
let resolveMissingProviderApiKey: typeof import("./models-config.providers.secrets.js").resolveMissingProviderApiKey;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.doUnmock("../plugins/provider-runtime.js");
|
||||
vi.resetModules();
|
||||
({ resolveApiKeyForProvider } = await import("./model-auth.js"));
|
||||
({ resolveEnvApiKeyVarName, resolveMissingProviderApiKey } =
|
||||
await import("./models-config.providers.secrets.js"));
|
||||
});
|
||||
|
||||
function createTestModel(id: string): ModelDefinitionConfig {
|
||||
return {
|
||||
id,
|
||||
@@ -93,17 +79,14 @@ describe("NVIDIA provider", () => {
|
||||
expect(provider.models?.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("resolves the nvidia api key value from env", async () => {
|
||||
const agentDir = mkdtempSync(join(tmpdir(), "openclaw-test-"));
|
||||
await withEnvAsync({ NVIDIA_API_KEY: "nvidia-test-api-key" }, async () => {
|
||||
const auth = await resolveApiKeyForProvider({
|
||||
provider: "nvidia",
|
||||
agentDir,
|
||||
});
|
||||
it("resolves the nvidia api key value from env", () => {
|
||||
const auth = resolveEnvApiKey("nvidia", {
|
||||
NVIDIA_API_KEY: "nvidia-test-api-key",
|
||||
} as NodeJS.ProcessEnv);
|
||||
|
||||
expect(auth.apiKey).toBe("nvidia-test-api-key");
|
||||
expect(auth.mode).toBe("api-key");
|
||||
expect(auth.source).toContain("NVIDIA_API_KEY");
|
||||
expect(auth).toEqual({
|
||||
apiKey: "nvidia-test-api-key",
|
||||
source: "env: NVIDIA_API_KEY",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,10 +21,21 @@ describe("Ollama auto-discovery", () => {
|
||||
delete process.env.OLLAMA_API_KEY;
|
||||
});
|
||||
|
||||
function createCleanProviderDiscoveryEnv(): NodeJS.ProcessEnv {
|
||||
const env = { ...process.env };
|
||||
delete env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
||||
delete env.OPENCLAW_DISABLE_BUNDLED_PLUGINS;
|
||||
delete env.OPENCLAW_SKIP_PROVIDERS;
|
||||
delete env.OPENCLAW_SKIP_CHANNELS;
|
||||
delete env.OPENCLAW_SKIP_CRON;
|
||||
delete env.OPENCLAW_TEST_MINIMAL_GATEWAY;
|
||||
return env;
|
||||
}
|
||||
|
||||
function createCatalogLoadEnv(): NodeJS.ProcessEnv {
|
||||
originalFetch = globalThis.fetch;
|
||||
return {
|
||||
...process.env,
|
||||
...createCleanProviderDiscoveryEnv(),
|
||||
OPENCLAW_TEST_ONLY_PROVIDER_PLUGIN_IDS: "ollama",
|
||||
VITEST: "1",
|
||||
NODE_ENV: "test",
|
||||
@@ -33,7 +44,7 @@ describe("Ollama auto-discovery", () => {
|
||||
|
||||
function createDiscoveryRunEnv(): NodeJS.ProcessEnv {
|
||||
return {
|
||||
...process.env,
|
||||
...createCleanProviderDiscoveryEnv(),
|
||||
OPENCLAW_TEST_ONLY_PROVIDER_PLUGIN_IDS: "ollama",
|
||||
VITEST: "",
|
||||
NODE_ENV: "development",
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "../plugins/provider-discovery.js";
|
||||
import type { ProviderPlugin } from "../plugins/types.js";
|
||||
import { withFetchPreconnect } from "../test-utils/fetch-mock.js";
|
||||
import { OLLAMA_LOCAL_AUTH_MARKER } from "./model-auth-markers.js";
|
||||
import { resolveImplicitProviders } from "./models-config.providers.js";
|
||||
import type { ProviderConfig } from "./models-config.providers.js";
|
||||
|
||||
@@ -351,6 +352,43 @@ describe("Ollama provider", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should use synthetic local auth for configured remote providers without apiKey", async () => {
|
||||
await withoutAmbientOllamaEnv(async () => {
|
||||
const fetchMock = vi.fn();
|
||||
vi.stubGlobal("fetch", withFetchPreconnect(fetchMock));
|
||||
|
||||
const provider = await runOllamaCatalog({
|
||||
config: {
|
||||
models: {
|
||||
providers: {
|
||||
ollama: {
|
||||
baseUrl: "http://remote-ollama:11434/v1",
|
||||
models: [
|
||||
{
|
||||
id: "gpt-oss:20b",
|
||||
name: "GPT-OSS 20B",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 8192,
|
||||
maxTokens: 81920,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
env: { VITEST: "", NODE_ENV: "development" },
|
||||
});
|
||||
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
expect(provider?.baseUrl).toBe("http://remote-ollama:11434");
|
||||
expect(provider?.api).toBe("ollama");
|
||||
expect(provider?.apiKey).toBe(OLLAMA_LOCAL_AUTH_MARKER);
|
||||
expect(provider?.models).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("should preserve explicit apiKey from configured remote providers", async () => {
|
||||
await withoutAmbientOllamaEnv(async () => {
|
||||
const fetchMock = vi.fn(async (input: unknown) => {
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { planOpenClawModelsJson } from "./models-config.plan.js";
|
||||
import {
|
||||
planOpenClawModelsJson,
|
||||
planOpenClawModelsJsonWithDeps,
|
||||
type ResolveImplicitProvidersForModelsJson,
|
||||
} from "./models-config.plan.js";
|
||||
import type { ProviderConfig } from "./models-config.providers.secrets.js";
|
||||
import { createProviderAuthResolver } from "./models-config.providers.secrets.js";
|
||||
|
||||
vi.mock("./models-config.providers.js", () => ({
|
||||
@@ -71,6 +76,54 @@ describe("models-config", () => {
|
||||
).toBe("https://copilot.local");
|
||||
});
|
||||
|
||||
it("passes explicit provider config to implicit discovery so plugins can skip duplicates", async () => {
|
||||
const resolveImplicitProviders = vi.fn<ResolveImplicitProvidersForModelsJson>(
|
||||
async ({ explicitProviders }) => {
|
||||
expect(explicitProviders.vllm?.baseUrl).toBe("http://127.0.0.1:8000/v1");
|
||||
return {};
|
||||
},
|
||||
);
|
||||
|
||||
const plan = await planOpenClawModelsJsonWithDeps(
|
||||
{
|
||||
cfg: {
|
||||
models: {
|
||||
providers: {
|
||||
vllm: {
|
||||
baseUrl: "http://127.0.0.1:8000/v1",
|
||||
api: "openai-completions",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
agentDir: "/tmp/openclaw-agent",
|
||||
env: { VLLM_API_KEY: "test-vllm-key" } as NodeJS.ProcessEnv,
|
||||
existingRaw: "",
|
||||
existingParsed: null,
|
||||
},
|
||||
{ resolveImplicitProviders },
|
||||
);
|
||||
|
||||
expect(resolveImplicitProviders).toHaveBeenCalledOnce();
|
||||
expect(plan).toEqual({
|
||||
action: "write",
|
||||
contents: `${JSON.stringify(
|
||||
{
|
||||
providers: {
|
||||
vllm: {
|
||||
baseUrl: "http://127.0.0.1:8000/v1",
|
||||
api: "openai-completions",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
});
|
||||
});
|
||||
|
||||
it("uses tokenRef env var when github-copilot profile omits plaintext token", () => {
|
||||
const auth = createProviderAuthResolver(
|
||||
{
|
||||
@@ -96,4 +149,59 @@ describe("models-config", () => {
|
||||
profileId: "github-copilot:default",
|
||||
});
|
||||
});
|
||||
|
||||
it("writes an implicit github-copilot provider discovered from a token exchange", async () => {
|
||||
const plan = await planCopilotWithImplicitProvider({
|
||||
provider: { baseUrl: "https://api.copilot.example", models: [] },
|
||||
});
|
||||
|
||||
expectCopilotProviderFromPlan(plan).toEqual({
|
||||
baseUrl: "https://api.copilot.example",
|
||||
models: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("writes default github-copilot baseUrl when the token exchange fails", async () => {
|
||||
const plan = await planCopilotWithImplicitProvider({
|
||||
provider: { baseUrl: "https://api.individual.githubcopilot.com", models: [] },
|
||||
});
|
||||
|
||||
expectCopilotProviderFromPlan(plan)?.toEqual({
|
||||
baseUrl: "https://api.individual.githubcopilot.com",
|
||||
models: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createCopilotImplicitResolver(
|
||||
provider: ProviderConfig,
|
||||
): ResolveImplicitProvidersForModelsJson {
|
||||
return async () => ({ "github-copilot": provider });
|
||||
}
|
||||
|
||||
async function planCopilotWithImplicitProvider(params: { provider: ProviderConfig }) {
|
||||
return await planOpenClawModelsJsonWithDeps(
|
||||
{
|
||||
cfg: { models: { providers: {} } },
|
||||
agentDir: "/tmp/openclaw-agent",
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
existingRaw: "",
|
||||
existingParsed: null,
|
||||
},
|
||||
{
|
||||
resolveImplicitProviders: createCopilotImplicitResolver(params.provider),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function expectCopilotProviderFromPlan(
|
||||
plan: Awaited<ReturnType<typeof planCopilotWithImplicitProvider>>,
|
||||
) {
|
||||
expect(plan.action).toBe("write");
|
||||
const parsed =
|
||||
plan.action === "write"
|
||||
? (JSON.parse(plan.contents) as { providers?: Record<string, unknown> })
|
||||
: {};
|
||||
expect(parsed.providers?.["github-copilot"]).toBeDefined();
|
||||
return expect(parsed.providers?.["github-copilot"]);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,11 @@ import {
|
||||
isAnthropicBillingError,
|
||||
isAnthropicRateLimitError,
|
||||
} from "./live-auth-keys.js";
|
||||
import { isHighSignalLiveModelRef, selectHighSignalLiveItems } from "./live-model-filter.js";
|
||||
import {
|
||||
isHighSignalLiveModelRef,
|
||||
resolveHighSignalLiveModelLimit,
|
||||
selectHighSignalLiveItems,
|
||||
} from "./live-model-filter.js";
|
||||
import { createLiveTargetMatcher } from "./live-target-matcher.js";
|
||||
import { isLiveProfileKeyModeEnabled, isLiveTestEnabled } from "./live-test-helpers.js";
|
||||
import { getApiKeyForModel, requireApiKey } from "./model-auth.js";
|
||||
@@ -148,7 +152,7 @@ function isModelNotFoundErrorMessage(raw: string): boolean {
|
||||
if (/does not exist or you do not have access/i.test(msg)) {
|
||||
return true;
|
||||
}
|
||||
if (/deprecated/i.test(msg) && /upgrade to/i.test(msg)) {
|
||||
if (/deprecated/i.test(msg) && /(upgrade|transition) to/i.test(msg)) {
|
||||
return true;
|
||||
}
|
||||
if (/stealth model/i.test(msg) && /find it here/i.test(msg)) {
|
||||
@@ -170,6 +174,14 @@ describe("isModelNotFoundErrorMessage", () => {
|
||||
expect(isModelNotFoundErrorMessage("404 model not_found")).toBe(true);
|
||||
expect(isModelNotFoundErrorMessage("404 model not-found")).toBe(true);
|
||||
});
|
||||
|
||||
it("matches deprecated free model transition messages", () => {
|
||||
expect(
|
||||
isModelNotFoundErrorMessage(
|
||||
"404 The free model has been deprecated. Transition to qwen/qwen3.6-plus for continued paid access.",
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
function isChatGPTUsageLimitErrorMessage(raw: string): boolean {
|
||||
@@ -412,7 +424,10 @@ describeLive("live models (profile keys)", () => {
|
||||
const allowNotFoundSkip = useModern;
|
||||
const providers = parseProviderFilter(process.env.OPENCLAW_LIVE_PROVIDERS);
|
||||
const perModelTimeoutMs = toInt(process.env.OPENCLAW_LIVE_MODEL_TIMEOUT_MS, 30_000);
|
||||
const maxModels = toInt(process.env.OPENCLAW_LIVE_MAX_MODELS, 0);
|
||||
const maxModels = resolveHighSignalLiveModelLimit({
|
||||
rawMaxModels: process.env.OPENCLAW_LIVE_MAX_MODELS,
|
||||
useExplicitModels: useExplicit,
|
||||
});
|
||||
const targetMatcher = createLiveTargetMatcher({
|
||||
providerFilter: providers,
|
||||
modelFilter: filter,
|
||||
@@ -781,6 +796,11 @@ describeLive("live models (profile keys)", () => {
|
||||
logProgress(`${progressLabel}: skip (provider unavailable)`);
|
||||
break;
|
||||
}
|
||||
if (allowNotFoundSkip && isModelNotFoundErrorMessage(message)) {
|
||||
skipped.push({ model: id, reason: message });
|
||||
logProgress(`${progressLabel}: skip (model not found)`);
|
||||
break;
|
||||
}
|
||||
if (allowNotFoundSkip && isAudioOnlyModelErrorMessage(message)) {
|
||||
skipped.push({ model: id, reason: message });
|
||||
logProgress(`${progressLabel}: skip (audio-only model)`);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { ensureOwnerDisplaySecret, resolveOwnerDisplaySetting } from "./owner-display.js";
|
||||
|
||||
describe("resolveOwnerDisplaySetting", () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import crypto from "node:crypto";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { normalizeOptionalString } from "../shared/string-coerce.js";
|
||||
|
||||
export type OwnerDisplaySetting = {
|
||||
|
||||
@@ -349,10 +349,17 @@ function resolveExplicitModelWithRegistry(params: {
|
||||
runtimeHooks?: ProviderRuntimeHooks;
|
||||
}): { kind: "resolved"; model: Model<Api> } | { kind: "suppressed" } | undefined {
|
||||
const { provider, modelId, modelRegistry, cfg, agentDir, runtimeHooks } = params;
|
||||
if (shouldSuppressBuiltInModel({ provider, id: modelId })) {
|
||||
const providerConfig = resolveConfiguredProviderConfig(cfg, provider);
|
||||
if (
|
||||
shouldSuppressBuiltInModel({
|
||||
provider,
|
||||
id: modelId,
|
||||
baseUrl: providerConfig?.baseUrl,
|
||||
config: cfg,
|
||||
})
|
||||
) {
|
||||
return { kind: "suppressed" };
|
||||
}
|
||||
const providerConfig = resolveConfiguredProviderConfig(cfg, provider);
|
||||
const inlineMatch = findInlineModelMatch({
|
||||
providers: cfg?.models?.providers ?? {},
|
||||
provider,
|
||||
|
||||
@@ -1,19 +1,49 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
|
||||
vi.unmock("../plugins/provider-runtime.js");
|
||||
vi.unmock("../plugins/provider-runtime.runtime.js");
|
||||
vi.unmock("../plugins/providers.runtime.js");
|
||||
|
||||
let resolveTranscriptPolicy: typeof import("./transcript-policy.js").resolveTranscriptPolicy;
|
||||
const MISTRAL_PLUGIN_CONFIG = {
|
||||
plugins: {
|
||||
entries: {
|
||||
mistral: { enabled: true },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
function createProviderRuntimeSmokeContext(): {
|
||||
config: OpenClawConfig;
|
||||
env: NodeJS.ProcessEnv;
|
||||
workspaceDir: string;
|
||||
} {
|
||||
const env = { ...process.env };
|
||||
delete env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
||||
delete env.OPENCLAW_SKIP_PROVIDERS;
|
||||
delete env.OPENCLAW_SKIP_CHANNELS;
|
||||
delete env.OPENCLAW_SKIP_CRON;
|
||||
delete env.OPENCLAW_TEST_MINIMAL_GATEWAY;
|
||||
return {
|
||||
config: {},
|
||||
env,
|
||||
workspaceDir: process.cwd(),
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
vi.doUnmock("../plugins/provider-runtime.js");
|
||||
vi.doUnmock("../plugins/provider-runtime.runtime.js");
|
||||
vi.doUnmock("../plugins/providers.runtime.js");
|
||||
({ resolveTranscriptPolicy } = await import("./transcript-policy.js"));
|
||||
});
|
||||
|
||||
describe("resolveTranscriptPolicy e2e smoke", () => {
|
||||
it("uses images-only sanitization without tool-call id rewriting for OpenAI models", () => {
|
||||
const policy = resolveTranscriptPolicy({
|
||||
...createProviderRuntimeSmokeContext(),
|
||||
provider: "openai",
|
||||
modelId: "gpt-4o",
|
||||
modelApi: "openai",
|
||||
@@ -25,8 +55,10 @@ describe("resolveTranscriptPolicy e2e smoke", () => {
|
||||
|
||||
it("uses strict9 tool-call sanitization for Mistral-family models", () => {
|
||||
const policy = resolveTranscriptPolicy({
|
||||
...createProviderRuntimeSmokeContext(),
|
||||
provider: "mistral",
|
||||
modelId: "mistral-large-latest",
|
||||
config: MISTRAL_PLUGIN_CONFIG,
|
||||
});
|
||||
expect(policy.sanitizeToolCallIds).toBe(true);
|
||||
expect(policy.toolCallIdMode).toBe("strict9");
|
||||
|
||||
234
src/auto-reply/command-status-builders.ts
Normal file
234
src/auto-reply/command-status-builders.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
import type { SkillCommandSpec } from "../agents/skills.js";
|
||||
import { getChannelPlugin } from "../channels/plugins/index.js";
|
||||
import { isCommandFlagEnabled } from "../config/commands.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { listPluginCommands } from "../plugins/commands.js";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalLowercaseString,
|
||||
normalizeOptionalString,
|
||||
} from "../shared/string-coerce.js";
|
||||
import {
|
||||
listChatCommands,
|
||||
listChatCommandsForConfig,
|
||||
type ChatCommandDefinition,
|
||||
} from "./commands-registry.js";
|
||||
import type { CommandCategory } from "./commands-registry.types.js";
|
||||
|
||||
const CATEGORY_LABELS: Record<CommandCategory, string> = {
|
||||
session: "Session",
|
||||
options: "Options",
|
||||
status: "Status",
|
||||
management: "Management",
|
||||
media: "Media",
|
||||
tools: "Tools",
|
||||
docks: "Docks",
|
||||
};
|
||||
|
||||
const CATEGORY_ORDER: CommandCategory[] = [
|
||||
"session",
|
||||
"options",
|
||||
"status",
|
||||
"management",
|
||||
"media",
|
||||
"tools",
|
||||
"docks",
|
||||
];
|
||||
|
||||
function groupCommandsByCategory(
|
||||
commands: ChatCommandDefinition[],
|
||||
): Map<CommandCategory, ChatCommandDefinition[]> {
|
||||
const grouped = new Map<CommandCategory, ChatCommandDefinition[]>();
|
||||
for (const category of CATEGORY_ORDER) {
|
||||
grouped.set(category, []);
|
||||
}
|
||||
for (const command of commands) {
|
||||
const category = command.category ?? "tools";
|
||||
const list = grouped.get(category) ?? [];
|
||||
list.push(command);
|
||||
grouped.set(category, list);
|
||||
}
|
||||
return grouped;
|
||||
}
|
||||
|
||||
export function buildHelpMessage(cfg?: OpenClawConfig): string {
|
||||
const lines = ["ℹ️ Help", ""];
|
||||
|
||||
lines.push("Session");
|
||||
lines.push(" /new | /reset | /compact [instructions] | /stop");
|
||||
lines.push("");
|
||||
|
||||
const optionParts = ["/think <level>", "/model <id>", "/fast status|on|off", "/verbose on|off"];
|
||||
if (isCommandFlagEnabled(cfg, "config")) {
|
||||
optionParts.push("/config");
|
||||
}
|
||||
if (isCommandFlagEnabled(cfg, "debug")) {
|
||||
optionParts.push("/debug");
|
||||
}
|
||||
lines.push("Options");
|
||||
lines.push(` ${optionParts.join(" | ")}`);
|
||||
lines.push("");
|
||||
|
||||
lines.push("Status");
|
||||
lines.push(" /status | /tasks | /whoami | /context");
|
||||
lines.push("");
|
||||
|
||||
lines.push("Skills");
|
||||
lines.push(" /skill <name> [input]");
|
||||
|
||||
lines.push("");
|
||||
lines.push("More: /commands for full list, /tools for available capabilities");
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
const COMMANDS_PER_PAGE = 8;
|
||||
|
||||
export type CommandsMessageOptions = {
|
||||
page?: number;
|
||||
surface?: string;
|
||||
forcePaginatedList?: boolean;
|
||||
};
|
||||
|
||||
export type CommandsMessageResult = {
|
||||
text: string;
|
||||
totalPages: number;
|
||||
currentPage: number;
|
||||
hasNext: boolean;
|
||||
hasPrev: boolean;
|
||||
};
|
||||
|
||||
function formatCommandEntry(command: ChatCommandDefinition): string {
|
||||
const primary = command.nativeName
|
||||
? `/${command.nativeName}`
|
||||
: normalizeOptionalString(command.textAliases[0]) || `/${command.key}`;
|
||||
const seen = new Set<string>();
|
||||
const aliases = command.textAliases
|
||||
.map((alias) => alias.trim())
|
||||
.filter(Boolean)
|
||||
.filter(
|
||||
(alias) =>
|
||||
normalizeLowercaseStringOrEmpty(alias) !== normalizeLowercaseStringOrEmpty(primary),
|
||||
)
|
||||
.filter((alias) => {
|
||||
const key = normalizeLowercaseStringOrEmpty(alias);
|
||||
if (seen.has(key)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
const aliasLabel = aliases.length ? ` (${aliases.join(", ")})` : "";
|
||||
const scopeLabel = command.scope === "text" ? " [text]" : "";
|
||||
return `${primary}${aliasLabel}${scopeLabel} - ${command.description}`;
|
||||
}
|
||||
|
||||
type CommandsListItem = {
|
||||
label: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
function buildCommandItems(
|
||||
commands: ChatCommandDefinition[],
|
||||
pluginCommands: ReturnType<typeof listPluginCommands>,
|
||||
): CommandsListItem[] {
|
||||
const grouped = groupCommandsByCategory(commands);
|
||||
const items: CommandsListItem[] = [];
|
||||
|
||||
for (const category of CATEGORY_ORDER) {
|
||||
const categoryCommands = grouped.get(category) ?? [];
|
||||
if (categoryCommands.length === 0) {
|
||||
continue;
|
||||
}
|
||||
const label = CATEGORY_LABELS[category];
|
||||
for (const command of categoryCommands) {
|
||||
items.push({ label, text: formatCommandEntry(command) });
|
||||
}
|
||||
}
|
||||
|
||||
for (const command of pluginCommands) {
|
||||
const pluginLabel = command.pluginId ? ` (${command.pluginId})` : "";
|
||||
items.push({
|
||||
label: "Plugins",
|
||||
text: `/${command.name}${pluginLabel} - ${command.description}`,
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
function formatCommandList(items: CommandsListItem[]): string {
|
||||
const lines: string[] = [];
|
||||
let currentLabel: string | null = null;
|
||||
|
||||
for (const item of items) {
|
||||
if (item.label !== currentLabel) {
|
||||
if (lines.length > 0) {
|
||||
lines.push("");
|
||||
}
|
||||
lines.push(item.label);
|
||||
currentLabel = item.label;
|
||||
}
|
||||
lines.push(` ${item.text}`);
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
export function buildCommandsMessage(
|
||||
cfg?: OpenClawConfig,
|
||||
skillCommands?: SkillCommandSpec[],
|
||||
options?: CommandsMessageOptions,
|
||||
): string {
|
||||
const result = buildCommandsMessagePaginated(cfg, skillCommands, options);
|
||||
return result.text;
|
||||
}
|
||||
|
||||
export function buildCommandsMessagePaginated(
|
||||
cfg?: OpenClawConfig,
|
||||
skillCommands?: SkillCommandSpec[],
|
||||
options?: CommandsMessageOptions,
|
||||
): CommandsMessageResult {
|
||||
const page = Math.max(1, options?.page ?? 1);
|
||||
const surface = normalizeOptionalLowercaseString(options?.surface);
|
||||
const prefersPaginatedList =
|
||||
options?.forcePaginatedList === true ||
|
||||
Boolean(surface && getChannelPlugin(surface)?.commands?.buildCommandsListChannelData);
|
||||
|
||||
const commands = cfg
|
||||
? listChatCommandsForConfig(cfg, { skillCommands })
|
||||
: listChatCommands({ skillCommands });
|
||||
const pluginCommands = listPluginCommands();
|
||||
const items = buildCommandItems(commands, pluginCommands);
|
||||
|
||||
if (!prefersPaginatedList) {
|
||||
const lines = ["ℹ️ Slash commands", ""];
|
||||
lines.push(formatCommandList(items));
|
||||
lines.push("", "More: /tools for available capabilities");
|
||||
return {
|
||||
text: lines.join("\n").trim(),
|
||||
totalPages: 1,
|
||||
currentPage: 1,
|
||||
hasNext: false,
|
||||
hasPrev: false,
|
||||
};
|
||||
}
|
||||
|
||||
const totalCommands = items.length;
|
||||
const totalPages = Math.max(1, Math.ceil(totalCommands / COMMANDS_PER_PAGE));
|
||||
const currentPage = Math.min(page, totalPages);
|
||||
const startIndex = (currentPage - 1) * COMMANDS_PER_PAGE;
|
||||
const endIndex = startIndex + COMMANDS_PER_PAGE;
|
||||
const pageItems = items.slice(startIndex, endIndex);
|
||||
|
||||
const lines = [`ℹ️ Commands (${currentPage}/${totalPages})`, ""];
|
||||
lines.push(formatCommandList(pageItems));
|
||||
|
||||
return {
|
||||
text: lines.join("\n").trim(),
|
||||
totalPages,
|
||||
currentPage,
|
||||
hasNext: currentPage < totalPages,
|
||||
hasPrev: currentPage > 1,
|
||||
};
|
||||
}
|
||||
@@ -15,15 +15,12 @@ import {
|
||||
resolveReasoningDefault,
|
||||
resolveThinkingDefault,
|
||||
} from "../../agents/model-selection.js";
|
||||
import { resolveSessionParentSessionKey } from "../../channels/plugins/session-conversation.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import type { SessionEntry } from "../../config/sessions/types.js";
|
||||
import { applyModelOverrideToSessionEntry } from "../../sessions/model-overrides.js";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalString,
|
||||
} from "../../shared/string-coerce.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
|
||||
import type { ThinkLevel } from "./directives.js";
|
||||
import { resolveStoredModelOverride } from "./stored-model-override.js";
|
||||
|
||||
export type ModelDirectiveSelection = {
|
||||
provider: string;
|
||||
@@ -142,61 +139,6 @@ function boundedLevenshteinDistance(a: string, b: string, maxDistance: number):
|
||||
return dist;
|
||||
}
|
||||
|
||||
export type StoredModelOverride = {
|
||||
provider?: string;
|
||||
model: string;
|
||||
source: "session" | "parent";
|
||||
};
|
||||
|
||||
function resolveParentSessionKeyCandidate(params: {
|
||||
sessionKey?: string;
|
||||
parentSessionKey?: string;
|
||||
}): string | null {
|
||||
const explicit = normalizeOptionalString(params.parentSessionKey);
|
||||
if (explicit && explicit !== params.sessionKey) {
|
||||
return explicit;
|
||||
}
|
||||
const derived = resolveSessionParentSessionKey(params.sessionKey);
|
||||
if (derived && derived !== params.sessionKey) {
|
||||
return derived;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function resolveStoredModelOverride(params: {
|
||||
sessionEntry?: SessionEntry;
|
||||
sessionStore?: Record<string, SessionEntry>;
|
||||
sessionKey?: string;
|
||||
parentSessionKey?: string;
|
||||
defaultProvider: string;
|
||||
}): StoredModelOverride | null {
|
||||
const direct = resolvePersistedOverrideModelRef({
|
||||
defaultProvider: params.defaultProvider,
|
||||
overrideProvider: params.sessionEntry?.providerOverride,
|
||||
overrideModel: params.sessionEntry?.modelOverride,
|
||||
});
|
||||
if (direct) {
|
||||
return { ...direct, source: "session" };
|
||||
}
|
||||
const parentKey = resolveParentSessionKeyCandidate({
|
||||
sessionKey: params.sessionKey,
|
||||
parentSessionKey: params.parentSessionKey,
|
||||
});
|
||||
if (!parentKey || !params.sessionStore) {
|
||||
return null;
|
||||
}
|
||||
const parentEntry = params.sessionStore[parentKey];
|
||||
const parentOverride = resolvePersistedOverrideModelRef({
|
||||
defaultProvider: params.defaultProvider,
|
||||
overrideProvider: parentEntry?.providerOverride,
|
||||
overrideModel: parentEntry?.modelOverride,
|
||||
});
|
||||
if (!parentOverride) {
|
||||
return null;
|
||||
}
|
||||
return { ...parentOverride, source: "parent" };
|
||||
}
|
||||
|
||||
function scoreFuzzyMatch(params: {
|
||||
provider: string;
|
||||
model: string;
|
||||
|
||||
59
src/auto-reply/reply/stored-model-override.ts
Normal file
59
src/auto-reply/reply/stored-model-override.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { resolvePersistedOverrideModelRef } from "../../agents/model-selection.js";
|
||||
import { resolveSessionParentSessionKey } from "../../channels/plugins/session-conversation.js";
|
||||
import type { SessionEntry } from "../../config/sessions/types.js";
|
||||
import { normalizeOptionalString } from "../../shared/string-coerce.js";
|
||||
|
||||
export type StoredModelOverride = {
|
||||
provider?: string;
|
||||
model: string;
|
||||
source: "session" | "parent";
|
||||
};
|
||||
|
||||
function resolveParentSessionKeyCandidate(params: {
|
||||
sessionKey?: string;
|
||||
parentSessionKey?: string;
|
||||
}): string | null {
|
||||
const explicit = normalizeOptionalString(params.parentSessionKey);
|
||||
if (explicit && explicit !== params.sessionKey) {
|
||||
return explicit;
|
||||
}
|
||||
const derived = resolveSessionParentSessionKey(params.sessionKey);
|
||||
if (derived && derived !== params.sessionKey) {
|
||||
return derived;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function resolveStoredModelOverride(params: {
|
||||
sessionEntry?: SessionEntry;
|
||||
sessionStore?: Record<string, SessionEntry>;
|
||||
sessionKey?: string;
|
||||
parentSessionKey?: string;
|
||||
defaultProvider: string;
|
||||
}): StoredModelOverride | null {
|
||||
const direct = resolvePersistedOverrideModelRef({
|
||||
defaultProvider: params.defaultProvider,
|
||||
overrideProvider: params.sessionEntry?.providerOverride,
|
||||
overrideModel: params.sessionEntry?.modelOverride,
|
||||
});
|
||||
if (direct) {
|
||||
return { ...direct, source: "session" };
|
||||
}
|
||||
const parentKey = resolveParentSessionKeyCandidate({
|
||||
sessionKey: params.sessionKey,
|
||||
parentSessionKey: params.parentSessionKey,
|
||||
});
|
||||
if (!parentKey || !params.sessionStore) {
|
||||
return null;
|
||||
}
|
||||
const parentEntry = params.sessionStore[parentKey];
|
||||
const parentOverride = resolvePersistedOverrideModelRef({
|
||||
defaultProvider: params.defaultProvider,
|
||||
overrideProvider: parentEntry?.providerOverride,
|
||||
overrideModel: parentEntry?.modelOverride,
|
||||
});
|
||||
if (!parentOverride) {
|
||||
return null;
|
||||
}
|
||||
return { ...parentOverride, source: "parent" };
|
||||
}
|
||||
@@ -10,13 +10,10 @@ import {
|
||||
import { resolveExtraParams } from "../agents/pi-embedded-runner/extra-params.js";
|
||||
import { resolveOpenAITextVerbosity } from "../agents/pi-embedded-runner/openai-stream-wrappers.js";
|
||||
import { resolveSandboxRuntimeStatus } from "../agents/sandbox.js";
|
||||
import type { SkillCommandSpec } from "../agents/skills.js";
|
||||
import { describeToolForVerbose } from "../agents/tool-description-summary.js";
|
||||
import { normalizeToolName } from "../agents/tool-policy-shared.js";
|
||||
import type { EffectiveToolInventoryResult } from "../agents/tools-effective-inventory.js";
|
||||
import { resolveChannelModelOverride } from "../channels/model-overrides.js";
|
||||
import { getChannelPlugin } from "../channels/plugins/index.js";
|
||||
import { isCommandFlagEnabled } from "../config/commands.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import {
|
||||
resolveMainSessionKey,
|
||||
@@ -29,7 +26,6 @@ import { readLatestSessionUsageFromTranscript } from "../gateway/session-utils.f
|
||||
import { formatTimeAgo } from "../infra/format-time/format-relative.ts";
|
||||
import { resolveCommitHash } from "../infra/git-commit.js";
|
||||
import type { MediaUnderstandingDecision } from "../media-understanding/types.js";
|
||||
import { listPluginCommands } from "../plugins/commands.js";
|
||||
import { resolveAgentIdFromSessionKey } from "../routing/session-key.js";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
@@ -44,12 +40,13 @@ import {
|
||||
resolveModelCostConfig,
|
||||
} from "../utils/usage-format.js";
|
||||
import { VERSION } from "../version.js";
|
||||
import {
|
||||
listChatCommands,
|
||||
listChatCommandsForConfig,
|
||||
type ChatCommandDefinition,
|
||||
} from "./commands-registry.js";
|
||||
import type { CommandCategory } from "./commands-registry.types.js";
|
||||
export {
|
||||
buildCommandsMessage,
|
||||
buildCommandsMessagePaginated,
|
||||
buildHelpMessage,
|
||||
type CommandsMessageOptions,
|
||||
type CommandsMessageResult,
|
||||
} from "./command-status-builders.js";
|
||||
import { resolveActiveFallbackState } from "./fallback-state.js";
|
||||
import { formatProviderModelRef, resolveSelectedAndActiveModel } from "./model-runtime.js";
|
||||
import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "./thinking.js";
|
||||
@@ -829,89 +826,6 @@ export function buildStatusMessage(args: StatusArgs): string {
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
const CATEGORY_LABELS: Record<CommandCategory, string> = {
|
||||
session: "Session",
|
||||
options: "Options",
|
||||
status: "Status",
|
||||
management: "Management",
|
||||
media: "Media",
|
||||
tools: "Tools",
|
||||
docks: "Docks",
|
||||
};
|
||||
|
||||
const CATEGORY_ORDER: CommandCategory[] = [
|
||||
"session",
|
||||
"options",
|
||||
"status",
|
||||
"management",
|
||||
"media",
|
||||
"tools",
|
||||
"docks",
|
||||
];
|
||||
|
||||
function groupCommandsByCategory(
|
||||
commands: ChatCommandDefinition[],
|
||||
): Map<CommandCategory, ChatCommandDefinition[]> {
|
||||
const grouped = new Map<CommandCategory, ChatCommandDefinition[]>();
|
||||
for (const category of CATEGORY_ORDER) {
|
||||
grouped.set(category, []);
|
||||
}
|
||||
for (const command of commands) {
|
||||
const category = command.category ?? "tools";
|
||||
const list = grouped.get(category) ?? [];
|
||||
list.push(command);
|
||||
grouped.set(category, list);
|
||||
}
|
||||
return grouped;
|
||||
}
|
||||
|
||||
export function buildHelpMessage(cfg?: OpenClawConfig): string {
|
||||
const lines = ["ℹ️ Help", ""];
|
||||
|
||||
lines.push("Session");
|
||||
lines.push(" /new | /reset | /compact [instructions] | /stop");
|
||||
lines.push("");
|
||||
|
||||
const optionParts = ["/think <level>", "/model <id>", "/fast status|on|off", "/verbose on|off"];
|
||||
if (isCommandFlagEnabled(cfg, "config")) {
|
||||
optionParts.push("/config");
|
||||
}
|
||||
if (isCommandFlagEnabled(cfg, "debug")) {
|
||||
optionParts.push("/debug");
|
||||
}
|
||||
lines.push("Options");
|
||||
lines.push(` ${optionParts.join(" | ")}`);
|
||||
lines.push("");
|
||||
|
||||
lines.push("Status");
|
||||
lines.push(" /status | /tasks | /whoami | /context");
|
||||
lines.push("");
|
||||
|
||||
lines.push("Skills");
|
||||
lines.push(" /skill <name> [input]");
|
||||
|
||||
lines.push("");
|
||||
lines.push("More: /commands for full list, /tools for available capabilities");
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
const COMMANDS_PER_PAGE = 8;
|
||||
|
||||
export type CommandsMessageOptions = {
|
||||
page?: number;
|
||||
surface?: string;
|
||||
forcePaginatedList?: boolean;
|
||||
};
|
||||
|
||||
export type CommandsMessageResult = {
|
||||
text: string;
|
||||
totalPages: number;
|
||||
currentPage: number;
|
||||
hasNext: boolean;
|
||||
hasPrev: boolean;
|
||||
};
|
||||
|
||||
type ToolsMessageItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -996,138 +910,3 @@ export function buildToolsMessage(
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function formatCommandEntry(command: ChatCommandDefinition): string {
|
||||
const primary = command.nativeName
|
||||
? `/${command.nativeName}`
|
||||
: normalizeOptionalString(command.textAliases[0]) || `/${command.key}`;
|
||||
const seen = new Set<string>();
|
||||
const aliases = command.textAliases
|
||||
.map((alias) => alias.trim())
|
||||
.filter(Boolean)
|
||||
.filter(
|
||||
(alias) =>
|
||||
normalizeLowercaseStringOrEmpty(alias) !== normalizeLowercaseStringOrEmpty(primary),
|
||||
)
|
||||
.filter((alias) => {
|
||||
const key = normalizeLowercaseStringOrEmpty(alias);
|
||||
if (seen.has(key)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
const aliasLabel = aliases.length ? ` (${aliases.join(", ")})` : "";
|
||||
const scopeLabel = command.scope === "text" ? " [text]" : "";
|
||||
return `${primary}${aliasLabel}${scopeLabel} - ${command.description}`;
|
||||
}
|
||||
|
||||
type CommandsListItem = {
|
||||
label: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
function buildCommandItems(
|
||||
commands: ChatCommandDefinition[],
|
||||
pluginCommands: ReturnType<typeof listPluginCommands>,
|
||||
): CommandsListItem[] {
|
||||
const grouped = groupCommandsByCategory(commands);
|
||||
const items: CommandsListItem[] = [];
|
||||
|
||||
for (const category of CATEGORY_ORDER) {
|
||||
const categoryCommands = grouped.get(category) ?? [];
|
||||
if (categoryCommands.length === 0) {
|
||||
continue;
|
||||
}
|
||||
const label = CATEGORY_LABELS[category];
|
||||
for (const command of categoryCommands) {
|
||||
items.push({ label, text: formatCommandEntry(command) });
|
||||
}
|
||||
}
|
||||
|
||||
for (const command of pluginCommands) {
|
||||
const pluginLabel = command.pluginId ? ` (${command.pluginId})` : "";
|
||||
items.push({
|
||||
label: "Plugins",
|
||||
text: `/${command.name}${pluginLabel} - ${command.description}`,
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
function formatCommandList(items: CommandsListItem[]): string {
|
||||
const lines: string[] = [];
|
||||
let currentLabel: string | null = null;
|
||||
|
||||
for (const item of items) {
|
||||
if (item.label !== currentLabel) {
|
||||
if (lines.length > 0) {
|
||||
lines.push("");
|
||||
}
|
||||
lines.push(item.label);
|
||||
currentLabel = item.label;
|
||||
}
|
||||
lines.push(` ${item.text}`);
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
export function buildCommandsMessage(
|
||||
cfg?: OpenClawConfig,
|
||||
skillCommands?: SkillCommandSpec[],
|
||||
options?: CommandsMessageOptions,
|
||||
): string {
|
||||
const result = buildCommandsMessagePaginated(cfg, skillCommands, options);
|
||||
return result.text;
|
||||
}
|
||||
|
||||
export function buildCommandsMessagePaginated(
|
||||
cfg?: OpenClawConfig,
|
||||
skillCommands?: SkillCommandSpec[],
|
||||
options?: CommandsMessageOptions,
|
||||
): CommandsMessageResult {
|
||||
const page = Math.max(1, options?.page ?? 1);
|
||||
const surface = normalizeOptionalLowercaseString(options?.surface);
|
||||
const prefersPaginatedList =
|
||||
options?.forcePaginatedList === true ||
|
||||
Boolean(surface && getChannelPlugin(surface)?.commands?.buildCommandsListChannelData);
|
||||
|
||||
const commands = cfg
|
||||
? listChatCommandsForConfig(cfg, { skillCommands })
|
||||
: listChatCommands({ skillCommands });
|
||||
const pluginCommands = listPluginCommands();
|
||||
const items = buildCommandItems(commands, pluginCommands);
|
||||
|
||||
if (!prefersPaginatedList) {
|
||||
const lines = ["ℹ️ Slash commands", ""];
|
||||
lines.push(formatCommandList(items));
|
||||
lines.push("", "More: /tools for available capabilities");
|
||||
return {
|
||||
text: lines.join("\n").trim(),
|
||||
totalPages: 1,
|
||||
currentPage: 1,
|
||||
hasNext: false,
|
||||
hasPrev: false,
|
||||
};
|
||||
}
|
||||
|
||||
const totalCommands = items.length;
|
||||
const totalPages = Math.max(1, Math.ceil(totalCommands / COMMANDS_PER_PAGE));
|
||||
const currentPage = Math.min(page, totalPages);
|
||||
const startIndex = (currentPage - 1) * COMMANDS_PER_PAGE;
|
||||
const endIndex = startIndex + COMMANDS_PER_PAGE;
|
||||
const pageItems = items.slice(startIndex, endIndex);
|
||||
|
||||
const lines = [`ℹ️ Commands (${currentPage}/${totalPages})`, ""];
|
||||
lines.push(formatCommandList(pageItems));
|
||||
|
||||
return {
|
||||
text: lines.join("\n").trim(),
|
||||
totalPages,
|
||||
currentPage,
|
||||
hasNext: currentPage < totalPages,
|
||||
hasPrev: currentPage > 1,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -886,7 +886,7 @@ describe("update-cli", () => {
|
||||
portableGitMingw,
|
||||
portableGitUsr,
|
||||
]);
|
||||
expect(updateOptions?.env?.NPM_CONFIG_SCRIPT_SHELL).toBe("cmd.exe");
|
||||
expect(updateOptions?.env?.NPM_CONFIG_SCRIPT_SHELL).toBeUndefined();
|
||||
expect(updateOptions?.env?.NODE_LLAMA_CPP_SKIP_DOWNLOAD).toBe("1");
|
||||
});
|
||||
|
||||
|
||||
@@ -22,6 +22,16 @@ import {
|
||||
} from "./agent-command.test-support.js";
|
||||
import { agentCommand } from "./agent.js";
|
||||
|
||||
vi.mock("../agents/auth-profiles/store.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../agents/auth-profiles/store.js")>(
|
||||
"../agents/auth-profiles/store.js",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
ensureAuthProfileStore: vi.fn(() => ({ version: 1, profiles: {} })),
|
||||
};
|
||||
});
|
||||
|
||||
const runtime: RuntimeEnv = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
|
||||
@@ -55,6 +55,16 @@ vi.mock("../agents/auth-profiles.js", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../agents/auth-profiles/store.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../agents/auth-profiles/store.js")>(
|
||||
"../agents/auth-profiles/store.js",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
ensureAuthProfileStore: vi.fn(() => ({ version: 1, profiles: {} })),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../agents/workspace.js", () => {
|
||||
const resolveDefaultAgentWorkspaceDir = () => "/tmp/openclaw-workspace";
|
||||
return {
|
||||
@@ -950,14 +960,6 @@ describe("agentCommand", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves topic transcript suffix when persisting missing sessionFile", async () => {
|
||||
await expectPersistedSessionFile({
|
||||
seedKey: "agent:main:telegram:group:123:topic:456",
|
||||
sessionId: "sess-topic",
|
||||
expectedPathFragment: `${path.sep}agents${path.sep}main${path.sep}sessions${path.sep}sess-topic.jsonl`,
|
||||
});
|
||||
});
|
||||
|
||||
it("derives session key from --agent when no routing target is provided", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const callArgs = await runWithDefaultAgentConfig({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ApplyAuthChoiceParams } from "./auth-choice.apply.js";
|
||||
import type { ApplyAuthChoiceParams } from "./auth-choice.apply.types.js";
|
||||
import { applyDefaultModelChoice } from "./auth-choice.default-model.js";
|
||||
|
||||
export type {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { resolveProviderMatch } from "../plugins/provider-auth-choice-helpers.js
|
||||
import { resolvePluginProviders } from "../plugins/provider-auth-choice.runtime.js";
|
||||
import type { ProviderAuthKind } from "../plugins/types.js";
|
||||
import { normalizeTokenProviderInput } from "./auth-choice.apply-helpers.js";
|
||||
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js";
|
||||
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.types.js";
|
||||
import type { AuthChoice } from "./onboard-types.js";
|
||||
|
||||
function resolveProviderAuthChoiceByKind(params: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js";
|
||||
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.types.js";
|
||||
|
||||
export async function applyAuthChoiceOAuth(
|
||||
_params: ApplyAuthChoiceParams,
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from "../plugins/provider-auth-choice.js";
|
||||
import type { ProviderPlugin } from "../plugins/types.js";
|
||||
import type { ProviderAuthMethod } from "../plugins/types.js";
|
||||
import type { ApplyAuthChoiceParams } from "./auth-choice.apply.js";
|
||||
import type { ApplyAuthChoiceParams } from "./auth-choice.apply.types.js";
|
||||
|
||||
const resolvePluginProviders = vi.hoisted(() => vi.fn<() => ProviderPlugin[]>(() => []));
|
||||
const resolveProviderPluginChoice = vi.hoisted(() =>
|
||||
|
||||
@@ -1,29 +1,11 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { applyAuthChoiceLoadedPluginProvider } from "../plugins/provider-auth-choice.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||
import { normalizeLegacyOnboardAuthChoice } from "./auth-choice-legacy.js";
|
||||
import { applyAuthChoiceApiProviders } from "./auth-choice.apply.api-providers.js";
|
||||
import { normalizeApiKeyTokenProviderAuthChoice } from "./auth-choice.apply.api-providers.js";
|
||||
import { applyAuthChoiceOAuth } from "./auth-choice.apply.oauth.js";
|
||||
import type { AuthChoice, OnboardOptions } from "./onboard-types.js";
|
||||
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.types.js";
|
||||
|
||||
export type ApplyAuthChoiceParams = {
|
||||
authChoice: AuthChoice;
|
||||
config: OpenClawConfig;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
prompter: WizardPrompter;
|
||||
runtime: RuntimeEnv;
|
||||
agentDir?: string;
|
||||
setDefaultModel: boolean;
|
||||
agentId?: string;
|
||||
opts?: Partial<OnboardOptions>;
|
||||
};
|
||||
|
||||
export type ApplyAuthChoiceResult = {
|
||||
config: OpenClawConfig;
|
||||
agentModelOverride?: string;
|
||||
};
|
||||
export type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.types.js";
|
||||
|
||||
export async function applyAuthChoice(
|
||||
params: ApplyAuthChoiceParams,
|
||||
|
||||
21
src/commands/auth-choice.apply.types.ts
Normal file
21
src/commands/auth-choice.apply.types.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||
import type { AuthChoice, OnboardOptions } from "./onboard-types.js";
|
||||
|
||||
export type ApplyAuthChoiceParams = {
|
||||
authChoice: AuthChoice;
|
||||
config: OpenClawConfig;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
prompter: WizardPrompter;
|
||||
runtime: RuntimeEnv;
|
||||
agentDir?: string;
|
||||
setDefaultModel: boolean;
|
||||
agentId?: string;
|
||||
opts?: Partial<OnboardOptions>;
|
||||
};
|
||||
|
||||
export type ApplyAuthChoiceResult = {
|
||||
config: OpenClawConfig;
|
||||
agentModelOverride?: string;
|
||||
};
|
||||
@@ -81,7 +81,7 @@ function validateAvailableModels(availableModels: unknown): Model<Api>[] {
|
||||
return availableModels as Model<Api>[];
|
||||
}
|
||||
|
||||
function loadAvailableModels(registry: ModelRegistry): Model<Api>[] {
|
||||
function loadAvailableModels(registry: ModelRegistry, cfg: OpenClawConfig): Model<Api>[] {
|
||||
let availableModels: unknown;
|
||||
try {
|
||||
availableModels = registry.getAvailable();
|
||||
@@ -90,7 +90,13 @@ function loadAvailableModels(registry: ModelRegistry): Model<Api>[] {
|
||||
}
|
||||
try {
|
||||
return validateAvailableModels(availableModels).filter(
|
||||
(model) => !shouldSuppressBuiltInModel({ provider: model.provider, id: model.id }),
|
||||
(model) =>
|
||||
!shouldSuppressBuiltInModel({
|
||||
provider: model.provider,
|
||||
id: model.id,
|
||||
baseUrl: model.baseUrl,
|
||||
config: cfg,
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
throw normalizeAvailabilityError(err);
|
||||
@@ -98,20 +104,26 @@ function loadAvailableModels(registry: ModelRegistry): Model<Api>[] {
|
||||
}
|
||||
|
||||
export async function loadModelRegistry(
|
||||
_cfg: OpenClawConfig,
|
||||
cfg: OpenClawConfig,
|
||||
_opts?: { sourceConfig?: OpenClawConfig },
|
||||
) {
|
||||
const agentDir = resolveOpenClawAgentDir();
|
||||
const authStorage = discoverAuthStorage(agentDir);
|
||||
const registry = discoverModels(authStorage, agentDir);
|
||||
const models = registry
|
||||
.getAll()
|
||||
.filter((model) => !shouldSuppressBuiltInModel({ provider: model.provider, id: model.id }));
|
||||
const models = registry.getAll().filter(
|
||||
(model) =>
|
||||
!shouldSuppressBuiltInModel({
|
||||
provider: model.provider,
|
||||
id: model.id,
|
||||
baseUrl: model.baseUrl,
|
||||
config: cfg,
|
||||
}),
|
||||
);
|
||||
let availableKeys: Set<string> | undefined;
|
||||
let availabilityErrorMessage: string | undefined;
|
||||
|
||||
try {
|
||||
const availableModels = loadAvailableModels(registry);
|
||||
const availableModels = loadAvailableModels(registry, cfg);
|
||||
availableKeys = new Set(availableModels.map((model) => modelKey(model.provider, model.id)));
|
||||
} catch (err) {
|
||||
if (!shouldFallbackToAuthHeuristics(err)) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user