Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ npm install -g claude-adapter

## Configuration

Runtime configuration priority:

`ANTHROPIC_*` environment variables > `~/.claude-adapter/config.json` > interactive prompt.

Environment variables are runtime-only overrides. They are not persisted back to `~/.claude-adapter/config.json`.

### CLI Options

The CLI accepts several flags to customize runtime behavior:
Expand All @@ -112,6 +118,29 @@ The core of the adapter's flexibility lies in its ability to map Claude's expect
| `sonnet` | Balanced tasks | `deepseek-3.2`, `minimax-m2.1` |
| `haiku` | Low-latency ops | `gpt-5-mini`, `gpt-oss-120b` |

### Runtime Environment Variable Mapping

These variables can override startup configuration for the current process:

| Environment Variable | Runtime Target |
| -------------------- | -------------- |
| `ANTHROPIC_BASE_URL` | `baseUrl` |
| `ANTHROPIC_API_KEY` | `apiKey` (highest priority) |
| `ANTHROPIC_AUTH_TOKEN` | `apiKey` fallback when `ANTHROPIC_API_KEY` is absent |
| `ANTHROPIC_DEFAULT_OPUS_MODEL` | `models.opus` |
| `ANTHROPIC_DEFAULT_SONNET_MODEL` | `models.sonnet` |
| `ANTHROPIC_DEFAULT_HAIKU_MODEL` | `models.haiku` |
| `ANTHROPIC_DEFAULT_MODEL` | fallback model for unset opus/sonnet/haiku |

Example:

```bash
ANTHROPIC_BASE_URL=https://api.openai.com/v1 \
ANTHROPIC_API_KEY=sk-*** \
ANTHROPIC_DEFAULT_MODEL=gpt-5-mini \
claude-adapter --no-claude-settings
```

---

## API Reference
Expand Down
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"openai": "4.76.0"
},
"devDependencies": {
"@types/commander": "^2.12.0",
"@types/inquirer": "^9.0.7",
"@types/jest": "^29.5.14",
"@types/node": "^22.10.2",
Expand All @@ -72,4 +73,4 @@
"engines": {
"node": ">=20.0.0"
}
}
}
48 changes: 42 additions & 6 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,29 @@ import { createServer, findAvailablePort } from './server';
import { UI } from './utils/ui';
import { checkForUpdates } from './utils/update';
import { getMetadata } from './utils/metadata';
import { resolveRuntimeConfig } from './utils/runtimeConfig';
import { version } from '../package.json';

const program = new Command();

interface CliOptions {
port: string;
reconfigure?: boolean;
claudeSettings: boolean;
}

function isValidUrl(input: string | undefined): boolean {
if (!input) {
return false;
}
try {
new URL(input);
return true;
} catch {
return false;
}
}

program
.name('claude-adapter')
.description('Proxy adapter to use OpenAI API with Claude Code')
Expand All @@ -26,7 +45,7 @@ program
.option('-p, --port <port>', 'Port to run the proxy server on', '3080')
.option('-r, --reconfigure', 'Force reconfiguration even if config exists')
.option('--no-claude-settings', 'Skip updating Claude Code settings files')
.action(async (options) => {
.action(async (options: CliOptions) => {
UI.banner();
UI.header('Adapt any model for Claude Code');

Expand All @@ -42,10 +61,18 @@ program
UI.info('Skipping Claude settings update (--no-claude-settings)');
}

// Step 2: Load or create configuration
let config = loadConfig();

if (!config || options.reconfigure) {
// Step 2: Load file config and resolve runtime config using env > file > prompt
const fileConfig = loadConfig();
const resolvedRuntime = resolveRuntimeConfig(fileConfig, process.env);
let config = resolvedRuntime.config as AdapterConfig | null;

if (
!config ||
options.reconfigure ||
!isValidUrl(config.baseUrl) ||
!config.apiKey ||
!config.models?.opus
) {
UI.log(''); // Spacing
config = await promptForConfiguration();
saveConfig(config);
Expand All @@ -56,14 +83,23 @@ program
UI.log(''); // Spacing
const toolStyle = await promptForToolCallingStyle();
Comment on lines 83 to 84

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid forcing tool-style prompt for env-only startup

When runtime env vars provide a complete config but ~/.claude-adapter/config.json is absent, this branch still prompts for toolFormat because config.toolFormat is undefined. That makes the new env-driven startup path interactive-only, so headless runs (CI/scripts/non-TTY) will block or fail despite having ANTHROPIC_BASE_URL, ANTHROPIC_API_KEY, and model env vars set. Since server code already defaults missing toolFormat to 'native', this prompt should be skipped (or defaulted) when no file-backed config exists.

Useful? React with 👍 / 👎.

config.toolFormat = toolStyle;
saveConfig(config);
if (fileConfig) {
saveConfig({ ...fileConfig, toolFormat: toolStyle });
}
console.log(`\x1b[2m✔\x1b[0m Tool Format: ${UI.dim(`[${config.toolFormat.toUpperCase()}]`)}`);
UI.info('Tool calling preference saved');
} else {
UI.info('Using existing configuration');
console.log(`\x1b[2m✔\x1b[0m Tool Format: ${UI.dim(`[${config.toolFormat.toUpperCase()}]`)}`);
}

for (const warning of resolvedRuntime.warnings) {
UI.info(warning);
}
UI.info(
`Config source: baseUrl=${resolvedRuntime.sources.baseUrl}, apiKey=${resolvedRuntime.sources.apiKey}, models(opus=${resolvedRuntime.sources.models.opus}, sonnet=${resolvedRuntime.sources.models.sonnet}, haiku=${resolvedRuntime.sources.models.haiku})`
);

// Step 3: Find available port and start server
const preferredPort = parseInt(options.port, 10) || 3080;
const port = await findAvailablePort(preferredPort);
Expand Down
115 changes: 115 additions & 0 deletions src/utils/runtimeConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { AdapterConfig, ModelConfig } from '../types/config';

type ConfigSource = 'env' | 'file' | 'prompt' | 'fallback';

export interface RuntimeConfigSources {
baseUrl: ConfigSource;
apiKey: string;
models: {
opus: ConfigSource;
sonnet: ConfigSource;
haiku: ConfigSource;
};
}

export interface RuntimeConfigResult {
config: Partial<AdapterConfig>;
sources: RuntimeConfigSources;
warnings: string[];
}

function readNonEmptyEnv(env: NodeJS.ProcessEnv, key: string): string | undefined {
const value = env[key]?.trim();
return value ? value : undefined;
}

function resolveModelTier(
envValue: string | undefined,
defaultModel: string | undefined,
fileValue: string | undefined,
fallbackValue?: string
): { value: string | undefined; source: ConfigSource } {
if (envValue) {
return { value: envValue, source: 'env' };
}
if (defaultModel) {
return { value: defaultModel, source: 'fallback' };
}
if (fileValue) {
return { value: fileValue, source: 'file' };
}
if (fallbackValue) {
return { value: fallbackValue, source: 'fallback' };
}
return { value: undefined, source: 'prompt' };
}

export function resolveRuntimeConfig(
fileConfig: AdapterConfig | null,
env: NodeJS.ProcessEnv = process.env
): RuntimeConfigResult {
const warnings: string[] = [];

const envBaseUrl = readNonEmptyEnv(env, 'ANTHROPIC_BASE_URL');
const envApiKey = readNonEmptyEnv(env, 'ANTHROPIC_API_KEY');
const envAuthToken = readNonEmptyEnv(env, 'ANTHROPIC_AUTH_TOKEN');
const envDefaultModel = readNonEmptyEnv(env, 'ANTHROPIC_DEFAULT_MODEL');

if (envApiKey && envAuthToken) {
warnings.push('ANTHROPIC_AUTH_TOKEN ignored because ANTHROPIC_API_KEY is set');
}

const opus = resolveModelTier(
readNonEmptyEnv(env, 'ANTHROPIC_DEFAULT_OPUS_MODEL'),
envDefaultModel,
fileConfig?.models?.opus
);
const sonnet = resolveModelTier(
readNonEmptyEnv(env, 'ANTHROPIC_DEFAULT_SONNET_MODEL'),
envDefaultModel,
fileConfig?.models?.sonnet,
opus.value
);
const haiku = resolveModelTier(
readNonEmptyEnv(env, 'ANTHROPIC_DEFAULT_HAIKU_MODEL'),
envDefaultModel,
fileConfig?.models?.haiku,
sonnet.value
);

const resolvedModels: ModelConfig | undefined = opus.value
? {
opus: opus.value,
sonnet: sonnet.value || opus.value,
haiku: haiku.value || sonnet.value || opus.value,
}
: undefined;

const resolvedApiKey = envApiKey ?? envAuthToken ?? fileConfig?.apiKey;

return {
config: {
baseUrl: envBaseUrl ?? fileConfig?.baseUrl,
apiKey: resolvedApiKey,
models: resolvedModels,
toolFormat: fileConfig?.toolFormat,
port: fileConfig?.port,
},
sources: {
baseUrl: envBaseUrl ? 'env' : fileConfig?.baseUrl ? 'file' : 'prompt',
apiKey: envApiKey
? 'env(ANTHROPIC_API_KEY)'
: envAuthToken
? 'env(ANTHROPIC_AUTH_TOKEN)'
: fileConfig?.apiKey
? 'file'
: 'prompt',
models: {
opus: opus.source,
sonnet: sonnet.source,
haiku: haiku.source,
},
},
warnings,
};
}
Loading