Merge branch 'main' into fix/anthropic-schema-format
This commit is contained in:
@@ -286,7 +286,7 @@ export function getAnthropicApiKeyWithSource(
|
||||
)
|
||||
}
|
||||
|
||||
if (apiKeyEnv) {
|
||||
if (apiKeyEnv && !isUsing3PServices()) {
|
||||
return {
|
||||
key: apiKeyEnv,
|
||||
source: 'ANTHROPIC_API_KEY',
|
||||
@@ -294,6 +294,7 @@ export function getAnthropicApiKeyWithSource(
|
||||
}
|
||||
|
||||
// OAuth token is present but this function returns API keys only
|
||||
// Also reached when 3P provider is active — ANTHROPIC_API_KEY is ignored
|
||||
return {
|
||||
key: null,
|
||||
source: 'none',
|
||||
|
||||
54
src/utils/toolResultStorage.test.ts
Normal file
54
src/utils/toolResultStorage.test.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { expect, test } from 'bun:test'
|
||||
|
||||
import { createUserMessage } from './messages.ts'
|
||||
import { applyToolResultReplacementsToMessages } from './toolResultStorage.ts'
|
||||
|
||||
test('applyToolResultReplacementsToMessages replaces matching tool results and preserves unrelated messages', () => {
|
||||
const unrelated = createUserMessage({ content: 'keep me' })
|
||||
const oversizedResult = createUserMessage({
|
||||
content: [
|
||||
{
|
||||
type: 'tool_result',
|
||||
tool_use_id: 'tool-1',
|
||||
content: 'very large tool output',
|
||||
is_error: false,
|
||||
},
|
||||
],
|
||||
})
|
||||
const messages = [unrelated, oversizedResult]
|
||||
const replacement =
|
||||
'<persisted-output>\nOutput too large. Preview\n</persisted-output>'
|
||||
|
||||
const next = applyToolResultReplacementsToMessages(
|
||||
messages,
|
||||
new Map([['tool-1', replacement]]),
|
||||
)
|
||||
|
||||
expect(next).not.toBe(messages)
|
||||
expect(next[0]).toBe(unrelated)
|
||||
expect(next[1]).not.toBe(oversizedResult)
|
||||
expect((next[1]!.message.content as Array<{ content: string }>)[0]!.content).toBe(
|
||||
replacement,
|
||||
)
|
||||
})
|
||||
|
||||
test('applyToolResultReplacementsToMessages is idempotent when messages are already hydrated', () => {
|
||||
const hydrated = createUserMessage({
|
||||
content: [
|
||||
{
|
||||
type: 'tool_result',
|
||||
tool_use_id: 'tool-1',
|
||||
content: '<persisted-output>\nPreview\n</persisted-output>',
|
||||
is_error: false,
|
||||
},
|
||||
],
|
||||
})
|
||||
const messages = [hydrated]
|
||||
|
||||
const next = applyToolResultReplacementsToMessages(
|
||||
messages,
|
||||
new Map([['tool-1', '<persisted-output>\nPreview\n</persisted-output>']]),
|
||||
)
|
||||
|
||||
expect(next).toBe(messages)
|
||||
})
|
||||
@@ -698,17 +698,22 @@ function selectFreshToReplace(
|
||||
*/
|
||||
function replaceToolResultContents(
|
||||
messages: Message[],
|
||||
replacementMap: Map<string, string>,
|
||||
replacementMap: ReadonlyMap<string, string>,
|
||||
): Message[] {
|
||||
return messages.map(message => {
|
||||
let changed = false
|
||||
const nextMessages = messages.map(message => {
|
||||
if (message.type !== 'user' || !Array.isArray(message.message.content)) {
|
||||
return message
|
||||
}
|
||||
const content = message.message.content
|
||||
const needsReplace = content.some(
|
||||
b => b.type === 'tool_result' && replacementMap.has(b.tool_use_id),
|
||||
b =>
|
||||
b.type === 'tool_result' &&
|
||||
replacementMap.has(b.tool_use_id) &&
|
||||
b.content !== replacementMap.get(b.tool_use_id),
|
||||
)
|
||||
if (!needsReplace) return message
|
||||
changed = true
|
||||
return {
|
||||
...message,
|
||||
message: {
|
||||
@@ -716,13 +721,28 @@ function replaceToolResultContents(
|
||||
content: content.map(block => {
|
||||
if (block.type !== 'tool_result') return block
|
||||
const replacement = replacementMap.get(block.tool_use_id)
|
||||
return replacement === undefined
|
||||
return replacement === undefined || block.content === replacement
|
||||
? block
|
||||
: { ...block, content: replacement }
|
||||
}),
|
||||
},
|
||||
}
|
||||
})
|
||||
return changed ? nextMessages : messages
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirror already-known tool-result replacements back into an in-memory
|
||||
* transcript. Used by the interactive REPL so once a large result has been
|
||||
* persisted/replaced for model use, the original oversized string can be
|
||||
* dropped from live session state as well.
|
||||
*/
|
||||
export function applyToolResultReplacementsToMessages(
|
||||
messages: Message[],
|
||||
replacements: ReadonlyMap<string, string>,
|
||||
): Message[] {
|
||||
if (replacements.size === 0) return messages
|
||||
return replaceToolResultContents(messages, replacements)
|
||||
}
|
||||
|
||||
async function buildReplacement(
|
||||
@@ -926,13 +946,16 @@ export async function applyToolResultBudget(
|
||||
state: ContentReplacementState | undefined,
|
||||
writeToTranscript?: (records: ToolResultReplacementRecord[]) => void,
|
||||
skipToolNames?: ReadonlySet<string>,
|
||||
): Promise<Message[]> {
|
||||
if (!state) return messages
|
||||
): Promise<{
|
||||
messages: Message[]
|
||||
newlyReplaced: ToolResultReplacementRecord[]
|
||||
}> {
|
||||
if (!state) return { messages, newlyReplaced: [] }
|
||||
const result = await enforceToolResultBudget(messages, state, skipToolNames)
|
||||
if (result.newlyReplaced.length > 0) {
|
||||
writeToTranscript?.(result.newlyReplaced)
|
||||
}
|
||||
return result.messages
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user