mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-10-20 11:16:59 +00:00
fix(local): a deleted connection is never asked about again, and a plan's test costs no money
Deleting a connection refreshed its own model list, which answered "connection not found" on the card being removed; it is now taken off the list and left out of the refresh. Its model list is cached by the connection, not its id alone, since SQLite gives a deleted id to the next connection. Testing a chat model on a ChatGPT or Claude plan says it counts toward the plan's limits instead of costing money. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
b810d57b45
commit
86b94aebfe
16 changed files with 257 additions and 11 deletions
|
|
@ -0,0 +1,132 @@
|
|||
import { afterEach, describe, expect, it, vi } from "vitest"
|
||||
import { cleanup, screen, waitFor } from "@testing-library/react"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
|
||||
import { render } from "@/test-utils"
|
||||
|
||||
import { useRefreshModels } from "../../models-query"
|
||||
import { useConnectionModels } from "../models/use-connection-models"
|
||||
import type { Connection } from "./api"
|
||||
import { useConnections, useDeleteConnection } from "./use-connections"
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
const CLAUDE: Connection = {
|
||||
id: 3,
|
||||
label: "Claude plan",
|
||||
provider: "openai_compatible",
|
||||
base_url: "https://api.anthropic.com",
|
||||
catalog_provider: "anthropic",
|
||||
has_api_key: false,
|
||||
auth_kind: "claude_plan",
|
||||
signed_in: false,
|
||||
account_email: null,
|
||||
serves: ["text_gen"],
|
||||
created_at: "2026-10-17T00:00:00",
|
||||
updated_at: "2026-10-17T00:00:00",
|
||||
}
|
||||
|
||||
function Card({ connection }: { connection: Connection }) {
|
||||
const remove = useDeleteConnection()
|
||||
const models = useConnectionModels(connection, true)
|
||||
return (
|
||||
<section aria-label={connection.label}>
|
||||
{models.isError ? <p>{models.error.message}</p> : null}
|
||||
<p>{models.data?.map((model) => model.name).join(", ")}</p>
|
||||
<button type="button" onClick={() => remove.mutate(connection.id)}>
|
||||
Delete
|
||||
</button>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function Settings() {
|
||||
const connections = useConnections()
|
||||
// What the app does once a connection is made.
|
||||
const refresh = useRefreshModels()
|
||||
return (
|
||||
<>
|
||||
{connections.data?.map((connection) => (
|
||||
<Card key={connection.id} connection={connection} />
|
||||
))}
|
||||
<button type="button" onClick={() => void refresh()}>
|
||||
Connected
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
describe("deleting a connection", () => {
|
||||
it("never asks the server about it again, so its card never says it is gone", async () => {
|
||||
let deleted = false
|
||||
const asked: string[] = []
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const path = String(input)
|
||||
asked.push(`${init?.method ?? "GET"} ${path}`)
|
||||
if (init?.method === "DELETE") {
|
||||
deleted = true
|
||||
return new Response(null, { status: 204 })
|
||||
}
|
||||
if (path === "/llm/connections")
|
||||
return Response.json(deleted ? [] : [CLAUDE])
|
||||
if (path === "/llm/connections/3/models")
|
||||
return deleted
|
||||
? Response.json({ detail: "connection not found" }, { status: 404 })
|
||||
: Response.json([])
|
||||
return Response.json({ detail: "not found" }, { status: 404 })
|
||||
})
|
||||
)
|
||||
const user = userEvent.setup()
|
||||
render(<Settings />)
|
||||
|
||||
await user.click(await screen.findByRole("button", { name: "Delete" }))
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByRole("region", { name: "Claude plan" })).toBeNull()
|
||||
)
|
||||
|
||||
const afterDelete = asked.slice(asked.indexOf("DELETE /llm/connections/3"))
|
||||
expect(afterDelete).not.toContain("GET /llm/connections/3/models")
|
||||
})
|
||||
})
|
||||
|
||||
describe("a new connection given a deleted one's id", () => {
|
||||
it("shows its own models, never the deleted one's", async () => {
|
||||
// SQLite gives the highest id out again once it is deleted.
|
||||
const again = { ...CLAUDE, created_at: "2026-10-17T00:05:00" }
|
||||
let deleted = false
|
||||
let added = false
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const path = String(input)
|
||||
if (init?.method === "DELETE") {
|
||||
deleted = true
|
||||
return new Response(null, { status: 204 })
|
||||
}
|
||||
if (path === "/llm/connections")
|
||||
return Response.json(added ? [again] : deleted ? [] : [CLAUDE])
|
||||
if (path === "/llm/connections/3/models")
|
||||
return Response.json([{ name: added ? "new-model" : "old-model" }])
|
||||
return Response.json({ detail: "not found" }, { status: 404 })
|
||||
})
|
||||
)
|
||||
const user = userEvent.setup()
|
||||
render(<Settings />)
|
||||
expect(await screen.findByText("old-model")).toBeTruthy()
|
||||
await user.click(screen.getByRole("button", { name: "Delete" }))
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByRole("region", { name: "Claude plan" })).toBeNull()
|
||||
)
|
||||
|
||||
added = true
|
||||
await user.click(screen.getByRole("button", { name: "Connected" }))
|
||||
|
||||
expect(await screen.findByText("new-model")).toBeTruthy()
|
||||
expect(screen.queryByText("old-model")).toBeNull()
|
||||
})
|
||||
})
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
import { useMutation, useQuery } from "@tanstack/react-query"
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
|
||||
import type { ModelType } from "../../model-type"
|
||||
import { MODELS_QUERY_KEY, useRefreshModels } from "../../models-query"
|
||||
import { deleteConnection, getConnections } from "./api"
|
||||
import { MODELS_QUERY_KEY } from "../../models-query"
|
||||
import { connectionIdQueryKey } from "../models/use-connection-models"
|
||||
import { deleteConnection, getConnections, type Connection } from "./api"
|
||||
|
||||
export const connectionsQueryKey = [
|
||||
...MODELS_QUERY_KEY,
|
||||
|
|
@ -34,11 +35,27 @@ export function useConnectionsServing(modelType: ModelType | null) {
|
|||
})
|
||||
}
|
||||
|
||||
/** Removing a server clears every slot it filled, whichever section asked. */
|
||||
/**
|
||||
* Removing a server clears every slot it filled, whichever section asked. The
|
||||
* server itself is taken off the list rather than asked about again: it would
|
||||
* answer "not found" on a card that is going.
|
||||
*/
|
||||
export function useDeleteConnection() {
|
||||
const refresh = useRefreshModels()
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => deleteConnection(id),
|
||||
onSuccess: () => refresh(),
|
||||
onSuccess: async (_deleted, id) => {
|
||||
const its = connectionIdQueryKey(id)
|
||||
queryClient.setQueryData<Connection[]>(
|
||||
connectionsQueryKey,
|
||||
(connections) =>
|
||||
connections?.filter((connection) => connection.id !== id)
|
||||
)
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: MODELS_QUERY_KEY,
|
||||
predicate: (query) =>
|
||||
!its.every((part, index) => query.queryKey[index] === part),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ export function ServerModels({
|
|||
unlisted: boolean
|
||||
} | null>(null)
|
||||
const [tryOpen, setTryOpen] = useState(false)
|
||||
const models = useConnectionModels(connection.id, open)
|
||||
const models = useConnectionModels(connection, open)
|
||||
const selection = useSelection(modelType)
|
||||
|
||||
const inUse = (name: string) =>
|
||||
|
|
@ -503,6 +503,10 @@ export function ServerModels({
|
|||
modelType={modelType}
|
||||
model={trying.model}
|
||||
unlisted={trying.unlisted}
|
||||
plan={
|
||||
connection.auth_kind === "chatgpt" ||
|
||||
connection.auth_kind === "claude_plan"
|
||||
}
|
||||
onOpenChange={setTryOpen}
|
||||
onOpenChangeComplete={(open) => {
|
||||
if (!open) setTrying(null)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
import { afterEach, describe, expect, it, vi } from "vitest"
|
||||
import { cleanup, screen } from "@testing-library/react"
|
||||
|
||||
import { render } from "@/test-utils"
|
||||
|
||||
import type { ConnectionModel } from "./api"
|
||||
import { TryModelDialog } from "./try-model-dialog"
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
const FABLE: ConnectionModel = {
|
||||
connection_id: 3,
|
||||
connection_label: "Claude plan",
|
||||
name: "claude-fable-5-1",
|
||||
types: ["text_gen"],
|
||||
capability_source: "catalog",
|
||||
selectable_for: ["text_gen"],
|
||||
reads_images: true,
|
||||
}
|
||||
|
||||
function open(plan: boolean) {
|
||||
render(
|
||||
<TryModelDialog
|
||||
open
|
||||
modelType="text_gen"
|
||||
model={FABLE}
|
||||
unlisted={false}
|
||||
plan={plan}
|
||||
onOpenChange={vi.fn()}
|
||||
onOpenChangeComplete={vi.fn()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
describe("what testing a chat model costs", () => {
|
||||
it("counts toward a plan's limits, and costs no money, on a plan", async () => {
|
||||
open(true)
|
||||
|
||||
const dialog = await screen.findByRole("dialog")
|
||||
expect(dialog.textContent).toContain("counts toward your plan’s limits")
|
||||
expect(dialog.textContent).not.toContain("may cost money")
|
||||
})
|
||||
|
||||
it("may cost money on a server that bills per request", async () => {
|
||||
open(false)
|
||||
|
||||
expect((await screen.findByRole("dialog")).textContent).toContain(
|
||||
"may cost money"
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
@ -46,11 +46,21 @@ function messageFrom(error: unknown) {
|
|||
function Explanation({
|
||||
modelType,
|
||||
model,
|
||||
plan,
|
||||
}: {
|
||||
modelType: ModelType
|
||||
model: ConnectionModel
|
||||
plan: boolean
|
||||
}) {
|
||||
const image = modelType === "image_gen"
|
||||
// A plan bills nothing per prompt; it fills the chat slot alone.
|
||||
if (plan && modelType === "text_gen") {
|
||||
return intl.formatMessage({
|
||||
id: "models_try_dialog_chat_plan_body",
|
||||
defaultMessage:
|
||||
"Chat support is confirmed. Testing sends one short prompt, which counts toward your plan’s limits.",
|
||||
})
|
||||
}
|
||||
if (modelType === "audio_gen") {
|
||||
return model.capability_source === "unknown"
|
||||
? intl.formatMessage({
|
||||
|
|
@ -100,6 +110,7 @@ export function TryModelDialog({
|
|||
modelType,
|
||||
model,
|
||||
unlisted,
|
||||
plan = false,
|
||||
onOpenChange,
|
||||
onOpenChangeComplete,
|
||||
onSelected,
|
||||
|
|
@ -108,6 +119,8 @@ export function TryModelDialog({
|
|||
modelType: ModelType
|
||||
model: ConnectionModel
|
||||
unlisted: boolean
|
||||
/** A ChatGPT or Claude plan: a test uses the plan's limits, not money. */
|
||||
plan?: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onOpenChangeComplete: (open: boolean) => void
|
||||
onSelected?: (selection: ModelSelection) => void
|
||||
|
|
@ -204,7 +217,7 @@ export function TryModelDialog({
|
|||
)}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<Explanation modelType={modelType} model={model} />
|
||||
<Explanation modelType={modelType} model={model} plan={plan} />
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{modelType === "audio_gen" ? (
|
||||
|
|
|
|||
|
|
@ -1,13 +1,32 @@
|
|||
import { useQuery } from "@tanstack/react-query"
|
||||
|
||||
import { MODELS_QUERY_KEY } from "../../models-query"
|
||||
import type { Connection } from "../connections/api"
|
||||
import { getConnectionModels } from "./api"
|
||||
|
||||
type Identity = Pick<Connection, "id" | "created_at">
|
||||
|
||||
/** Every query about one connection id, whichever connection held it. */
|
||||
export function connectionIdQueryKey(id: number) {
|
||||
return [...MODELS_QUERY_KEY, "remote", "connection", id] as const
|
||||
}
|
||||
|
||||
/**
|
||||
* By the connection, not its id alone: SQLite gives a deleted connection's id
|
||||
* to the next one, which must never see the last one's models.
|
||||
*/
|
||||
export function connectionModelsQueryKey(connection: Identity) {
|
||||
return [
|
||||
...connectionIdQueryKey(connection.id),
|
||||
connection.created_at,
|
||||
] as const
|
||||
}
|
||||
|
||||
/** Fetched only once asked for: listing a hosted catalog is a network call. */
|
||||
export function useConnectionModels(connectionId: number, enabled: boolean) {
|
||||
export function useConnectionModels(connection: Identity, enabled: boolean) {
|
||||
return useQuery({
|
||||
queryKey: [...MODELS_QUERY_KEY, "remote", "connection", connectionId],
|
||||
queryFn: ({ signal }) => getConnectionModels(connectionId, signal),
|
||||
queryKey: connectionModelsQueryKey(connection),
|
||||
queryFn: ({ signal }) => getConnectionModels(connection.id, signal),
|
||||
enabled,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -707,6 +707,7 @@
|
|||
"models_speech_model_languages_button": "{count, plural, one {# Sprache} other {# Sprachen}}",
|
||||
"models_try_dialog_cancel_button": "Abbrechen",
|
||||
"models_try_dialog_chat_confirmed_body": "Chat-Unterstützung ist bestätigt. Der Test sendet einen kurzen Prompt und kann Kosten verursachen.",
|
||||
"models_try_dialog_chat_plan_body": "Chat-Unterstützung ist bestätigt. Der Test sendet einen kurzen Prompt, der auf die Limits deines Plans angerechnet wird.",
|
||||
"models_try_dialog_chat_unconfirmed_body": "Dieser Endpunkt veröffentlicht keine Fähigkeiten, daher ist Chat-Unterstützung nicht bestätigt. Der Test sendet einen kurzen Prompt und kann Kosten verursachen.",
|
||||
"models_try_dialog_image_confirmed_body": "Bildunterstützung ist bestätigt. Der Test führt eine echte Inferenz aus und kann Kosten verursachen.",
|
||||
"models_try_dialog_image_unconfirmed_body": "Dieser Endpunkt veröffentlicht keine Fähigkeiten, daher ist Bildunterstützung nicht bestätigt. Er muss /images/generations oder /images unterstützen. Der Test führt eine echte Inferenz aus und kann Kosten verursachen.",
|
||||
|
|
|
|||
|
|
@ -707,6 +707,7 @@
|
|||
"models_speech_model_languages_button": "{count, plural, one {# language} other {# languages}}",
|
||||
"models_try_dialog_cancel_button": "Cancel",
|
||||
"models_try_dialog_chat_confirmed_body": "Chat support is confirmed. Testing sends one short prompt and may cost money.",
|
||||
"models_try_dialog_chat_plan_body": "Chat support is confirmed. Testing sends one short prompt, which counts toward your plan’s limits.",
|
||||
"models_try_dialog_chat_unconfirmed_body": "This endpoint does not publish capabilities, so chat support is unconfirmed. Testing sends one short prompt and may cost money.",
|
||||
"models_try_dialog_image_confirmed_body": "Image support is confirmed. Testing runs real inference and may cost money.",
|
||||
"models_try_dialog_image_unconfirmed_body": "This endpoint does not publish capabilities, so image support is unconfirmed. It must implement /images/generations or /images. Testing runs real inference and may cost money.",
|
||||
|
|
|
|||
|
|
@ -707,6 +707,7 @@
|
|||
"models_speech_model_languages_button": "{count, plural, one {# idioma} many {# idiomas} other {# idiomas}}",
|
||||
"models_try_dialog_cancel_button": "Cancelar",
|
||||
"models_try_dialog_chat_confirmed_body": "La compatibilidad con chat está confirmada. La prueba envía un prompt corto y puede costar dinero.",
|
||||
"models_try_dialog_chat_plan_body": "La compatibilidad con chat está confirmada. La prueba envía un prompt corto, que cuenta para los límites de tu plan.",
|
||||
"models_try_dialog_chat_unconfirmed_body": "Este endpoint no publica sus capacidades, así que la compatibilidad con chat no está confirmada. La prueba envía un prompt corto y puede costar dinero.",
|
||||
"models_try_dialog_image_confirmed_body": "La compatibilidad con imagen está confirmada. La prueba ejecuta una inferencia real y puede costar dinero.",
|
||||
"models_try_dialog_image_unconfirmed_body": "Este endpoint no publica sus capacidades, así que la compatibilidad con imagen no está confirmada. Debe implementar /images/generations o /images. La prueba ejecuta una inferencia real y puede costar dinero.",
|
||||
|
|
|
|||
|
|
@ -707,6 +707,7 @@
|
|||
"models_speech_model_languages_button": "{count, plural, one {# langue} many {# langues} other {# langues}}",
|
||||
"models_try_dialog_cancel_button": "Annuler",
|
||||
"models_try_dialog_chat_confirmed_body": "La prise en charge du chat est confirmée. Le test envoie un prompt court et peut être facturé.",
|
||||
"models_try_dialog_chat_plan_body": "La prise en charge du chat est confirmée. Le test envoie un prompt court, qui compte dans les limites de votre forfait.",
|
||||
"models_try_dialog_chat_unconfirmed_body": "Cet endpoint ne publie pas ses capacités, la prise en charge du chat n’est donc pas confirmée. Le test envoie un prompt court et peut être facturé.",
|
||||
"models_try_dialog_image_confirmed_body": "La prise en charge de l’image est confirmée. Le test lance une véritable inférence et peut être facturé.",
|
||||
"models_try_dialog_image_unconfirmed_body": "Cet endpoint ne publie pas ses capacités, la prise en charge de l’image n’est donc pas confirmée. Il doit implémenter /images/generations ou /images. Le test lance une véritable inférence et peut être facturé.",
|
||||
|
|
|
|||
|
|
@ -707,6 +707,7 @@
|
|||
"models_speech_model_languages_button": "{count, plural, one {# भाषा} other {# भाषाएं}}",
|
||||
"models_try_dialog_cancel_button": "रद्द करें",
|
||||
"models_try_dialog_chat_confirmed_body": "चैट सपोर्ट की पुष्टि हो चुकी है। जांच में एक छोटा प्रॉम्प्ट भेजा जाता है, जिसका शुल्क लग सकता है।",
|
||||
"models_try_dialog_chat_plan_body": "चैट सपोर्ट की पुष्टि हो चुकी है। जांच में एक छोटा प्रॉम्प्ट भेजा जाता है, जो आपके प्लान की सीमा में गिना जाता है।",
|
||||
"models_try_dialog_chat_unconfirmed_body": "यह एंडपॉइंट अपनी क्षमताएं प्रकाशित नहीं करता, इसलिए चैट सपोर्ट की पुष्टि नहीं हुई है। जांच में एक छोटा प्रॉम्प्ट भेजा जाता है, जिसका शुल्क लग सकता है।",
|
||||
"models_try_dialog_image_confirmed_body": "इमेज सपोर्ट की पुष्टि हो चुकी है। जांच में असली इन्फ़रेंस चलता है, जिसका शुल्क लग सकता है।",
|
||||
"models_try_dialog_image_unconfirmed_body": "यह एंडपॉइंट अपनी क्षमताएं प्रकाशित नहीं करता, इसलिए इमेज सपोर्ट की पुष्टि नहीं हुई है। इसे /images/generations या /images लागू करना ज़रूरी है। जांच में असली इन्फ़रेंस चलता है, जिसका शुल्क लग सकता है।",
|
||||
|
|
|
|||
|
|
@ -707,6 +707,7 @@
|
|||
"models_speech_model_languages_button": "{count, plural, other {#言語}}",
|
||||
"models_try_dialog_cancel_button": "キャンセル",
|
||||
"models_try_dialog_chat_confirmed_body": "チャットへの対応は確認済みです。テストでは短いプロンプトを1回送信するため、料金が発生する場合があります。",
|
||||
"models_try_dialog_chat_plan_body": "チャットへの対応は確認済みです。テストでは短いプロンプトを1回送信し、プランの使用上限に含まれます。",
|
||||
"models_try_dialog_chat_unconfirmed_body": "このエンドポイントは機能を公開していないため、チャットへの対応は未確認です。テストでは短いプロンプトを1回送信するため、料金が発生する場合があります。",
|
||||
"models_try_dialog_image_confirmed_body": "画像への対応は確認済みです。テストでは実際に推論を行うため、料金が発生する場合があります。",
|
||||
"models_try_dialog_image_unconfirmed_body": "このエンドポイントは機能を公開していないため、画像への対応は未確認です。/images/generationsまたは/imagesを実装している必要があります。テストでは実際に推論を行うため、料金が発生する場合があります。",
|
||||
|
|
|
|||
|
|
@ -707,6 +707,7 @@
|
|||
"models_speech_model_languages_button": "{count, plural, other {#개 언어}}",
|
||||
"models_try_dialog_cancel_button": "취소",
|
||||
"models_try_dialog_chat_confirmed_body": "채팅 지원이 확인되었습니다. 테스트는 짧은 프롬프트를 한 번 전송하며 비용이 발생할 수 있습니다.",
|
||||
"models_try_dialog_chat_plan_body": "채팅 지원이 확인되었습니다. 테스트는 짧은 프롬프트를 한 번 보내며, 플랜 한도에 포함됩니다.",
|
||||
"models_try_dialog_chat_unconfirmed_body": "이 엔드포인트는 기능을 공개하지 않으므로 채팅 지원 여부가 확인되지 않았습니다. 테스트는 짧은 프롬프트를 한 번 전송하며 비용이 발생할 수 있습니다.",
|
||||
"models_try_dialog_image_confirmed_body": "이미지 지원이 확인되었습니다. 테스트는 실제 추론을 실행하며 비용이 발생할 수 있습니다.",
|
||||
"models_try_dialog_image_unconfirmed_body": "이 엔드포인트는 기능을 공개하지 않으므로 이미지 지원 여부가 확인되지 않았습니다. /images/generations 또는 /images를 구현해야 합니다. 테스트는 실제 추론을 실행하며 비용이 발생할 수 있습니다.",
|
||||
|
|
|
|||
|
|
@ -707,6 +707,7 @@
|
|||
"models_speech_model_languages_button": "{count, plural, one {# idioma} many {# idiomas} other {# idiomas}}",
|
||||
"models_try_dialog_cancel_button": "Cancelar",
|
||||
"models_try_dialog_chat_confirmed_body": "O suporte a chat está confirmado. O teste envia um prompt curto e pode gerar custos.",
|
||||
"models_try_dialog_chat_plan_body": "O suporte a chat está confirmado. O teste envia um prompt curto, que conta para os limites do seu plano.",
|
||||
"models_try_dialog_chat_unconfirmed_body": "Este endpoint não publica suas capacidades, então o suporte a chat não está confirmado. O teste envia um prompt curto e pode gerar custos.",
|
||||
"models_try_dialog_image_confirmed_body": "O suporte a imagem está confirmado. O teste executa uma inferência real e pode gerar custos.",
|
||||
"models_try_dialog_image_unconfirmed_body": "Este endpoint não publica suas capacidades, então o suporte a imagem não está confirmado. Ele precisa implementar /images/generations ou /images. O teste executa uma inferência real e pode gerar custos.",
|
||||
|
|
|
|||
|
|
@ -707,6 +707,7 @@
|
|||
"models_speech_model_languages_button": "{count, plural, one {# язык} few {# языка} many {# языков} other {# языка}}",
|
||||
"models_try_dialog_cancel_button": "Отмена",
|
||||
"models_try_dialog_chat_confirmed_body": "Поддержка чата подтверждена. При тесте отправляется один короткий промпт, это может стоить денег.",
|
||||
"models_try_dialog_chat_plan_body": "Поддержка чата подтверждена. Тест отправляет один короткий промпт, который учитывается в лимитах вашего плана.",
|
||||
"models_try_dialog_chat_unconfirmed_body": "Эта конечная точка не сообщает о своих возможностях, поэтому поддержка чата не подтверждена. При тесте отправляется один короткий промпт, это может стоить денег.",
|
||||
"models_try_dialog_image_confirmed_body": "Поддержка изображений подтверждена. Тест выполняет настоящий инференс, это может стоить денег.",
|
||||
"models_try_dialog_image_unconfirmed_body": "Эта конечная точка не сообщает о своих возможностях, поэтому поддержка изображений не подтверждена. Она должна реализовывать /images/generations или /images. Тест выполняет настоящий инференс, это может стоить денег.",
|
||||
|
|
|
|||
|
|
@ -707,6 +707,7 @@
|
|||
"models_speech_model_languages_button": "{count, plural, other {# 种语言}}",
|
||||
"models_try_dialog_cancel_button": "取消",
|
||||
"models_try_dialog_chat_confirmed_body": "已确认支持对话。测试会发送一条简短的提示词,可能产生费用。",
|
||||
"models_try_dialog_chat_plan_body": "已确认支持对话。测试会发送一条简短提示词,计入你的套餐额度。",
|
||||
"models_try_dialog_chat_unconfirmed_body": "这个端点没有公布能力,因此是否支持对话尚未确认。测试会发送一条简短的提示词,可能产生费用。",
|
||||
"models_try_dialog_image_confirmed_body": "已确认支持图像。测试会真实执行推理,可能产生费用。",
|
||||
"models_try_dialog_image_unconfirmed_body": "这个端点没有公布能力,因此是否支持图像尚未确认。它必须实现 /images/generations 或 /images。测试会真实执行推理,可能产生费用。",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue