fix: remove graph extraction budget and unify image download buttons
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s

Graph extraction could exhaust its fixed 8192-token budget before returning
structured content. OpenAI requests now omit output-token limits, while
Anthropic uses the model connection limit required by its protocol.
Provider limits, timeout, cancellation and bounded response handling remain.

Use the shared accessible icon button for image downloads in chat and the
image viewer. Add graph request regression coverage and document the limits.

Release note: 知识图谱抽取不再额外限制为 8192 tokens;OpenAI 由模型服务决定输出上限,Anthropic 沿用模型连接配置。聊天和图片预览中的下载按钮统一为带提示的图标按钮。
This commit is contained in:
mesalogo
2026-09-19 21:02:43 +08:00
parent c98eb69cbd
commit b5d8204986
5 changed files with 93 additions and 10 deletions
@@ -1,4 +1,5 @@
import { describe, expect, it, vi } from 'vitest'
import { defaultAnthropicMaximumOutputTokens } from '../../shared/contracts'
import type {
ResolvedRuntimeSettings,
RuntimeSettingsStore
@@ -14,6 +15,8 @@ function store(
modelName: 'intranet-model',
modelProtocol: 'anthropic-messages',
modelAuthentication: 'none',
modelProfiles: [],
defaultModelProfileId: 'default-model',
...overrides
} as ResolvedRuntimeSettings
return {
@@ -29,6 +32,70 @@ function jsonResponse(payload: unknown, status = 200): Response {
}
describe('createModelGraphExtractor', () => {
it.each([
'openai-chat-completions',
'openai-responses'
] as const)('does not impose an output budget for %s', async (protocol) => {
const fetcher = vi.fn<typeof fetch>(async () =>
jsonResponse({
choices: [{ message: { content: '{"entities":[]}' } }],
output_text: '{"entities":[]}'
})
)
const extract = createModelGraphExtractor(
store({
modelProtocol: protocol,
modelProfiles: [{
id: 'default-model',
name: 'Default model',
baseUrl: 'http://localhost:8000',
modelName: 'intranet-model',
protocol,
authentication: 'none',
maximumOutputTokens: 8192
}]
}),
fetcher
)
await expect(extract('extract this')).resolves.toEqual({ entities: [] })
const body = JSON.parse(String(fetcher.mock.calls[0]?.[1]?.body))
expect(body).not.toHaveProperty('max_tokens')
expect(body).not.toHaveProperty('max_completion_tokens')
expect(body).not.toHaveProperty('max_output_tokens')
})
it.each([undefined, 48_000])(
'uses the Anthropic connection protocol limit (%s), not an extraction budget',
async (maximumOutputTokens) => {
const fetcher = vi.fn<typeof fetch>(async () =>
jsonResponse({ content: [{ type: 'text', text: '{"entities":[]}' }] })
)
const profile = {
name: 'Model',
baseUrl: 'http://localhost:8000',
modelName: 'intranet-model',
protocol: 'anthropic-messages' as const,
authentication: 'none' as const
}
const extract = createModelGraphExtractor(
store({
modelProfiles: [
{ ...profile, id: 'other-model', maximumOutputTokens: 1024 },
{ ...profile, id: 'default-model', maximumOutputTokens }
]
}),
fetcher
)
await expect(extract('extract this')).resolves.toEqual({ entities: [] })
const body = JSON.parse(String(fetcher.mock.calls[0]?.[1]?.body))
expect(body.max_tokens).toBe(
maximumOutputTokens ?? defaultAnthropicMaximumOutputTokens
)
}
)
it('uses an unauthenticated Anthropic endpoint with its path and query', async () => {
const fetcher = vi.fn(async () =>
jsonResponse({
+6 -3
View File
@@ -1,4 +1,5 @@
import type { RuntimeSettingsStore } from '../runtime-settings-store'
import { defaultAnthropicMaximumOutputTokens } from '../../shared/contracts'
import {
createOpenAIChatCompletionsUrl,
createOpenAIResponsesUrl
@@ -208,7 +209,6 @@ export function createModelGraphExtractor(
protocol === 'openai-responses'
? {
model: settings.modelName,
max_output_tokens: 8192,
stream: false,
instructions: system,
input: userPrompt
@@ -216,14 +216,17 @@ export function createModelGraphExtractor(
: protocol === 'anthropic-messages'
? {
model: settings.modelName,
max_tokens: 8192,
// Anthropic requires this field; reuse the connection's protocol limit.
max_tokens:
settings.modelProfiles.find(
(profile) => profile.id === settings.defaultModelProfileId
)?.maximumOutputTokens ?? defaultAnthropicMaximumOutputTokens,
stream: false,
system,
messages: [{ role: 'user', content: userPrompt }]
}
: {
model: settings.modelName,
max_tokens: 8192,
stream: false,
messages: [
{ role: 'system', content: system },
+5 -3
View File
@@ -11670,12 +11670,14 @@ function App(): React.JSX.Element {
</strong>
<div>
<button
className="secondary-button"
aria-label={t("chat.images.downloadImage")}
className="icon-button attachment-action"
title={t("chat.images.downloadImage")}
data-tooltip={t("chat.images.downloadImage")}
onClick={() => downloadImage(imageViewerItem)}
type="button"
>
<Download size={14} />
{t("chat.images.downloadImage")}
<Download size={16} aria-hidden="true" />
</button>
<button
aria-label={t("chat.images.closeViewer")}
+8 -4
View File
@@ -769,11 +769,13 @@ function ChatMessageRowView({
aria-label={t('chat.images.downloadNamed', {
title: attachment.name
})}
className="icon-button attachment-action"
title={t('chat.images.downloadImage')}
data-tooltip={t('chat.images.downloadImage')}
onClick={() => onDownloadImage(imageItem)}
type="button"
>
<Download size={12} />
{t('chat.images.download')}
<Download size={16} aria-hidden="true" />
</button>
</span>
)}
@@ -911,6 +913,9 @@ function ChatMessageRowView({
aria-label={t('chat.images.downloadNamed', {
title: artifact.title
})}
className="icon-button attachment-action"
title={t('chat.images.downloadImage')}
data-tooltip={t('chat.images.downloadImage')}
onClick={() =>
onDownloadImage({
src: artifact.content!,
@@ -919,8 +924,7 @@ function ChatMessageRowView({
}
type="button"
>
<Download size={12} />
{t('chat.images.download')}
<Download size={16} aria-hidden="true" />
</button>
</div>
</figure>