mirror of
https://github.com/ValueCell-ai/ClawX.git
synced 2026-08-14 08:53:09 +00:00
fix(cc-connect): sync codex modelhub responses config
This commit is contained in:
+1
-1
@@ -97,7 +97,7 @@ ClawXは公式の**OpenClaw**コアを直接ベースに構築されています
|
||||
|
||||
ClawX には runtime 抽象レイヤーもあります。OpenClaw は既定 runtime とロールバック経路のままで、**設定 → Gateway → Runtime** から任意の同梱 `cc-connect` runtime に切り替えられます。パッケージ版は cc-connect バイナリと OpenAI Codex ネイティブ CLI bundle の両方を app resources に含め、runtime 起動はグローバルインストール、PATH 上のバイナリ、起動時ダウンロードに依存しません。cc-connect は `~/.cc-connect` を自動変更せず、app userData 配下の ClawX 管理ディレクトリを使います。GUI chat は cc-connect BridgePlatform 経由で Codex project agent に接続し、provider/model、cron、enabled skills は管理された cc-connect/Codex runtime に同期されます。
|
||||
|
||||
cc-connect mode では、Codex provider sync は OpenAI API key、OpenAI OAuth/Codex、Ollama、および Responses API を公開する OpenAI-compatible Custom provider をサポートします。Chat Completions として設定された Custom provider は、Codex 0.137 が Responses wire API のみを受け付けるため、chat 配信前に unsupported として報告されます。
|
||||
cc-connect mode では、Codex provider sync は OpenAI API key、OpenAI OAuth/Codex、Ollama、および Responses API を公開する OpenAI-compatible Custom provider をサポートします。ByteDance ModelHub-compatible Custom provider は Codex が使う `/api/modelhub/online` Responses endpoint に正規化され、管理 config には環境変数形式の header 参照だけを書き込むため、secret や sticky-session header は永続化されません。Chat Completions として設定された Custom provider は、Codex 0.137 が Responses wire API のみを受け付けるため、chat 配信前に unsupported として報告されます。
|
||||
|
||||
cc-connect はメッセージング platform bridge も担当します。cc-connect が active runtime の場合、channel status probe は OpenClaw Gateway に固定せず runtime abstraction 経由でルーティングされ、Developer Mode のサイドバーのページショートカットは cc-connect Web Admin を開きます。
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ When Developer Mode is enabled, the sidebar also provides a native Dreams page f
|
||||
|
||||
ClawX also includes a runtime abstraction layer. OpenClaw remains the default runtime and rollback path, while **Settings → Gateway → Runtime** can switch to an optional bundled `cc-connect` runtime. Packaged builds include both the cc-connect binary and the native OpenAI Codex CLI bundle in app resources; runtime startup does not depend on global installs, PATH binaries, or app-time downloads. cc-connect uses a ClawX-managed configuration directory under app user data instead of modifying `~/.cc-connect`, and GUI chat connects through cc-connect BridgePlatform with Codex as the project agent. Provider/model selections, cron tasks, and enabled skills are synchronized into the managed cc-connect/Codex runtime.
|
||||
|
||||
In cc-connect mode, Codex provider sync supports OpenAI API key, OpenAI OAuth/Codex, Ollama, and Custom OpenAI-compatible providers that expose the Responses API. Custom providers configured for Chat Completions are reported as unsupported before chat delivery because Codex 0.137 accepts only the Responses wire API.
|
||||
In cc-connect mode, Codex provider sync supports OpenAI API key, OpenAI OAuth/Codex, Ollama, and Custom OpenAI-compatible providers that expose the Responses API. ByteDance ModelHub-compatible Custom providers are normalized to Codex's `/api/modelhub/online` Responses endpoint and written with environment-variable header references so secrets and sticky-session headers are not persisted in managed config files. Custom providers configured for Chat Completions are reported as unsupported before chat delivery because Codex 0.137 accepts only the Responses wire API.
|
||||
|
||||
cc-connect also owns messaging platform bridges. When cc-connect is the active runtime, channel status probes are routed through the runtime abstraction instead of the OpenClaw Gateway, and the Developer Mode sidebar page shortcut opens cc-connect Web Admin.
|
||||
|
||||
|
||||
+1
-1
@@ -98,7 +98,7 @@ ClawX 直接基于官方 **OpenClaw** 核心构建。无需单独安装,我们
|
||||
|
||||
ClawX 现在也包含 runtime 抽象层。OpenClaw 仍是默认 runtime 和回滚路径,你可以在 **设置 → 网关 → Runtime** 切换到可选的内置 `cc-connect` runtime。打包产物会同时内置 cc-connect 二进制和 OpenAI Codex 原生 CLI bundle;runtime 启动不依赖全局安装、PATH 二进制或运行时下载。cc-connect 使用 ClawX 在 app userData 下托管的配置目录,不会自动修改 `~/.cc-connect`,GUI chat 会通过 cc-connect BridgePlatform 连接到 Codex project agent。Provider/model、cron 任务和已启用 skills 会同步到托管的 cc-connect/Codex runtime。
|
||||
|
||||
在 cc-connect 模式下,Codex provider 同步支持 OpenAI API Key、OpenAI OAuth/Codex、Ollama,以及暴露 Responses API 的 OpenAI-compatible Custom provider。配置为 Chat Completions 的 Custom provider 会在 chat 投递前被明确标记为不支持,因为 Codex 0.137 只接受 Responses wire API。
|
||||
在 cc-connect 模式下,Codex provider 同步支持 OpenAI API Key、OpenAI OAuth/Codex、Ollama,以及暴露 Responses API 的 OpenAI-compatible Custom provider。ByteDance ModelHub-compatible Custom provider 会被规范化到 Codex 使用的 `/api/modelhub/online` Responses endpoint,并且托管配置中只写入环境变量形式的 header 引用,避免持久化密钥或 sticky-session header。配置为 Chat Completions 的 Custom provider 会在 chat 投递前被明确标记为不支持,因为 Codex 0.137 只接受 Responses wire API。
|
||||
|
||||
cc-connect 也负责消息平台桥接。当 cc-connect 是当前 runtime 时,频道状态探测会通过 runtime 抽象层路由,而不是继续固定查询 OpenClaw Gateway;开发者模式侧边栏的页面入口会打开 cc-connect Web Admin。
|
||||
|
||||
|
||||
@@ -19,6 +19,13 @@ export type CodexProviderProfile = {
|
||||
codexArgs: string[];
|
||||
env?: Record<string, string>;
|
||||
envKeys?: string[];
|
||||
ccConnectProvider?: {
|
||||
name: string;
|
||||
apiKeyEnvKey?: string;
|
||||
baseUrl?: string;
|
||||
model?: string;
|
||||
wireApi?: 'responses';
|
||||
};
|
||||
secretAvailable: boolean;
|
||||
codexHomeDir?: string;
|
||||
updatedAt: string;
|
||||
@@ -49,10 +56,68 @@ function tomlString(value: string): string {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
function tomlInlineStringMap(values: Record<string, string>): string {
|
||||
return `{ ${Object.entries(values).map(([key, value]) => `${tomlString(key)} = ${tomlString(value)}`).join(', ')} }`;
|
||||
}
|
||||
|
||||
function normalizeOpenAIResponsesBaseUrl(baseUrl: string): string {
|
||||
return baseUrl.trim().replace(/\/+$/, '').replace(/\/responses$/i, '');
|
||||
}
|
||||
|
||||
function normalizeModelHubCodexResponsesBaseUrl(baseUrl: string): string | null {
|
||||
const trimmed = baseUrl.trim();
|
||||
if (!trimmed) return null;
|
||||
try {
|
||||
const url = new URL(trimmed);
|
||||
if (url.hostname !== 'aidp.bytedance.net') return null;
|
||||
if (!url.pathname.startsWith('/api/modelhub/online')) return null;
|
||||
url.pathname = '/api/modelhub/online';
|
||||
url.search = '';
|
||||
url.hash = '';
|
||||
return url.toString().replace(/\/$/, '');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function writeManagedCodexResponsesConfig(options: {
|
||||
providerKey: string;
|
||||
providerName: string;
|
||||
baseUrl: string;
|
||||
envKey: string;
|
||||
model?: string;
|
||||
envHttpHeaders?: Record<string, string>;
|
||||
}): Promise<string> {
|
||||
const codexHomeDir = getCcConnectCodexHomeDir();
|
||||
await mkdir(codexHomeDir, { recursive: true });
|
||||
const configPath = join(codexHomeDir, 'config.toml');
|
||||
const tableKey = /^[A-Za-z_][A-Za-z0-9_-]*$/.test(options.providerKey)
|
||||
? options.providerKey
|
||||
: tomlString(options.providerKey);
|
||||
const envHeaderEntries = Object.entries(options.envHttpHeaders ?? {});
|
||||
const lines = [
|
||||
...(options.model ? [`model = ${tomlString(options.model)}`] : []),
|
||||
`model_provider = ${tomlString(options.providerKey)}`,
|
||||
'',
|
||||
`[model_providers.${tableKey}]`,
|
||||
`name = ${tomlString(options.providerName)}`,
|
||||
`base_url = ${tomlString(options.baseUrl)}`,
|
||||
`env_key = ${tomlString(options.envKey)}`,
|
||||
'wire_api = "responses"',
|
||||
...(envHeaderEntries.length > 0
|
||||
? [`env_http_headers = ${tomlInlineStringMap(options.envHttpHeaders ?? {})}`]
|
||||
: []),
|
||||
'',
|
||||
];
|
||||
await writeFile(configPath, lines.join('\n'), { encoding: 'utf8', mode: 0o600 });
|
||||
await chmod(configPath, 0o600).catch(() => {});
|
||||
return codexHomeDir;
|
||||
}
|
||||
|
||||
function stableModelHubSessionId(account: ProviderAccount): string {
|
||||
return `clawx-cc-connect-${account.id}`;
|
||||
}
|
||||
|
||||
async function writeManagedOpenAIOAuthAuthFile(
|
||||
tokens: OpenAIOAuthTokenSet,
|
||||
): Promise<string> {
|
||||
@@ -188,6 +253,11 @@ async function buildProfileForAccount(account: ProviderAccount): Promise<CodexPr
|
||||
supported: true,
|
||||
codexArgs: model ? ['--model', model] : [],
|
||||
env,
|
||||
ccConnectProvider: {
|
||||
name: 'openai',
|
||||
...(env.OPENAI_API_KEY ? { apiKeyEnvKey: 'OPENAI_API_KEY' } : {}),
|
||||
...(model ? { model } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -221,9 +291,29 @@ async function buildProfileForAccount(account: ProviderAccount): Promise<CodexPr
|
||||
};
|
||||
}
|
||||
|
||||
const providerKey = 'clawx-custom';
|
||||
const envKey = 'CLAWX_CODEX_CUSTOM_API_KEY';
|
||||
const normalizedBaseUrl = normalizeOpenAIResponsesBaseUrl(baseUrl);
|
||||
const modelHubBaseUrl = normalizeModelHubCodexResponsesBaseUrl(baseUrl);
|
||||
const providerKey = modelHubBaseUrl ? 'modelhub_openapi' : 'clawx-custom';
|
||||
const envKey = modelHubBaseUrl ? 'BYTEDANCE_OPENAI_API_KEY' : 'CLAWX_CODEX_CUSTOM_API_KEY';
|
||||
const extraHeaderEnvKey = 'CODEX_MODELHUB_EXTRA_HEADER';
|
||||
const normalizedBaseUrl = modelHubBaseUrl ?? normalizeOpenAIResponsesBaseUrl(baseUrl);
|
||||
const env: Record<string, string> = { [envKey]: secret.apiKey };
|
||||
const envHttpHeaders = modelHubBaseUrl
|
||||
? {
|
||||
'Api-Key': envKey,
|
||||
extra: extraHeaderEnvKey,
|
||||
}
|
||||
: undefined;
|
||||
if (modelHubBaseUrl) {
|
||||
env[extraHeaderEnvKey] = JSON.stringify({ session_id: stableModelHubSessionId(account) });
|
||||
}
|
||||
const codexHomeDir = await writeManagedCodexResponsesConfig({
|
||||
providerKey,
|
||||
providerName: modelHubBaseUrl ? 'ByteDance ModelHub OpenAPI' : (account.label || 'Custom'),
|
||||
baseUrl: normalizedBaseUrl,
|
||||
envKey,
|
||||
model,
|
||||
envHttpHeaders,
|
||||
});
|
||||
return {
|
||||
...base,
|
||||
supported: true,
|
||||
@@ -231,16 +321,31 @@ async function buildProfileForAccount(account: ProviderAccount): Promise<CodexPr
|
||||
'-c',
|
||||
`model_provider=${tomlString(providerKey)}`,
|
||||
'-c',
|
||||
`model_providers.${providerKey}.name=${tomlString(account.label || 'Custom')}`,
|
||||
`model_providers.${providerKey}.name=${tomlString(modelHubBaseUrl ? 'ByteDance ModelHub OpenAPI' : (account.label || 'Custom'))}`,
|
||||
'-c',
|
||||
`model_providers.${providerKey}.base_url=${tomlString(normalizedBaseUrl)}`,
|
||||
'-c',
|
||||
`model_providers.${providerKey}.env_key=${tomlString(envKey)}`,
|
||||
'-c',
|
||||
`model_providers.${providerKey}.wire_api="responses"`,
|
||||
...(envHttpHeaders ? [
|
||||
'-c',
|
||||
`model_providers.${providerKey}.env_http_headers=${tomlInlineStringMap(envHttpHeaders)}`,
|
||||
] : []),
|
||||
...(model ? ['--model', model] : []),
|
||||
],
|
||||
env: { [envKey]: secret.apiKey },
|
||||
env: {
|
||||
...env,
|
||||
CODEX_HOME: codexHomeDir,
|
||||
},
|
||||
codexHomeDir,
|
||||
ccConnectProvider: {
|
||||
name: providerKey,
|
||||
apiKeyEnvKey: envKey,
|
||||
baseUrl: normalizedBaseUrl,
|
||||
wireApi: 'responses',
|
||||
...(model ? { model } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -74,6 +74,22 @@ function escapeToml(value: string): string {
|
||||
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||
}
|
||||
|
||||
function ccConnectProviderConfig(providerProfile?: CodexProviderProfile | null): string[] {
|
||||
const provider = providerProfile?.ccConnectProvider;
|
||||
if (!provider?.name) return [];
|
||||
return [
|
||||
`provider = "${escapeToml(provider.name)}"`,
|
||||
'',
|
||||
'[[projects.agent.providers]]',
|
||||
`name = "${escapeToml(provider.name)}"`,
|
||||
...(provider.apiKeyEnvKey ? [`api_key = "\${${escapeToml(provider.apiKeyEnvKey)}}"`] : []),
|
||||
...(provider.baseUrl ? [`base_url = "${escapeToml(provider.baseUrl)}"`] : []),
|
||||
...(provider.model ? [`model = "${escapeToml(provider.model)}"`] : []),
|
||||
...(provider.wireApi ? [`wire_api = "${escapeToml(provider.wireApi)}"`] : []),
|
||||
'',
|
||||
];
|
||||
}
|
||||
|
||||
function defaultConfig(options: {
|
||||
codexPath: string;
|
||||
providerProfile?: CodexProviderProfile | null;
|
||||
@@ -117,6 +133,7 @@ function defaultConfig(options: {
|
||||
'mode = "full-auto"',
|
||||
`cmd = "${escapeToml(options.codexPath)}"`,
|
||||
...(model ? [`model = "${escapeToml(model)}"`] : []),
|
||||
...ccConnectProviderConfig(options.providerProfile),
|
||||
'',
|
||||
'# cc-connect requires at least one project platform before the bridge can start.',
|
||||
'# ClawX GUI traffic is delivered by the local [bridge] adapter above; this LINE webhook',
|
||||
@@ -509,6 +526,11 @@ export class CcConnectRuntimeProvider extends EventEmitter implements RuntimePro
|
||||
|
||||
private async syncProviderProfile(payload?: { providerId?: string; reason?: string }) {
|
||||
const profile = await this.loadAndApplyProviderProfile(payload);
|
||||
if (this.status.state === 'running') {
|
||||
await this.restart();
|
||||
} else {
|
||||
await this.ensureManagedConfig(profile, this.resolveCodexPath()).catch(() => undefined);
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
profile: toPublicCodexProviderProfile(profile),
|
||||
|
||||
@@ -249,6 +249,14 @@ describe('cc-connect provider profile sync', () => {
|
||||
model: 'gpt-custom',
|
||||
supported: true,
|
||||
env: { CLAWX_CODEX_CUSTOM_API_KEY: 'custom-secret-value' },
|
||||
codexHomeDir: join(tempDir, 'runtimes', 'cc-connect', 'codex-home'),
|
||||
ccConnectProvider: {
|
||||
name: 'clawx-custom',
|
||||
apiKeyEnvKey: 'CLAWX_CODEX_CUSTOM_API_KEY',
|
||||
baseUrl: 'https://gateway.example/openai',
|
||||
model: 'gpt-custom',
|
||||
wireApi: 'responses',
|
||||
},
|
||||
secretAvailable: true,
|
||||
});
|
||||
expect(profile.codexArgs).toEqual([
|
||||
@@ -268,6 +276,70 @@ describe('cc-connect provider profile sync', () => {
|
||||
const profileFile = await readFile(join(tempDir, 'runtimes', 'cc-connect', 'provider-profile.json'), 'utf8');
|
||||
expect(profileFile).toContain('CLAWX_CODEX_CUSTOM_API_KEY');
|
||||
expect(profileFile).not.toContain('custom-secret-value');
|
||||
const codexConfig = await readFile(join(tempDir, 'runtimes', 'cc-connect', 'codex-home', 'config.toml'), 'utf8');
|
||||
expect(codexConfig).toContain('model_provider = "clawx-custom"');
|
||||
expect(codexConfig).toContain('base_url = "https://gateway.example/openai"');
|
||||
expect(codexConfig).not.toContain('custom-secret-value');
|
||||
});
|
||||
|
||||
it('maps ByteDance ModelHub custom providers to the Codex Responses endpoint with env header refs', async () => {
|
||||
getDefaultProviderAccountIdMock.mockResolvedValue('modelhub-responses');
|
||||
getProviderAccountMock.mockResolvedValue({
|
||||
id: 'modelhub-responses',
|
||||
vendorId: 'custom',
|
||||
label: 'modelhub-openai',
|
||||
authMode: 'api_key',
|
||||
baseUrl: 'https://aidp.bytedance.net/api/modelhub/online/v2/crawl',
|
||||
apiProtocol: 'openai-responses',
|
||||
model: 'gpt-5.5-2026-04-24',
|
||||
enabled: true,
|
||||
isDefault: true,
|
||||
createdAt: '2026-06-07T00:00:00.000Z',
|
||||
updatedAt: '2026-06-07T00:00:00.000Z',
|
||||
});
|
||||
getProviderSecretMock.mockResolvedValue({
|
||||
type: 'api_key',
|
||||
accountId: 'modelhub-responses',
|
||||
apiKey: 'modelhub-secret-value',
|
||||
});
|
||||
const { syncCcConnectProviderProfile } = await import('@electron/runtime/cc-connect-provider-profile');
|
||||
|
||||
const profile = await syncCcConnectProviderProfile();
|
||||
|
||||
expect(profile).toMatchObject({
|
||||
providerId: 'modelhub-responses',
|
||||
vendorId: 'custom',
|
||||
supported: true,
|
||||
model: 'gpt-5.5-2026-04-24',
|
||||
env: {
|
||||
BYTEDANCE_OPENAI_API_KEY: 'modelhub-secret-value',
|
||||
CODEX_MODELHUB_EXTRA_HEADER: '{"session_id":"clawx-cc-connect-modelhub-responses"}',
|
||||
CODEX_HOME: join(tempDir, 'runtimes', 'cc-connect', 'codex-home'),
|
||||
},
|
||||
ccConnectProvider: {
|
||||
name: 'modelhub_openapi',
|
||||
apiKeyEnvKey: 'BYTEDANCE_OPENAI_API_KEY',
|
||||
baseUrl: 'https://aidp.bytedance.net/api/modelhub/online',
|
||||
model: 'gpt-5.5-2026-04-24',
|
||||
wireApi: 'responses',
|
||||
},
|
||||
});
|
||||
expect(profile.codexArgs).toContain('model_provider="modelhub_openapi"');
|
||||
expect(profile.codexArgs).toContain('model_providers.modelhub_openapi.base_url="https://aidp.bytedance.net/api/modelhub/online"');
|
||||
expect(profile.codexArgs).toContain('model_providers.modelhub_openapi.env_http_headers={ "Api-Key" = "BYTEDANCE_OPENAI_API_KEY", "extra" = "CODEX_MODELHUB_EXTRA_HEADER" }');
|
||||
|
||||
const codexConfig = await readFile(join(tempDir, 'runtimes', 'cc-connect', 'codex-home', 'config.toml'), 'utf8');
|
||||
expect(codexConfig).toContain('model_provider = "modelhub_openapi"');
|
||||
expect(codexConfig).toContain('base_url = "https://aidp.bytedance.net/api/modelhub/online"');
|
||||
expect(codexConfig).toContain('env_http_headers = { "Api-Key" = "BYTEDANCE_OPENAI_API_KEY", "extra" = "CODEX_MODELHUB_EXTRA_HEADER" }');
|
||||
expect(codexConfig).not.toContain('modelhub-secret-value');
|
||||
expect(codexConfig).not.toContain('clawx-cc-connect-modelhub-responses');
|
||||
|
||||
const profileFile = await readFile(join(tempDir, 'runtimes', 'cc-connect', 'provider-profile.json'), 'utf8');
|
||||
expect(profileFile).toContain('BYTEDANCE_OPENAI_API_KEY');
|
||||
expect(profileFile).toContain('CODEX_MODELHUB_EXTRA_HEADER');
|
||||
expect(profileFile).not.toContain('modelhub-secret-value');
|
||||
expect(profileFile).not.toContain('clawx-cc-connect-modelhub-responses');
|
||||
});
|
||||
|
||||
it('marks custom chat completions providers unsupported because Codex only accepts responses wire api', async () => {
|
||||
|
||||
@@ -179,6 +179,63 @@ describe('CcConnectRuntimeProvider', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('writes cc-connect provider config for custom Responses providers without persisting secrets', async () => {
|
||||
const binaryPath = join(tempDir, 'cc-connect');
|
||||
await writeFile(binaryPath, '#!/bin/sh\n', { mode: 0o755 });
|
||||
const providerProfileLoader = vi.fn(async () => createProviderProfile({
|
||||
providerId: 'custom-responses',
|
||||
vendorId: 'custom',
|
||||
label: 'ModelHub',
|
||||
model: 'gpt-5.5',
|
||||
codexArgs: [],
|
||||
env: { CLAWX_CODEX_CUSTOM_API_KEY: 'secret-value' },
|
||||
ccConnectProvider: {
|
||||
name: 'clawx-custom',
|
||||
apiKeyEnvKey: 'CLAWX_CODEX_CUSTOM_API_KEY',
|
||||
baseUrl: 'https://aidp.bytedance.net/api/modelhub/online/v2/crawl',
|
||||
model: 'gpt-5.5',
|
||||
wireApi: 'responses',
|
||||
},
|
||||
secretAvailable: true,
|
||||
}));
|
||||
const bridgeAdapter = createBridgeAdapterMock();
|
||||
|
||||
const { CcConnectRuntimeProvider } = await import('@electron/runtime/cc-connect-provider');
|
||||
const provider = new CcConnectRuntimeProvider({
|
||||
binaryPath,
|
||||
codexPath: join(tempDir, 'codex'),
|
||||
codexBridge: createBridgeMock() as never,
|
||||
bridgeAdapter: bridgeAdapter as never,
|
||||
skillSyncer: vi.fn(async () => ({ skills: [] })),
|
||||
providerProfileLoader: providerProfileLoader as never,
|
||||
});
|
||||
const child = createChild();
|
||||
forkMock.mockReturnValueOnce(child);
|
||||
|
||||
const startPromise = provider.start();
|
||||
await vi.waitFor(() => expect(forkMock).toHaveBeenCalledOnce());
|
||||
child.emit('spawn');
|
||||
await startPromise;
|
||||
|
||||
const config = await readFile(join(tempDir, 'runtimes', 'cc-connect', 'config.toml'), 'utf8');
|
||||
expect(config).toContain('provider = "clawx-custom"');
|
||||
expect(config).toContain('[[projects.agent.providers]]');
|
||||
expect(config).toContain('name = "clawx-custom"');
|
||||
expect(config).toContain('api_key = "${CLAWX_CODEX_CUSTOM_API_KEY}"');
|
||||
expect(config).toContain('base_url = "https://aidp.bytedance.net/api/modelhub/online/v2/crawl"');
|
||||
expect(config).toContain('model = "gpt-5.5"');
|
||||
expect(config).toContain('wire_api = "responses"');
|
||||
expect(config).not.toContain('secret-value');
|
||||
expect(forkMock).toHaveBeenCalledWith(binaryPath, [
|
||||
'-config',
|
||||
join(tempDir, 'runtimes', 'cc-connect', 'config.toml'),
|
||||
], expect.objectContaining({
|
||||
env: expect.objectContaining({
|
||||
CLAWX_CODEX_CUSTOM_API_KEY: 'secret-value',
|
||||
}),
|
||||
}));
|
||||
});
|
||||
|
||||
it('returns cc-connect skills status instead of rejecting skill RPC', async () => {
|
||||
const binaryPath = join(tempDir, 'cc-connect');
|
||||
await writeFile(binaryPath, '#!/bin/sh\n', { mode: 0o755 });
|
||||
@@ -420,6 +477,53 @@ describe('CcConnectRuntimeProvider', () => {
|
||||
}));
|
||||
});
|
||||
|
||||
it('restarts the running cc-connect process when provider sync changes launch config', async () => {
|
||||
const binaryPath = join(tempDir, 'cc-connect');
|
||||
await writeFile(binaryPath, '#!/bin/sh\n', { mode: 0o755 });
|
||||
const bridgeAdapter = createBridgeAdapterMock();
|
||||
const providerProfileLoader = vi.fn(async () => createProviderProfile({
|
||||
providerId: 'openai-main',
|
||||
vendorId: 'openai',
|
||||
model: 'gpt-5.5',
|
||||
codexArgs: ['--model', 'gpt-5.5'],
|
||||
env: { OPENAI_API_KEY: 'sk-test' },
|
||||
ccConnectProvider: {
|
||||
name: 'openai',
|
||||
apiKeyEnvKey: 'OPENAI_API_KEY',
|
||||
model: 'gpt-5.5',
|
||||
},
|
||||
secretAvailable: true,
|
||||
}));
|
||||
const { CcConnectRuntimeProvider } = await import('@electron/runtime/cc-connect-provider');
|
||||
const provider = new CcConnectRuntimeProvider({
|
||||
binaryPath,
|
||||
codexPath: join(tempDir, 'codex'),
|
||||
codexBridge: createBridgeMock() as never,
|
||||
bridgeAdapter: bridgeAdapter as never,
|
||||
skillSyncer: vi.fn(async () => ({ skills: [] })),
|
||||
providerProfileLoader: providerProfileLoader as never,
|
||||
});
|
||||
const firstChild = createChild();
|
||||
const secondChild = createChild();
|
||||
forkMock.mockReturnValueOnce(firstChild).mockReturnValueOnce(secondChild);
|
||||
const startPromise = provider.start();
|
||||
await vi.waitFor(() => expect(forkMock).toHaveBeenCalledTimes(1));
|
||||
firstChild.emit('spawn');
|
||||
await startPromise;
|
||||
|
||||
const syncPromise = provider.rpc('providers.sync', {
|
||||
providerId: 'openai-main',
|
||||
reason: 'set-default',
|
||||
});
|
||||
await vi.waitFor(() => expect(forkMock).toHaveBeenCalledTimes(2));
|
||||
secondChild.emit('spawn');
|
||||
await syncPromise;
|
||||
|
||||
expect(firstChild.kill).toHaveBeenCalledOnce();
|
||||
expect(bridgeAdapter.close).toHaveBeenCalledOnce();
|
||||
expect(bridgeAdapter.connect).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('runs cc-connect doctor against the managed config', async () => {
|
||||
const binaryPath = join(tempDir, 'cc-connect');
|
||||
await writeFile(binaryPath, '#!/bin/sh\n', { mode: 0o755 });
|
||||
|
||||
Reference in New Issue
Block a user