vibe-coding-master 0.7.10 → 0.7.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -0
- package/dist/backend/adapters/ccr-gateway-adapter.js +122 -0
- package/dist/backend/adapters/claude-adapter.js +4 -1
- package/dist/backend/adapters/filesystem.js +3 -0
- package/dist/backend/api/app-settings-routes.js +9 -0
- package/dist/backend/api/session-routes.js +1 -0
- package/dist/backend/api/translation-worker-routes.js +1 -0
- package/dist/backend/server.js +13 -1
- package/dist/backend/services/app-settings-service.js +31 -1
- package/dist/backend/services/ccr-integration-service.js +194 -0
- package/dist/backend/services/session-service.js +33 -6
- package/dist/shared/types/session.js +31 -4
- package/dist-frontend/assets/index-BAE_pjXJ.js +97 -0
- package/dist-frontend/assets/index-CiEUp9Si.css +32 -0
- package/dist-frontend/index.html +2 -2
- package/package.json +1 -1
- package/dist-frontend/assets/index-42EpETgd.js +0 -97
- package/dist-frontend/assets/index-D65x2x0F.css +0 -32
package/README.md
CHANGED
|
@@ -228,6 +228,34 @@ such as a Dev Container or VM.
|
|
|
228
228
|
Model and effort can be selected before start/resume/restart. Changes affect the
|
|
229
229
|
next launched process, not a currently running Claude Code process.
|
|
230
230
|
|
|
231
|
+
### GPT Through Claude Code Router
|
|
232
|
+
|
|
233
|
+
VCM can launch its normal Claude Code sessions with `GPT-5.6 Sol (CCR)` through
|
|
234
|
+
a host-running [Claude Code Router](https://github.com/musistudio/claude-code-router).
|
|
235
|
+
CCR must already be installed, authenticated, configured, and running on the
|
|
236
|
+
host. VCM does not manage the CCR process.
|
|
237
|
+
|
|
238
|
+
VCM uses this fixed Dev Container endpoint:
|
|
239
|
+
|
|
240
|
+
```text
|
|
241
|
+
http://host.docker.internal:3456
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Configure CCR to listen on port `3456` with an API key and make it reachable
|
|
245
|
+
from the container. In the VCM `Settings` section:
|
|
246
|
+
|
|
247
|
+
1. enter and save the CCR API key;
|
|
248
|
+
2. enable `CCR GPT models`;
|
|
249
|
+
3. confirm the status is `available`;
|
|
250
|
+
4. select `GPT-5.6 Sol (CCR)` in any Session model control.
|
|
251
|
+
|
|
252
|
+
The key is stored in global VCM state (`~/.vcm/settings.json`) with owner-only
|
|
253
|
+
permissions and is never returned by the settings API. CCR settings are applied
|
|
254
|
+
only to the child Claude Code process being launched. Native Claude selections
|
|
255
|
+
remain unchanged. If CCR is disabled, unreachable, rejects the key, or does not
|
|
256
|
+
expose `Codex API/gpt-5.6-sol`, VCM blocks the new Start, Resume, or Restart and
|
|
257
|
+
does not fall back to another model.
|
|
258
|
+
|
|
231
259
|
## Launch Template
|
|
232
260
|
|
|
233
261
|
The launch template stores per-role defaults:
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { CCR_GATEWAY_BASE_URL, CCR_GPT_MODEL_ID } from "../../shared/types/session.js";
|
|
2
|
+
const DEFAULT_TIMEOUT_MS = 3_000;
|
|
3
|
+
export function createCcrGatewayAdapter(deps = {}) {
|
|
4
|
+
const baseUrl = (deps.baseUrl ?? CCR_GATEWAY_BASE_URL).replace(/\/+$/, "");
|
|
5
|
+
const fetchImpl = deps.fetch ?? globalThis.fetch;
|
|
6
|
+
const timeoutMs = deps.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
7
|
+
return {
|
|
8
|
+
async probe(apiKey) {
|
|
9
|
+
try {
|
|
10
|
+
const root = await requestJson(fetchImpl, `${baseUrl}/`, timeoutMs);
|
|
11
|
+
if (!root.response.ok) {
|
|
12
|
+
return invalidResponse(`CCR gateway root returned HTTP ${root.response.status}.`);
|
|
13
|
+
}
|
|
14
|
+
if (!isCcrGateway(root.payload)) {
|
|
15
|
+
return {
|
|
16
|
+
connectionState: "not-ccr",
|
|
17
|
+
modelAvailable: false,
|
|
18
|
+
error: `The fixed CCR endpoint ${baseUrl} did not identify a Claude Code Router gateway.`
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const models = await requestJson(fetchImpl, `${baseUrl}/v1/models`, timeoutMs, {
|
|
22
|
+
authorization: `Bearer ${apiKey}`,
|
|
23
|
+
"user-agent": "Claude Code"
|
|
24
|
+
});
|
|
25
|
+
if (models.response.status === 401 || models.response.status === 403) {
|
|
26
|
+
return {
|
|
27
|
+
connectionState: "unauthorized",
|
|
28
|
+
modelAvailable: false,
|
|
29
|
+
error: "CCR rejected the configured API key."
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (!models.response.ok) {
|
|
33
|
+
return invalidResponse(`CCR model discovery returned HTTP ${models.response.status}.`);
|
|
34
|
+
}
|
|
35
|
+
const modelIds = readModelIds(models.payload);
|
|
36
|
+
if (!modelIds) {
|
|
37
|
+
return invalidResponse("CCR returned an invalid /v1/models response.");
|
|
38
|
+
}
|
|
39
|
+
const modelAvailable = modelIds.includes(CCR_GPT_MODEL_ID);
|
|
40
|
+
return {
|
|
41
|
+
connectionState: "available",
|
|
42
|
+
modelAvailable,
|
|
43
|
+
...(modelAvailable
|
|
44
|
+
? {}
|
|
45
|
+
: { error: `CCR does not expose the required model ${CCR_GPT_MODEL_ID}.` })
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
const timedOut = isAbortError(error);
|
|
50
|
+
return {
|
|
51
|
+
connectionState: "unreachable",
|
|
52
|
+
modelAvailable: false,
|
|
53
|
+
error: timedOut
|
|
54
|
+
? `CCR connection timed out after ${timeoutMs} ms at ${baseUrl}.`
|
|
55
|
+
: `CCR could not be reached from the VCM container backend at ${baseUrl}. Reason: ${errorMessage(error)}`
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
async function requestJson(fetchImpl, url, timeoutMs, headers = {}) {
|
|
62
|
+
const controller = new AbortController();
|
|
63
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
64
|
+
try {
|
|
65
|
+
const response = await fetchImpl(url, {
|
|
66
|
+
method: "GET",
|
|
67
|
+
headers: {
|
|
68
|
+
accept: "application/json",
|
|
69
|
+
...headers
|
|
70
|
+
},
|
|
71
|
+
signal: controller.signal
|
|
72
|
+
});
|
|
73
|
+
let payload;
|
|
74
|
+
try {
|
|
75
|
+
payload = await response.json();
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
payload = undefined;
|
|
79
|
+
}
|
|
80
|
+
return { response, payload };
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
clearTimeout(timeout);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function isCcrGateway(value) {
|
|
87
|
+
if (!isObject(value)) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
return value.name === "claude-code-router"
|
|
91
|
+
|| value.plugin === "claude-code-router"
|
|
92
|
+
|| value.core === "next-ai-gateway";
|
|
93
|
+
}
|
|
94
|
+
function readModelIds(value) {
|
|
95
|
+
if (!isObject(value) || !Array.isArray(value.data)) {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
const ids = [];
|
|
99
|
+
for (const item of value.data) {
|
|
100
|
+
if (!isObject(item) || typeof item.id !== "string" || !item.id.trim()) {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
ids.push(item.id.trim());
|
|
104
|
+
}
|
|
105
|
+
return ids;
|
|
106
|
+
}
|
|
107
|
+
function invalidResponse(error) {
|
|
108
|
+
return {
|
|
109
|
+
connectionState: "invalid-response",
|
|
110
|
+
modelAvailable: false,
|
|
111
|
+
error
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function isAbortError(error) {
|
|
115
|
+
return error instanceof Error && error.name === "AbortError";
|
|
116
|
+
}
|
|
117
|
+
function errorMessage(error) {
|
|
118
|
+
return error instanceof Error ? error.message : String(error);
|
|
119
|
+
}
|
|
120
|
+
function isObject(value) {
|
|
121
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
122
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isCcrSessionModel } from "../../shared/types/session.js";
|
|
1
2
|
import { VcmError } from "../errors.js";
|
|
2
3
|
export function createClaudeAdapter(runner) {
|
|
3
4
|
return {
|
|
@@ -22,7 +23,9 @@ export function createClaudeAdapter(runner) {
|
|
|
22
23
|
if (claudeSessionId) {
|
|
23
24
|
args.push(resume ? "--resume" : "--session-id", claudeSessionId);
|
|
24
25
|
}
|
|
25
|
-
|
|
26
|
+
if (!isCcrSessionModel(model)) {
|
|
27
|
+
args.push("--model", model);
|
|
28
|
+
}
|
|
26
29
|
if (effort === "ultracode") {
|
|
27
30
|
args.push("--settings", JSON.stringify({ ultracode: true }));
|
|
28
31
|
}
|
|
@@ -89,6 +89,9 @@ export function createNodeFileSystemAdapter() {
|
|
|
89
89
|
await fs.rename(tempPath, targetPath);
|
|
90
90
|
});
|
|
91
91
|
},
|
|
92
|
+
async chmod(targetPath, mode) {
|
|
93
|
+
await runFileOperation(() => fs.chmod(targetPath, mode));
|
|
94
|
+
},
|
|
92
95
|
async ensureFile(targetPath, content, options = {}) {
|
|
93
96
|
if (!options.overwrite && await this.pathExists(targetPath)) {
|
|
94
97
|
return false;
|
|
@@ -5,4 +5,13 @@ export function registerAppSettingsRoutes(app, deps) {
|
|
|
5
5
|
app.put("/api/settings/preferences", async (request) => {
|
|
6
6
|
return deps.appSettings.updatePreferences(request.body ?? {});
|
|
7
7
|
});
|
|
8
|
+
app.get("/api/settings/ccr", async () => {
|
|
9
|
+
return deps.ccrIntegration.getStatus();
|
|
10
|
+
});
|
|
11
|
+
app.put("/api/settings/ccr", async (request) => {
|
|
12
|
+
return deps.ccrIntegration.updateSettings(request.body ?? {});
|
|
13
|
+
});
|
|
14
|
+
app.post("/api/settings/ccr/check", async () => {
|
|
15
|
+
return deps.ccrIntegration.checkConnection();
|
|
16
|
+
});
|
|
8
17
|
}
|
|
@@ -22,6 +22,7 @@ export function registerSessionRoutes(app, deps) {
|
|
|
22
22
|
const project = await requireCurrentProject(deps.projectService);
|
|
23
23
|
const role = parseRole(request.params.role);
|
|
24
24
|
const existing = await deps.sessionService.getRoleSession(project.repoRoot, request.params.taskSlug, role);
|
|
25
|
+
await deps.sessionService.assertModelLaunchReady(request.body?.model ?? existing?.model);
|
|
25
26
|
if (existing) {
|
|
26
27
|
await deps.translationService.stopSession(existing.id, { clearCache: true });
|
|
27
28
|
deps.roundService.stopSession(existing.id);
|
|
@@ -40,6 +40,7 @@ export function registerTranslationWorkerRoutes(app, deps) {
|
|
|
40
40
|
const project = await requireCurrentProject(deps.projectService);
|
|
41
41
|
const taskSlug = requireTaskSlug(request.body?.taskSlug, "Translator");
|
|
42
42
|
const existing = await deps.sessionService.getRoleSession(project.repoRoot, taskSlug, "translator");
|
|
43
|
+
await deps.sessionService.assertModelLaunchReady(request.body?.model ?? existing?.model);
|
|
43
44
|
if (existing) {
|
|
44
45
|
await deps.translationService.stopSession(existing.id, { clearCache: true });
|
|
45
46
|
}
|
package/dist/backend/server.js
CHANGED
|
@@ -4,11 +4,13 @@ import Fastify from "fastify";
|
|
|
4
4
|
import fastifyStatic from "@fastify/static";
|
|
5
5
|
import { createArtifactService } from "./services/artifact-service.js";
|
|
6
6
|
import { createClaudeAdapter } from "./adapters/claude-adapter.js";
|
|
7
|
+
import { createCcrGatewayAdapter } from "./adapters/ccr-gateway-adapter.js";
|
|
7
8
|
import { createCommandRunner } from "./adapters/command-runner.js";
|
|
8
9
|
import { createCommandDispatcher } from "./services/command-dispatcher.js";
|
|
9
10
|
import { createClaudeHookService } from "./services/claude-hook-service.js";
|
|
10
11
|
import { createGitAdapter } from "./adapters/git-adapter.js";
|
|
11
12
|
import { createAppSettingsService } from "./services/app-settings-service.js";
|
|
13
|
+
import { createCcrIntegrationService } from "./services/ccr-integration-service.js";
|
|
12
14
|
import { createAutoMemoryService } from "./services/auto-memory-service.js";
|
|
13
15
|
import { createClaudeTranscriptService } from "./services/claude-transcript-service.js";
|
|
14
16
|
import { createGateReviewService } from "./services/gate-review-service.js";
|
|
@@ -78,7 +80,10 @@ export async function createServer(deps, options = {}) {
|
|
|
78
80
|
});
|
|
79
81
|
});
|
|
80
82
|
registerDiagnosticsRoutes(app, { diagnosticsService: deps.diagnosticsService });
|
|
81
|
-
registerAppSettingsRoutes(app, {
|
|
83
|
+
registerAppSettingsRoutes(app, {
|
|
84
|
+
appSettings: deps.appSettings,
|
|
85
|
+
ccrIntegration: deps.ccrIntegration
|
|
86
|
+
});
|
|
82
87
|
registerClaudeHookRoutes(app, { claudeHookService: deps.claudeHookService });
|
|
83
88
|
registerGateReviewRoutes(app, {
|
|
84
89
|
projectService: deps.projectService,
|
|
@@ -156,6 +161,7 @@ export async function createServer(deps, options = {}) {
|
|
|
156
161
|
onManualInterrupt: (sessionId) => deps.terminalInterruptService.handleManualInterrupt(sessionId)
|
|
157
162
|
});
|
|
158
163
|
app.addHook("onReady", async () => {
|
|
164
|
+
await deps.ccrIntegration.initialize();
|
|
159
165
|
await cleanupRecentTranslationRuntime(deps);
|
|
160
166
|
deps.runtimeCoordinator.start();
|
|
161
167
|
await deps.gatewayService.start();
|
|
@@ -202,6 +208,10 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
202
208
|
const git = createGitAdapter(runner);
|
|
203
209
|
const claude = createClaudeAdapter(runner);
|
|
204
210
|
const appSettings = createAppSettingsService({ fs });
|
|
211
|
+
const ccrIntegration = createCcrIntegrationService({
|
|
212
|
+
settings: appSettings,
|
|
213
|
+
gateway: createCcrGatewayAdapter()
|
|
214
|
+
});
|
|
205
215
|
const runtime = createNodePtyTerminalRuntime({ fs });
|
|
206
216
|
const registry = createSessionRegistry();
|
|
207
217
|
const artifactService = createArtifactService(fs);
|
|
@@ -217,6 +227,7 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
217
227
|
projectService,
|
|
218
228
|
taskService,
|
|
219
229
|
taskWorkflowService,
|
|
230
|
+
ccrIntegration,
|
|
220
231
|
apiUrl: options.apiUrl
|
|
221
232
|
});
|
|
222
233
|
const harnessService = createHarnessService({
|
|
@@ -396,6 +407,7 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
396
407
|
});
|
|
397
408
|
return {
|
|
398
409
|
appSettings,
|
|
410
|
+
ccrIntegration,
|
|
399
411
|
projectService,
|
|
400
412
|
taskService,
|
|
401
413
|
taskCloseService,
|
|
@@ -3,7 +3,7 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import { VCM_ROLE_NAMES } from "../../shared/constants.js";
|
|
4
4
|
import { GATE_REVIEW_GATES } from "../../shared/types/gate-review.js";
|
|
5
5
|
import { createDefaultLaunchTemplate, DEFAULT_TRANSLATION_OUTPUT_MODE, DEFAULT_TRANSLATION_TARGET_LANGUAGE, TRANSLATION_OUTPUT_MODE_OPTIONS, TRANSLATION_TARGET_LANGUAGE_OPTIONS } from "../../shared/types/app-settings.js";
|
|
6
|
-
import { CLAUDE_MODEL_OPTIONS, SESSION_EFFORT_OPTIONS } from "../../shared/types/session.js";
|
|
6
|
+
import { CLAUDE_MODEL_OPTIONS, CCR_GPT_SESSION_MODEL, SESSION_EFFORT_OPTIONS } from "../../shared/types/session.js";
|
|
7
7
|
import { resolveVcmDataDir } from "../vcm-data-dir.js";
|
|
8
8
|
const MAX_RECENT_REPOSITORIES = 5;
|
|
9
9
|
export function createAppSettingsService(deps) {
|
|
@@ -20,6 +20,7 @@ export function createAppSettingsService(deps) {
|
|
|
20
20
|
let shouldSave = false;
|
|
21
21
|
if (await deps.fs.pathExists(settingsPath)) {
|
|
22
22
|
raw = await deps.fs.readJson(settingsPath);
|
|
23
|
+
await deps.fs.chmod?.(settingsPath, 0o600);
|
|
23
24
|
}
|
|
24
25
|
else {
|
|
25
26
|
shouldSave = true;
|
|
@@ -33,6 +34,7 @@ export function createAppSettingsService(deps) {
|
|
|
33
34
|
async function saveSettings(settings) {
|
|
34
35
|
cachedSettings = settings;
|
|
35
36
|
await deps.fs.writeJsonAtomic(settingsPath, settings);
|
|
37
|
+
await deps.fs.chmod?.(settingsPath, 0o600);
|
|
36
38
|
}
|
|
37
39
|
async function loadProjectIndex() {
|
|
38
40
|
if (cachedProjectIndex) {
|
|
@@ -144,6 +146,21 @@ export function createAppSettingsService(deps) {
|
|
|
144
146
|
requiredGates: normalizedRequiredGates
|
|
145
147
|
};
|
|
146
148
|
},
|
|
149
|
+
async getCcrIntegrationSettings() {
|
|
150
|
+
return normalizeCcrIntegrationSettings((await loadSettings()).ccr);
|
|
151
|
+
},
|
|
152
|
+
async updateCcrIntegrationSettings(input) {
|
|
153
|
+
const current = await loadSettings();
|
|
154
|
+
const ccr = normalizeCcrIntegrationSettings({
|
|
155
|
+
...current.ccr,
|
|
156
|
+
...input
|
|
157
|
+
});
|
|
158
|
+
await saveSettings({
|
|
159
|
+
...current,
|
|
160
|
+
ccr
|
|
161
|
+
});
|
|
162
|
+
return ccr;
|
|
163
|
+
},
|
|
147
164
|
getSettingsPath() {
|
|
148
165
|
return settingsPath;
|
|
149
166
|
},
|
|
@@ -229,8 +246,18 @@ function normalizeSettingsFile(input) {
|
|
|
229
246
|
if (gateReview) {
|
|
230
247
|
settings.gateReview = gateReview;
|
|
231
248
|
}
|
|
249
|
+
settings.ccr = normalizeCcrIntegrationSettings(input.ccr);
|
|
232
250
|
return settings;
|
|
233
251
|
}
|
|
252
|
+
function normalizeCcrIntegrationSettings(input) {
|
|
253
|
+
const candidate = isObject(input) ? input : {};
|
|
254
|
+
const apiKey = typeof candidate.apiKey === "string" ? candidate.apiKey.trim() : "";
|
|
255
|
+
return {
|
|
256
|
+
version: 1,
|
|
257
|
+
enabled: candidate.enabled === true && apiKey.length > 0,
|
|
258
|
+
apiKey
|
|
259
|
+
};
|
|
260
|
+
}
|
|
234
261
|
function normalizePreferences(input) {
|
|
235
262
|
const candidate = isObject(input) ? input : {};
|
|
236
263
|
const rawFlowPauseAlerts = "flowPauseAlerts" in candidate
|
|
@@ -304,6 +331,9 @@ function normalizeClaudeModel(input, fallback) {
|
|
|
304
331
|
if (typeof input !== "string") {
|
|
305
332
|
return fallback;
|
|
306
333
|
}
|
|
334
|
+
if (input === CCR_GPT_SESSION_MODEL) {
|
|
335
|
+
return input;
|
|
336
|
+
}
|
|
307
337
|
const model = CLAUDE_MODEL_OPTIONS.find((option) => option.value === input);
|
|
308
338
|
return model?.value ?? fallback;
|
|
309
339
|
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { CCR_GATEWAY_BASE_URL, CCR_GPT_MODEL_ID, createSessionModelOptions, isCcrSessionModel } from "../../shared/types/session.js";
|
|
2
|
+
import { VcmError } from "../errors.js";
|
|
3
|
+
const DEFAULT_CACHE_TTL_MS = 10_000;
|
|
4
|
+
export function createCcrIntegrationService(deps) {
|
|
5
|
+
const now = deps.now ?? (() => new Date());
|
|
6
|
+
const cacheTtlMs = deps.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
|
|
7
|
+
let cachedProbe;
|
|
8
|
+
let inFlight;
|
|
9
|
+
async function probe(force = false) {
|
|
10
|
+
const settings = await deps.settings.getCcrIntegrationSettings();
|
|
11
|
+
if (!settings.apiKey) {
|
|
12
|
+
throw missingApiKeyError();
|
|
13
|
+
}
|
|
14
|
+
const currentTime = now();
|
|
15
|
+
if (!force && cachedProbe && currentTime.getTime() - cachedProbe.checkedAtMs <= cacheTtlMs) {
|
|
16
|
+
return cachedProbe;
|
|
17
|
+
}
|
|
18
|
+
if (inFlight) {
|
|
19
|
+
return inFlight;
|
|
20
|
+
}
|
|
21
|
+
inFlight = deps.gateway.probe(settings.apiKey).then((result) => {
|
|
22
|
+
const checkedAtDate = now();
|
|
23
|
+
cachedProbe = {
|
|
24
|
+
result,
|
|
25
|
+
checkedAt: checkedAtDate.toISOString(),
|
|
26
|
+
checkedAtMs: checkedAtDate.getTime()
|
|
27
|
+
};
|
|
28
|
+
return cachedProbe;
|
|
29
|
+
}).finally(() => {
|
|
30
|
+
inFlight = undefined;
|
|
31
|
+
});
|
|
32
|
+
return inFlight;
|
|
33
|
+
}
|
|
34
|
+
async function buildStatus(options = {}) {
|
|
35
|
+
const settings = await deps.settings.getCcrIntegrationSettings();
|
|
36
|
+
if (!settings.enabled) {
|
|
37
|
+
return createStatus({
|
|
38
|
+
enabled: false,
|
|
39
|
+
apiKeyConfigured: Boolean(settings.apiKey),
|
|
40
|
+
connectionState: "disabled",
|
|
41
|
+
modelAvailable: false,
|
|
42
|
+
error: settings.apiKey ? undefined : "Save a CCR API key before enabling CCR GPT models."
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
if (!cachedProbe && options.refreshIfMissing) {
|
|
46
|
+
await probe();
|
|
47
|
+
}
|
|
48
|
+
if (!cachedProbe) {
|
|
49
|
+
return createStatus({
|
|
50
|
+
enabled: true,
|
|
51
|
+
apiKeyConfigured: true,
|
|
52
|
+
connectionState: "checking",
|
|
53
|
+
modelAvailable: false
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return createStatus({
|
|
57
|
+
enabled: true,
|
|
58
|
+
apiKeyConfigured: true,
|
|
59
|
+
connectionState: cachedProbe.result.connectionState,
|
|
60
|
+
modelAvailable: cachedProbe.result.modelAvailable,
|
|
61
|
+
checkedAt: cachedProbe.checkedAt,
|
|
62
|
+
error: cachedProbe.result.error
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
async initialize() {
|
|
67
|
+
const settings = await deps.settings.getCcrIntegrationSettings();
|
|
68
|
+
if (settings.enabled) {
|
|
69
|
+
await probe(true);
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
async getStatus() {
|
|
73
|
+
return buildStatus({ refreshIfMissing: true });
|
|
74
|
+
},
|
|
75
|
+
async updateSettings(input) {
|
|
76
|
+
if (input.apiKey !== undefined && typeof input.apiKey !== "string") {
|
|
77
|
+
throw invalidSettingsError("CCR API key must be a string.");
|
|
78
|
+
}
|
|
79
|
+
if (input.enabled !== undefined && typeof input.enabled !== "boolean") {
|
|
80
|
+
throw invalidSettingsError("CCR enabled state must be a boolean.");
|
|
81
|
+
}
|
|
82
|
+
if (input.clearApiKey !== undefined && typeof input.clearApiKey !== "boolean") {
|
|
83
|
+
throw invalidSettingsError("CCR clear-key state must be a boolean.");
|
|
84
|
+
}
|
|
85
|
+
const current = await deps.settings.getCcrIntegrationSettings();
|
|
86
|
+
const clearApiKey = input.clearApiKey === true;
|
|
87
|
+
const nextApiKey = clearApiKey
|
|
88
|
+
? ""
|
|
89
|
+
: input.apiKey === undefined
|
|
90
|
+
? current.apiKey
|
|
91
|
+
: input.apiKey.trim();
|
|
92
|
+
const nextEnabled = clearApiKey ? false : input.enabled ?? current.enabled;
|
|
93
|
+
if (nextEnabled && !nextApiKey) {
|
|
94
|
+
throw missingApiKeyError();
|
|
95
|
+
}
|
|
96
|
+
await deps.settings.updateCcrIntegrationSettings({
|
|
97
|
+
enabled: nextEnabled,
|
|
98
|
+
apiKey: nextApiKey
|
|
99
|
+
});
|
|
100
|
+
if (clearApiKey || !nextEnabled) {
|
|
101
|
+
cachedProbe = undefined;
|
|
102
|
+
return buildStatus();
|
|
103
|
+
}
|
|
104
|
+
if (input.apiKey !== undefined || input.enabled === true) {
|
|
105
|
+
cachedProbe = undefined;
|
|
106
|
+
await probe(true);
|
|
107
|
+
}
|
|
108
|
+
return buildStatus();
|
|
109
|
+
},
|
|
110
|
+
async checkConnection() {
|
|
111
|
+
const settings = await deps.settings.getCcrIntegrationSettings();
|
|
112
|
+
if (!settings.enabled) {
|
|
113
|
+
throw new VcmError({
|
|
114
|
+
code: "CCR_DISABLED",
|
|
115
|
+
message: "CCR GPT models are disabled.",
|
|
116
|
+
statusCode: 409,
|
|
117
|
+
hint: "Save the CCR API key and enable CCR GPT models before checking the connection."
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
await probe(true);
|
|
121
|
+
return buildStatus();
|
|
122
|
+
},
|
|
123
|
+
async getLaunchEnvironment(model) {
|
|
124
|
+
if (!isCcrSessionModel(model)) {
|
|
125
|
+
return {};
|
|
126
|
+
}
|
|
127
|
+
const settings = await deps.settings.getCcrIntegrationSettings();
|
|
128
|
+
if (!settings.enabled) {
|
|
129
|
+
throw new VcmError({
|
|
130
|
+
code: "CCR_DISABLED",
|
|
131
|
+
message: "CCR GPT models are disabled.",
|
|
132
|
+
statusCode: 409,
|
|
133
|
+
hint: "Enable CCR GPT models in VCM Settings before starting this session."
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
const checked = await probe();
|
|
137
|
+
if (checked.result.connectionState !== "available" || !checked.result.modelAvailable) {
|
|
138
|
+
throw new VcmError({
|
|
139
|
+
code: "CCR_MODEL_UNAVAILABLE",
|
|
140
|
+
message: checked.result.error ?? `CCR model ${CCR_GPT_MODEL_ID} is unavailable.`,
|
|
141
|
+
statusCode: 409,
|
|
142
|
+
hint: "Check that host CCR is running on port 3456 and exposes GPT-5.6 Sol."
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
const noProxy = mergeNoProxy(deps.baseEnv?.NO_PROXY ?? deps.baseEnv?.no_proxy ?? process.env.NO_PROXY ?? process.env.no_proxy, "host.docker.internal");
|
|
146
|
+
return {
|
|
147
|
+
ANTHROPIC_BASE_URL: CCR_GATEWAY_BASE_URL,
|
|
148
|
+
ANTHROPIC_API_BASE_URL: CCR_GATEWAY_BASE_URL,
|
|
149
|
+
CLAUDE_AGENT_API_BASE_URL: CCR_GATEWAY_BASE_URL,
|
|
150
|
+
ANTHROPIC_AUTH_TOKEN: settings.apiKey,
|
|
151
|
+
ANTHROPIC_MODEL: CCR_GPT_MODEL_ID,
|
|
152
|
+
CCR_CLAUDE_CODE_MODEL: CCR_GPT_MODEL_ID,
|
|
153
|
+
CODEXL_CLAUDE_CODE_MODEL: CCR_GPT_MODEL_ID,
|
|
154
|
+
ANTHROPIC_SMALL_FAST_MODEL: CCR_GPT_MODEL_ID,
|
|
155
|
+
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: "1",
|
|
156
|
+
NO_PROXY: noProxy,
|
|
157
|
+
no_proxy: noProxy
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function createStatus(input) {
|
|
163
|
+
const reason = input.error
|
|
164
|
+
?? (input.enabled ? "CCR GPT-5.6 Sol is unavailable." : "Enable CCR GPT models in Settings.");
|
|
165
|
+
return {
|
|
166
|
+
...input,
|
|
167
|
+
modelOptions: createSessionModelOptions(input.enabled && input.modelAvailable, reason)
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function missingApiKeyError() {
|
|
171
|
+
return new VcmError({
|
|
172
|
+
code: "CCR_API_KEY_MISSING",
|
|
173
|
+
message: "CCR API key is not configured.",
|
|
174
|
+
statusCode: 400,
|
|
175
|
+
hint: "Save the CCR API key before enabling CCR GPT models."
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
function invalidSettingsError(message) {
|
|
179
|
+
return new VcmError({
|
|
180
|
+
code: "CCR_SETTINGS_INVALID",
|
|
181
|
+
message,
|
|
182
|
+
statusCode: 400
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
export function mergeNoProxy(current, host) {
|
|
186
|
+
const values = (current ?? "")
|
|
187
|
+
.split(",")
|
|
188
|
+
.map((value) => value.trim())
|
|
189
|
+
.filter(Boolean);
|
|
190
|
+
if (!values.includes(host)) {
|
|
191
|
+
values.push(host);
|
|
192
|
+
}
|
|
193
|
+
return values.join(",");
|
|
194
|
+
}
|