feat: add natural language configuration tools

This commit is contained in:
lofyer
2026-08-14 01:23:32 +08:00
parent 445ebbc002
commit 7a86c96623
19 changed files with 3284 additions and 119 deletions
@@ -579,6 +579,12 @@ describe('ContinueHostAdapter', () => {
'note_get',
'--allow',
'note_search',
'--allow',
'goodbuddy_config_capabilities',
'--allow',
'goodbuddy_config_get',
'--allow',
'goodbuddy_config_plan',
'--exclude',
'*',
'serve',
+5 -14
View File
@@ -39,6 +39,7 @@ import {
} from './approval-summary'
import { stageRuntimeSkillPackages } from './runtime-skill-packages'
import { readBoundedResponseText } from './bounded-response'
import { scopedReadToolNames } from '../../shared/scoped-data-tools'
const supportedVersion = '1.5.47'
const supportedBundleHashes = new Set([
@@ -1058,20 +1059,10 @@ export class ContinueHostAdapter {
runOptions.workMode === 'ask' &&
runOptions.knowledgeCapability
) {
args.push(
'--allow',
'knowledge_list',
'--allow',
'knowledge_search',
'--allow',
'note_list',
'--allow',
'note_get',
'--allow',
'note_search',
'--exclude',
'*'
)
for (const toolName of scopedReadToolNames) {
args.push('--allow', toolName)
}
args.push('--exclude', '*')
} else if (runOptions.workMode === 'execute') {
args.push('--auto')
} else if (this.options.mode === 'chat') {
@@ -78,6 +78,78 @@ afterEach(async () => {
})
describe('KnowledgeMcpGateway', () => {
it('exposes GoodBuddy config reads in Ask and apply only in Execute', async () => {
const { service } = createService()
const configService = {
getCapabilities: vi.fn(() => ({ server: 'goodbuddy_config' })),
getSnapshot: vi.fn(async () => ({ application: {}, skills: [], mcpServers: [] })),
plan: vi.fn(async () => ({ planId: 'plan' })),
apply: vi.fn(async () => ({ status: 'applied' })),
revokeRequest: vi.fn()
}
const gateway = new KnowledgeMcpGateway(service, {
configService: configService as never
})
gateways.push(gateway)
const readToken = gateway.grant(
'config-read',
[],
new AbortController().signal,
'none',
{ access: 'read', workspacePath: process.cwd() }
)!
const authorizeApply = vi.fn(async () => true)
const writeToken = gateway.grant(
'config-write',
[],
new AbortController().signal,
'none',
{
access: 'write',
workspacePath: process.cwd(),
authorizeApply
}
)!
expect(gateway.getAvailableToolNames(readToken)).toEqual([
'goodbuddy_config_capabilities',
'goodbuddy_config_get',
'goodbuddy_config_plan'
])
expect(gateway.getAvailableToolNames(writeToken)).toEqual([
'goodbuddy_config_capabilities',
'goodbuddy_config_get',
'goodbuddy_config_plan',
'goodbuddy_config_apply'
])
await gateway.callGoodBuddyConfigTool(
readToken,
'goodbuddy_config_capabilities',
{}
)
expect(configService.getCapabilities).toHaveBeenCalledWith({})
await expect(
gateway.callGoodBuddyConfigTool(
readToken,
'goodbuddy_config_apply',
{ planId: crypto.randomUUID() }
)
).rejects.toThrow('unavailable')
await gateway.callGoodBuddyConfigTool(
writeToken,
'goodbuddy_config_apply',
{ planId: crypto.randomUUID() }
)
expect(configService.apply).toHaveBeenCalledWith(
'config-write',
expect.any(Object),
expect.any(AbortSignal),
authorizeApply
)
gateway.revoke(writeToken)
expect(configService.revokeRequest).toHaveBeenCalledWith('config-write')
})
it('keeps scope server-side, strips markup, bounds model arguments, and drains references', async () => {
const { service, searchHybridMany } = createService()
const gateway = new KnowledgeMcpGateway(service)
+116 -3
View File
@@ -12,12 +12,15 @@ import { stripKnowledgeHighlightTags } from '../../shared/knowledge-text'
import {
knowledgeToolNames,
knowledgeScopedDataToolCatalog,
goodbuddyConfigReadToolNames,
goodbuddyConfigWriteToolNames,
magicNoteScopedDataToolCatalog,
magicNoteReadToolNames,
magicNoteWriteToolNames,
maximumScopedToolCount,
scopedDataToolByName,
scopedReadToolNames,
type GoodBuddyConfigToolName,
type ScopedDataToolName
} from '../../shared/scoped-data-tools'
import type { KnowledgeService } from '../knowledge/knowledge-service'
@@ -32,6 +35,10 @@ import {
magicNotePlainText,
validateMagicNoteRichContent
} from '../magic-notes/rich-content'
import type {
GoodBuddyConfigApplyAuthorizer,
GoodBuddyConfigService
} from '../goodbuddy-config-service'
const MAX_REQUEST_BODY_BYTES = 64 * 1024
const MAX_RESULT_BYTES = 128 * 1024
@@ -128,6 +135,9 @@ type Capability = {
requestId: string
libraryIds: readonly string[]
magicNotesAccess: MagicNotesCapabilityAccess
configAccess: MagicNotesCapabilityAccess
configWorkspacePath?: string
authorizeConfigApply?: GoodBuddyConfigApplyAuthorizer
expiresAt: number
signal: AbortSignal
references: Map<string, KnowledgeSearchReference>
@@ -139,6 +149,7 @@ export type KnowledgeMcpGatewayOptions = {
maximumBodyBytes?: number
now?: () => number
magicNotesDatabase?: MagicNotesDatabase
configService?: GoodBuddyConfigService
}
function toMagicNoteToolSummary(
@@ -224,6 +235,7 @@ export class KnowledgeMcpGateway {
private readonly capabilityTtlMs: number
private readonly maximumBodyBytes: number
private readonly magicNotesDatabase?: MagicNotesDatabase
private readonly configService?: GoodBuddyConfigService
private server?: Server
private endpoint?: string
@@ -244,6 +256,7 @@ export class KnowledgeMcpGateway {
options.maximumBodyBytes ?? MAX_REQUEST_BODY_BYTES
this.now = options.now ?? Date.now
this.magicNotesDatabase = options.magicNotesDatabase
this.configService = options.configService
}
async start(): Promise<void> {
@@ -289,14 +302,23 @@ export class KnowledgeMcpGateway {
requestId: string,
authorizedLibraryIds: readonly string[],
signal: AbortSignal,
magicNotesAccess: MagicNotesCapabilityAccess = 'none'
magicNotesAccess: MagicNotesCapabilityAccess = 'none',
config?: {
access: MagicNotesCapabilityAccess
workspacePath: string
authorizeApply?: GoodBuddyConfigApplyAuthorizer
}
): string | undefined {
const effectiveMagicNotesAccess = this.magicNotesDatabase
? magicNotesAccess
: 'none'
const effectiveConfigAccess = this.configService
? config?.access ?? 'none'
: 'none'
if (
authorizedLibraryIds.length === 0 &&
effectiveMagicNotesAccess === 'none'
effectiveMagicNotesAccess === 'none' &&
effectiveConfigAccess === 'none'
) {
return undefined
}
@@ -311,6 +333,13 @@ export class KnowledgeMcpGateway {
requestId,
libraryIds,
magicNotesAccess: effectiveMagicNotesAccess,
configAccess: effectiveConfigAccess,
...(effectiveConfigAccess !== 'none'
? {
configWorkspacePath: config?.workspacePath,
authorizeConfigApply: config?.authorizeApply
}
: {}),
expiresAt: this.now() + this.capabilityTtlMs,
signal,
references: new Map(),
@@ -330,6 +359,7 @@ export class KnowledgeMcpGateway {
}
capability.removeAbortListener()
this.capabilities.delete(token)
this.configService?.revokeRequest(capability.requestId)
}
drainReferences(
@@ -474,10 +504,81 @@ export class KnowledgeMcpGateway {
: []),
...(capability.magicNotesAccess === 'write'
? magicNoteWriteToolNames
: []),
...(capability.configAccess !== 'none'
? goodbuddyConfigReadToolNames
: []),
...(capability.configAccess === 'write'
? goodbuddyConfigWriteToolNames
: [])
]
}
private requireConfig(
token: string,
requiredAccess: Exclude<MagicNotesCapabilityAccess, 'none'>
): {
capability: Capability
service: GoodBuddyConfigService
workspacePath: string
} {
const capability = this.getCapability(token)
const allowed =
capability.configAccess === 'write' ||
(requiredAccess === 'read' && capability.configAccess === 'read')
if (
!allowed ||
!this.configService ||
!capability.configWorkspacePath
) {
throw new Error('GoodBuddy configuration capability is unavailable')
}
return {
capability,
service: this.configService,
workspacePath: capability.configWorkspacePath
}
}
async callGoodBuddyConfigTool(
token: string,
name: GoodBuddyConfigToolName,
input: unknown,
signal?: AbortSignal
): Promise<Record<string, unknown>> {
const requiredAccess =
name === 'goodbuddy_config_apply' ? 'write' : 'read'
const { capability, service, workspacePath } =
this.requireConfig(token, requiredAccess)
const effectiveSignal = signal
? AbortSignal.any([signal, capability.signal])
: capability.signal
effectiveSignal.throwIfAborted()
switch (name) {
case 'goodbuddy_config_capabilities':
return { capabilities: service.getCapabilities(input) }
case 'goodbuddy_config_get':
return { config: await service.getSnapshot(input) }
case 'goodbuddy_config_plan':
return {
plan: await service.plan(
capability.requestId,
workspacePath,
input
)
}
case 'goodbuddy_config_apply':
return {
result: await service.apply(
capability.requestId,
input,
effectiveSignal,
capability.authorizeConfigApply
)
}
}
}
private requireMagicNotes(
token: string,
requiredAccess: Exclude<MagicNotesCapabilityAccess, 'none'>
@@ -686,6 +787,11 @@ export class KnowledgeMcpGateway {
return { note: this.deleteMagicNoteEntry(token, input) }
case 'note_delete':
return this.deleteMagicNote(token, input)
case 'goodbuddy_config_capabilities':
case 'goodbuddy_config_get':
case 'goodbuddy_config_plan':
case 'goodbuddy_config_apply':
return this.callGoodBuddyConfigTool(token, name, input)
}
}
@@ -750,7 +856,14 @@ export class KnowledgeMcpGateway {
{
title: definition.title,
description: definition.description,
inputSchema: definition.inputSchema.shape
inputSchema: definition.inputSchema,
annotations: {
readOnlyHint: definition.access === 'read',
destructiveHint:
name === 'goodbuddy_config_apply' ||
name === 'note_delete' ||
name === 'note_entry_delete'
}
},
async (input: Record<string, unknown>) => ({
content: [
+51 -6
View File
@@ -215,6 +215,9 @@ describe('ModelToolProvider', () => {
const createMagicNote = vi.fn(() => ({
id: '00000000-0000-4000-8000-000000000701'
}))
const callGoodBuddyConfigTool = vi.fn(async () => ({
capabilities: { server: 'goodbuddy_config' }
}))
const gateway = {
listLibraries,
search,
@@ -222,6 +225,7 @@ describe('ModelToolProvider', () => {
listMagicNotes,
getMagicNote,
createMagicNote,
callGoodBuddyConfigTool,
getAvailableToolNames: vi.fn(() => [
'knowledge_list',
'knowledge_search',
@@ -233,7 +237,11 @@ describe('ModelToolProvider', () => {
'note_entry_create',
'note_entry_update',
'note_entry_delete',
'note_delete'
'note_delete',
'goodbuddy_config_capabilities',
'goodbuddy_config_get',
'goodbuddy_config_plan',
'goodbuddy_config_apply'
])
} as unknown as KnowledgeMcpGateway
const provider = new ModelToolProvider(
@@ -255,7 +263,10 @@ describe('ModelToolProvider', () => {
'knowledge_search',
'note_list',
'note_get',
'note_search'
'note_search',
'goodbuddy_config_capabilities',
'goodbuddy_config_get',
'goodbuddy_config_plan'
])
expect(
JSON.stringify(
@@ -306,6 +317,18 @@ describe('ModelToolProvider', () => {
expect(getMagicNote).toHaveBeenCalledWith('main-only-token', {
noteId: '00000000-0000-4000-8000-000000000701'
})
await provider.callTool(
'goodbuddy_config_capabilities',
{},
signal,
askContext
)
expect(callGoodBuddyConfigTool).toHaveBeenCalledWith(
'main-only-token',
'goodbuddy_config_capabilities',
{},
signal
)
await expect(
provider.listTools(
@@ -333,7 +356,11 @@ describe('ModelToolProvider', () => {
'note_entry_create',
'note_entry_update',
'note_entry_delete',
'note_delete'
'note_delete',
'goodbuddy_config_capabilities',
'goodbuddy_config_get',
'goodbuddy_config_plan',
'goodbuddy_config_apply'
])
)
await provider.callTool(
@@ -375,6 +402,20 @@ describe('ModelToolProvider', () => {
allowPermanent: false,
description: expect.stringContaining('永久删除')
})
const configApplyTool = executeTools.find(
(tool) => tool.name === 'goodbuddy_config_apply'
)!
expect(
provider.getApproval(
configApplyTool,
{ planId: '00000000-0000-4000-8000-000000000702' },
'{"planId":"00000000-0000-4000-8000-000000000702"}',
{ ...askContext, workMode: 'execute' }
)
).toMatchObject({
scopeKey: 'model:goodbuddy-config:apply',
allowPermanent: false
})
})
it('reserves all scoped data tool slots for Execute', async () => {
@@ -394,7 +435,11 @@ describe('ModelToolProvider', () => {
'note_entry_create',
'note_entry_update',
'note_entry_delete',
'note_delete'
'note_delete',
'goodbuddy_config_capabilities',
'goodbuddy_config_get',
'goodbuddy_config_plan',
'goodbuddy_config_apply'
])
} as unknown as KnowledgeMcpGateway
const context = {
@@ -414,7 +459,7 @@ describe('ModelToolProvider', () => {
}))
mocks.client.listTools.mockResolvedValueOnce({
tools: createTools(86)
tools: createTools(82)
})
const validProvider = new ModelToolProvider(
workspace,
@@ -428,7 +473,7 @@ describe('ModelToolProvider', () => {
await validProvider.dispose()
mocks.client.listTools.mockResolvedValueOnce({
tools: createTools(87)
tools: createTools(83)
})
const overflowingProvider = new ModelToolProvider(
workspace,
+43 -1
View File
@@ -17,6 +17,7 @@ import { isIP } from 'node:net'
import { z } from 'zod'
import { builtinModelTools } from '../../shared/builtin-model-tools'
import {
goodbuddyConfigWriteToolNames,
magicNoteWriteToolNames,
maximumScopedToolCount,
scopedDataToolByName,
@@ -78,6 +79,9 @@ const webFetchTool = builtinModelTools.find(
const magicNoteWriteToolNameSet = new Set<string>(
magicNoteWriteToolNames
)
const goodbuddyConfigWriteToolNameSet = new Set<string>(
goodbuddyConfigWriteToolNames
)
const scopedReadToolNameSet = new Set<string>(scopedReadToolNames)
const scopedToolJsonSchemas = new Map(
[...scopedDataToolByName].map(([name, definition]) => {
@@ -543,7 +547,10 @@ export class ModelToolProvider implements ModelToolProviderLike {
return [
{
name: definition.name,
displayName: definition.displayName,
displayName:
'displayName' in definition
? definition.displayName
: definition.title,
description: definition.description,
inputSchema,
source: 'builtin'
@@ -1047,6 +1054,17 @@ export class ModelToolProvider implements ModelToolProviderLike {
allowPermanent: false
}
}
if (goodbuddyConfigWriteToolNameSet.has(tool.name)) {
return {
scopeKey: 'model:goodbuddy-config:apply',
title: '允许应用 GoodBuddy 配置计划?',
description:
'该操作会修改 GoodBuddy 应用偏好或扩展能力。主进程还会显示计划中的具体变更并再次要求确认。',
toolName: tool.displayName,
argumentSummary,
allowPermanent: false
}
}
if (tool.name === 'web_search' || tool.name === 'web_fetch') {
return {
scopeKey: `model:web:${tool.name}`,
@@ -1295,6 +1313,30 @@ export class ModelToolProvider implements ModelToolProviderLike {
)
)
}
if (
name === 'goodbuddy_config_capabilities' ||
name === 'goodbuddy_config_get' ||
name === 'goodbuddy_config_plan' ||
name === 'goodbuddy_config_apply'
) {
if (
!this.knowledgeGateway ||
!context.knowledgeCapabilityToken
) {
throw new Error('GoodBuddy 配置授权不可用')
}
return createTextToolResult(
boundedJson(
await this.knowledgeGateway.callGoodBuddyConfigTool(
context.knowledgeCapabilityToken,
name,
argumentsValue,
signal
),
'GoodBuddy 配置工具结果无法序列化'
)
)
}
if (name === 'web_search' || name === 'web_fetch') {
try {
const binding = (await this.getWebSearchBindings(signal)).get(name)
+216 -8
View File
@@ -1,20 +1,55 @@
import { mkdtemp, readFile, rm } from 'node:fs/promises'
import { mkdir, mkdtemp, readFile, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { z } from 'zod'
import { modelProtocolSchema } from '../../shared/contracts'
import { ContinueAgentRuntime } from './continue-runtime'
import { ModelAgentRuntime } from './model-runtime'
import { OpenCodeRuntime } from './opencode-runtime'
import { AgentRuntimeController } from './runtime-controller'
import type { RuntimeEvent } from './runtime'
import type {
ModelToolCallContext,
ModelToolDefinition,
ModelToolProviderLike,
ModelToolResult
} from './model-tool-provider'
import { GoodBuddyConfigService } from '../goodbuddy-config-service'
import { ApplicationSettingsStore } from '../application-settings-store'
import {
BrowserProfileService,
MemoryBrowserProfileStore
} from '../capabilities/browser-profile-service'
import {
CapabilityService,
type CapabilityCipher
} from '../capabilities/capability-service'
import {
goodbuddyConfigToolByName,
goodbuddyConfigTools
} from '../../shared/goodbuddy-config-tools'
const enabled = process.env.GOODBUDDY_RUN_RUNTIME_E2E === '1'
const apiKey = process.env.ANTHROPIC_API_KEY ?? ''
const apiKey =
process.env.GOODBUDDY_E2E_API_KEY ??
process.env.ANTHROPIC_API_KEY ??
''
const configuredBaseUrl =
process.env.ANTHROPIC_BASE_URL ?? 'https://api.anthropic.com'
const baseUrl = new URL(configuredBaseUrl).origin
process.env.GOODBUDDY_E2E_BASE_URL ??
process.env.ANTHROPIC_BASE_URL ??
'https://api.anthropic.com'
const configuredUrl = new URL(configuredBaseUrl)
configuredUrl.search = ''
configuredUrl.hash = ''
const baseUrl = configuredUrl.toString().replace(/\/$/u, '')
const modelName =
process.env.GOODBUDDY_E2E_MODEL ?? 'claude-sonnet-5'
const protocol = modelProtocolSchema
.exclude(['openai-images-generations'])
.parse(
process.env.GOODBUDDY_E2E_PROTOCOL ?? 'anthropic-messages'
)
const portableRoot = join(
process.cwd(),
'dist',
@@ -33,6 +68,100 @@ async function collectText(
return output
}
function textResult(value: unknown): ModelToolResult {
const text = JSON.stringify(value)
return {
parts: [{ type: 'text', text }],
contextBytes: Buffer.byteLength(text)
}
}
class RealModelConfigToolProvider implements ModelToolProviderLike {
readonly calls: string[] = []
private planId?: string
constructor(
private readonly service: GoodBuddyConfigService,
private readonly workspacePath: string,
private readonly requestId: string
) {}
async listTools(
context: ModelToolCallContext
): Promise<ModelToolDefinition[]> {
return goodbuddyConfigTools
.filter(
(tool) =>
context.workMode === 'execute' || tool.access === 'read'
)
.map((tool) => {
const schema = z.toJSONSchema(tool.inputSchema, {
target: 'draft-7'
}) as Record<string, unknown>
Reflect.deleteProperty(schema, '$schema')
return {
name: tool.name,
displayName: tool.title,
description: tool.description,
inputSchema: schema,
source: 'builtin'
}
})
}
getApproval() {
return {
scopeKey: 'real-model-config-test',
title: 'Unexpected config write',
description: 'Real config discovery test must not apply changes',
allowPermanent: false
}
}
async callTool(
name: string,
argumentsValue: Record<string, unknown>,
signal: AbortSignal
): Promise<ModelToolResult> {
signal.throwIfAborted()
this.calls.push(name)
const tool = goodbuddyConfigToolByName.get(
name as Parameters<typeof goodbuddyConfigToolByName.get>[0]
)
if (!tool) {
throw new Error(`Unexpected tool: ${name}`)
}
switch (name) {
case 'goodbuddy_config_capabilities':
return textResult({
capabilities: this.service.getCapabilities(argumentsValue)
})
case 'goodbuddy_config_get':
return textResult({
config: await this.service.getSnapshot(argumentsValue)
})
case 'goodbuddy_config_plan': {
const plan = await this.service.plan(
this.requestId,
this.workspacePath,
argumentsValue
)
this.planId = plan.planId
return textResult({ plan })
}
default:
throw new Error('Apply is forbidden in the real discovery test')
}
}
async releaseConversation(): Promise<void> {}
async dispose(): Promise<void> {}
getPlannedId(): string | undefined {
return this.planId
}
}
describe.runIf(enabled)('runtime end-to-end', () => {
let workspace = ''
@@ -57,7 +186,7 @@ describe.runIf(enabled)('runtime end-to-end', () => {
apiKey,
baseUrl,
model: modelName,
protocol: 'anthropic-messages',
protocol,
authentication: 'api-key'
})
@@ -82,6 +211,85 @@ describe.runIf(enabled)('runtime end-to-end', () => {
120_000
)
it(
'discovers and plans GoodBuddy configuration through a real model',
async () => {
const testRoot = await mkdtemp(
join(tmpdir(), 'goodbuddy-config-model-e2e-')
)
const builtinSkillsRoot = join(testRoot, 'builtin-skills')
const importedSkillsRoot = join(testRoot, 'imported-skills')
await mkdir(builtinSkillsRoot, { recursive: true })
const cipher: CapabilityCipher = {
isAvailable: () => true,
encrypt: (value) => Buffer.from(value),
decrypt: (value) => value.toString()
}
const configService = new GoodBuddyConfigService(
new ApplicationSettingsStore(join(testRoot, 'application.json')),
new CapabilityService(
join(testRoot, 'capabilities.json'),
builtinSkillsRoot,
importedSkillsRoot,
cipher,
{
browserProfiles: new BrowserProfileService(
new MemoryBrowserProfileStore()
)
}
)
)
const requestId = crypto.randomUUID()
const toolProvider = new RealModelConfigToolProvider(
configService,
workspace,
requestId
)
const runtime = new ModelAgentRuntime({
apiKey,
baseUrl,
model: modelName,
protocol,
authentication: 'api-key',
defaultWorkspace: workspace,
toolProvider
})
try {
const output = await collectText(
runtime.run(
{
requestId,
conversationId: crypto.randomUUID(),
workMode: 'execute',
prompt:
'Use GoodBuddy configuration tools. First discover capabilities and examples, then read the sanitized current configuration, then create (but do not apply) a plan that sets checkUpdatesOnStartup to false. Finish with CONFIG_PLAN_OK and the plan risk. Never call apply.'
},
new AbortController().signal,
async (event) =>
event.toolName === 'goodbuddy_config_apply'
? 'deny'
: 'once'
)
)
expect(toolProvider.calls).toEqual(
expect.arrayContaining([
'goodbuddy_config_capabilities',
'goodbuddy_config_get',
'goodbuddy_config_plan'
])
)
expect(toolProvider.calls).not.toContain('goodbuddy_config_apply')
expect(toolProvider.getPlannedId()).toBeDefined()
expect(output).toContain('CONFIG_PLAN_OK')
} finally {
await runtime.dispose()
await rm(testRoot, { recursive: true, force: true })
}
},
120_000
)
it(
'cancels an in-flight direct model task',
async () => {
@@ -89,7 +297,7 @@ describe.runIf(enabled)('runtime end-to-end', () => {
apiKey,
baseUrl,
model: modelName,
protocol: 'anthropic-messages',
protocol,
authentication: 'api-key'
})
const abortController = new AbortController()
@@ -140,7 +348,7 @@ describe.runIf(enabled)('runtime end-to-end', () => {
baseUrl,
modelName,
apiKey,
protocol: 'anthropic-messages',
protocol,
authentication: 'api-key'
}
})
@@ -203,7 +411,7 @@ describe.runIf(enabled)('runtime end-to-end', () => {
baseUrl,
modelName,
apiKey,
protocol: 'anthropic-messages',
protocol,
authentication: 'api-key'
}
})