Merge remote app attribution branch
Resolve the RFC and implementation to defer OpenRouter-specific attribution headers and keep mandatory attribution to User-Agent only.
This commit is contained in:
@@ -1,16 +1,16 @@
|
||||
/**
|
||||
* LLM service: adapter registry with waterfall-interceptable streaming and
|
||||
* non-streaming call surfaces. Exports the `LlmService` default, the abstract
|
||||
* `LlmAdapter` for provider backends, and `BlockAssembler` for chunk assembly.
|
||||
* LLM service: adapter registry with a waterfall-interceptable streaming call
|
||||
* surface. Exports the `LlmService` default, the abstract `LlmAdapter` for
|
||||
* provider backends, and `BlockAssembler` for chunk assembly.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-llm
|
||||
*/
|
||||
|
||||
import { Context, Service } from 'cordis'
|
||||
import type { ContentBlock, GenerateOptions, GenerateResult, StreamChunk } from './types.ts'
|
||||
import { BlockAssembler } from './assembler.ts'
|
||||
import type { GenerateOptions, StreamChunk } from './types.ts'
|
||||
import { HarnessError } from './error.ts'
|
||||
|
||||
export * from './attribution.ts'
|
||||
export * from './brand.ts'
|
||||
export * from './never.ts'
|
||||
export * from './error.ts'
|
||||
@@ -30,17 +30,6 @@ declare module 'cordis' {
|
||||
* @mode waterfall
|
||||
*/
|
||||
'llm/stream'(this: LlmService, options: GenerateOptions, next: () => AsyncIterable<StreamChunk>): AsyncIterable<StreamChunk>
|
||||
/**
|
||||
* Waterfall around every non-streaming model call. Bound to the
|
||||
* {@link LlmService}; call `next()` to delegate to the adapter.
|
||||
* @mode waterfall
|
||||
*/
|
||||
'llm/generate'(this: LlmService, options: GenerateOptions, next: () => Promise<GenerateResult>): Promise<GenerateResult>
|
||||
/**
|
||||
* An adapter was registered or unregistered (the model→adapter map changed).
|
||||
* @mode emit
|
||||
*/
|
||||
'llm/adapter-change'(): void
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +57,13 @@ export class LlmError extends HarnessError {
|
||||
* fetch/SSE) and `@deepseek-ai/dsh-llm-pi-ai` (pi-ai-backed) — two
|
||||
* deliberately different internals over the same contract; see the
|
||||
* adapter contract documented on `StreamChunk` in `./types.ts`.
|
||||
*
|
||||
* App attribution is part of the adapter contract: every HTTP request to a
|
||||
* provider carries the headers from `attributionHeaders()` (`./attribution.ts`)
|
||||
* — the standard `User-Agent` baseline everywhere. An adapter proves it with
|
||||
* a wire-level test (a mock server asserting the received header), or, for a
|
||||
* library-backed adapter, by asserting the library's header hook delivers the
|
||||
* same value to the wire.
|
||||
*/
|
||||
export abstract class LlmAdapter {
|
||||
/** Stream one model call as raw chunks. The only required method. */
|
||||
@@ -75,8 +71,8 @@ export abstract class LlmAdapter {
|
||||
}
|
||||
|
||||
/**
|
||||
* The abstract `llm` service: an adapter registry plus streaming /
|
||||
* non-streaming call surfaces, both interceptable via waterfall events.
|
||||
* The abstract `llm` service: an adapter registry plus a streaming model-call
|
||||
* surface, interceptable via the `llm/stream` waterfall.
|
||||
*/
|
||||
export class LlmService extends Service {
|
||||
private adapters = new Map<string, LlmAdapter>()
|
||||
@@ -88,8 +84,7 @@ export class LlmService extends Service {
|
||||
/**
|
||||
* Register an adapter for the given model names. Throws `LlmError` with code
|
||||
* `DUPLICATE_ADAPTER` if any model already has an adapter (all-or-nothing).
|
||||
* Emits `llm/adapter-change` on registration and disposal. Disposed with the
|
||||
* fiber.
|
||||
* Disposed with the fiber.
|
||||
*/
|
||||
registerAdapter(models: string[], adapter: LlmAdapter): () => void {
|
||||
const dispose = this.ctx.effect(function* (this: LlmService) {
|
||||
@@ -99,17 +94,9 @@ export class LlmService extends Service {
|
||||
}
|
||||
}
|
||||
for (const model of models) this.adapters.set(model, adapter)
|
||||
// Yield the rollback BEFORE emitting the change event: a generator effect
|
||||
// collects each yielded disposer before running the next step, so a
|
||||
// throwing `llm/adapter-change` listener rolls the mutation back instead
|
||||
// of leaking the entry (which would wedge the duplicate check until
|
||||
// restart). The duplicate throws above fire before any mutation, so they
|
||||
// correctly leak nothing.
|
||||
yield () => {
|
||||
for (const model of models) this.adapters.delete(model)
|
||||
this.ctx.emit('llm/adapter-change')
|
||||
}
|
||||
this.ctx.emit('llm/adapter-change')
|
||||
}.bind(this), 'llm.registerAdapter()')
|
||||
// ctx.effect's disposer returns Promise<void>; our disposer API is
|
||||
// synchronous fire-and-forget — discard the (always-resolved) promise.
|
||||
@@ -137,36 +124,6 @@ export class LlmService extends Service {
|
||||
return this.adapter(options.model).stream(options)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream one model call as completed content blocks — a convenience view
|
||||
* for consumers that don't care about token-level deltas. Blocks are
|
||||
* yielded strictly in stream order as soon as they (and everything before
|
||||
* them) complete; blocks left open at end of stream (delta-only protocols)
|
||||
* are assembled and flushed last, so the sequence always equals
|
||||
* `generate()`'s `message.content`.
|
||||
*/
|
||||
async * streamBlocks(options: GenerateOptions): AsyncIterable<ContentBlock> {
|
||||
const assembler = new BlockAssembler()
|
||||
for await (const chunk of this.stream(options)) {
|
||||
assembler.push(chunk)
|
||||
yield * assembler.flushReady()
|
||||
}
|
||||
yield * assembler.flushRemaining()
|
||||
}
|
||||
|
||||
/**
|
||||
* One model call, fully assembled (drains the chunk stream). Dispatches
|
||||
* through the `llm/generate` waterfall (and the inner stream through
|
||||
* `llm/stream`). Same completion guarantees as `streamBlocks()`.
|
||||
*/
|
||||
generate(options: GenerateOptions): Promise<GenerateResult> {
|
||||
return this.ctx.waterfall(this, 'llm/generate', options, async () => {
|
||||
const assembler = new BlockAssembler()
|
||||
for await (const chunk of this.stream(options)) assembler.push(chunk)
|
||||
return assembler.result()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default LlmService
|
||||
|
||||
Reference in New Issue
Block a user