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
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:
@@ -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({
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user