Merge pull request #2023 from KeXin95/fix/custom-anthropic-base-url
feat: pass ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN into agent containers
This commit is contained in:
@@ -22,6 +22,8 @@
|
|||||||
* headless `claude -p` call for IANA-zone resolution.
|
* headless `claude -p` call for IANA-zone resolution.
|
||||||
*/
|
*/
|
||||||
import { spawn, spawnSync } from 'child_process';
|
import { spawn, spawnSync } from 'child_process';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
import * as p from '@clack/prompts';
|
import * as p from '@clack/prompts';
|
||||||
import k from 'kleur';
|
import k from 'kleur';
|
||||||
@@ -635,6 +637,16 @@ async function runAuthStep(): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Custom Anthropic-compatible endpoint flow. Both URL and token must be set;
|
||||||
|
// OneCLI stores the token as a generic Bearer secret keyed to the URL host,
|
||||||
|
// so the container only ever sees ANTHROPIC_BASE_URL + a placeholder.
|
||||||
|
const customBaseUrl = process.env.NANOCLAW_ANTHROPIC_BASE_URL?.trim();
|
||||||
|
const customAuthToken = process.env.NANOCLAW_ANTHROPIC_AUTH_TOKEN?.trim();
|
||||||
|
if (customBaseUrl && customAuthToken) {
|
||||||
|
await runCustomEndpointAuth(customBaseUrl, customAuthToken);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const method = ensureAnswer(
|
const method = ensureAnswer(
|
||||||
await brightSelect({
|
await brightSelect({
|
||||||
message: 'How would you like to connect to Claude?',
|
message: 'How would you like to connect to Claude?',
|
||||||
@@ -740,6 +752,92 @@ async function runPasteAuth(method: 'oauth' | 'api'): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up Anthropic auth for a custom endpoint. The token is stored as a
|
||||||
|
* OneCLI generic secret with header injection so the proxy rewrites the
|
||||||
|
* Authorization header on the wire — the container only ever sees
|
||||||
|
* ANTHROPIC_BASE_URL + a placeholder bearer.
|
||||||
|
*/
|
||||||
|
async function runCustomEndpointAuth(
|
||||||
|
baseUrl: string,
|
||||||
|
token: string,
|
||||||
|
): Promise<void> {
|
||||||
|
let host: string;
|
||||||
|
try {
|
||||||
|
host = new URL(baseUrl).hostname;
|
||||||
|
} catch {
|
||||||
|
await fail(
|
||||||
|
'auth',
|
||||||
|
`Invalid Anthropic base URL: ${baseUrl}`,
|
||||||
|
'Check --anthropic-base-url and retry.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await runQuietChild(
|
||||||
|
'auth',
|
||||||
|
'onecli',
|
||||||
|
[
|
||||||
|
'secrets',
|
||||||
|
'create',
|
||||||
|
'--name',
|
||||||
|
'Anthropic',
|
||||||
|
'--type',
|
||||||
|
'generic',
|
||||||
|
'--value',
|
||||||
|
token,
|
||||||
|
'--host-pattern',
|
||||||
|
host,
|
||||||
|
'--header-name',
|
||||||
|
'Authorization',
|
||||||
|
'--value-format',
|
||||||
|
'Bearer {value}',
|
||||||
|
],
|
||||||
|
{
|
||||||
|
running: `Saving your Anthropic auth token to your OneCLI vault…`,
|
||||||
|
done: 'Claude account connected.',
|
||||||
|
},
|
||||||
|
{ extraFields: { METHOD: 'custom-endpoint', HOST: host } },
|
||||||
|
);
|
||||||
|
if (!res.ok) {
|
||||||
|
await fail(
|
||||||
|
'auth',
|
||||||
|
`Couldn't save your Anthropic auth token to the vault.`,
|
||||||
|
'Make sure OneCLI is running (`onecli version`), then retry.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ANTHROPIC_BASE_URL has to be in .env so the runtime provider config
|
||||||
|
// reads it when building container env. The token is *not* written —
|
||||||
|
// OneCLI holds it.
|
||||||
|
writeEnvLine('ANTHROPIC_BASE_URL', baseUrl);
|
||||||
|
|
||||||
|
// Register the claude provider so the runtime passes ANTHROPIC_BASE_URL
|
||||||
|
// and the placeholder bearer into the container. Only appended when the
|
||||||
|
// user has configured a custom endpoint; standard installs don't load
|
||||||
|
// the file at all.
|
||||||
|
appendProviderImport('./claude.js');
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeEnvLine(key: string, value: string): void {
|
||||||
|
const envFile = path.join(process.cwd(), '.env');
|
||||||
|
const content = fs.existsSync(envFile) ? fs.readFileSync(envFile, 'utf-8') : '';
|
||||||
|
const re = new RegExp(`^${key}=.*$`, 'm');
|
||||||
|
const next = re.test(content)
|
||||||
|
? content.replace(re, `${key}=${value}`)
|
||||||
|
: content.trimEnd() + (content ? '\n' : '') + `${key}=${value}\n`;
|
||||||
|
fs.writeFileSync(envFile, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendProviderImport(modulePath: string): void {
|
||||||
|
const file = path.join(process.cwd(), 'src', 'providers', 'index.ts');
|
||||||
|
const content = fs.existsSync(file) ? fs.readFileSync(file, 'utf-8') : '';
|
||||||
|
const line = `import '${modulePath}';`;
|
||||||
|
if (content.includes(line)) return;
|
||||||
|
const sep = content && !content.endsWith('\n') ? '\n' : '';
|
||||||
|
fs.writeFileSync(file, content + sep + line + '\n');
|
||||||
|
}
|
||||||
|
|
||||||
// ─── timezone step ─────────────────────────────────────────────────────
|
// ─── timezone step ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -95,6 +95,16 @@ export const CONFIG: Entry[] = [
|
|||||||
placeholder: 'https://api.anthropic.com',
|
placeholder: 'https://api.anthropic.com',
|
||||||
validate: httpUrl,
|
validate: httpUrl,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'anthropicAuthToken',
|
||||||
|
label: 'Anthropic auth token',
|
||||||
|
help: 'Bearer token for the custom Anthropic endpoint. Used together with --anthropic-base-url.',
|
||||||
|
surface: 'flag+ui',
|
||||||
|
group: 'Anthropic',
|
||||||
|
type: 'string',
|
||||||
|
secret: true,
|
||||||
|
validate: (v) => (v.trim() ? undefined : 'Required'),
|
||||||
|
},
|
||||||
|
|
||||||
// Existing env-var knobs — flag-only so they don't clutter the UI screen.
|
// Existing env-var knobs — flag-only so they don't clutter the UI screen.
|
||||||
{
|
{
|
||||||
|
|||||||
28
src/providers/claude.ts
Normal file
28
src/providers/claude.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* Claude provider container config — only registered when the user has
|
||||||
|
* configured a custom Anthropic-compatible endpoint via setup. Setup
|
||||||
|
* appends `import './claude.js'` to providers/index.ts at that point;
|
||||||
|
* standard installs hitting api.anthropic.com don't need this file
|
||||||
|
* loaded.
|
||||||
|
*
|
||||||
|
* The real auth token never enters the container. Setup creates an
|
||||||
|
* OneCLI generic secret (host-pattern = base URL hostname, header-name
|
||||||
|
* = Authorization, value-format = "Bearer {value}") so the proxy
|
||||||
|
* rewrites the Authorization header on the wire. The container only
|
||||||
|
* needs:
|
||||||
|
* - ANTHROPIC_BASE_URL — so the SDK knows where to call
|
||||||
|
* - ANTHROPIC_AUTH_TOKEN=placeholder — so the SDK adds an
|
||||||
|
* Authorization: Bearer header for OneCLI to overwrite
|
||||||
|
*/
|
||||||
|
import { readEnvFile } from '../env.js';
|
||||||
|
import { registerProviderContainerConfig } from './provider-container-registry.js';
|
||||||
|
|
||||||
|
registerProviderContainerConfig('claude', () => {
|
||||||
|
const dotenv = readEnvFile(['ANTHROPIC_BASE_URL']);
|
||||||
|
const env: Record<string, string> = {};
|
||||||
|
if (dotenv.ANTHROPIC_BASE_URL) {
|
||||||
|
env.ANTHROPIC_BASE_URL = dotenv.ANTHROPIC_BASE_URL;
|
||||||
|
env.ANTHROPIC_AUTH_TOKEN = 'placeholder';
|
||||||
|
}
|
||||||
|
return { env };
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user