mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 02:43:49 +08:00
Merge remote-tracking branch 'origin/deploy/konwledge' into deploy/konwledge
This commit is contained in:
commit
addd754eb3
@ -132,8 +132,8 @@ describe("createApiVisualEmbeddingOptions", () => {
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
dense: [[0.2, 0.8]],
|
||||
metadata: { model: "clip-multimodal@1", provider: "dify-model-runtime" },
|
||||
model: "clip-multimodal@1",
|
||||
metadata: { model: "clip-multimodal", provider: "dify-model-runtime" },
|
||||
model: "clip-multimodal",
|
||||
});
|
||||
|
||||
const queryPayload = (await requests[1]?.json()) as Record<string, unknown>;
|
||||
|
||||
@ -29,7 +29,7 @@ const BASE = {
|
||||
} as const;
|
||||
|
||||
describe("Dify model runtime embedding provider", () => {
|
||||
it("embeds through Dify and maps the ModelInstance response", async () => {
|
||||
it("embeds through Dify and preserves the selected route across upstream model aliases", async () => {
|
||||
const calls: DifyTextEmbeddingInput[] = [];
|
||||
const provider = createDifyModelRuntimeEmbeddingProvider({
|
||||
...BASE,
|
||||
@ -61,11 +61,11 @@ describe("Dify model runtime embedding provider", () => {
|
||||
],
|
||||
metadata: {
|
||||
dimension: 2,
|
||||
model: "resolved-model",
|
||||
model: "text-embedding-3-large",
|
||||
provider: "dify-model-runtime",
|
||||
usage: { totalTokens: 7 },
|
||||
},
|
||||
model: "resolved-model",
|
||||
model: "text-embedding-3-large",
|
||||
});
|
||||
expect(calls[0]).toMatchObject({
|
||||
inputType: "document",
|
||||
@ -109,6 +109,17 @@ describe("Dify model runtime embedding provider", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects a model outside the provider's bound Dify route", async () => {
|
||||
const provider = createDifyModelRuntimeEmbeddingProvider({
|
||||
...BASE,
|
||||
client: fakeClient(async () => ({ embeddings: [[1, 1]] })),
|
||||
});
|
||||
|
||||
await expect(
|
||||
provider.embed({ model: "other-embedding", tenantId: "t", texts: ["q"] }),
|
||||
).rejects.toThrow("is bound to model text-embedding-3-large");
|
||||
});
|
||||
|
||||
it("rejects invalid or mismatched embedding responses", async () => {
|
||||
const invalid = createDifyModelRuntimeEmbeddingProvider({
|
||||
...BASE,
|
||||
|
||||
@ -87,6 +87,22 @@ describe("Dify model runtime reranker provider", () => {
|
||||
).rejects.toBeInstanceOf(ProviderInputError);
|
||||
});
|
||||
|
||||
it("rejects a model outside the provider's bound Dify route", async () => {
|
||||
const provider = createDifyModelRuntimeRerankerProvider({
|
||||
...BASE,
|
||||
client: fakeClient(async () => ({ docs: [] })),
|
||||
});
|
||||
|
||||
await expect(
|
||||
provider.rerank({
|
||||
documents: DOCS,
|
||||
model: "other-reranker",
|
||||
query: "q",
|
||||
tenantId: "t",
|
||||
}),
|
||||
).rejects.toThrow("is bound to model rerank-english-v3.0");
|
||||
});
|
||||
|
||||
it("fails closed for out-of-range indices and invalid responses", async () => {
|
||||
const outOfRange = createDifyModelRuntimeRerankerProvider({
|
||||
...BASE,
|
||||
@ -121,10 +137,6 @@ describe("Dify model runtime reranker provider", () => {
|
||||
},
|
||||
label: "duplicate indices",
|
||||
},
|
||||
{
|
||||
data: { docs: [{ index: 0, score: 0.9 }], model: "different-model" },
|
||||
label: "a mismatched model identity",
|
||||
},
|
||||
])("rejects $label", async ({ data }) => {
|
||||
const provider = createDifyModelRuntimeRerankerProvider({
|
||||
...BASE,
|
||||
@ -136,6 +148,23 @@ describe("Dify model runtime reranker provider", () => {
|
||||
).rejects.toBeInstanceOf(ProviderResponseError);
|
||||
});
|
||||
|
||||
it("preserves the selected route when Dify reports an upstream model alias", async () => {
|
||||
const provider = createDifyModelRuntimeRerankerProvider({
|
||||
...BASE,
|
||||
client: fakeClient(async () => ({
|
||||
docs: [{ index: 0, score: 0.9 }],
|
||||
model: "rerank-english-v3.0-runtime-deployment",
|
||||
})),
|
||||
});
|
||||
|
||||
await expect(
|
||||
provider.rerank({ documents: DOCS, model: BASE.model, query: "q", tenantId: "t" }),
|
||||
).resolves.toMatchObject({
|
||||
metadata: { model: BASE.model },
|
||||
model: BASE.model,
|
||||
});
|
||||
});
|
||||
|
||||
it("synthesizes a model descriptor and validates constructor options", async () => {
|
||||
const provider = createDifyModelRuntimeRerankerProvider({
|
||||
...BASE,
|
||||
|
||||
@ -464,6 +464,7 @@ export function createDifyModelRuntimeEmbeddingProvider(
|
||||
kind: "dify-model-runtime",
|
||||
async embed(input) {
|
||||
validateEmbedInput(input, { maxBatchSize, maxTextBytes });
|
||||
assertDifyRouteModel(input.model, options.model, "embedding");
|
||||
|
||||
const tenantId = input.tenantId?.trim();
|
||||
|
||||
@ -495,10 +496,8 @@ export function createDifyModelRuntimeEmbeddingProvider(
|
||||
);
|
||||
}
|
||||
|
||||
const model = parsed.data.model ?? input.model;
|
||||
const dimension = validateEmbeddingResponseVectors(parsed.data.embeddings);
|
||||
const configuredDimension =
|
||||
observedDimensions.get(input.model) ?? observedDimensions.get(model);
|
||||
const configuredDimension = observedDimensions.get(input.model);
|
||||
|
||||
if (configuredDimension !== undefined && configuredDimension !== dimension) {
|
||||
throw new ProviderResponseError(
|
||||
@ -507,18 +506,20 @@ export function createDifyModelRuntimeEmbeddingProvider(
|
||||
}
|
||||
|
||||
observedDimensions.set(input.model, dimension);
|
||||
observedDimensions.set(model, dimension);
|
||||
const totalTokens = parsed.data.usage?.total_tokens ?? parsed.data.usage?.tokens;
|
||||
|
||||
return {
|
||||
dense: parsed.data.embeddings.map((vector) => [...vector]),
|
||||
metadata: {
|
||||
dimension,
|
||||
model,
|
||||
// Dify has already resolved and invoked the exact catalog route selected by the
|
||||
// knowledge-space profile. Upstream providers may report a deployment name or
|
||||
// versioned alias, so expose the stable Dify route identity to downstream checks.
|
||||
model: input.model,
|
||||
provider: "dify-model-runtime",
|
||||
...(totalTokens === undefined ? {} : { usage: { totalTokens } }),
|
||||
},
|
||||
model,
|
||||
model: input.model,
|
||||
};
|
||||
},
|
||||
async models() {
|
||||
@ -624,6 +625,7 @@ export function createDifyModelRuntimeRerankerProvider(
|
||||
kind: "dify-model-runtime",
|
||||
async rerank(input) {
|
||||
validateRerankInput(input, { maxDocuments, maxTextBytes });
|
||||
assertDifyRouteModel(input.model, options.model, "rerank");
|
||||
|
||||
const tenantId = input.tenantId?.trim();
|
||||
|
||||
@ -680,17 +682,12 @@ export function createDifyModelRuntimeRerankerProvider(
|
||||
};
|
||||
});
|
||||
|
||||
const model = (parsed.data.model ?? input.model).trim();
|
||||
if (!model || model !== input.model) {
|
||||
throw new ProviderResponseError(
|
||||
`Dify rerank model mismatch: requested=${input.model}, returned=${parsed.data.model ?? ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
items,
|
||||
metadata: { model, provider: "dify-model-runtime" },
|
||||
model,
|
||||
// The response model may be an upstream deployment name or versioned alias.
|
||||
// Catalog resolution plus the tenant-bound Dify invocation establish the route identity.
|
||||
metadata: { model: input.model, provider: "dify-model-runtime" },
|
||||
model: input.model,
|
||||
};
|
||||
},
|
||||
async models() {
|
||||
@ -711,6 +708,18 @@ function defaultDifyModelRuntimeRerankerModel(
|
||||
};
|
||||
}
|
||||
|
||||
function assertDifyRouteModel(
|
||||
requestedModel: string,
|
||||
configuredModel: string,
|
||||
capability: "embedding" | "rerank",
|
||||
): void {
|
||||
if (requestedModel.trim() !== configuredModel.trim()) {
|
||||
throw new ProviderInputError(
|
||||
`Dify model runtime ${capability} provider is bound to model ${configuredModel.trim()}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function validateEmbedInput(
|
||||
input: EmbedTextsInput,
|
||||
limits: { readonly maxBatchSize: number; readonly maxTextBytes: number },
|
||||
|
||||
@ -36,15 +36,18 @@ function deltaChunk(content: string): unknown {
|
||||
}
|
||||
|
||||
describe("Dify model runtime LLM provider", () => {
|
||||
it("streams delta events then a done event with usage, and maps the request", async () => {
|
||||
it("streams deltas and preserves the selected route across upstream model aliases", async () => {
|
||||
let captured: DifyLlmInput | undefined;
|
||||
const provider = createDifyModelRuntimeLlmProvider({
|
||||
...BASE,
|
||||
client: fakeClient(
|
||||
() => [
|
||||
{ delta: { message: { content: "Hel" } }, model: "gpt-4.1-mini-2025" },
|
||||
{ delta: { message: { content: "Hel" } }, model: BASE.model },
|
||||
deltaChunk("lo"),
|
||||
{ delta: { finish_reason: "stop", usage: { completion_tokens: 2, prompt_tokens: 5 } } },
|
||||
{
|
||||
delta: { finish_reason: "stop", usage: { completion_tokens: 2, prompt_tokens: 5 } },
|
||||
model: "gpt-4.1-mini-runtime-deployment",
|
||||
},
|
||||
],
|
||||
(input) => {
|
||||
captured = input;
|
||||
@ -73,7 +76,7 @@ describe("Dify model runtime LLM provider", () => {
|
||||
{
|
||||
finishReason: "stop",
|
||||
metadata: {
|
||||
model: "gpt-4.1-mini-2025",
|
||||
model: BASE.model,
|
||||
provider: "dify-model-runtime",
|
||||
usage: { completionTokens: 2, promptTokens: 5 },
|
||||
},
|
||||
@ -186,6 +189,13 @@ describe("Dify model runtime LLM provider", () => {
|
||||
await expect(
|
||||
provider.generate({ messages: [{ content: "hi", role: "user" }], model: "gpt-4.1-mini" }),
|
||||
).rejects.toThrow("requires a tenantId");
|
||||
await expect(
|
||||
provider.generate({
|
||||
messages: [{ content: "hi", role: "user" }],
|
||||
model: "other-llm",
|
||||
tenantId: "t",
|
||||
}),
|
||||
).rejects.toThrow("is bound to model gpt-4.1-mini");
|
||||
});
|
||||
|
||||
it("threads model parameters, skips unparseable chunks, and maps partial usage", async () => {
|
||||
|
||||
@ -778,6 +778,11 @@ export function createDifyModelRuntimeLlmProvider(
|
||||
if (!input.model.trim()) {
|
||||
throw new ProviderInputError("Dify model runtime LLM model is required");
|
||||
}
|
||||
if (input.model.trim() !== options.model.trim()) {
|
||||
throw new ProviderInputError(
|
||||
`Dify model runtime LLM provider is bound to model ${options.model.trim()}`,
|
||||
);
|
||||
}
|
||||
|
||||
const tenantId = input.tenantId?.trim();
|
||||
|
||||
@ -786,7 +791,6 @@ export function createDifyModelRuntimeLlmProvider(
|
||||
}
|
||||
|
||||
const maxTokens = input.maxOutputTokens ?? options.maxOutputTokens;
|
||||
let model = input.model;
|
||||
let finishReason = "stop";
|
||||
let usage: LlmUsage | undefined;
|
||||
|
||||
@ -811,10 +815,6 @@ export function createDifyModelRuntimeLlmProvider(
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parsed.data.model) {
|
||||
model = parsed.data.model;
|
||||
}
|
||||
|
||||
const content = parsed.data.delta?.message?.content;
|
||||
|
||||
if (content) {
|
||||
@ -844,7 +844,10 @@ export function createDifyModelRuntimeLlmProvider(
|
||||
|
||||
yield {
|
||||
finishReason,
|
||||
metadata: { model, provider: "dify-model-runtime", ...(usage ? { usage } : {}) },
|
||||
// Dify invokes the exact tenant-scoped catalog route requested above. Individual
|
||||
// stream chunks can expose different upstream aliases (for example a terminal chunk
|
||||
// may report a routed deployment name), so keep the logical Dify route deterministic.
|
||||
metadata: { model: input.model, provider: "dify-model-runtime", ...(usage ? { usage } : {}) },
|
||||
type: "done",
|
||||
};
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user