Skip to content
Closed
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ Feature flag `VOICE_MODE`,dev/build 默认启用。Push-to-Talk 语音输入
- **`src/services/api/openai/`** — client、消息/工具转换、流适配、模型映射
- **`src/utils/model/providers.ts`** — 添加 `'openai'` provider 类型(最高优先级)

关键环境变量:`CLAUDE_CODE_USE_OPENAI`、`OPENAI_API_KEY`、`OPENAI_BASE_URL`、`OPENAI_MODEL`、`OPENAI_MODEL_MAP`。详见 `docs/plans/openai-compatibility.md`。
关键环境变量:`CLAUDE_CODE_USE_OPENAI`、`OPENAI_API_KEY`、`OPENAI_BASE_URL`、`OPENAI_MODEL`、`OPENAI_DEFAULT_OPUS_MODEL`、`OPENAI_DEFAULT_SONNET_MODEL`、`OPENAI_DEFAULT_HAIKU_MODEL`。详见 `docs/plans/openai-compatibility.md`。

### Key Type Files

Expand Down
11 changes: 7 additions & 4 deletions docs/plans/openai-compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ claude-code 支持通过 OpenAI Chat Completions API(`/v1/chat/completions`)
| `OPENAI_API_KEY` | 是 | API key(Ollama 等可设为任意值) |
| `OPENAI_BASE_URL` | 推荐 | 端点 URL(如 `http://localhost:11434/v1`) |
| `OPENAI_MODEL` | 可选 | 覆盖所有请求的模型名(跳过映射) |
| `OPENAI_MODEL_MAP` | 可选 | JSON 映射,如 `{"claude-sonnet-4-6":"gpt-4o"}` |
| `OPENAI_DEFAULT_OPUS_MODEL` | 可选 | 覆盖 opus 家族对应的模型(如 `o3`, `o3-mini`, `o1-pro`) |
| `OPENAI_DEFAULT_SONNET_MODEL` | 可选 | 覆盖 sonnet 家族对应的模型(如 `gpt-4o`, `gpt-4.1`) |
| `OPENAI_DEFAULT_HAIKU_MODEL` | 可选 | 覆盖 haiku 家族对应的模型(如 `gpt-4o-mini`, `gpt-4.0-mini`) |
| `OPENAI_ORG_ID` | 可选 | Organization ID |
| `OPENAI_PROJECT_ID` | 可选 | Project ID |

Expand Down Expand Up @@ -85,9 +87,10 @@ queryModel() [claude.ts]
`resolveOpenAIModel()` 的解析顺序:

1. `OPENAI_MODEL` 环境变量 → 直接使用,覆盖所有
2. `OPENAI_MODEL_MAP` JSON 查表 → 自定义映射
3. 内置默认映射(见下表)
4. 以上都不匹配 → 原名透传
2. `OPENAI_DEFAULT_{FAMILY}_MODEL` 变量(如 `OPENAI_DEFAULT_SONNET_MODEL`)→ 按模型家族覆盖
3. `ANTHROPIC_DEFAULT_{FAMILY}_MODEL` 变量(向后兼容)
4. 内置默认映射(见下表)
5. 以上都不匹配 → 原名透传

### 内置模型映射

Expand Down
63 changes: 42 additions & 21 deletions src/QueryEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ import { type Tools, type ToolUseContext, toolMatchesName } from './Tool.js'
import type { AgentDefinition } from './tools/AgentTool/loadAgentsDir.js'
import { SYNTHETIC_OUTPUT_TOOL_NAME } from './tools/SyntheticOutputTool/SyntheticOutputTool.js'
import type { APIError } from '@anthropic-ai/sdk'
import type { CompactMetadata, Message, SystemCompactBoundaryMessage } from './types/message.js'
import type {
CompactMetadata,
Message,
SystemCompactBoundaryMessage,
} from './types/message.js'
import type { OrphanedPermission } from './types/textInputTypes.js'
import { createAbortController } from './utils/abortController.js'
import type { AttributionState } from './utils/commitAttribution.js'
Expand Down Expand Up @@ -708,7 +712,8 @@ export class QueryEngine {
message.subtype === 'compact_boundary'
) {
const compactMsg = message as SystemCompactBoundaryMessage
const tailUuid = compactMsg.compactMetadata?.preservedSegment?.tailUuid
const tailUuid =
compactMsg.compactMetadata?.preservedSegment?.tailUuid
if (tailUuid) {
const tailIdx = this.mutableMessages.findLastIndex(
m => m.uuid === tailUuid,
Expand Down Expand Up @@ -768,7 +773,10 @@ export class QueryEngine {
// streamed responses, this is null at content_block_stop time;
// the real value arrives via message_delta (handled below).
const msg = message as Message
const stopReason = msg.message?.stop_reason as string | null | undefined
const stopReason = msg.message?.stop_reason as
| string
| null
| undefined
if (stopReason != null) {
lastStopReason = stopReason
}
Expand Down Expand Up @@ -798,11 +806,15 @@ export class QueryEngine {
break
}
case 'stream_event': {
const event = (message as unknown as { event: Record<string, unknown> }).event
const event = (
message as unknown as { event: Record<string, unknown> }
).event
if (event.type === 'message_start') {
// Reset current message usage for new message
currentMessageUsage = EMPTY_USAGE
const eventMessage = event.message as { usage: BetaMessageDeltaUsage }
const eventMessage = event.message as {
usage: BetaMessageDeltaUsage
}
currentMessageUsage = updateUsage(
currentMessageUsage,
eventMessage.usage,
Expand Down Expand Up @@ -851,7 +863,15 @@ export class QueryEngine {
void recordTranscript(messages)
}

const attachment = msg.attachment as { type: string; data?: unknown; turnCount?: number; maxTurns?: number; prompt?: string; source_uuid?: string; [key: string]: unknown }
const attachment = msg.attachment as {
type: string
data?: unknown
turnCount?: number
maxTurns?: number
prompt?: string
source_uuid?: string
[key: string]: unknown
}

// Extract structured output from StructuredOutput tool calls
if (attachment.type === 'structured_output') {
Expand Down Expand Up @@ -892,10 +912,7 @@ export class QueryEngine {
return
}
// Yield queued_command attachments as SDK user message replays
else if (
replayUserMessages &&
attachment.type === 'queued_command'
) {
else if (replayUserMessages && attachment.type === 'queued_command') {
yield {
type: 'user',
message: {
Expand Down Expand Up @@ -923,10 +940,7 @@ export class QueryEngine {
// never shrinks (memory leak in long SDK sessions). The subtype
// check lives inside the injected callback so feature-gated strings
// stay out of this file (excluded-strings check).
const snipResult = this.config.snipReplay?.(
msg,
this.mutableMessages,
)
const snipResult = this.config.snipReplay?.(msg, this.mutableMessages)
if (snipResult !== undefined) {
if (snipResult.executed) {
this.mutableMessages.length = 0
Expand All @@ -936,10 +950,7 @@ export class QueryEngine {
}
this.mutableMessages.push(msg)
// Yield compact boundary messages to SDK
if (
msg.subtype === 'compact_boundary' &&
msg.compactMetadata
) {
if (msg.subtype === 'compact_boundary' && msg.compactMetadata) {
const compactMsg = msg as SystemCompactBoundaryMessage
// Release pre-compaction messages for GC. The boundary was just
// pushed so it's the last element. query.ts already uses
Expand All @@ -959,11 +970,18 @@ export class QueryEngine {
subtype: 'compact_boundary' as const,
session_id: getSessionId(),
uuid: msg.uuid,
compact_metadata: toSDKCompactMetadata(compactMsg.compactMetadata),
compact_metadata: toSDKCompactMetadata(
compactMsg.compactMetadata,
),
}
}
if (msg.subtype === 'api_error') {
const apiErrorMsg = msg as Message & { retryAttempt: number; maxRetries: number; retryInMs: number; error: APIError }
const apiErrorMsg = msg as Message & {
retryAttempt: number
maxRetries: number
retryInMs: number
error: APIError
}
yield {
type: 'system',
subtype: 'api_retry' as const,
Expand All @@ -980,7 +998,10 @@ export class QueryEngine {
break
}
case 'tool_use_summary': {
const msg = message as Message & { summary: unknown; precedingToolUseIds: unknown }
const msg = message as Message & {
summary: unknown
precedingToolUseIds: unknown
}
// Yield tool use summary messages to SDK
yield {
type: 'tool_use_summary' as const,
Expand Down
5 changes: 3 additions & 2 deletions src/assistant/AssistantSessionChooser.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Auto-generated stub — replace with real implementation
export {};
export const AssistantSessionChooser: (props: Record<string, unknown>) => null = () => null;
export {}
export const AssistantSessionChooser: (props: Record<string, unknown>) => null =
() => null
5 changes: 3 additions & 2 deletions src/assistant/gate.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Auto-generated stub — replace with real implementation
export {};
export const isKairosEnabled: () => Promise<boolean> = () => Promise.resolve(false);
export {}
export const isKairosEnabled: () => Promise<boolean> = () =>
Promise.resolve(false)
15 changes: 8 additions & 7 deletions src/assistant/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// Auto-generated stub — replace with real implementation
export {};
export const isAssistantMode: () => boolean = () => false;
export const initializeAssistantTeam: () => Promise<void> = async () => {};
export const markAssistantForced: () => void = () => {};
export const isAssistantForced: () => boolean = () => false;
export const getAssistantSystemPromptAddendum: () => string = () => '';
export const getAssistantActivationPath: () => string | undefined = () => undefined;
export {}
export const isAssistantMode: () => boolean = () => false
export const initializeAssistantTeam: () => Promise<void> = async () => {}
export const markAssistantForced: () => void = () => {}
export const isAssistantForced: () => boolean = () => false
export const getAssistantSystemPromptAddendum: () => string = () => ''
export const getAssistantActivationPath: () => string | undefined = () =>
undefined
5 changes: 3 additions & 2 deletions src/assistant/sessionDiscovery.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Auto-generated stub — replace with real implementation
export type AssistantSession = { id: string; [key: string]: unknown };
export const discoverAssistantSessions: () => Promise<AssistantSession[]> = () => Promise.resolve([]);
export type AssistantSession = { id: string; [key: string]: unknown }
export const discoverAssistantSessions: () => Promise<AssistantSession[]> =
() => Promise.resolve([])
4 changes: 2 additions & 2 deletions src/bootstrap/src/entrypoints/agentSdkTypes.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
// Auto-generated type stub — replace with real implementation
export type HookEvent = any;
export type ModelUsage = any;
export type HookEvent = any
export type ModelUsage = any
2 changes: 1 addition & 1 deletion src/bootstrap/src/tools/AgentTool/agentColorManager.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type AgentColorName = any;
export type AgentColorName = any
2 changes: 1 addition & 1 deletion src/bootstrap/src/types/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type HookCallbackMatcher = any;
export type HookCallbackMatcher = any
2 changes: 1 addition & 1 deletion src/bootstrap/src/types/ids.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type SessionId = any;
export type SessionId = any
2 changes: 1 addition & 1 deletion src/bootstrap/src/utils/crypto.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type randomUUID = any;
export type randomUUID = any
2 changes: 1 addition & 1 deletion src/bootstrap/src/utils/model/model.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type ModelSetting = any;
export type ModelSetting = any
2 changes: 1 addition & 1 deletion src/bootstrap/src/utils/model/modelStrings.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type ModelStrings = any;
export type ModelStrings = any
2 changes: 1 addition & 1 deletion src/bootstrap/src/utils/settings/constants.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type SettingSource = any;
export type SettingSource = any
2 changes: 1 addition & 1 deletion src/bootstrap/src/utils/settings/settingsCache.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type resetSettingsCache = any;
export type resetSettingsCache = any
2 changes: 1 addition & 1 deletion src/bootstrap/src/utils/settings/types.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type PluginHookMatcher = any;
export type PluginHookMatcher = any
2 changes: 1 addition & 1 deletion src/bootstrap/src/utils/signal.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type createSignal = any;
export type createSignal = any
4 changes: 3 additions & 1 deletion src/bootstrap/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1755,4 +1755,6 @@ export function getPromptId(): string | null {
export function setPromptId(id: string | null): void {
STATE.promptId = id
}
export function isReplBridgeActive(): boolean { return false; }
export function isReplBridgeActive(): boolean {
return false
}
11 changes: 9 additions & 2 deletions src/bridge/bridgeMessaging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ export function isEligibleBridgeMessage(m: Message): boolean {
export function extractTitleText(m: Message): string | undefined {
if (m.type !== 'user' || m.isMeta || m.toolUseResult || m.isCompactSummary)
return undefined
if (m.origin && (m.origin as { kind?: string }).kind !== 'human') return undefined
if (m.origin && (m.origin as { kind?: string }).kind !== 'human')
return undefined
const content = m.message.content
let raw: string | undefined
if (typeof content === 'string') {
Expand Down Expand Up @@ -265,7 +266,13 @@ export function handleServerControlRequest(
// Outbound-only: reply error for mutable requests so claude.ai doesn't show
// false success. initialize must still succeed (server kills the connection
// if it doesn't — see comment above).
const req = request.request as { subtype: string; model?: string; max_thinking_tokens?: number | null; mode?: string; [key: string]: unknown }
const req = request.request as {
subtype: string
model?: string
max_thinking_tokens?: number | null
mode?: string
[key: string]: unknown
}
if (outboundOnly && req.subtype !== 'initialize') {
response = {
type: 'control_response',
Expand Down
4 changes: 3 additions & 1 deletion src/bridge/inboundMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ export function extractInboundMessageFields(
| { content: string | Array<ContentBlockParam>; uuid: UUID | undefined }
| undefined {
if (msg.type !== 'user') return undefined
const content = (msg.message as { content?: string | Array<ContentBlockParam> } | undefined)?.content
const content = (
msg.message as { content?: string | Array<ContentBlockParam> } | undefined
)?.content
if (!content) return undefined
if (Array.isArray(content) && content.length === 0) return undefined

Expand Down
5 changes: 4 additions & 1 deletion src/bridge/remoteBridgeCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -829,7 +829,10 @@ export async function initEnvLessBridgeCore(
return
}
const event = { ...request, session_id: sessionId }
if ((request as { request?: { subtype?: string } }).request?.subtype === 'can_use_tool') {
if (
(request as { request?: { subtype?: string } }).request?.subtype ===
'can_use_tool'
) {
transport.reportState('requires_action')
}
void transport.write(event)
Expand Down
2 changes: 0 additions & 2 deletions src/bridge/replBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,6 @@ export async function initBridgeCore(
// re-created after a connection loss.
let currentSessionId: string


if (reusedPriorSession && prior) {
currentSessionId = prior.sessionId
logForDebugging(
Expand Down Expand Up @@ -826,7 +825,6 @@ export async function initBridgeCore(
// UUIDs are scoped per-session on the server, so re-flushing is safe.
previouslyFlushedUUIDs?.clear()


// Reset the counter so independent reconnections hours apart don't
// exhaust the limit — it guards against rapid consecutive failures,
// not lifetime total.
Expand Down
2 changes: 1 addition & 1 deletion src/bridge/src/entrypoints/sdk/controlTypes.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation
export type StdoutMessage = any;
export type StdoutMessage = any
37 changes: 30 additions & 7 deletions src/bridge/webhookSanitizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,44 @@
/** Patterns that match known secret/token formats. */
const SECRET_PATTERNS: Array<{ pattern: RegExp; replacement: string }> = [
// GitHub tokens (PAT, OAuth, App, Server-to-server)
{ pattern: /\b(ghp|gho|ghs|ghu|github_pat)_[A-Za-z0-9_]{10,}\b/g, replacement: '[REDACTED_GITHUB_TOKEN]' },
{
pattern: /\b(ghp|gho|ghs|ghu|github_pat)_[A-Za-z0-9_]{10,}\b/g,
replacement: '[REDACTED_GITHUB_TOKEN]',
},
// Anthropic API keys
{ pattern: /\bsk-ant-[A-Za-z0-9_-]{10,}\b/g, replacement: '[REDACTED_ANTHROPIC_KEY]' },
{
pattern: /\bsk-ant-[A-Za-z0-9_-]{10,}\b/g,
replacement: '[REDACTED_ANTHROPIC_KEY]',
},
// Generic Bearer tokens in headers
{ pattern: /(Bearer\s+)[A-Za-z0-9._\-/+=]{20,}/gi, replacement: '$1[REDACTED_TOKEN]' },
{
pattern: /(Bearer\s+)[A-Za-z0-9._\-/+=]{20,}/gi,
replacement: '$1[REDACTED_TOKEN]',
},
// AWS access keys
{ pattern: /\b(AKIA|ASIA)[A-Z0-9]{16}\b/g, replacement: '[REDACTED_AWS_KEY]' },
{
pattern: /\b(AKIA|ASIA)[A-Z0-9]{16}\b/g,
replacement: '[REDACTED_AWS_KEY]',
},
// AWS secret keys (40-char base64-like strings after common labels)
{ pattern: /(aws_secret_access_key|secret_key|SecretAccessKey)['":\s=]+[A-Za-z0-9/+=]{30,}/gi, replacement: '$1=[REDACTED_AWS_SECRET]' },
{
pattern:
/(aws_secret_access_key|secret_key|SecretAccessKey)['":\s=]+[A-Za-z0-9/+=]{30,}/gi,
replacement: '$1=[REDACTED_AWS_SECRET]',
},
// Generic API key patterns (key=value or "key": "value")
{ pattern: /(api[_-]?key|apikey|secret|password|token|credential)['":\s=]+["']?[A-Za-z0-9._\-/+=]{16,}["']?/gi, replacement: '$1=[REDACTED]' },
{
pattern:
/(api[_-]?key|apikey|secret|password|token|credential)['":\s=]+["']?[A-Za-z0-9._\-/+=]{16,}["']?/gi,
replacement: '$1=[REDACTED]',
},
// npm tokens
{ pattern: /\bnpm_[A-Za-z0-9]{36}\b/g, replacement: '[REDACTED_NPM_TOKEN]' },
// Slack tokens
{ pattern: /\bxox[bporas]-[A-Za-z0-9-]{10,}\b/g, replacement: '[REDACTED_SLACK_TOKEN]' },
{
pattern: /\bxox[bporas]-[A-Za-z0-9-]{10,}\b/g,
replacement: '[REDACTED_SLACK_TOKEN]',
},
]

/** Maximum content length before truncation (100KB). */
Expand Down
Loading
Loading