feat(auth): add Anthropic OAuth support and deferred credential loading
- Read Claude Code's OAuth token from ~/.claude/.credentials.json as a fallback source for auth_mode: oauth (with expiry checking) - Fix OAuth callback server to bind to localhost (not 127.0.0.1) and use JSON content type for token exchange - Null out apiKey when authToken is set to prevent SDK from falling back to ANTHROPIC_API_KEY env var (routes to wrong billing) - Add DeferredErrorClient so daemon starts even when credentials are missing, surfacing the error on first chat() call instead of crash - Prompt to complete OAuth flow immediately when setting auth_mode to oauth with no token stored Note: Anthropic currently rejects OAuth for API access (Feb 2026 policy change), but the plumbing is in place for if/when re-enabled. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+33
-6
@@ -78,6 +78,33 @@ function resolveZaiCredential(cfg: ModelConfig): string {
|
||||
return raw.startsWith('Bearer ') ? raw.slice('Bearer '.length) : raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* A ModelClient that defers a credential error to the first chat() call.
|
||||
* Used so the daemon can start even when credentials are not yet configured.
|
||||
*/
|
||||
class DeferredErrorClient implements ModelClient {
|
||||
constructor(private readonly error: Error) {}
|
||||
|
||||
chat(): Promise<never> {
|
||||
return Promise.reject(this.error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Like createClientFromConfig but never throws at construction time.
|
||||
* If credentials are missing, returns a DeferredErrorClient that will
|
||||
* surface the error on the first chat() call.
|
||||
*/
|
||||
export function createClientFromConfigOrDeferred(cfg: ModelConfig): ModelClient {
|
||||
try {
|
||||
return createClientFromConfig(cfg);
|
||||
} catch (error) {
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
logger.warn(`Deferred credential error for provider "${cfg.provider}": ${err.message}`);
|
||||
return new DeferredErrorClient(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a ModelClient from a provider config entry.
|
||||
* Dispatches on the `provider` field so all tiers and fallback entries
|
||||
@@ -391,11 +418,11 @@ export function createAutoFallbackClient(tierConfig: { provider: string; model:
|
||||
export function createModelRouter(config: Config): ModelRouter {
|
||||
const models = config.models;
|
||||
|
||||
const defaultClient = createClientFromConfig(models.default);
|
||||
const defaultClient = createClientFromConfigOrDeferred(models.default);
|
||||
|
||||
const fastClient = models.fast ? createClientFromConfig(models.fast) : undefined;
|
||||
const complexClient = models.complex ? createClientFromConfig(models.complex) : undefined;
|
||||
const localClient = models.local ? createClientFromConfig(models.local) : undefined;
|
||||
const fastClient = models.fast ? createClientFromConfigOrDeferred(models.fast) : undefined;
|
||||
const complexClient = models.complex ? createClientFromConfigOrDeferred(models.complex) : undefined;
|
||||
const localClient = models.local ? createClientFromConfigOrDeferred(models.local) : undefined;
|
||||
|
||||
// Build fallback chain — each entry references a tier name or 'local'
|
||||
const fallbackChain: ModelClient[] = [];
|
||||
@@ -411,7 +438,7 @@ export function createModelRouter(config: Config): ModelRouter {
|
||||
fallbackChain.push(complexClient);
|
||||
} else if (models.local_providers?.[providerName]) {
|
||||
// Named provider from local_providers map
|
||||
fallbackChain.push(createClientFromConfig(models.local_providers[providerName]));
|
||||
fallbackChain.push(createClientFromConfigOrDeferred(models.local_providers[providerName]));
|
||||
} else {
|
||||
logger.warn(`Fallback chain entry "${providerName}" not found — skipping`);
|
||||
}
|
||||
@@ -448,7 +475,7 @@ export function createModelRouter(config: Config): ModelRouter {
|
||||
|
||||
// User-configured inline fallback
|
||||
if (cfg.fallback) {
|
||||
fallbackList.push(createClientFromConfig(cfg.fallback));
|
||||
fallbackList.push(createClientFromConfigOrDeferred(cfg.fallback));
|
||||
}
|
||||
|
||||
if (fallbackList.length > 0) {
|
||||
|
||||
Reference in New Issue
Block a user