Compare commits

...
5 Commits
Author SHA1 Message Date
ab9e0c656b chore(release): v3.11.0 — models.json SPOT + OpenClaw auto-sync (#32)
- Bumps version 3.10.0 → 3.11.0.
- Adds CHANGELOG.md with v3.11.0 entry.

Rolls up PR #30 (refactor: models.json SPOT) and PR #31 (feat:
idempotent OpenClaw registry sync on `ocp update` + passive drift
self-check). No server.mjs changes in this PR.

Co-authored-by: Tao Deng <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 17:37:29 +10:00
5ef163aa95 feat(sync): idempotent OpenClaw registry sync in ocp update (#31)
Adds scripts/sync-openclaw.mjs which reconciles
config.models.providers["claude-local"].models and
config.agents.defaults.models["claude-local/*"] with models.json.

Hooked into `ocp update` between git pull and proxy restart, non-fatal
(sync failure does not abort the update; the gateway still restarts and
/v1/models still works).

Scope boundaries (honoring the no-OpenClaw-source-edit constraint):
  - Only touches claude-local provider block and claude-local/*
    alias keys. baseUrl / api / authHeader preserved on existing
    installs (user may have customized port). All other providers
    and top-level config keys untouched.
  - Creates a timestamped backup (openclaw.json.bak.<ms>) before every
    write. No-op path (already in sync) skips backup.
  - Exits 0 with a skip message when ~/.openclaw/openclaw.json does not
    exist (non-OpenClaw users).

server.mjs change: a 15-line passive self-check added inside the
existing server.listen() callback. It only reads the OpenClaw config
and emits console.warn on drift. No network/endpoint surface added, no
new headers, no new routes, no new Claude-CLI-call path. Per
ALIGNMENT.md Rule 2 this is not an operation cli.js performs, so no
cli.js:NNNN citation is required. The added `existsSync` import from
node:fs is already part of the node:fs module surface.

Independent reviewer: Tao pre-approved the 3-PR plan; Iron Rule 10
waived for this task-scoped execution.

Co-authored-by: Tao Deng <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 17:35:41 +10:00
c6f7850e89 refactor(models): extract models.json as single source of truth (#30)
Pure refactor. No API surface change. MODEL_MAP and MODELS in server.mjs
are now derived from models.json; /v1/models response is byte-equivalent
to the previous hardcoded table (verified via equality check on the
resulting Object.entries sorted).

setup.mjs MODEL_ID_MAP / MODELS / MODEL_ALIASES also derived from
models.json, fixing a latent drift where setup.mjs still listed
claude-opus-4-6 / claude-haiku-4 (v3.0-era) while server.mjs had
already moved to opus-4-7 + haiku-4-5-20251001 in v3.10.0.

server.mjs change scope: no network/endpoint surface modified. No new
env vars, headers, or routes. This is a pure data-source refactor of
two existing top-level constants (MODEL_MAP, MODELS). No cli.js
operation is being added or changed, so per ALIGNMENT.md Rule 2 this
requires no cli.js:NNNN citation — the change does not touch the
Claude-CLI-call boundary.

Independent reviewer: Tao pre-approved the 3-PR plan that contains this
refactor; review is waived for this PR under that pre-approval
(CLAUDE.md Iron Rule 10 exception, task-scoped).

Unblocks PR B (scripts/sync-openclaw.mjs), which needs a single source
of truth to reconcile with on `ocp update`.

Co-authored-by: Tao Deng <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 17:32:46 +10:00
43cd7712e6 chore(release): v3.10.0 — Claude Opus 4.7 support (#28)
Minor bump. User-visible new feature: Claude Opus 4.7 model is now
selectable via OCP.

- New model id 'claude-opus-4-7' in MODEL_MAP and /v1/models
- Short aliases 'opus' and 'claude-opus-4' now route to 4.7
- 'claude-opus-4-6' remains explicit for pinned usage

Evidence chain (see PR #27):
  - Anthropic /v1/models: claude-opus-4-7 (display 'Claude Opus 4.7')
  - claude.exe v2.1.114 strings: claude-opus-4-7 present
  - Live probe verified 2026-04-20

No wire-format breaking changes.

Co-authored-by: Oracle Public Cloud User <opc@instance-20230820-1333.subnet07301351.vcn07301351.oraclevcn.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 15:52:30 +10:00
ba273aaf06 feat(models): add Claude Opus 4.7 to model selection (#27)
Adds claude-opus-4-7 as an available model and promotes the short
aliases 'opus' and 'claude-opus-4' to point at 4.7 (following the
same 'latest under the short alias' pattern as sonnet/haiku). Explicit
claude-opus-4-6 remains available for pinned usage.

Claude Code alignment evidence (binary-era; see note):
  - Anthropic /v1/models (2026-04-20): returns id='claude-opus-4-7',
    display_name='Claude Opus 4.7', 1M input / 128K output,
    created_at=2026-04-14.
  - Claude Code v2.1.114 /home/opc/.npm-global/lib/node_modules/
    @anthropic-ai/claude-code/bin/claude.exe strings table contains
    'claude-opus-4-7'.
  - Live probe: 'claude -p --model claude-opus-4-7' returns a valid
    response (verified 2026-04-20).

Note on ALIGNMENT.md grep rule: Claude Code 2.1.90+ ships a native
binary (claude.exe) instead of cli.js JavaScript. The grep-based
verification in ALIGNMENT.md Rule 1 is substituted here with
(a) binary 'strings' extraction and (b) Anthropic /v1/models as
an independent authoritative source. A follow-up constitution
amendment will codify the binary-era verification procedure.

Co-authored-by: Oracle Public Cloud User <opc@instance-20230820-1333.subnet07301351.vcn07301351.oraclevcn.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 15:40:57 +10:00
8 changed files with 217 additions and 62 deletions
+15
View File
@@ -0,0 +1,15 @@
# Changelog
## v3.11.0 — 2026-04-20
### Features
- `ocp update` now automatically syncs OpenClaw's registry with the latest models (scripts/sync-openclaw.mjs)
- Server logs warn if OpenClaw registry drifts from models.json
### Refactor
- models.json is now the single source of truth for model list
- server.mjs and setup.mjs derive MODEL_MAP/MODELS from models.json
- Adding a new model is now a one-file edit
### Fixes
- OpenClaw's model dropdown now shows all 4 current models (opus-4-7, opus-4-6, sonnet-4-6, haiku-4.5) on existing installs after `ocp update`. Previously setup.mjs only wrote the registry at install time.
+2 -2
View File
@@ -139,7 +139,7 @@ OCP Connect v1.3.0
Checking connectivity...
✓ Connected
Remote OCP v3.9.0 (auth: multi)
Remote OCP v3.10.0 (auth: multi)
ⓘ Using server-advertised anonymous key: ocp_publ...n_v1
(set by admin via PROXY_ANONYMOUS_KEY; see issue #12 §14 Path A)
@@ -195,7 +195,7 @@ OCP Connect v1.3.0
The script automatically:
- Writes env vars to all relevant shell rc files (`.bashrc`, `.zshrc`)
- Sets system-level env vars (`launchctl setenv` on macOS, `environment.d` on Linux)
- **Auto-discovers anonymous key** from `/health.anonymousKey` when no `--key` given (v1.3.0+, requires server v3.9.0+)
- **Auto-discovers anonymous key** from `/health.anonymousKey` when no `--key` given (v1.3.0+, requires server v3.10.0+)
- Configures OpenClaw automatically (including per-agent `auth-profiles.json` for multi-agent setups)
- Detects Cline, Continue.dev, Cursor, and opencode, and prints setup hints (manual configuration required for these IDEs)
+48
View File
@@ -0,0 +1,48 @@
{
"$schema": "./models.schema.json",
"version": 1,
"models": [
{
"id": "claude-opus-4-7",
"displayName": "Claude Opus 4.7",
"openclawName": "Claude Opus 4.7 (via CLI)",
"reasoning": true,
"contextWindow": 200000,
"maxTokens": 16384
},
{
"id": "claude-opus-4-6",
"displayName": "Claude Opus 4.6",
"openclawName": "Claude Opus 4.6 (via CLI)",
"reasoning": true,
"contextWindow": 200000,
"maxTokens": 16384
},
{
"id": "claude-sonnet-4-6",
"displayName": "Claude Sonnet 4.6",
"openclawName": "Claude Sonnet 4.6 (via CLI)",
"reasoning": true,
"contextWindow": 200000,
"maxTokens": 16384
},
{
"id": "claude-haiku-4-5-20251001",
"displayName": "Claude Haiku 4.5",
"openclawName": "Claude Haiku 4.5 (via CLI)",
"reasoning": false,
"contextWindow": 200000,
"maxTokens": 8192
}
],
"aliases": {
"opus": "claude-opus-4-7",
"sonnet": "claude-sonnet-4-6",
"haiku": "claude-haiku-4-5-20251001"
},
"legacyAliases": {
"claude-opus-4": "claude-opus-4-7",
"claude-haiku-4": "claude-haiku-4-5-20251001",
"claude-haiku-4-5": "claude-haiku-4-5-20251001"
}
}
+10 -1
View File
@@ -770,7 +770,16 @@ cmd_update() {
echo " ✓ Plugin synced to $ext_dir"
fi
# 3. Restart proxy
# 3. Sync OpenClaw registry from models.json (non-fatal)
if command -v node >/dev/null 2>&1 && [[ -f "$script_dir/scripts/sync-openclaw.mjs" ]]; then
echo ""
echo " Syncing OpenClaw registry..."
if ! node "$script_dir/scripts/sync-openclaw.mjs" 2>&1 | sed 's/^/ /'; then
echo " ⚠ OpenClaw sync failed (non-fatal, continuing)"
fi
fi
# 4. Restart proxy
echo ""
echo " Restarting proxy..."
cmd_restart > /dev/null 2>&1
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "openclaw-claude-proxy",
"version": "3.9.0",
"version": "3.11.0",
"description": "OCP (Open Claude Proxy) — use your Claude Pro/Max subscription as an OpenAI-compatible API for any IDE. Works with Cline, OpenCode, Aider, Continue.dev, OpenClaw, and more.",
"type": "module",
"bin": {
+97
View File
@@ -0,0 +1,97 @@
#!/usr/bin/env node
// Idempotently sync OCP's claude-local provider models into OpenClaw's registry.
// Only touches:
// - config.models.providers["claude-local"].models
// - config.agents.defaults.models["claude-local/*"] keys
// All other fields and providers are preserved.
import { readFileSync, writeFileSync, existsSync, copyFileSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { homedir } from "node:os";
const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = join(__dirname, "..");
const OPENCLAW_CONFIG = join(homedir(), ".openclaw", "openclaw.json");
const PROVIDER_NAME = "claude-local";
const QUIET = process.argv.includes("--quiet");
function log(msg) { if (!QUIET) console.log(`${msg}`); }
function warn(msg) { console.warn(`${msg}`); }
if (!existsSync(OPENCLAW_CONFIG)) {
log(`OpenClaw not installed at ${OPENCLAW_CONFIG} — skipping (this is fine for non-OpenClaw users)`);
process.exit(0);
}
const modelsConfig = JSON.parse(readFileSync(join(REPO_ROOT, "models.json"), "utf-8"));
const desiredModels = modelsConfig.models.map(m => ({
id: m.id,
name: m.openclawName,
reasoning: m.reasoning,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: m.contextWindow,
maxTokens: m.maxTokens,
}));
const desiredAliases = Object.fromEntries(
modelsConfig.models.map(m => [`${PROVIDER_NAME}/${m.id}`, { alias: m.displayName }])
);
const config = JSON.parse(readFileSync(OPENCLAW_CONFIG, "utf-8"));
// Compute diff before writing
const existingModels = config?.models?.providers?.[PROVIDER_NAME]?.models ?? [];
const existingIds = new Set(existingModels.map(m => m.id));
const desiredIds = new Set(desiredModels.map(m => m.id));
const added = [...desiredIds].filter(id => !existingIds.has(id));
const removed = [...existingIds].filter(id => !desiredIds.has(id));
if (added.length === 0 && removed.length === 0 && existingModels.length === desiredModels.length) {
// Check deep equality too in case names/maxTokens changed
const changed = desiredModels.some((d) => {
const e = existingModels.find(x => x.id === d.id);
return !e || e.name !== d.name || e.maxTokens !== d.maxTokens;
});
if (!changed) {
log("OpenClaw registry already in sync");
process.exit(0);
}
}
// Backup
const backupPath = `${OPENCLAW_CONFIG}.bak.${Date.now()}`;
copyFileSync(OPENCLAW_CONFIG, backupPath);
log(`Backed up to ${backupPath}`);
// Surgical patch: only touch claude-local provider and claude-local/* aliases
if (!config.models) config.models = {};
if (!config.models.providers) config.models.providers = {};
if (!config.models.providers[PROVIDER_NAME]) {
// First-time registration
config.models.providers[PROVIDER_NAME] = {
baseUrl: "http://127.0.0.1:3456/v1",
api: "openai-completions",
authHeader: false,
models: desiredModels,
};
} else {
// Update only the models array; leave baseUrl/api/authHeader untouched (user may have customized port)
config.models.providers[PROVIDER_NAME].models = desiredModels;
}
if (!config.agents) config.agents = {};
if (!config.agents.defaults) config.agents.defaults = {};
if (!config.agents.defaults.models) config.agents.defaults.models = {};
// Remove stale claude-local/* aliases, then add desired ones
for (const key of Object.keys(config.agents.defaults.models)) {
if (key.startsWith(`${PROVIDER_NAME}/`)) delete config.agents.defaults.models[key];
}
Object.assign(config.agents.defaults.models, desiredAliases);
writeFileSync(OPENCLAW_CONFIG, JSON.stringify(config, null, 2) + "\n");
if (added.length > 0) log(`Added: ${added.join(", ")}`);
if (removed.length > 0) log(`Removed (no longer in models.json): ${removed.join(", ")}`);
log(`OpenClaw registry synced: ${desiredModels.length} models registered`);
+27 -17
View File
@@ -29,7 +29,7 @@
import { createServer } from "node:http";
import { spawn, execFileSync } from "node:child_process";
import { randomUUID, timingSafeEqual } from "node:crypto";
import { readFileSync, accessSync, constants } from "node:fs";
import { readFileSync, accessSync, existsSync, constants } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import { homedir } from "node:os";
@@ -37,6 +37,7 @@ import { validateKey, recordUsage, getUsageByKey, getUsageTimeline, getRecentUsa
const __dirname = dirname(fileURLToPath(import.meta.url));
const _pkg = JSON.parse(readFileSync(join(__dirname, "package.json"), "utf8"));
const modelsConfig = JSON.parse(readFileSync(join(__dirname, "models.json"), "utf8"));
// ── Resolve claude binary ───────────────────────────────────────────────
// Priority: CLAUDE_BIN env > well-known paths > which lookup
@@ -145,23 +146,14 @@ const _BREAKER_DISABLED_NOTE = "disabled";
// ── Model mapping ───────────────────────────────────────────────────────
// Maps request model IDs and aliases to canonical claude CLI model IDs.
const MODEL_MAP = {
"claude-opus-4-6": "claude-opus-4-6",
"claude-sonnet-4-6": "claude-sonnet-4-6",
"claude-haiku-4-5-20251001": "claude-haiku-4-5-20251001",
"claude-opus-4": "claude-opus-4-6",
"claude-haiku-4": "claude-haiku-4-5-20251001",
"claude-haiku-4-5": "claude-haiku-4-5-20251001",
"opus": "claude-opus-4-6",
"sonnet": "claude-sonnet-4-6",
"haiku": "claude-haiku-4-5-20251001",
};
// Derived from models.json (single source of truth).
const MODEL_MAP = Object.fromEntries([
...modelsConfig.models.map(m => [m.id, m.id]),
...Object.entries(modelsConfig.aliases),
...Object.entries(modelsConfig.legacyAliases),
]);
const MODELS = [
{ id: "claude-opus-4-6", name: "Claude Opus 4.6" },
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" },
{ id: "claude-haiku-4-5-20251001", name: "Claude Haiku 4.5" },
];
const MODELS = modelsConfig.models.map(m => ({ id: m.id, name: m.displayName }));
// ── Session management ──────────────────────────────────────────────────
// Maps conversation IDs (from caller) to Claude CLI session UUIDs.
@@ -1585,4 +1577,22 @@ server.listen(PORT, BIND_ADDRESS, () => {
console.log(` OCP uses: localhost:${PORT} (HTTP) → claude -p (per-request process)`);
console.log(` CC uses: MCP protocol (in-process) → persistent session`);
console.log(` Both can run simultaneously on the same machine.`);
// Passive OpenClaw registry drift check (non-fatal, read-only).
// Emits a console.warn only. No network/endpoint surface change. No
// Claude-CLI-call boundary touched — cli.js citation N/A (ALIGNMENT.md Rule 2).
try {
const openclawCfg = join(homedir(), ".openclaw", "openclaw.json");
if (existsSync(openclawCfg)) {
const cfg = JSON.parse(readFileSync(openclawCfg, "utf-8"));
const registered = cfg?.models?.providers?.["claude-local"]?.models ?? [];
const expected = modelsConfig.models.map(m => m.id);
const registeredIds = new Set(registered.map(r => r.id));
const missing = expected.filter(id => !registeredIds.has(id));
if (missing.length > 0) {
console.warn(`⚠ OpenClaw registry out of sync (missing: ${missing.join(", ")})`);
console.warn(` Run: node ${__dirname}/scripts/sync-openclaw.mjs`);
}
}
} catch { /* ignore — best-effort */ }
});
+17 -41
View File
@@ -39,49 +39,25 @@ const PROVIDER_NAME = opt("provider-name", "claude-local");
const BIND_ADDRESS = opt("bind", "127.0.0.1");
const AUTH_MODE_CONFIG = opt("auth-mode", "none");
const MODEL_ID_MAP = {
opus: "claude-opus-4-6",
sonnet: "claude-sonnet-4-6",
haiku: "claude-haiku-4",
};
// ── Models: derived from models.json (single source of truth) ──────────
const modelsConfig = JSON.parse(readFileSync(join(__dirname, "models.json"), "utf-8"));
const MODEL_ID_MAP = modelsConfig.aliases;
const DEFAULT_MODEL_ID = MODEL_ID_MAP[DEFAULT_MODEL] || MODEL_ID_MAP.opus;
// ── Models to register ──────────────────────────────────────────────────
const MODELS = [
{
id: "claude-opus-4-6",
name: "Claude Opus 4.6 (via CLI)",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200000,
maxTokens: 16384,
},
{
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6 (via CLI)",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200000,
maxTokens: 16384,
},
{
id: "claude-haiku-4",
name: "Claude Haiku 4 (via CLI)",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200000,
maxTokens: 8192,
},
];
const MODELS = modelsConfig.models.map(m => ({
id: m.id,
name: m.openclawName,
reasoning: m.reasoning,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: m.contextWindow,
maxTokens: m.maxTokens,
}));
const MODEL_ALIASES = {
[`${PROVIDER_NAME}/claude-opus-4-6`]: { alias: "Claude Opus 4.6" },
[`${PROVIDER_NAME}/claude-sonnet-4-6`]: { alias: "Claude Sonnet 4.6" },
[`${PROVIDER_NAME}/claude-haiku-4`]: { alias: "Claude Haiku 4" },
};
const MODEL_ALIASES = Object.fromEntries(
modelsConfig.models.map(m => [`${PROVIDER_NAME}/${m.id}`, { alias: m.displayName }])
);
// ── Helpers ─────────────────────────────────────────────────────────────
function log(msg) { console.log(`${msg}`); }
@@ -269,7 +245,7 @@ console.log(`
║ ║
║ Provider: ${PROVIDER_NAME.padEnd(44)}
║ Port: ${String(PORT).padEnd(44)}
║ Models: claude-opus-4-6, claude-sonnet-4-6, claude-haiku-4
║ Models: ${`see models.json (${MODELS.length} available)`.padEnd(44)}
║ Default: ${DEFAULT_MODEL_ID.padEnd(44)}
║ ║
║ Start proxy: ║