DeepSeek Harness LLM-Adapter: Jedes Modell verbinden
Modellanbieter sind nicht auf DeepSeek beschränkt. Wenn Sie dsh mit Ihrem eigenen Modell-Endpunkt verbinden möchten, müssen Sie einen LLM-Adapter schreiben.
In diesem Abschnitt behandeln wir die abstrakte Klasse LlmAdapter, die Methode stream(), die Routenregistrierung und die Konfiguration von cordis.yml.
Was ist ein LLM-Adapter?
Ein LLM-Adapter ist eine Klasse, die von LlmAdapter erbt und die Methode stream() implementiert.
Er wandelt die anbieterunabhängigen Anfragen des Harness in API-Aufrufe für einen bestimmten Anbieter um.
Anschließend wandelt er die Antwort wieder in Harness-Chunks um, nämlich StreamChunk.
Daher konsumiert agent-loop eine einheitliche Schnittstelle und kümmert sich nicht darum, welche API eines Anbieters dahinter steckt.
Ganz oben steht agent-loop, der einen anbieterunabhängigen Streaming-Generierungsdienst konsumiert.
In der Mitte befindet sich die Registry ctx.llm, die den abstrakten Vertrag von LlmAdapter pflegt.
Unten befinden sich die einzelnen Adapter, die jeweils eine Verbindung zu einem anderen API-Format herstellen.
Wird bei der Registrierung verwendetctx.llm.registerAdapter(['my-provider'], adapter)Route binden.
Minimale Implementierung
Die offizielle Dokumentation liefert ein minimales Grundgerüst; wir folgen ihr und fügen Kommentare hinzu.
stream() ist der Kern und gibt einen asynchronen Generator AsyncIterable zurück.
Die Drei-Schritt-Kommentare markieren den Standardablauf: das Konvertieren des Nachrichtenformats, das Aufrufen der Streaming-API und das Umwandeln der Antwort in StreamChunk.
Beispiel
import type { Context } from '@deepseek-ai/cordis'
import Schema from '@deepseek-ai/schemastery'
import { LlmAdapter, type GenerateOptions, type StreamChunk } from '@deepseek-ai/dsh-llm'
// Adapter: erbt von der abstrakten Klasse und implementiert stream().
class MyAdapter extends LlmAdapter {
private apiKey: string
constructor(apiKey: string) {
super()
this.apiKey = apiKey
}
// stream() gibt einen Async-Generator zurück, der StreamChunk Stück für Stück liefert
async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
// 1. options.messages in das Provider-Format konvertieren.
// 2. Die Streaming-API aufrufen.
// 3. Die Antwort in StreamChunk-Werte konvertieren.
}
}
// Plugin-Konfiguration: apiKey und providers sind beide erforderlich.
export interface Config {
apiKey: string
providers: string[]
}
// Schemastery-Schema mit demselben Namen, Konfiguration beim Laden validieren
export const Config: Schema<Config> = Schema.Objekt({
apiKey: Schema.string().required(),
providers: Schema.array(Schema.string()).required(),
})
export const name = 'my-llm-adapter'
// Abhängigkeit vom llm-Dienst deklarieren, um sicherzustellen, dass ctx.llm bereit ist.
export const inject = ['llm']
export function apply(ctx: Context, config: Config) {
const adapter = new MyAdapter(config.apiKey)
// Binde die Provider-Routenliste an diesen Adapter
ctx.llm.registerAdapter(config.providers, adapter)
}
apiKey stammt aus der Plugin-Konfiguration, wodurch vermieden wird, das Secret fest im Code zu hinterlegen.
Das providers-Array deklariert, für welche Provider-Routen dieser Adapter zuständig ist.
In apply wird zuerst new MyAdapter(config.apiKey) ausgeführt, dann registerAdapter, um die Registrierung abzuschließen.
inject: ['llm'] stellt sicher, dass apply erst ausgeführt wird, nachdem der Dienst ctx.llm bereit ist.
stream() gibt einen asynchronen Generator zurück, der yield verwendet, um Chunks einzeln auszugeben.
Der nächste Artikel erklärt das StreamChunk-Chunking-Protokoll im Detail.
GenerateOptions: Was der Adapter empfängt
stream() empfängt die vom Repository exportierten GenerateOptions.
Es umfasst Modell, Reasoning-Aufwand, Konversationsverlauf, System-Prompt, Tool-Schema, Generierungsparameter, Stoppsequenzen und Abbruchsignal.
Die vollständigen Felder richten sich nach den TypeScript-Typen, die von @deepseek-ai/dsh-llm exportiert werden.
| Fields | Beschreibung |
|---|---|
| Anbieter | Einen registrierten Adapter auswählen |
| model | Vom Adapter verwaltete Modell-IDs müssen beim Start nicht registriert werden. |
| messages | Gesprächsverlauf |
| System-Prompt | System-Prompt |
| tools | Tool-Schema |
| Reasoning | Die vom Adapter unterstützte Reasoning-Effort-ID (optional). |
| signal | Abbruchsignal, das von Abbruch und Ressourcenfreigabe verwendet wird, um vollständig zu stoppen. |
Der Adapter muss die unterstützten Felder auf die spezifische API abbilden.
Wenn ein Feld nicht unterstützt werden kann, sollte ein LlmError mit einem stabilen Code ausgelöst werden und es darf nicht stillschweigend verworfen werden.
Nicht unterstützte Felder sollten explizite Fehler auslösen, anstatt stillschweigend ignoriert zu werden; andernfalls erhält das Modell unvollständige Ergebnisse.
Adapter registrieren
In apply ctx.llm.registerAdapter aufrufen, um die Routenregistrierung abzuschließen.
Beispiel
ctx.llm.registerAdapter(['my-provider'], adapter)
Der erste Parameter ist die Liste der vom Adapter verarbeiteten Provider-Routen.
GenerateOptions.provider wählt einen registrierten Adapter aus.
GenerateOptions.model übergibt die Modell-ID, die dem Adapter gehört und nicht beim Start des Lebenszyklus registriert werden muss.
Wenn der Adapter Modelloptionen für den Selektor bereitstellen kann, kann er listModels() überschreiben.
Wenn Sie die exakte Provider- und Modellidentität zurückgeben müssen, können Sie resolveModel(provider, model, signal?) überschreiben.
resolveModel gibt in einer einzigen Abfrage den exakten Provider, die Modellidentität sowie optionale Kontext- und Reasoning-Metadaten zurück.
Die Reasoning-Metadaten beschreiben die optionalen Reasoning-Aufwände des Modells: geordnete opake IDs, Anzeigenamen und optionale Standardwerte der Konfiguration.
Bewahren Sie die vom Adapter angegebene maßgebliche optionale Liste auf, einschließlich des von der Upstream-Capability-API zurückgegebenen off-Werts; überführen Sie diese Werte nicht in Kern-Enums.
Asynchrone Abfragen müssen auf das optionale Signal reagieren, damit Abbruch und Ressourcenfreigabe vollständig beendet werden.
Der Dienst validiert das aggregierte Ergebnis: Ein explizit angegebener, aber nicht unterstützter Reasoning-Aufwand wird abgelehnt, bevor stream() aufgerufen wird.
Das Weglassen von Reasoning zeigt an, dass das Modell keine optionale Reasoning-Aufwandsfähigkeit besitzt.
Verwendung in cordis.yml
Laden Sie das Adapter-Plugin zusammen mit agent-loop und sorgen Sie dafür, dass agent-loop den neuen Provider und das neue Modell verwendet.
Beispiel
# Adapter-Plugin laden; apiKey wird aus Umgebungsvariablen gelesen.
- id: my-llm
name: './src/my-llm-adapter.ts'
config:
apiKey: !!js process.env.MY_API_KEY
providers:
- my-provider
# agent-loop so konfigurieren, dass Provider und Modell des neuen Adapters verwendet werden
- id: agent-loop
name: '@deepseek-ai/dsh-agent-loop'
config:
agents:
- id: main
provider: my-provider
model: my-model-v1
Die config.apiKey des my-llm-Plugins wird aus der Umgebungsvariable MY_API_KEY gelesen und nicht auf die Festplatte geschrieben.
Das providers-Array registriert die Route my-provider bei MyAdapter.
Konfiguriere Provider und Modell in agents.main von agent-loop, und generierte Anfragen werden auf den neuen Adapter treffen.
Praktische Referenz
Das Repository enthält zwei vollständige bestehende Implementierungen zum Vergleich.
llm-deepseek adaptiert die DeepSeek-API und verwendet das OpenAI-kompatible Format.
llm-pi-ai adaptiert Pi AI, das ein anderes API-Format verwendet.
Der Vergleich dieser beiden Adapter zeigt, wie derselbe Harness-Vertrag auf Basis unterschiedlicher Provider-SDKs implementiert wird.
Wenn man zuerst llm-deepseek und dann llm-pi-ai liest, lässt sich die Seam-Idee "Vertrag unverändert, Implementierungen variieren" am leichtesten erkennen.
Zusammenfassung und Selbsttest
Zusammenfassung in einem Satz: LLM-Adapter = von LlmAdapter erben + stream() implementieren + registerAdapter-Routenregistrierung + cordis.yml-Konfiguration.
Selbsttestfrage 1: Was ist der Rückgabetyp von stream()?
Selbsttestfrage 2: Was repräsentiert der erste Parameter von registerAdapter?
Selbsttestfrage 3: Welche Konfigurationsoptionen verwendet agent-loop, um einen neuen Adapter auszuwählen?
Weitere Erweiterungen