delete: images()
This commit is contained in:
@@ -1,26 +1,19 @@
|
||||
import type {
|
||||
AssistantImagesEventStream,
|
||||
ImagesApi,
|
||||
ImagesContext,
|
||||
ImagesFunction,
|
||||
ImagesModel,
|
||||
ImagesOptions,
|
||||
} from "./types.js";
|
||||
import type { AssistantImages, ImagesApi, ImagesContext, ImagesFunction, ImagesModel, ImagesOptions } from "./types.js";
|
||||
|
||||
export type ImagesApiFunction = (
|
||||
model: ImagesModel<ImagesApi>,
|
||||
context: ImagesContext,
|
||||
options?: ImagesOptions,
|
||||
) => AssistantImagesEventStream;
|
||||
) => Promise<AssistantImages>;
|
||||
|
||||
export interface ImagesApiProvider<TApi extends ImagesApi = ImagesApi, TOptions extends ImagesOptions = ImagesOptions> {
|
||||
api: TApi;
|
||||
images: ImagesFunction<TApi, TOptions>;
|
||||
generateImages: ImagesFunction<TApi, TOptions>;
|
||||
}
|
||||
|
||||
interface ImagesApiProviderInternal {
|
||||
api: ImagesApi;
|
||||
images: ImagesApiFunction;
|
||||
generateImages: ImagesApiFunction;
|
||||
}
|
||||
|
||||
type RegisteredImagesApiProvider = {
|
||||
@@ -30,15 +23,15 @@ type RegisteredImagesApiProvider = {
|
||||
|
||||
const imagesApiProviderRegistry = new Map<string, RegisteredImagesApiProvider>();
|
||||
|
||||
function wrapImages<TApi extends ImagesApi, TOptions extends ImagesOptions>(
|
||||
function wrapGenerateImages<TApi extends ImagesApi, TOptions extends ImagesOptions>(
|
||||
api: TApi,
|
||||
images: ImagesFunction<TApi, TOptions>,
|
||||
generateImages: ImagesFunction<TApi, TOptions>,
|
||||
): ImagesApiFunction {
|
||||
return (model, context, options) => {
|
||||
if (model.api !== api) {
|
||||
throw new Error(`Mismatched api: ${model.api} expected ${api}`);
|
||||
}
|
||||
return images(model as ImagesModel<TApi>, context, options as TOptions);
|
||||
return generateImages(model as ImagesModel<TApi>, context, options as TOptions);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -49,7 +42,7 @@ export function registerImagesApiProvider<TApi extends ImagesApi, TOptions exten
|
||||
imagesApiProviderRegistry.set(provider.api, {
|
||||
provider: {
|
||||
api: provider.api,
|
||||
images: wrapImages(provider.api, provider.images),
|
||||
generateImages: wrapGenerateImages(provider.api, provider.generateImages),
|
||||
},
|
||||
sourceId,
|
||||
});
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
import "./providers/images/register-builtins.js";
|
||||
|
||||
import { getImagesApiProvider } from "./images-api-registry.js";
|
||||
import type {
|
||||
AssistantImages,
|
||||
AssistantImagesEventStream,
|
||||
ImagesApi,
|
||||
ImagesContext,
|
||||
ImagesModel,
|
||||
ImagesOptions,
|
||||
ProviderImagesOptions,
|
||||
} from "./types.js";
|
||||
import type { AssistantImages, ImagesApi, ImagesContext, ImagesModel, ProviderImagesOptions } from "./types.js";
|
||||
|
||||
function resolveImagesApiProvider(api: ImagesApi) {
|
||||
const provider = getImagesApiProvider(api);
|
||||
@@ -19,20 +11,11 @@ function resolveImagesApiProvider(api: ImagesApi) {
|
||||
return provider;
|
||||
}
|
||||
|
||||
export function images<TApi extends ImagesApi>(
|
||||
model: ImagesModel<TApi>,
|
||||
context: ImagesContext,
|
||||
options?: ProviderImagesOptions,
|
||||
): AssistantImagesEventStream {
|
||||
const provider = resolveImagesApiProvider(model.api);
|
||||
return provider.images(model, context, options as ImagesOptions);
|
||||
}
|
||||
|
||||
export async function generateImages<TApi extends ImagesApi>(
|
||||
model: ImagesModel<TApi>,
|
||||
context: ImagesContext,
|
||||
options?: ProviderImagesOptions,
|
||||
): Promise<AssistantImages> {
|
||||
const s = images(model, context, options);
|
||||
return s.result();
|
||||
const provider = resolveImagesApiProvider(model.api);
|
||||
return provider.generateImages(model, context, options);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import type {
|
||||
ImagesOptions,
|
||||
TextContent,
|
||||
} from "../../types.js";
|
||||
import { AssistantImagesEventStream } from "../../utils/event-stream.js";
|
||||
import { headersToRecord } from "../../utils/headers.js";
|
||||
import { sanitizeSurrogates } from "../../utils/sanitize-unicode.js";
|
||||
|
||||
@@ -36,87 +35,70 @@ type OpenRouterImageGenerationResponse = ChatCompletion & {
|
||||
choices: OpenRouterImageGenerationChoice[];
|
||||
};
|
||||
|
||||
export const imagesOpenRouter: ImagesFunction<"openrouter-images", ImagesOptions> = (
|
||||
export const generateImagesOpenRouter: ImagesFunction<"openrouter-images", ImagesOptions> = async (
|
||||
model: ImagesModel<"openrouter-images">,
|
||||
context: ImagesContext,
|
||||
options?: ImagesOptions,
|
||||
) => {
|
||||
const stream = new AssistantImagesEventStream();
|
||||
const output: AssistantImages = {
|
||||
api: model.api,
|
||||
provider: model.provider,
|
||||
model: model.id,
|
||||
output: [],
|
||||
stopReason: "stop",
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
(async () => {
|
||||
const output: AssistantImages = {
|
||||
api: model.api,
|
||||
provider: model.provider,
|
||||
model: model.id,
|
||||
output: [],
|
||||
stopReason: "stop",
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
try {
|
||||
const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
|
||||
const client = createClient(model, apiKey, options?.headers);
|
||||
let params = buildParams(model, context);
|
||||
const nextParams = await options?.onPayload?.(params, model);
|
||||
if (nextParams !== undefined) {
|
||||
params = nextParams as typeof params;
|
||||
}
|
||||
const requestOptions = {
|
||||
...(options?.signal ? { signal: options.signal } : {}),
|
||||
...(options?.timeoutMs !== undefined ? { timeout: options.timeoutMs } : {}),
|
||||
...(options?.maxRetries !== undefined ? { maxRetries: options.maxRetries } : {}),
|
||||
};
|
||||
const { data: response, response: rawResponse } = await client.chat.completions
|
||||
.create(params as unknown as ChatCompletionCreateParamsNonStreaming, requestOptions)
|
||||
.withResponse();
|
||||
await options?.onResponse?.(
|
||||
{ status: rawResponse.status, headers: headersToRecord(rawResponse.headers) },
|
||||
model,
|
||||
);
|
||||
|
||||
stream.push({ type: "start", partial: output });
|
||||
|
||||
const imageResponse = response as OpenRouterImageGenerationResponse;
|
||||
output.responseId = imageResponse.id;
|
||||
if (imageResponse.usage) {
|
||||
output.usage = parseUsage(imageResponse.usage, model);
|
||||
}
|
||||
|
||||
const choice = imageResponse.choices[0];
|
||||
if (choice) {
|
||||
const content = choice.message.content;
|
||||
if (typeof content === "string" && content.length > 0) {
|
||||
output.output.push({ type: "text", text: content } satisfies TextContent);
|
||||
}
|
||||
|
||||
for (const image of choice.message.images ?? []) {
|
||||
const imageUrl = typeof image.image_url === "string" ? image.image_url : image.image_url?.url;
|
||||
if (!imageUrl?.startsWith("data:")) continue;
|
||||
const matches = imageUrl.match(/^data:([^;]+);base64,(.+)$/);
|
||||
if (!matches) continue;
|
||||
const imageBlock: ImageContent = {
|
||||
type: "image",
|
||||
mimeType: matches[1],
|
||||
data: matches[2],
|
||||
};
|
||||
output.output.push(imageBlock);
|
||||
const contentIndex = output.output.length - 1;
|
||||
stream.push({ type: "image_start", contentIndex, partial: output });
|
||||
stream.push({ type: "image_end", contentIndex, image: imageBlock, partial: output });
|
||||
}
|
||||
}
|
||||
|
||||
stream.push({ type: "done", reason: "stop", images: output });
|
||||
stream.end();
|
||||
} catch (error) {
|
||||
output.stopReason = options?.signal?.aborted ? "aborted" : "error";
|
||||
output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
|
||||
stream.push({ type: "error", reason: output.stopReason, error: output });
|
||||
stream.end(output);
|
||||
try {
|
||||
const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
|
||||
const client = createClient(model, apiKey, options?.headers);
|
||||
let params = buildParams(model, context);
|
||||
const nextParams = await options?.onPayload?.(params, model);
|
||||
if (nextParams !== undefined) {
|
||||
params = nextParams as typeof params;
|
||||
}
|
||||
})();
|
||||
const requestOptions = {
|
||||
...(options?.signal ? { signal: options.signal } : {}),
|
||||
...(options?.timeoutMs !== undefined ? { timeout: options.timeoutMs } : {}),
|
||||
...(options?.maxRetries !== undefined ? { maxRetries: options.maxRetries } : {}),
|
||||
};
|
||||
const { data: response, response: rawResponse } = await client.chat.completions
|
||||
.create(params as unknown as ChatCompletionCreateParamsNonStreaming, requestOptions)
|
||||
.withResponse();
|
||||
await options?.onResponse?.({ status: rawResponse.status, headers: headersToRecord(rawResponse.headers) }, model);
|
||||
|
||||
return stream;
|
||||
const imageResponse = response as OpenRouterImageGenerationResponse;
|
||||
output.responseId = imageResponse.id;
|
||||
if (imageResponse.usage) {
|
||||
output.usage = parseUsage(imageResponse.usage, model);
|
||||
}
|
||||
|
||||
const choice = imageResponse.choices[0];
|
||||
if (choice) {
|
||||
const content = choice.message.content;
|
||||
if (typeof content === "string" && content.length > 0) {
|
||||
output.output.push({ type: "text", text: content } satisfies TextContent);
|
||||
}
|
||||
|
||||
for (const image of choice.message.images ?? []) {
|
||||
const imageUrl = typeof image.image_url === "string" ? image.image_url : image.image_url?.url;
|
||||
if (!imageUrl?.startsWith("data:")) continue;
|
||||
const matches = imageUrl.match(/^data:([^;]+);base64,(.+)$/);
|
||||
if (!matches) continue;
|
||||
output.output.push({
|
||||
type: "image",
|
||||
mimeType: matches[1],
|
||||
data: matches[2],
|
||||
} satisfies ImageContent);
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
} catch (error) {
|
||||
output.stopReason = options?.signal?.aborted ? "aborted" : "error";
|
||||
output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
|
||||
return output;
|
||||
}
|
||||
};
|
||||
|
||||
function createClient(
|
||||
|
||||
@@ -1,74 +1,10 @@
|
||||
import { registerImagesApiProvider } from "../../images-api-registry.js";
|
||||
import type {
|
||||
AssistantImages,
|
||||
AssistantImagesEvent,
|
||||
ImagesContext,
|
||||
ImagesFunction,
|
||||
ImagesModel,
|
||||
ImagesOptions,
|
||||
} from "../../types.js";
|
||||
import { AssistantImagesEventStream } from "../../utils/event-stream.js";
|
||||
import type { imagesOpenRouter as imagesOpenRouterFunction } from "./openrouter.js";
|
||||
|
||||
interface OpenRouterImagesProviderModule {
|
||||
imagesOpenRouter: typeof imagesOpenRouterFunction;
|
||||
}
|
||||
|
||||
let openRouterImagesProviderModulePromise: Promise<OpenRouterImagesProviderModule> | undefined;
|
||||
|
||||
function forwardImagesStream(target: AssistantImagesEventStream, source: AsyncIterable<AssistantImagesEvent>): void {
|
||||
(async () => {
|
||||
for await (const event of source) {
|
||||
target.push(event);
|
||||
}
|
||||
target.end();
|
||||
})();
|
||||
}
|
||||
|
||||
function createLazyLoadErrorImages(model: ImagesModel<"openrouter-images">, error: unknown): AssistantImages {
|
||||
return {
|
||||
api: model.api,
|
||||
provider: model.provider,
|
||||
model: model.id,
|
||||
output: [],
|
||||
stopReason: "error",
|
||||
errorMessage: error instanceof Error ? error.message : String(error),
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
function loadOpenRouterImagesProviderModule(): Promise<OpenRouterImagesProviderModule> {
|
||||
openRouterImagesProviderModulePromise ||= import("./openrouter.js").then(
|
||||
(module) => module as OpenRouterImagesProviderModule,
|
||||
);
|
||||
return openRouterImagesProviderModulePromise;
|
||||
}
|
||||
|
||||
export const imagesOpenRouter: ImagesFunction<"openrouter-images", ImagesOptions> = (
|
||||
model: ImagesModel<"openrouter-images">,
|
||||
context: ImagesContext,
|
||||
options?: ImagesOptions,
|
||||
) => {
|
||||
const outer = new AssistantImagesEventStream();
|
||||
|
||||
loadOpenRouterImagesProviderModule()
|
||||
.then((module) => {
|
||||
const inner = module.imagesOpenRouter(model, context, options);
|
||||
forwardImagesStream(outer, inner);
|
||||
})
|
||||
.catch((error) => {
|
||||
const images = createLazyLoadErrorImages(model, error);
|
||||
outer.push({ type: "error", reason: "error", error: images });
|
||||
outer.end(images);
|
||||
});
|
||||
|
||||
return outer;
|
||||
};
|
||||
import { generateImagesOpenRouter } from "./openrouter.js";
|
||||
|
||||
export function registerBuiltInImagesApiProviders(): void {
|
||||
registerImagesApiProvider({
|
||||
api: "openrouter-images",
|
||||
images: imagesOpenRouter,
|
||||
generateImages: generateImagesOpenRouter,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import type { AssistantMessageDiagnostic } from "./utils/diagnostics.js";
|
||||
import type { AssistantImagesEventStream, AssistantMessageEventStream } from "./utils/event-stream.js";
|
||||
import type { AssistantMessageEventStream } from "./utils/event-stream.js";
|
||||
|
||||
export type {
|
||||
AssistantImagesEventStream,
|
||||
AssistantMessageEventStream,
|
||||
} from "./utils/event-stream.js";
|
||||
export type { AssistantMessageEventStream } from "./utils/event-stream.js";
|
||||
|
||||
export type KnownApi =
|
||||
| "openai-completions"
|
||||
@@ -215,7 +212,7 @@ export type ImagesFunction<TApi extends ImagesApi = ImagesApi, TOptions extends
|
||||
model: ImagesModel<TApi>,
|
||||
context: ImagesContext,
|
||||
options?: TOptions,
|
||||
) => AssistantImagesEventStream;
|
||||
) => Promise<AssistantImages>;
|
||||
|
||||
export interface TextSignatureV1 {
|
||||
v: 1;
|
||||
@@ -360,13 +357,6 @@ export type AssistantMessageEvent =
|
||||
| { type: "done"; reason: Extract<StopReason, "stop" | "length" | "toolUse">; message: AssistantMessage }
|
||||
| { type: "error"; reason: Extract<StopReason, "aborted" | "error">; error: AssistantMessage };
|
||||
|
||||
export type AssistantImagesEvent =
|
||||
| { type: "start"; partial: AssistantImages }
|
||||
| { type: "image_start"; contentIndex: number; partial: AssistantImages }
|
||||
| { type: "image_end"; contentIndex: number; image: ImageContent; partial: AssistantImages }
|
||||
| { type: "done"; reason: Extract<ImagesStopReason, "stop">; images: AssistantImages }
|
||||
| { type: "error"; reason: Extract<ImagesStopReason, "aborted" | "error">; error: AssistantImages };
|
||||
|
||||
/**
|
||||
* Compatibility settings for OpenAI-compatible completions APIs.
|
||||
* Use this to override URL-based auto-detection for custom providers.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AssistantImages, AssistantImagesEvent, AssistantMessage, AssistantMessageEvent } from "../types.js";
|
||||
import type { AssistantMessage, AssistantMessageEvent } from "../types.js";
|
||||
|
||||
// Generic event stream class for async iteration
|
||||
export class EventStream<T, R = T> implements AsyncIterable<T> {
|
||||
@@ -81,28 +81,7 @@ export class AssistantMessageEventStream extends EventStream<AssistantMessageEve
|
||||
}
|
||||
}
|
||||
|
||||
export class AssistantImagesEventStream extends EventStream<AssistantImagesEvent, AssistantImages> {
|
||||
constructor() {
|
||||
super(
|
||||
(event) => event.type === "done" || event.type === "error",
|
||||
(event) => {
|
||||
if (event.type === "done") {
|
||||
return event.images;
|
||||
} else if (event.type === "error") {
|
||||
return event.error;
|
||||
}
|
||||
throw new Error("Unexpected event type for final result");
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** Factory function for AssistantMessageEventStream (for use in extensions) */
|
||||
export function createAssistantMessageEventStream(): AssistantMessageEventStream {
|
||||
return new AssistantMessageEventStream();
|
||||
}
|
||||
|
||||
/** Factory function for AssistantImagesEventStream (for use in extensions) */
|
||||
export function createAssistantImagesEventStream(): AssistantImagesEventStream {
|
||||
return new AssistantImagesEventStream();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user