feat: strengthen private runtime and adaptive UI behavior
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
parent
b3fdf96962
commit
e8dc4d03fd
@@ -130,6 +130,22 @@ describe('ContinueHostAdapter', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('blocks runs without an explicit model profile or config file', async () => {
|
||||
const launchHost = vi.fn()
|
||||
const adapter = new ContinueHostAdapter({
|
||||
binaryPath: 'C:\\unused\\cn.js',
|
||||
configPath: '',
|
||||
workspace: process.cwd(),
|
||||
cacheRoot: 'C:\\unused\\cache',
|
||||
launchHost: launchHost as unknown as ContinueHostLauncher
|
||||
})
|
||||
|
||||
await expect(
|
||||
adapter.run('hello', new AbortController().signal, async () => 'deny')
|
||||
).rejects.toThrow('尚未配置模型连接')
|
||||
expect(launchHost).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('launches the prepared host through the injected launcher', async () => {
|
||||
const distribution = await createDistribution()
|
||||
let launch:
|
||||
@@ -266,10 +282,18 @@ describe('ContinueHostAdapter', () => {
|
||||
CONTINUE_CLI_ENABLE_TELEMETRY: '0',
|
||||
CONTINUE_METRICS_ENABLED: '0',
|
||||
CONTINUE_GLOBAL_DIR: expect.stringContaining('isolated-global'),
|
||||
DO_NOT_TRACK: '1',
|
||||
GOODBUDDY_DISABLE_CONTINUE_UPDATES: '1',
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: '',
|
||||
OTEL_EXPORTER_OTLP_HEADERS: '',
|
||||
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: '',
|
||||
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: '',
|
||||
OTEL_LOG_USER_PROMPTS: '0'
|
||||
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: '',
|
||||
OTEL_LOGS_EXPORTER: 'none',
|
||||
OTEL_LOG_USER_PROMPTS: '0',
|
||||
OTEL_METRICS_EXPORTER: 'none',
|
||||
OTEL_SDK_DISABLED: 'true',
|
||||
OTEL_TRACES_EXPORTER: 'none'
|
||||
})
|
||||
expect(killed).toBe(true)
|
||||
expect(JSON.parse(generatedConfig)).toMatchObject({
|
||||
@@ -391,4 +415,79 @@ describe('ContinueHostAdapter', () => {
|
||||
expect(launchedEnvironment).not.toHaveProperty('OPENAI_API_KEY')
|
||||
expect(launchedEnvironment).not.toHaveProperty('ANTHROPIC_API_KEY')
|
||||
})
|
||||
|
||||
it('turns a strict upstream error envelope into a failed run', async () => {
|
||||
const distribution = await createDistribution()
|
||||
let killed = false
|
||||
const launchHost: ContinueHostLauncher = () => ({
|
||||
exitCode: null,
|
||||
get killed() {
|
||||
return killed
|
||||
},
|
||||
stderr: null,
|
||||
once: () => undefined,
|
||||
kill: () => {
|
||||
killed = true
|
||||
return true
|
||||
}
|
||||
})
|
||||
let stateRequests = 0
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn(async (input: string | URL | Request) => {
|
||||
if (String(input).endsWith('/state')) {
|
||||
stateRequests += 1
|
||||
return Response.json({
|
||||
session: {
|
||||
history:
|
||||
stateRequests === 1
|
||||
? []
|
||||
: [
|
||||
{
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: 'Partial response'
|
||||
}
|
||||
},
|
||||
{
|
||||
message: {
|
||||
role: 'system',
|
||||
content:
|
||||
'Error: {"error":{"message":"Request not allowed"}}'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
isProcessing: false,
|
||||
messageQueueLength: 0,
|
||||
pendingPermission: null
|
||||
})
|
||||
}
|
||||
return Response.json({})
|
||||
})
|
||||
)
|
||||
const adapter = new ContinueHostAdapter({
|
||||
binaryPath: distribution.entryPath,
|
||||
configPath: '',
|
||||
workspace: process.cwd(),
|
||||
cacheRoot: distribution.cacheRoot,
|
||||
trustedBundleHashes: [distribution.sourceHash],
|
||||
launchHost,
|
||||
modelProfile: {
|
||||
id: '00000000-0000-4000-8000-000000000013',
|
||||
name: 'Local model',
|
||||
baseUrl: 'http://127.0.0.1:11434/v1',
|
||||
modelName: 'local-model',
|
||||
protocol: 'openai-chat-completions',
|
||||
authentication: 'none'
|
||||
}
|
||||
})
|
||||
|
||||
await expect(
|
||||
adapter.run('hello', new AbortController().signal, async () => 'deny')
|
||||
).rejects.toThrow(
|
||||
'Continue 模型请求失败:Request not allowed'
|
||||
)
|
||||
expect(killed).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -29,10 +29,16 @@ import {
|
||||
createContinuePermissionRule
|
||||
} from './continue-permissions'
|
||||
import { getAvailableLoopbackPort } from './loopback-port'
|
||||
import { buildRuntimeEnvironment } from './process-environment'
|
||||
import {
|
||||
buildRuntimeEnvironment,
|
||||
runtimePrivacyEnvironment
|
||||
} from './process-environment'
|
||||
import { createAnthropicApiBaseUrl } from './anthropic-endpoint'
|
||||
import { createOpenAIApiBaseUrl } from './openai-endpoint'
|
||||
import { safeToolArgumentSummary } from './approval-summary'
|
||||
import {
|
||||
redactSensitiveText,
|
||||
safeToolArgumentSummary
|
||||
} from './approval-summary'
|
||||
|
||||
const supportedVersion = '1.5.47'
|
||||
const supportedBundleHashes = new Set([
|
||||
@@ -40,6 +46,8 @@ const supportedBundleHashes = new Set([
|
||||
])
|
||||
const maximumBundleBytes = 32 * 1024 * 1024
|
||||
const maximumStateBytes = 8 * 1024 * 1024
|
||||
export const continueConfigurationRequiredMessage =
|
||||
'Continue 尚未配置模型连接,请在设置中选择 GoodBuddy 模型连接或指定 Continue 配置文件'
|
||||
const utilityBootstrap = [
|
||||
"import { pathToFileURL } from 'node:url'",
|
||||
'const entryPath = process.argv[2]',
|
||||
@@ -115,6 +123,13 @@ export type ContinueHostAdapterOptions = {
|
||||
modelProfile?: ResolvedModelProfile
|
||||
}
|
||||
|
||||
export function hasContinueModelConfiguration(
|
||||
configPath: string,
|
||||
modelProfile?: ResolvedModelProfile
|
||||
): boolean {
|
||||
return Boolean(modelProfile || configPath.trim())
|
||||
}
|
||||
|
||||
export type ContinueHostChild = {
|
||||
exitCode: number | null
|
||||
killed: boolean
|
||||
@@ -229,6 +244,58 @@ function extractAssistantText(
|
||||
return ''
|
||||
}
|
||||
|
||||
function parseContinueFailure(text: string): string | undefined {
|
||||
const match = /^Error:\s*(\{[\s\S]{1,16384}\})$/u.exec(text.trim())
|
||||
if (!match?.[1]) {
|
||||
return undefined
|
||||
}
|
||||
try {
|
||||
const payload = JSON.parse(match[1]) as unknown
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
return undefined
|
||||
}
|
||||
const record = payload as Record<string, unknown>
|
||||
const error = record.error
|
||||
const message =
|
||||
typeof error === 'string'
|
||||
? error
|
||||
: error && typeof error === 'object'
|
||||
? (error as Record<string, unknown>).message
|
||||
: record.message
|
||||
const detail =
|
||||
typeof message === 'string' && message.trim()
|
||||
? `:${redactSensitiveText(message.trim()).slice(0, 500)}`
|
||||
: ''
|
||||
return `Continue 模型请求失败${detail}`
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
function extractContinueFailure(
|
||||
history: unknown[],
|
||||
startIndex: number
|
||||
): string | undefined {
|
||||
for (const item of history.slice(startIndex).reverse()) {
|
||||
if (!item || typeof item !== 'object') {
|
||||
continue
|
||||
}
|
||||
const message = (item as Record<string, unknown>).message
|
||||
if (!message || typeof message !== 'object') {
|
||||
continue
|
||||
}
|
||||
const content = (message as Record<string, unknown>).content
|
||||
if (typeof content !== 'string') {
|
||||
continue
|
||||
}
|
||||
const failure = parseContinueFailure(content)
|
||||
if (failure) {
|
||||
return failure
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function subtractTokenCount(completed: number, initial: number): number {
|
||||
return Math.max(0, completed - initial)
|
||||
}
|
||||
@@ -480,6 +547,14 @@ export class ContinueHostAdapter {
|
||||
authorize: RuntimeAuthorizer
|
||||
): Promise<ContinueHostRunResult> {
|
||||
signal.throwIfAborted()
|
||||
if (
|
||||
!hasContinueModelConfiguration(
|
||||
this.options.configPath,
|
||||
this.options.modelProfile
|
||||
)
|
||||
) {
|
||||
throw new Error(continueConfigurationRequiredMessage)
|
||||
}
|
||||
let generatedConfigPath: string | undefined
|
||||
if (this.options.modelProfile) {
|
||||
if (
|
||||
@@ -547,6 +622,7 @@ export class ContinueHostAdapter {
|
||||
}
|
||||
args.push('serve', '--port', String(port), '--timeout', '300')
|
||||
const environment = buildRuntimeEnvironment({
|
||||
...runtimePrivacyEnvironment,
|
||||
CONTINUE_CLI_DISABLE_COMMIT_SIGNATURE: '1',
|
||||
CONTINUE_CLI_AUTO_UPDATED: '1',
|
||||
CONTINUE_CLI_ENABLE_TELEMETRY: '0',
|
||||
@@ -554,12 +630,7 @@ export class ContinueHostAdapter {
|
||||
CONTINUE_GLOBAL_DIR: isolatedGlobalDirectory,
|
||||
FORCE_NO_TTY: '1',
|
||||
GOODBUDDY_CONTINUE_HOST_TOKEN: token,
|
||||
GOODBUDDY_DISABLE_CONTINUE_UPDATES: '1',
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: '',
|
||||
OTEL_EXPORTER_OTLP_HEADERS: '',
|
||||
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: '',
|
||||
OTEL_METRICS_EXPORTER: '',
|
||||
OTEL_LOG_USER_PROMPTS: '0'
|
||||
GOODBUDDY_DISABLE_CONTINUE_UPDATES: '1'
|
||||
})
|
||||
if (this.options.modelProfile) {
|
||||
delete environment.ANTHROPIC_API_KEY
|
||||
@@ -699,6 +770,13 @@ export class ContinueHostAdapter {
|
||||
!state.pendingPermission &&
|
||||
state.session.history.length > startIndex
|
||||
) {
|
||||
const failure = extractContinueFailure(
|
||||
state.session.history,
|
||||
startIndex
|
||||
)
|
||||
if (failure) {
|
||||
throw new Error(failure)
|
||||
}
|
||||
const text = extractAssistantText(
|
||||
state.session.history,
|
||||
startIndex
|
||||
|
||||
@@ -152,7 +152,7 @@ describe('ContinueAgentRuntime', () => {
|
||||
it('adds assigned Skill instructions to the Continue prompt', async () => {
|
||||
const runtime = new ContinueAgentRuntime({
|
||||
binaryPath: '',
|
||||
configPath: '',
|
||||
configPath: 'C:\\safe config\\continue.yaml',
|
||||
mode: 'chat',
|
||||
defaultWorkspace: process.cwd(),
|
||||
hostCacheRoot: 'C:\\safe\\continue-host',
|
||||
@@ -172,6 +172,39 @@ describe('ContinueAgentRuntime', () => {
|
||||
expect(prompt).toContain('test')
|
||||
})
|
||||
|
||||
it('blocks anonymous platform fallback without an explicit model configuration', async () => {
|
||||
const runtime = new ContinueAgentRuntime({
|
||||
binaryPath: '',
|
||||
configPath: '',
|
||||
mode: 'chat',
|
||||
defaultWorkspace: process.cwd(),
|
||||
hostCacheRoot: 'C:\\safe\\continue-host',
|
||||
createHostAdapter: () => ({
|
||||
getPreparedHost: mocks.prepareHost,
|
||||
run: mocks.runHost,
|
||||
dispose: mocks.disposeHost
|
||||
})
|
||||
})
|
||||
|
||||
await expect(runtime.getStatus()).resolves.toMatchObject({
|
||||
available: false,
|
||||
detail: expect.stringContaining('尚未配置模型连接')
|
||||
})
|
||||
const stream = runtime.run(
|
||||
{
|
||||
requestId: '3f496642-f47d-4e0a-8944-a32c77b0d6ef',
|
||||
conversationId: 'conversation-1',
|
||||
prompt: 'test'
|
||||
},
|
||||
new AbortController().signal,
|
||||
vi.fn(async () => 'once' as const)
|
||||
)
|
||||
await expect(stream.next()).rejects.toThrow('尚未配置模型连接')
|
||||
expect(mocks.detectRuntimeBinary).not.toHaveBeenCalled()
|
||||
expect(mocks.prepareHost).not.toHaveBeenCalled()
|
||||
expect(mocks.runHost).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('places the current request before untrusted conversation history', async () => {
|
||||
const runtime = createRuntime()
|
||||
for await (const _event of runtime.run(
|
||||
|
||||
@@ -13,6 +13,8 @@ import { detectRuntimeBinary } from './runtime-discovery'
|
||||
import type { ResolvedModelProfile } from '../runtime-settings-store'
|
||||
import {
|
||||
ContinueHostAdapter,
|
||||
continueConfigurationRequiredMessage,
|
||||
hasContinueModelConfiguration,
|
||||
type ContinueHostAdapterOptions,
|
||||
type ContinueHostLauncher
|
||||
} from './continue-host-adapter'
|
||||
@@ -137,6 +139,20 @@ export class ContinueAgentRuntime implements AgentRuntime {
|
||||
'Continue 宿主暂不支持严格 OS 沙箱,请改用自动模式或嵌入式 OpenCode'
|
||||
}
|
||||
}
|
||||
if (
|
||||
!hasContinueModelConfiguration(
|
||||
this.options.configPath,
|
||||
this.options.modelProfile
|
||||
)
|
||||
) {
|
||||
return {
|
||||
id: 'continue',
|
||||
label: 'Continue CLI',
|
||||
available: false,
|
||||
supportsToolExecution: this.supportsToolExecution,
|
||||
detail: continueConfigurationRequiredMessage
|
||||
}
|
||||
}
|
||||
const detection = await this.getDetection()
|
||||
if (detection.available && detection.path) {
|
||||
try {
|
||||
@@ -179,6 +195,14 @@ export class ContinueAgentRuntime implements AgentRuntime {
|
||||
if (request.images?.length) {
|
||||
throw new Error('Continue Runtime 暂不支持图片上下文,请切换到视觉模型')
|
||||
}
|
||||
if (
|
||||
!hasContinueModelConfiguration(
|
||||
this.options.configPath,
|
||||
this.options.modelProfile
|
||||
)
|
||||
) {
|
||||
throw new Error(continueConfigurationRequiredMessage)
|
||||
}
|
||||
const prompt = buildContinuePrompt(request)
|
||||
const skillPrefix = this.options.skillInstructions
|
||||
? [
|
||||
|
||||
@@ -439,6 +439,10 @@ describe('OpenCodeRuntime embedded launcher', () => {
|
||||
OPENCODE_DISABLE_MODELS_FETCH: '1',
|
||||
OPENCODE_DISABLE_SHARE: '1',
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: '',
|
||||
OTEL_EXPORTER_OTLP_HEADERS: '',
|
||||
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: '',
|
||||
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: '',
|
||||
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: '',
|
||||
OTEL_SDK_DISABLED: 'true'
|
||||
})
|
||||
await runtime.dispose()
|
||||
|
||||
@@ -21,7 +21,10 @@ import { detectRuntimeBinary } from './runtime-discovery'
|
||||
import { getAvailableLoopbackPort } from './loopback-port'
|
||||
import type { ResolvedMcpServer } from '../capabilities/capability-service'
|
||||
import type { ResolvedModelProfile } from '../runtime-settings-store'
|
||||
import { buildRuntimeEnvironment } from './process-environment'
|
||||
import {
|
||||
buildRuntimeEnvironment,
|
||||
runtimePrivacyEnvironment
|
||||
} from './process-environment'
|
||||
import {
|
||||
buildBubblewrapLaunch,
|
||||
type RuntimeSandboxResolution
|
||||
@@ -366,7 +369,7 @@ export class OpenCodeRuntime implements AgentRuntime {
|
||||
throw new Error('OpenCode Server 启动已取消')
|
||||
}
|
||||
|
||||
const env = buildRuntimeEnvironment({})
|
||||
const env = buildRuntimeEnvironment(runtimePrivacyEnvironment)
|
||||
if (this.options.modelProfile && !this.options.modelProfile.apiKey) {
|
||||
throw new Error('OpenCode 独立模型连接尚未配置 API Key')
|
||||
}
|
||||
@@ -380,18 +383,11 @@ export class OpenCodeRuntime implements AgentRuntime {
|
||||
).toString('base64')}`
|
||||
env.OPENCODE_SERVER_USERNAME = EMBEDDED_SERVER_USERNAME
|
||||
env.OPENCODE_SERVER_PASSWORD = serverPassword
|
||||
env.DO_NOT_TRACK = '1'
|
||||
env.OPENCODE_DISABLE_AUTOUPDATE = '1'
|
||||
env.OPENCODE_DISABLE_EMBEDDED_WEB_UI = '1'
|
||||
env.OPENCODE_DISABLE_LSP_DOWNLOAD = '1'
|
||||
env.OPENCODE_DISABLE_MODELS_FETCH = '1'
|
||||
env.OPENCODE_DISABLE_SHARE = '1'
|
||||
env.OTEL_EXPORTER_OTLP_ENDPOINT = ''
|
||||
env.OTEL_EXPORTER_OTLP_HEADERS = ''
|
||||
env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = ''
|
||||
env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = ''
|
||||
env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = ''
|
||||
env.OTEL_SDK_DISABLED = 'true'
|
||||
if (this.options.modelProfile) {
|
||||
env.OPENCODE_CONFIG_CONTENT = JSON.stringify({
|
||||
model: `anthropic/${this.options.modelProfile.modelName}`,
|
||||
|
||||
@@ -38,6 +38,20 @@ const runtimeEnvironmentAllowlist = [
|
||||
'COHERE_API_KEY'
|
||||
] as const
|
||||
|
||||
export const runtimePrivacyEnvironment: NodeJS.ProcessEnv = {
|
||||
DO_NOT_TRACK: '1',
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: '',
|
||||
OTEL_EXPORTER_OTLP_HEADERS: '',
|
||||
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: '',
|
||||
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: '',
|
||||
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: '',
|
||||
OTEL_LOGS_EXPORTER: 'none',
|
||||
OTEL_LOG_USER_PROMPTS: '0',
|
||||
OTEL_METRICS_EXPORTER: 'none',
|
||||
OTEL_SDK_DISABLED: 'true',
|
||||
OTEL_TRACES_EXPORTER: 'none'
|
||||
}
|
||||
|
||||
export function buildRuntimeEnvironment(
|
||||
overrides: NodeJS.ProcessEnv,
|
||||
source: NodeJS.ProcessEnv = process.env
|
||||
|
||||
@@ -118,7 +118,7 @@ describe('RuntimeSettingsStore', () => {
|
||||
).rejects.toThrow('请重新输入或清除')
|
||||
})
|
||||
|
||||
it('repairs a gpt-image profile saved with chat protocol and origin-only URL', async () => {
|
||||
it('does not infer image capability from the model name', async () => {
|
||||
const { store } = await createStore()
|
||||
await store.update(
|
||||
settings({
|
||||
@@ -130,28 +130,28 @@ describe('RuntimeSettingsStore', () => {
|
||||
)
|
||||
|
||||
await expect(store.getPublicSettings()).resolves.toMatchObject({
|
||||
modelBaseUrl: 'https://bigtoken.ai/v1',
|
||||
modelBaseUrl: 'https://bigtoken.ai',
|
||||
modelName: 'gpt-image-2',
|
||||
modelProtocol: 'openai-images-generations',
|
||||
modelProtocol: 'anthropic-messages',
|
||||
apiKeyConfigured: true,
|
||||
credentialSource: 'encrypted',
|
||||
modelProfiles: [
|
||||
expect.objectContaining({
|
||||
baseUrl: 'https://bigtoken.ai/v1',
|
||||
baseUrl: 'https://bigtoken.ai',
|
||||
modelName: 'gpt-image-2',
|
||||
protocol: 'openai-images-generations'
|
||||
protocol: 'anthropic-messages'
|
||||
})
|
||||
]
|
||||
})
|
||||
await expect(store.getResolvedSettings()).resolves.toMatchObject({
|
||||
modelBaseUrl: 'https://bigtoken.ai/v1',
|
||||
modelBaseUrl: 'https://bigtoken.ai',
|
||||
modelName: 'gpt-image-2',
|
||||
modelProtocol: 'openai-images-generations',
|
||||
modelProtocol: 'anthropic-messages',
|
||||
apiKey: 'image-secret'
|
||||
})
|
||||
})
|
||||
|
||||
it('repairs nondefault image protocols without rewriting custom root endpoints', async () => {
|
||||
it('uses the explicit protocol as the image-generation capability marker', async () => {
|
||||
const { store } = await createStore()
|
||||
const chatId = crypto.randomUUID()
|
||||
const imageId = crypto.randomUUID()
|
||||
@@ -170,34 +170,36 @@ describe('RuntimeSettingsStore', () => {
|
||||
{
|
||||
id: imageId,
|
||||
name: 'Custom Image',
|
||||
baseUrl: 'https://images.example',
|
||||
modelName: 'gpt-image-custom',
|
||||
protocol: 'anthropic-messages',
|
||||
baseUrl: 'https://images.example/custom/v2',
|
||||
modelName: 'vendor/custom-renderer',
|
||||
protocol: 'openai-images-generations',
|
||||
authentication: 'api-key',
|
||||
apiKey: { action: 'replace', value: 'image-secret' }
|
||||
}
|
||||
],
|
||||
defaultModelProfileId: chatId,
|
||||
continueModelSource: { kind: 'profile', profileId: imageId }
|
||||
defaultModelProfileId: imageId
|
||||
})
|
||||
)
|
||||
|
||||
await expect(store.getPublicSettings()).resolves.toMatchObject({
|
||||
modelBaseUrl: 'https://images.example/custom/v2',
|
||||
modelName: 'vendor/custom-renderer',
|
||||
modelProtocol: 'openai-images-generations',
|
||||
modelProfiles: [
|
||||
expect.objectContaining({ id: chatId }),
|
||||
expect.objectContaining({
|
||||
id: imageId,
|
||||
baseUrl: 'https://images.example',
|
||||
baseUrl: 'https://images.example/custom/v2',
|
||||
modelName: 'vendor/custom-renderer',
|
||||
protocol: 'openai-images-generations'
|
||||
})
|
||||
]
|
||||
})
|
||||
await expect(store.getResolvedSettings()).resolves.toMatchObject({
|
||||
continueModelProfile: {
|
||||
id: imageId,
|
||||
baseUrl: 'https://images.example',
|
||||
protocol: 'openai-images-generations'
|
||||
}
|
||||
modelBaseUrl: 'https://images.example/custom/v2',
|
||||
modelName: 'vendor/custom-renderer',
|
||||
modelProtocol: 'openai-images-generations',
|
||||
apiKey: 'image-secret'
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -277,31 +277,6 @@ function normalizeModelBaseUrl(value: string): string {
|
||||
return url.toString().replace(/\/$/u, '')
|
||||
}
|
||||
|
||||
function normalizeEffectiveModelConnection(
|
||||
baseUrl: string,
|
||||
model: string,
|
||||
protocol: RuntimeSettings['modelProtocol']
|
||||
): {
|
||||
baseUrl: string
|
||||
protocol: RuntimeSettings['modelProtocol']
|
||||
} {
|
||||
if (!/^gpt-image-/iu.test(model)) {
|
||||
return { baseUrl, protocol }
|
||||
}
|
||||
const url = new URL(baseUrl)
|
||||
if (
|
||||
protocol !== 'openai-images-generations' &&
|
||||
url.hostname.toLowerCase() === 'bigtoken.ai' &&
|
||||
(url.pathname === '/' || url.pathname === '')
|
||||
) {
|
||||
url.pathname = '/v1'
|
||||
}
|
||||
return {
|
||||
baseUrl: url.toString().replace(/\/$/u, ''),
|
||||
protocol: 'openai-images-generations'
|
||||
}
|
||||
}
|
||||
|
||||
export class RuntimeSettingsStore {
|
||||
private settings?: StoredSettings
|
||||
private loadWarning?: string
|
||||
@@ -475,16 +450,11 @@ export class RuntimeSettingsStore {
|
||||
const model = environmentApiKey
|
||||
? environmentModel || defaultRuntimeSettings.modelName
|
||||
: profile.modelName
|
||||
const effectiveConnection = normalizeEffectiveModelConnection(
|
||||
baseUrl,
|
||||
model,
|
||||
profile.protocol
|
||||
)
|
||||
return {
|
||||
apiKey: environmentApiKey ?? storedApiKey,
|
||||
baseUrl: effectiveConnection.baseUrl,
|
||||
baseUrl,
|
||||
model,
|
||||
protocol: effectiveConnection.protocol,
|
||||
protocol: profile.protocol,
|
||||
authentication: profile.authentication,
|
||||
credentialSource: environmentApiKey
|
||||
? 'environment'
|
||||
@@ -516,17 +486,12 @@ export class RuntimeSettingsStore {
|
||||
apiKey: effective.apiKey
|
||||
}
|
||||
}
|
||||
const connection = normalizeEffectiveModelConnection(
|
||||
profile.baseUrl,
|
||||
profile.modelName,
|
||||
profile.protocol
|
||||
)
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.name,
|
||||
baseUrl: connection.baseUrl,
|
||||
baseUrl: profile.baseUrl,
|
||||
modelName: profile.modelName,
|
||||
protocol: connection.protocol,
|
||||
protocol: profile.protocol,
|
||||
authentication: profile.authentication,
|
||||
apiKey:
|
||||
profile.authentication === 'api-key'
|
||||
@@ -589,13 +554,6 @@ export class RuntimeSettingsStore {
|
||||
const agent = this.resolveAgentSettings(settings)
|
||||
const modelProfiles = settings.modelProfiles.map((profile) => {
|
||||
const isDefault = profile.id === settings.defaultModelProfileId
|
||||
const connection = isDefault
|
||||
? undefined
|
||||
: normalizeEffectiveModelConnection(
|
||||
profile.baseUrl,
|
||||
profile.modelName,
|
||||
profile.protocol
|
||||
)
|
||||
const apiKey =
|
||||
profile.authentication === 'api-key'
|
||||
? this.getStoredApiKey(profile)
|
||||
@@ -605,11 +563,11 @@ export class RuntimeSettingsStore {
|
||||
name: profile.name,
|
||||
baseUrl: isDefault
|
||||
? effective.baseUrl
|
||||
: (connection?.baseUrl ?? profile.baseUrl),
|
||||
: profile.baseUrl,
|
||||
modelName: isDefault ? effective.model : profile.modelName,
|
||||
protocol: isDefault
|
||||
? effective.protocol
|
||||
: (connection?.protocol ?? profile.protocol),
|
||||
: profile.protocol,
|
||||
authentication: isDefault
|
||||
? effective.authentication
|
||||
: profile.authentication,
|
||||
|
||||
Reference in New Issue
Block a user