@cyrilmarin/dsh-lemonade 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.fr.md +186 -0
- package/README.md +189 -0
- package/lib/adapter.js +287 -0
- package/lib/client.js +561 -0
- package/lib/index.js +263 -0
- package/lib/serialize.js +113 -0
- package/lib/server-api.js +356 -0
- package/lib/translate.js +163 -0
- package/lib/types/adapter.d.ts +94 -0
- package/lib/types/index.d.ts +91 -0
- package/lib/types/serialize.d.ts +68 -0
- package/lib/types/server-api.d.ts +77 -0
- package/lib/types/translate.d.ts +10 -0
- package/package.json +94 -0
- package/src/adapter.ts +381 -0
- package/src/client/index.js +561 -0
- package/src/index.ts +323 -0
- package/src/serialize.ts +171 -0
- package/src/server-api.ts +402 -0
- package/src/translate.ts +190 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ✨ Lemonade Server LLM provider plugin for the DeepSeek Harness.
|
|
3
|
+
*
|
|
4
|
+
* Registers a `LemonadeAdapter` for the `lemonade` provider route on
|
|
5
|
+
* `ctx.llm`, speaking the OpenAI-compatible Chat Completions API at a local
|
|
6
|
+
* (or remote) Lemonade Server. Connection facts resolve per request instead of
|
|
7
|
+
* at load: the plugin layers its `cordis.yml` entry config under the optional
|
|
8
|
+
* `llm-lemonade` user-settings section (`ctx.settings`) and resolves the
|
|
9
|
+
* optional API key through the credential seam (`ctx.credentials`) or the
|
|
10
|
+
* launch environment, so a changed base URL, catalog, or key reaches the next
|
|
11
|
+
* request without a restart, while an in-flight stream keeps the facts it
|
|
12
|
+
* started with. The one registration-captured fact — the retry policy —
|
|
13
|
+
* re-registers the route in place when it changes.
|
|
14
|
+
*
|
|
15
|
+
* @module dsh-lemonade-provider
|
|
16
|
+
*/
|
|
17
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
18
|
+
import type { RetryPolicyConfig } from '@deepseek-ai/dsh-llm';
|
|
19
|
+
import type { LaunchEnvironmentSnapshot } from '@deepseek-ai/dsh-launch-environment';
|
|
20
|
+
import z from '@deepseek-ai/schemastery';
|
|
21
|
+
import { LemonadeAdapter } from './adapter.js';
|
|
22
|
+
import type { LemonadeCatalogModel, LemonadeOptions } from './adapter.js';
|
|
23
|
+
/** Short plugin name used in logs and configuration surfaces. */
|
|
24
|
+
export declare const name = "llm-lemonade";
|
|
25
|
+
/** The provider route this plugin registers. */
|
|
26
|
+
export declare const PROVIDER = "lemonade";
|
|
27
|
+
/** Hard service dependency: the LLM registry seat. */
|
|
28
|
+
export declare const inject: string[];
|
|
29
|
+
/** Settings namespace this plugin owns. */
|
|
30
|
+
export declare const NS: import("@deepseek-ai/dsh-settings").SettingsNamespace;
|
|
31
|
+
/** Environment variable naming this server's endpoint, honored from trusted layers only. */
|
|
32
|
+
export declare const BASE_URL_ENV = "LEMONADE_BASE_URL";
|
|
33
|
+
/** Default environment variable naming the optional API key. */
|
|
34
|
+
export declare const DEFAULT_API_KEY_ENV = "LEMONADE_API_KEY";
|
|
35
|
+
/** Default environment variable naming the optional admin API key (internal endpoints). */
|
|
36
|
+
export declare const DEFAULT_ADMIN_API_KEY_ENV = "LEMONADE_ADMIN_API_KEY";
|
|
37
|
+
/** The static advisory model catalog schema (user-pinned entries). */
|
|
38
|
+
export declare const catalogModel: z<Schemastery.ObjectS<{
|
|
39
|
+
id: z<string, string>;
|
|
40
|
+
name: z<string, string>;
|
|
41
|
+
description: z<string, string>;
|
|
42
|
+
contextWindow: z<number, number>;
|
|
43
|
+
maxTokens: z<number, number>;
|
|
44
|
+
vision: z<boolean, boolean>;
|
|
45
|
+
}>, Schemastery.ObjectT<{
|
|
46
|
+
id: z<string, string>;
|
|
47
|
+
name: z<string, string>;
|
|
48
|
+
description: z<string, string>;
|
|
49
|
+
contextWindow: z<number, number>;
|
|
50
|
+
maxTokens: z<number, number>;
|
|
51
|
+
vision: z<boolean, boolean>;
|
|
52
|
+
}>>;
|
|
53
|
+
/**
|
|
54
|
+
* Resolved plugin configuration schema. All fields except `baseURL` have
|
|
55
|
+
* defaults; `baseURL` itself can come from `LEMONADE_BASE_URL` when unset.
|
|
56
|
+
*/
|
|
57
|
+
export declare const Config: z<LemonadeResolvedConfig>;
|
|
58
|
+
/** Resolved plugin configuration (what the schema produces). */
|
|
59
|
+
export interface LemonadeResolvedConfig {
|
|
60
|
+
baseURL: string;
|
|
61
|
+
apiKeyEnv: string;
|
|
62
|
+
adminApiKeyEnv: string;
|
|
63
|
+
requireAuth: boolean;
|
|
64
|
+
defaultContextWindow: number;
|
|
65
|
+
maxTokens: number;
|
|
66
|
+
models: LemonadeCatalogModel[];
|
|
67
|
+
streamIdleTimeoutMs: number;
|
|
68
|
+
retryPolicy?: RetryPolicyConfig;
|
|
69
|
+
}
|
|
70
|
+
/** Raw composition entry: every field optional (schema defaults apply on resolution). */
|
|
71
|
+
export type LemonadeRawConfig = Partial<LemonadeResolvedConfig>;
|
|
72
|
+
/** Validate and detach the advisory model catalog. */
|
|
73
|
+
export declare function resolveModels(models: readonly LemonadeCatalogModel[] | undefined): LemonadeCatalogModel[];
|
|
74
|
+
/**
|
|
75
|
+
* The explicit step from raw config to validated connection facts.
|
|
76
|
+
* Programmatic construction may bypass Schemastery normalization, so every
|
|
77
|
+
* default and bound is re-judged here — for the composition entry at load
|
|
78
|
+
* (fail loud) and for each settings snapshot at its first use.
|
|
79
|
+
*
|
|
80
|
+
* @param config - raw plugin config or resolved settings snapshot.
|
|
81
|
+
* @param environment - this run's environment layers, when the product CLI provided them.
|
|
82
|
+
* @returns validated connection facts plus the credential reference.
|
|
83
|
+
*/
|
|
84
|
+
export declare function resolveAdapterOptions(config: LemonadeRawConfig, environment?: LaunchEnvironmentSnapshot): LemonadeOptions;
|
|
85
|
+
/**
|
|
86
|
+
* Register a {@link LemonadeAdapter} for the `lemonade` provider route.
|
|
87
|
+
* See the module header for the layering story.
|
|
88
|
+
*/
|
|
89
|
+
export declare function apply(ctx: Context, config: LemonadeRawConfig): void;
|
|
90
|
+
export { LemonadeAdapter };
|
|
91
|
+
export type { LemonadeOptions } from './adapter.js';
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serialize harness messages into Lemonade's OpenAI-compatible chat
|
|
3
|
+
* completions wire format.
|
|
4
|
+
*
|
|
5
|
+
* User text is joined; user image blocks become OpenAI `image_url` data-URL
|
|
6
|
+
* parts (Lemonade serves vision models through the same endpoint); assistant
|
|
7
|
+
* text becomes `content`, tool calls become `tool_calls`, and tool results
|
|
8
|
+
* become standalone `{role: 'tool'}` messages. Reasoning blocks are not
|
|
9
|
+
* replayed on the wire: Lemonade's OpenAI route has no reasoning passback
|
|
10
|
+
* field (unlike DeepSeek's `reasoning_content`), and re-sending thinking
|
|
11
|
+
* text as plain content would corrupt the conversation.
|
|
12
|
+
*
|
|
13
|
+
* @module dsh-lemonade-provider/serialize
|
|
14
|
+
*/
|
|
15
|
+
import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment';
|
|
16
|
+
import type { GenerateOptions } from '@deepseek-ai/dsh-llm';
|
|
17
|
+
/** One OpenAI function tool call as sent on the wire. */
|
|
18
|
+
export interface WireToolCall {
|
|
19
|
+
id: string;
|
|
20
|
+
type: 'function';
|
|
21
|
+
function: {
|
|
22
|
+
name: string;
|
|
23
|
+
arguments: string;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** OpenAI content parts for user messages that carry images. */
|
|
27
|
+
export type WireContentPart = {
|
|
28
|
+
type: 'text';
|
|
29
|
+
text: string;
|
|
30
|
+
} | {
|
|
31
|
+
type: 'image_url';
|
|
32
|
+
image_url: {
|
|
33
|
+
url: string;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
/** One OpenAI chat-completions wire message. */
|
|
37
|
+
export type WireMessage = {
|
|
38
|
+
role: 'system';
|
|
39
|
+
content: string;
|
|
40
|
+
} | {
|
|
41
|
+
role: 'user';
|
|
42
|
+
content: string | WireContentPart[];
|
|
43
|
+
} | {
|
|
44
|
+
role: 'assistant';
|
|
45
|
+
content: string | null;
|
|
46
|
+
tool_calls?: WireToolCall[];
|
|
47
|
+
} | {
|
|
48
|
+
role: 'tool';
|
|
49
|
+
tool_call_id: string;
|
|
50
|
+
content: string;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Resolve one durable image reference into the data: URL sent as an
|
|
54
|
+
* `image_url` part. Provided by the adapter from the attachment service.
|
|
55
|
+
*/
|
|
56
|
+
export type ResolveImage = (attachment: ImageAttachmentRef, signal?: AbortSignal) => Promise<string>;
|
|
57
|
+
/**
|
|
58
|
+
* Build the full wire request body. Always streaming with usage reporting on
|
|
59
|
+
* (Lemonade's llamacpp backends honor `stream_options.include_usage`; servers
|
|
60
|
+
* that ignore it simply never send a usage chunk, and the translate step
|
|
61
|
+
* tolerates that). Optional sampling fields are omitted rather than sent as
|
|
62
|
+
* null so provider defaults apply.
|
|
63
|
+
*
|
|
64
|
+
* @param options - the harness request (model, history, system, tools, sampling).
|
|
65
|
+
* @param resolveImage - resolves image blocks; `undefined` when no attachment
|
|
66
|
+
* service is available, in which case image content is refused up front.
|
|
67
|
+
*/
|
|
68
|
+
export declare function serializeRequest(options: GenerateOptions, resolveImage: ResolveImage | undefined): Promise<Record<string, unknown>>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host proxy for the Lemonade-specific API (https://lemonade-server.ai/docs/api/lemonade/),
|
|
3
|
+
* mounted by src/index.ts on the dsh web server under the /dsh-lemonade/api
|
|
4
|
+
* prefix route (ctx.webServer.register). The browser client half calls these
|
|
5
|
+
* routes same-origin; the host resolves the base URL from the llm-lemonade
|
|
6
|
+
* settings section and the optional API key through the credentials seam, so
|
|
7
|
+
* the key never reaches the browser.
|
|
8
|
+
*
|
|
9
|
+
* @module dsh-lemonade-provider/server-api
|
|
10
|
+
*/
|
|
11
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
12
|
+
import type { CredentialRef } from '@deepseek-ai/dsh-credentials';
|
|
13
|
+
/** Route prefix registered on ctx.webServer. */
|
|
14
|
+
export declare const API_ROUTE = "/dsh-lemonade/api";
|
|
15
|
+
/** Maximum accepted request body and proxied response body in bytes. */
|
|
16
|
+
export declare const MAX_BODY_BYTES = 1000000;
|
|
17
|
+
/** Fetch timeout for proxied Lemonade calls. */
|
|
18
|
+
export declare const API_TIMEOUT_MS = 10000;
|
|
19
|
+
/** Host-side connection facts the proxy resolves per request. */
|
|
20
|
+
export interface LemonadeApiConfig {
|
|
21
|
+
baseURL(): string;
|
|
22
|
+
requireAuth(): boolean;
|
|
23
|
+
/** Credential reference for the regular API key (LEMONADE_API_KEY). */
|
|
24
|
+
apiKeyRef(): CredentialRef;
|
|
25
|
+
/** Credential reference for the admin API key (LEMONADE_ADMIN_API_KEY). */
|
|
26
|
+
adminApiKeyRef(): CredentialRef;
|
|
27
|
+
/**
|
|
28
|
+
* Resolve one credential reference to its current value (never throws).
|
|
29
|
+
* @param ref - the reference to resolve through the credentials seam / env.
|
|
30
|
+
* @returns the usable key, or undefined when unconfigured.
|
|
31
|
+
*/
|
|
32
|
+
resolveKey(ref: CredentialRef): Promise<string | undefined>;
|
|
33
|
+
}
|
|
34
|
+
/** Successful wire result. */
|
|
35
|
+
export interface LemonadeWireOk {
|
|
36
|
+
ok: true;
|
|
37
|
+
value: unknown;
|
|
38
|
+
}
|
|
39
|
+
/** Failed wire result. */
|
|
40
|
+
export interface LemonadeWireError {
|
|
41
|
+
ok: false;
|
|
42
|
+
error: {
|
|
43
|
+
message: string;
|
|
44
|
+
code: string;
|
|
45
|
+
status?: number;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export type LemonadeWireResult = LemonadeWireOk | LemonadeWireError;
|
|
49
|
+
/** Map a Lemonade HTTP status to a stable harness-style code. */
|
|
50
|
+
export declare function mapLemonadeStatus(status: number): string;
|
|
51
|
+
/**
|
|
52
|
+
* Dispatch one proxied Lemonade API call.
|
|
53
|
+
* @param cfg - connection facts (thunks resolved per call).
|
|
54
|
+
* @param method - HTTP method from the client (GET/POST/DELETE).
|
|
55
|
+
* @param op - first path segment after the route prefix.
|
|
56
|
+
* @param segments - remaining path segments after the route prefix.
|
|
57
|
+
* @param query - parsed query string.
|
|
58
|
+
* @param body - parsed request body (undefined when none).
|
|
59
|
+
* @param signal - optional caller cancellation.
|
|
60
|
+
*/
|
|
61
|
+
export declare function serveLemonadeApi(cfg: LemonadeApiConfig, method: string, op: string, args: readonly string[], query: URLSearchParams, body: unknown, signal?: AbortSignal): Promise<LemonadeWireResult>;
|
|
62
|
+
/**
|
|
63
|
+
* Stream the Lemonade server logs (WS /logs/stream) as newline-delimited JSON
|
|
64
|
+
* to the browser. The spec: the log WebSocket shares the Realtime Audio port,
|
|
65
|
+
* discovered via /v1/health (websocket_port) — not the main HTTP port — then
|
|
66
|
+
* `ws://<host>:<port>/logs/stream`, subscribe with `{ type: 'logs.subscribe',
|
|
67
|
+
* after_seq: <int|null> }`, and the server answers `logs.snapshot` (up to
|
|
68
|
+
* 5000 retained entries) then `logs.entry` lines. Messages are relayed as-is
|
|
69
|
+
* (`{ type: 'logs.snapshot' | 'logs.entry' | 'error', ... }`); the response is
|
|
70
|
+
* held open and closed when the browser disconnects.
|
|
71
|
+
*/
|
|
72
|
+
/**
|
|
73
|
+
* Build the node:http handler mounting the Lemonade-specific API proxy at the
|
|
74
|
+
* /dsh-lemonade/api prefix route (ctx.webServer.register). Never throws out:
|
|
75
|
+
* every outcome is normalized to a JSON wire result.
|
|
76
|
+
*/
|
|
77
|
+
export declare function createLemonadeApiHandler(cfg: LemonadeApiConfig): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { StreamChunk } from '@deepseek-ai/dsh-llm';
|
|
2
|
+
/** Parse an SSE byte stream into its `data` payloads. */
|
|
3
|
+
export declare function parseSse(stream: ReadableStream<Uint8Array>, onComment?: (comment: string) => void): AsyncGenerator<string>;
|
|
4
|
+
/**
|
|
5
|
+
* Consume SSE data payloads (optionally ending with `[DONE]`) and yield
|
|
6
|
+
* harness StreamChunks. Malformed JSON payloads abort the stream with
|
|
7
|
+
* `MALFORMED_RESPONSE`. A `stop` (or absent) finish with no opened blocks is a
|
|
8
|
+
* degenerate provider completion and maps to an `EMPTY_RESPONSE` error finish.
|
|
9
|
+
*/
|
|
10
|
+
export declare function translate(payloads: AsyncIterable<string>): AsyncGenerator<StreamChunk>;
|
package/package.json
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cyrilmarin/dsh-lemonade",
|
|
3
|
+
"description": "Lemonade Server (OpenAI-compatible) LLM provider plugin for the DeepSeek Harness",
|
|
4
|
+
"version": "0.2.1",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"types": "lib/types/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/types/index.d.ts",
|
|
11
|
+
"default": "./lib/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./src/*": "./src/*",
|
|
14
|
+
"./package.json": "./package.json",
|
|
15
|
+
"./client": {
|
|
16
|
+
"default": "./lib/client.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"lib/index.js",
|
|
21
|
+
"lib/*.js",
|
|
22
|
+
"lib/types/**/*.d.ts",
|
|
23
|
+
"src",
|
|
24
|
+
"lib/client.js"
|
|
25
|
+
],
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
32
|
+
"@deepseek-ai/dsh-attachment": "^0.1.0-rc.7",
|
|
33
|
+
"@deepseek-ai/dsh-credentials": "^0.1.0-rc.7",
|
|
34
|
+
"@deepseek-ai/dsh-launch-environment": "^0.1.0-rc.7",
|
|
35
|
+
"@deepseek-ai/dsh-llm": "^0.1.0-rc.7",
|
|
36
|
+
"@deepseek-ai/dsh-settings": "^0.1.0-rc.7",
|
|
37
|
+
"@deepseek-ai/dsh-timeout": "^0.1.0-rc.7",
|
|
38
|
+
"react": "^18.2.0",
|
|
39
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.7",
|
|
40
|
+
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.7",
|
|
41
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.7",
|
|
42
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.7",
|
|
43
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.0-rc.7",
|
|
44
|
+
"@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.7"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@deepseek-ai/schemastery": "^3.18.1",
|
|
48
|
+
"eventsource-parser": "^3.1.0"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
52
|
+
"@deepseek-ai/dsh-attachment": "^0.1.0-rc.7",
|
|
53
|
+
"@deepseek-ai/dsh-credentials": "^0.1.0-rc.7",
|
|
54
|
+
"@deepseek-ai/dsh-launch-environment": "^0.1.0-rc.7",
|
|
55
|
+
"@deepseek-ai/dsh-llm": "^0.1.0-rc.7",
|
|
56
|
+
"@deepseek-ai/dsh-settings": "^0.1.0-rc.7",
|
|
57
|
+
"@deepseek-ai/dsh-timeout": "^0.1.0-rc.7",
|
|
58
|
+
"@deepseek-ai/schemastery": "^3.18.1",
|
|
59
|
+
"@types/node": "^24.0.0",
|
|
60
|
+
"eventsource-parser": "^3.1.0",
|
|
61
|
+
"typescript": "^5.5.0",
|
|
62
|
+
"react": "^18.2.0",
|
|
63
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.7",
|
|
64
|
+
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.7",
|
|
65
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.7",
|
|
66
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.7",
|
|
67
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.0-rc.7",
|
|
68
|
+
"@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.7",
|
|
69
|
+
"@types/react": "~18.3.1"
|
|
70
|
+
},
|
|
71
|
+
"dsh": {
|
|
72
|
+
"client": {
|
|
73
|
+
"inject": [
|
|
74
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
75
|
+
"@deepseek-ai/dsh-client-ui-conversation",
|
|
76
|
+
"@deepseek-ai/dsh-client-ui-slots",
|
|
77
|
+
"@deepseek-ai/dsh-client-locale",
|
|
78
|
+
"@deepseek-ai/dsh-client-connection",
|
|
79
|
+
"@deepseek-ai/dsh-api-remotes"
|
|
80
|
+
],
|
|
81
|
+
"platform": "web"
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
"scripts": {
|
|
85
|
+
"build": "tsc -p tsconfig.json && node scripts/copy-client.mjs",
|
|
86
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
87
|
+
"test": "node test/adapter.test.mjs && node test/server-api.test.mjs && node test/client-bundle.test.mjs lib/client.js",
|
|
88
|
+
"release": "node scripts/release.mjs",
|
|
89
|
+
"release:dry": "node scripts/release.mjs --dry-run",
|
|
90
|
+
"release:major": "node scripts/release.mjs --bump major",
|
|
91
|
+
"release:minor": "node scripts/release.mjs --bump minor",
|
|
92
|
+
"release:patch": "node scripts/release.mjs --bump patch"
|
|
93
|
+
}
|
|
94
|
+
}
|