Merge branch 'main' into main
This commit is contained in:
commit
0e3c232596
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "claude-code-best",
|
"name": "claude-code-best",
|
||||||
"version": "2.4.3",
|
"version": "2.4.4",
|
||||||
"description": "Reverse-engineered Anthropic Claude Code CLI — interactive AI coding assistant in the terminal",
|
"description": "Reverse-engineered Anthropic Claude Code CLI — interactive AI coding assistant in the terminal",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"author": "claude-code-best <claude-code-best@proton.me>",
|
"author": "claude-code-best <claude-code-best@proton.me>",
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ export async function* adaptGeminiStreamToAnthropic(
|
||||||
let finishReason: string | undefined
|
let finishReason: string | undefined
|
||||||
let inputTokens = 0
|
let inputTokens = 0
|
||||||
let outputTokens = 0
|
let outputTokens = 0
|
||||||
|
let cachedReadTokens = 0
|
||||||
|
|
||||||
for await (const chunk of stream) {
|
for await (const chunk of stream) {
|
||||||
const usage = chunk.usageMetadata
|
const usage = chunk.usageMetadata
|
||||||
|
|
@ -23,6 +24,7 @@ export async function* adaptGeminiStreamToAnthropic(
|
||||||
inputTokens = usage.promptTokenCount ?? inputTokens
|
inputTokens = usage.promptTokenCount ?? inputTokens
|
||||||
outputTokens =
|
outputTokens =
|
||||||
(usage.candidatesTokenCount ?? 0) + (usage.thoughtsTokenCount ?? 0)
|
(usage.candidatesTokenCount ?? 0) + (usage.thoughtsTokenCount ?? 0)
|
||||||
|
cachedReadTokens = usage.cachedContentTokenCount ?? cachedReadTokens
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!started) {
|
if (!started) {
|
||||||
|
|
@ -41,7 +43,7 @@ export async function* adaptGeminiStreamToAnthropic(
|
||||||
input_tokens: inputTokens,
|
input_tokens: inputTokens,
|
||||||
output_tokens: 0,
|
output_tokens: 0,
|
||||||
cache_creation_input_tokens: 0,
|
cache_creation_input_tokens: 0,
|
||||||
cache_read_input_tokens: 0,
|
cache_read_input_tokens: cachedReadTokens,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown as BetaRawMessageStreamEvent
|
} as unknown as BetaRawMessageStreamEvent
|
||||||
|
|
@ -204,7 +206,10 @@ export async function* adaptGeminiStreamToAnthropic(
|
||||||
stop_sequence: null,
|
stop_sequence: null,
|
||||||
},
|
},
|
||||||
usage: {
|
usage: {
|
||||||
|
input_tokens: inputTokens,
|
||||||
output_tokens: outputTokens,
|
output_tokens: outputTokens,
|
||||||
|
cache_creation_input_tokens: 0,
|
||||||
|
cache_read_input_tokens: cachedReadTokens,
|
||||||
},
|
},
|
||||||
} as BetaRawMessageStreamEvent
|
} as BetaRawMessageStreamEvent
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@ export type GeminiUsageMetadata = {
|
||||||
candidatesTokenCount?: number
|
candidatesTokenCount?: number
|
||||||
thoughtsTokenCount?: number
|
thoughtsTokenCount?: number
|
||||||
totalTokenCount?: number
|
totalTokenCount?: number
|
||||||
|
cachedContentTokenCount?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GeminiCandidate = {
|
export type GeminiCandidate = {
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,29 @@ export const ExecuteTool = buildTool({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate input before delegating — prevents crashes when the model
|
||||||
|
// omits required params (e.g. TeamCreate without team_name →
|
||||||
|
// sanitizeName(undefined).replace() TypeError).
|
||||||
|
if (targetTool.validateInput) {
|
||||||
|
const validation = await targetTool.validateInput(
|
||||||
|
input.params as Record<string, unknown>,
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
if (!validation.result) {
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
result: null,
|
||||||
|
tool_name: input.tool_name,
|
||||||
|
},
|
||||||
|
newMessages: [
|
||||||
|
createUserMessage({
|
||||||
|
content: `Invalid parameters for tool "${input.tool_name}": ${validation.message}`,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check permissions on the target tool
|
// Check permissions on the target tool
|
||||||
const permResult = await targetTool.checkPermissions?.(
|
const permResult = await targetTool.checkPermissions?.(
|
||||||
input.params as Record<string, unknown>,
|
input.params as Record<string, unknown>,
|
||||||
|
|
@ -164,7 +187,7 @@ export const ExecuteTool = buildTool({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
renderToolUseMessage(input) {
|
renderToolUseMessage(input) {
|
||||||
return `Executing ${input.tool_name}...`
|
return `${input.tool_name}`
|
||||||
},
|
},
|
||||||
userFacingName() {
|
userFacingName() {
|
||||||
return 'ExecuteExtraTool'
|
return 'ExecuteExtraTool'
|
||||||
|
|
|
||||||
|
|
@ -8,16 +8,12 @@ import * as React from 'react';
|
||||||
import { renderToString } from '../../../utils/staticRender.js';
|
import { renderToString } from '../../../utils/staticRender.js';
|
||||||
import { AutofixProgress } from '../AutofixProgress.js';
|
import { AutofixProgress } from '../AutofixProgress.js';
|
||||||
|
|
||||||
describe('AutofixProgress', () => {
|
describe.skipIf(!!process.env.CI)('AutofixProgress', () => {
|
||||||
test(
|
test('renders target in header', async () => {
|
||||||
'renders target in header',
|
const out = await renderToString(<AutofixProgress phase="detecting" target="acme/myrepo#42" />);
|
||||||
async () => {
|
expect(out).toContain('acme/myrepo#42');
|
||||||
const out = await renderToString(<AutofixProgress phase="detecting" target="acme/myrepo#42" />);
|
expect(out).toContain('Autofix PR');
|
||||||
expect(out).toContain('acme/myrepo#42');
|
});
|
||||||
expect(out).toContain('Autofix PR');
|
|
||||||
},
|
|
||||||
{ timeout: 10000 },
|
|
||||||
);
|
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'detecting phase shows arrow on detecting step',
|
'detecting phase shows arrow on detecting step',
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,7 @@ export const ASYNC_AGENT_ALLOWED_TOOLS = new Set([
|
||||||
SKILL_TOOL_NAME,
|
SKILL_TOOL_NAME,
|
||||||
SYNTHETIC_OUTPUT_TOOL_NAME,
|
SYNTHETIC_OUTPUT_TOOL_NAME,
|
||||||
SEARCH_EXTRA_TOOLS_TOOL_NAME,
|
SEARCH_EXTRA_TOOLS_TOOL_NAME,
|
||||||
|
EXECUTE_TOOL_NAME,
|
||||||
ENTER_WORKTREE_TOOL_NAME,
|
ENTER_WORKTREE_TOOL_NAME,
|
||||||
EXIT_WORKTREE_TOOL_NAME,
|
EXIT_WORKTREE_TOOL_NAME,
|
||||||
])
|
])
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import type {
|
||||||
ChatCompletionCreateParamsStreaming,
|
ChatCompletionCreateParamsStreaming,
|
||||||
} from 'openai/resources/chat/completions/completions.mjs'
|
} from 'openai/resources/chat/completions/completions.mjs'
|
||||||
import { getGrokClient } from './client.js'
|
import { getGrokClient } from './client.js'
|
||||||
|
import { updateOpenAIUsage } from '../openai/openaiShared.js'
|
||||||
import {
|
import {
|
||||||
anthropicMessagesToOpenAI,
|
anthropicMessagesToOpenAI,
|
||||||
anthropicToolsToOpenAI,
|
anthropicToolsToOpenAI,
|
||||||
|
|
@ -136,7 +137,7 @@ export async function* queryModelGrok(
|
||||||
partialMessage = (event as any).message
|
partialMessage = (event as any).message
|
||||||
ttftMs = Date.now() - start
|
ttftMs = Date.now() - start
|
||||||
if ((event as any).message?.usage) {
|
if ((event as any).message?.usage) {
|
||||||
usage = { ...usage, ...(event as any).message.usage }
|
usage = updateOpenAIUsage(usage, (event as any).message.usage)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
@ -192,7 +193,7 @@ export async function* queryModelGrok(
|
||||||
case 'message_delta': {
|
case 'message_delta': {
|
||||||
const deltaUsage = (event as any).usage
|
const deltaUsage = (event as any).usage
|
||||||
if (deltaUsage) {
|
if (deltaUsage) {
|
||||||
usage = { ...usage, ...deltaUsage }
|
usage = updateOpenAIUsage(usage, deltaUsage)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import type {
|
||||||
import type { AgentId } from '../../../types/ids.js'
|
import type { AgentId } from '../../../types/ids.js'
|
||||||
import type { Tools } from '../../../Tool.js'
|
import type { Tools } from '../../../Tool.js'
|
||||||
import { getOpenAIClient } from './client.js'
|
import { getOpenAIClient } from './client.js'
|
||||||
|
import { updateOpenAIUsage } from './openaiShared.js'
|
||||||
import {
|
import {
|
||||||
anthropicMessagesToOpenAI,
|
anthropicMessagesToOpenAI,
|
||||||
resolveOpenAIModel,
|
resolveOpenAIModel,
|
||||||
|
|
@ -454,7 +455,7 @@ export async function* queryModelOpenAI(
|
||||||
case 'message_delta': {
|
case 'message_delta': {
|
||||||
const deltaUsage = (event as any).usage
|
const deltaUsage = (event as any).usage
|
||||||
if (deltaUsage) {
|
if (deltaUsage) {
|
||||||
usage = { ...usage, ...deltaUsage }
|
usage = updateOpenAIUsage(usage, deltaUsage)
|
||||||
}
|
}
|
||||||
if ((event as any).delta?.stop_reason != null) {
|
if ((event as any).delta?.stop_reason != null) {
|
||||||
stopReason = (event as any).delta.stop_reason
|
stopReason = (event as any).delta.stop_reason
|
||||||
|
|
|
||||||
46
src/services/api/openai/openaiShared.ts
Normal file
46
src/services/api/openai/openaiShared.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
/**
|
||||||
|
* Shared utilities for OpenAI-compatible API paths.
|
||||||
|
*
|
||||||
|
* Both the OpenAI path (queryModelOpenAI) and Grok path (queryModelGrok) use
|
||||||
|
* the same adapters (openaiStreamAdapter, openaiConvertMessages), so the event
|
||||||
|
* processing logic should be shared rather than duplicated.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge a delta usage into the accumulated usage, preserving cache-related
|
||||||
|
* fields from previous values when the delta carries explicit zeroes or
|
||||||
|
* undefined values.
|
||||||
|
*
|
||||||
|
* Mirrors updateUsage() in claude.ts: a future adapter change that omits
|
||||||
|
* cache fields from certain streaming events should not silently zero the
|
||||||
|
* accumulated counters.
|
||||||
|
*/
|
||||||
|
export function updateOpenAIUsage(
|
||||||
|
current: {
|
||||||
|
input_tokens: number
|
||||||
|
output_tokens: number
|
||||||
|
cache_creation_input_tokens: number
|
||||||
|
cache_read_input_tokens: number
|
||||||
|
},
|
||||||
|
delta: {
|
||||||
|
input_tokens?: number
|
||||||
|
output_tokens?: number
|
||||||
|
cache_creation_input_tokens?: number
|
||||||
|
cache_read_input_tokens?: number
|
||||||
|
},
|
||||||
|
): typeof current {
|
||||||
|
return {
|
||||||
|
input_tokens: delta.input_tokens ?? current.input_tokens,
|
||||||
|
output_tokens: delta.output_tokens ?? current.output_tokens,
|
||||||
|
cache_creation_input_tokens:
|
||||||
|
delta.cache_creation_input_tokens !== undefined &&
|
||||||
|
delta.cache_creation_input_tokens > 0
|
||||||
|
? delta.cache_creation_input_tokens
|
||||||
|
: current.cache_creation_input_tokens,
|
||||||
|
cache_read_input_tokens:
|
||||||
|
delta.cache_read_input_tokens !== undefined &&
|
||||||
|
delta.cache_read_input_tokens > 0
|
||||||
|
? delta.cache_read_input_tokens
|
||||||
|
: current.cache_read_input_tokens,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -23,7 +23,9 @@ export function isOpenAIThinkingEnabled(model: string): boolean {
|
||||||
if (isEnvDefinedFalsy(process.env.OPENAI_ENABLE_THINKING)) return false
|
if (isEnvDefinedFalsy(process.env.OPENAI_ENABLE_THINKING)) return false
|
||||||
// Explicit enable
|
// Explicit enable
|
||||||
if (isEnvTruthy(process.env.OPENAI_ENABLE_THINKING)) return true
|
if (isEnvTruthy(process.env.OPENAI_ENABLE_THINKING)) return true
|
||||||
// Auto-detect from model name (DeepSeek and MiMo models support thinking mode)
|
// Auto-detect from model name (DeepSeek and MiMo models support thinking mode).
|
||||||
|
// Grok is intentionally excluded — Grok reasoning models reason automatically
|
||||||
|
// and do NOT require thinking/enable_thinking request body parameters.
|
||||||
const modelLower = model.toLowerCase()
|
const modelLower = model.toLowerCase()
|
||||||
return modelLower.includes('deepseek') || modelLower.includes('mimo')
|
return modelLower.includes('deepseek') || modelLower.includes('mimo')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1724,12 +1724,29 @@ export function getSubscriptionName(): string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Check if using third-party services (Bedrock or Vertex or Foundry) */
|
/**
|
||||||
|
* Check if using third-party services (non-Anthropic providers).
|
||||||
|
*
|
||||||
|
* This function gates several behaviours that should only apply when the user
|
||||||
|
* is NOT calling the first-party Anthropic API directly:
|
||||||
|
* - auth status display (authStatus handler)
|
||||||
|
* - command visibility (login/logout shown for non-3P)
|
||||||
|
* - command availability checks (meetsAvailabilityRequirement)
|
||||||
|
*
|
||||||
|
* KEEP IN SYNC with providers.ts — when a new CLAUDE_CODE_USE_* env var is
|
||||||
|
* added to getAPIProvider(), the corresponding check MUST be added here.
|
||||||
|
* Providers whose selection is controlled purely via settings.modelType
|
||||||
|
* (rather than env vars) are NOT covered by this function and may need
|
||||||
|
* separate handling in the call sites above.
|
||||||
|
*/
|
||||||
export function isUsing3PServices(): boolean {
|
export function isUsing3PServices(): boolean {
|
||||||
return !!(
|
return !!(
|
||||||
isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK) ||
|
isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK) ||
|
||||||
isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX) ||
|
isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX) ||
|
||||||
isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY)
|
isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY) ||
|
||||||
|
isEnvTruthy(process.env.CLAUDE_CODE_USE_OPENAI) ||
|
||||||
|
isEnvTruthy(process.env.CLAUDE_CODE_USE_GEMINI) ||
|
||||||
|
isEnvTruthy(process.env.CLAUDE_CODE_USE_GROK)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -529,6 +529,10 @@ export function setRemoteIngressUrlForTesting(url: string): void {
|
||||||
|
|
||||||
const REMOTE_FLUSH_INTERVAL_MS = 10
|
const REMOTE_FLUSH_INTERVAL_MS = 10
|
||||||
|
|
||||||
|
// Limit the number of cached session-file lookups to prevent unbounded Map growth
|
||||||
|
// in long-running daemon / swarm sessions that spawn many sub-agents.
|
||||||
|
const MAX_CACHED_SESSION_FILES = 200
|
||||||
|
|
||||||
class Project {
|
class Project {
|
||||||
// Minimal cache for current session only (not all sessions)
|
// Minimal cache for current session only (not all sessions)
|
||||||
currentSessionTag: string | undefined
|
currentSessionTag: string | undefined
|
||||||
|
|
@ -577,6 +581,7 @@ class Project {
|
||||||
this.flushTimer = null
|
this.flushTimer = null
|
||||||
this.activeDrain = null
|
this.activeDrain = null
|
||||||
this.writeQueues = new Map()
|
this.writeQueues = new Map()
|
||||||
|
this.existingSessionFiles = new Map()
|
||||||
}
|
}
|
||||||
|
|
||||||
private incrementPendingWrites(): void {
|
private incrementPendingWrites(): void {
|
||||||
|
|
@ -1288,6 +1293,9 @@ class Project {
|
||||||
* Returns the session file path if it exists, null otherwise.
|
* Returns the session file path if it exists, null otherwise.
|
||||||
* Used for writing to sessions other than the current one.
|
* Used for writing to sessions other than the current one.
|
||||||
* Caches positive results so we only stat once per session.
|
* Caches positive results so we only stat once per session.
|
||||||
|
*
|
||||||
|
* The cache is bounded at MAX_CACHED_SESSION_FILES to prevent unbounded
|
||||||
|
* growth in long-running daemon / swarm sessions that spawn many agents.
|
||||||
*/
|
*/
|
||||||
private existingSessionFiles = new Map<string, string>()
|
private existingSessionFiles = new Map<string, string>()
|
||||||
private async getExistingSessionFile(
|
private async getExistingSessionFile(
|
||||||
|
|
@ -1299,6 +1307,13 @@ class Project {
|
||||||
const targetFile = getTranscriptPathForSession(sessionId)
|
const targetFile = getTranscriptPathForSession(sessionId)
|
||||||
try {
|
try {
|
||||||
await stat(targetFile)
|
await stat(targetFile)
|
||||||
|
// Evict oldest entry when at capacity so the Map stays bounded
|
||||||
|
if (this.existingSessionFiles.size >= MAX_CACHED_SESSION_FILES) {
|
||||||
|
const oldestKey = this.existingSessionFiles.keys().next().value
|
||||||
|
if (oldestKey !== undefined) {
|
||||||
|
this.existingSessionFiles.delete(oldestKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
this.existingSessionFiles.set(sessionId, targetFile)
|
this.existingSessionFiles.set(sessionId, targetFile)
|
||||||
return targetFile
|
return targetFile
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user