Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions sdk/js/examples/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
// Licensed under the MIT License.
// -------------------------------------------------------------------------

import { FoundryLocalManager, getOutputText } from '../src/index.js';
import type { StreamingEvent, FunctionToolDefinition, FunctionCallItem } from '../src/types.js';
import * as fs from 'fs';
import { FoundryLocalManager, getOutputText, createImageContentFromFile } from '../src/index.js';
import type { StreamingEvent, FunctionToolDefinition, FunctionCallItem, MessageItem } from '../src/types.js';

async function main() {
try {
Expand Down Expand Up @@ -121,6 +122,35 @@ async function main() {
const deleted = await client.delete(stored.id);
console.log(`Deleted: ${deleted.deleted}`);

// =================================================================
// Example 6: List all stored responses
// =================================================================
console.log('\n--- Example 6: List stored responses ---');
const allResponses = await client.list();
console.log(`Listed ${allResponses.data.length} stored responses`);

// =================================================================
// Example 7: Vision — describe an image
// =================================================================
console.log('\n--- Example 7: Vision ---');
const testImagePath = 'path/to/test-image.png'; // Replace with a real image path
Comment thread
MaanavD marked this conversation as resolved.
Outdated
if (fs.existsSync(testImagePath)) {
const imageContent = await createImageContentFromFile(testImagePath);
const visionResponse = await client.create([
{
type: 'message',
role: 'user',
content: [
{ type: 'input_text', text: 'Describe this image in one sentence.' },
imageContent,
],
} as MessageItem,
]);
console.log(`Vision: ${getOutputText(visionResponse)}`);
} else {
console.log('(Skipped: test image not found)');
}

// Cleanup
manager.stopWebService();
await model.unload();
Expand Down
1 change: 1 addition & 0 deletions sdk/js/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export { EmbeddingClient } from './openai/embeddingClient.js';
export { LiveAudioTranscriptionSession, LiveAudioTranscriptionOptions } from './openai/liveAudioTranscriptionClient.js';
export type { LiveAudioTranscriptionResponse, TranscriptionContentPart } from './openai/liveAudioTranscriptionTypes.js';
export { ResponsesClient, ResponsesClientSettings, getOutputText } from './openai/responsesClient.js';
export { createImageContentFromFile, createImageContentFromUrl } from './openai/vision.js';
export { ModelLoadManager } from './detail/modelLoadManager.js';
/** @internal */
export { CoreInterop } from './detail/coreInterop.js';
Expand Down
17 changes: 16 additions & 1 deletion sdk/js/src/openai/responsesClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
StreamingEvent,
InputItemsListResponse,
DeleteResponseResult,
ListResponsesResult,
ResponseInputItem,
MessageItem,
ContentPart,
Expand Down Expand Up @@ -52,6 +53,11 @@ export class ResponsesClientSettings {
toolChoice?: ResponseToolChoice;
truncation?: TruncationStrategy;
parallelToolCalls?: boolean;
/**
* Whether to store the response server-side so it can be retrieved via `get()`, `list()`,
* `getInputItems()`, or referenced by `previous_response_id`. Defaults to `true` when not
* explicitly set. Set to `false` to disable persistence for a given client.
*/
store?: boolean;
metadata?: Record<string, string>;
reasoning?: ReasoningConfig;
Expand All @@ -76,7 +82,8 @@ export class ResponsesClientSettings {
tool_choice: this.toolChoice,
truncation: this.truncation,
parallel_tool_calls: this.parallelToolCalls,
store: this.store,
// Default store to true when not explicitly set
store: this.store !== undefined ? this.store : true,
Comment thread
MaanavD marked this conversation as resolved.
Outdated
metadata: this.metadata,
reasoning: this.reasoning ? filterUndefined(this.reasoning) : undefined,
text: this.text ? filterUndefined(this.text) : undefined,
Expand Down Expand Up @@ -275,6 +282,14 @@ export class ResponsesClient {
);
}

/**
* Lists all stored responses.
* @returns The list of Response objects.
*/
public async list(): Promise<ListResponsesResult> {
return this.fetchJson<ListResponsesResult>('/v1/responses', { method: 'GET' });
}
Comment thread
MaanavD marked this conversation as resolved.
Outdated

// ========================================================================
// Internal helpers
// ========================================================================
Expand Down
134 changes: 134 additions & 0 deletions sdk/js/src/openai/vision.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// -------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
// -------------------------------------------------------------------------

import * as fs from 'fs';
import * as path from 'path';
import type { InputImageContent } from '../types.js';

const MEDIA_TYPE_MAP: Record<string, string> = {
'.png': 'image/png',
Comment thread
apsonawane marked this conversation as resolved.
Outdated
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.webp': 'image/webp',
'.bmp': 'image/bmp',
};

/**
* Options for `createImageContentFromFile`.
*/
export interface ImageContentOptions {
/** Detail level hint for the model. */
detail?: 'low' | 'high' | 'auto';
/**
* If set, the longest dimension of the image will be scaled down to this value
* (preserving aspect ratio) before encoding. Requires the `sharp` package to be
* installed as an optional peer dependency (`npm install sharp`). If `sharp` is
* not available and the image exceeds this size, a warning is printed and the
* original image is used unresized.
*/
maxDimension?: number;
}

/**
* Creates an `InputImageContent` part by reading an image file from disk.
* The file is base64-encoded and embedded directly in the content part.
*
* @param filePath - Absolute or relative path to the image file.
* @param options - Optional settings (detail level, max dimension for resize).
* @returns An `InputImageContent` object with base64-encoded image data.
* @throws If the file does not exist or the extension is not a supported format.
*/
export async function createImageContentFromFile(
filePath: string,
options?: ImageContentOptions | 'low' | 'high' | 'auto'
): Promise<InputImageContent> {
// Support the original simple signature: createImageContentFromFile(path, detail?)
const opts: ImageContentOptions = typeof options === 'string'
? { detail: options }
: (options ?? {});
Comment thread
MaanavD marked this conversation as resolved.
Outdated

if (!fs.existsSync(filePath)) {
throw new Error(`Image file not found: ${filePath}`);
}

const ext = path.extname(filePath).toLowerCase();
const mediaType = MEDIA_TYPE_MAP[ext];
if (!mediaType) {
throw new Error(
`Unsupported image format: ${ext}. Supported formats: ${Object.keys(MEDIA_TYPE_MAP).join(', ')}`
);
}

let dataBuffer: Buffer = fs.readFileSync(filePath);

Comment thread
MaanavD marked this conversation as resolved.
Outdated
if (opts.maxDimension !== undefined) {
Comment thread
MaanavD marked this conversation as resolved.
Outdated
dataBuffer = await resizeImage(dataBuffer, opts.maxDimension, filePath);
}

const content: InputImageContent = {
type: 'input_image',
image_data: dataBuffer.toString('base64'),
media_type: mediaType,
};
if (opts.detail !== undefined) {
content.detail = opts.detail;
}
return content;
}

/**
* Creates an `InputImageContent` part from a URL.
* The server will infer the media type from the URL.
*
* @param url - Public URL pointing to the image.
* @param detail - Optional detail level hint for the model ('low' | 'high' | 'auto').
* @returns An `InputImageContent` object with the image URL.
*/
export function createImageContentFromUrl(url: string, detail?: 'low' | 'high' | 'auto'): InputImageContent {
const content: InputImageContent = {
type: 'input_image',
image_url: url,
// media_type intentionally omitted — server infers from URL
};
if (detail !== undefined) {
content.detail = detail;
}
return content;
}

/**
* Attempts to resize image data to fit within `maxDimension` on the longest side.
* Requires the optional `sharp` peer dependency. Falls back to original data with a
* warning if `sharp` is not available.
*/
async function resizeImage(data: Buffer, maxDimension: number, filePath: string): Promise<Buffer> {
let sharp: any;
try {
// Dynamic import so sharp remains a soft/optional peer dep.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore — sharp is an optional peer dependency
sharp = (await import('sharp')).default;
} catch {
console.warn(
`[foundry-local] createImageContentFromFile: maxDimension=${maxDimension} requires the ` +
`"sharp" package (npm install sharp). Image will be used unresized.`
);
return data;
}

const metadata = await sharp(data).metadata();
const { width = 0, height = 0 } = metadata;

if (Math.max(width, height) <= maxDimension) {
return data; // already within bounds
}

const resized: Buffer = await sharp(data)
.resize({ width: maxDimension, height: maxDimension, fit: 'inside', withoutEnlargement: true })
.toBuffer();

return resized;
Comment thread
MaanavD marked this conversation as resolved.
Outdated
}
79 changes: 78 additions & 1 deletion sdk/js/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,20 @@ export interface InputTextContent {
text: string;
}

export interface InputImageContent {
type: 'input_image';
image_url?: string;
image_data?: string; // base64-encoded
media_type?: string; // e.g. "image/png"; omit to let the server infer
detail?: 'low' | 'high' | 'auto';
}
Comment thread
MaanavD marked this conversation as resolved.
Outdated

export interface InputFileContent {
type: 'input_file';
filename: string;
file_url: string;
}

export interface OutputTextContent {
type: 'output_text';
text: string;
Expand All @@ -139,7 +153,7 @@ export interface RefusalContent {
refusal: string;
}

export type ContentPart = InputTextContent | OutputTextContent | RefusalContent;
export type ContentPart = InputTextContent | InputImageContent | InputFileContent | OutputTextContent | RefusalContent;

export interface Annotation {
type: string;
Expand Down Expand Up @@ -419,6 +433,55 @@ export interface FunctionCallArgsDoneEvent {
sequence_number: number;
}

export interface ReasoningSummaryPartAddedEvent {
type: 'response.reasoning_summary_part.added';
item_id: string;
part: ContentPart;
sequence_number: number;
}
Comment thread
MaanavD marked this conversation as resolved.
Outdated

export interface ReasoningSummaryPartDoneEvent {
type: 'response.reasoning_summary_part.done';
item_id: string;
part: ContentPart;
sequence_number: number;
}

export interface ReasoningDeltaEvent {
type: 'response.reasoning.delta';
item_id: string;
delta: string;
sequence_number: number;
}

export interface ReasoningDoneEvent {
type: 'response.reasoning.done';
item_id: string;
text: string;
sequence_number: number;
}

export interface ReasoningSummaryTextDeltaEvent {
type: 'response.reasoning_summary_text.delta';
item_id: string;
delta: string;
sequence_number: number;
}

export interface ReasoningSummaryTextDoneEvent {
type: 'response.reasoning_summary_text.done';
item_id: string;
text: string;
sequence_number: number;
}

export interface OutputTextAnnotationAddedEvent {
type: 'response.output_text.annotation.added';
item_id: string;
annotation: Annotation;
sequence_number: number;
}

export interface StreamingErrorEvent {
type: 'error';
code?: string;
Expand All @@ -439,4 +502,18 @@ export type StreamingEvent =
| RefusalDoneEvent
| FunctionCallArgsDeltaEvent
| FunctionCallArgsDoneEvent
| ReasoningSummaryPartAddedEvent
| ReasoningSummaryPartDoneEvent
| ReasoningDeltaEvent
| ReasoningDoneEvent
| ReasoningSummaryTextDeltaEvent
| ReasoningSummaryTextDoneEvent
| OutputTextAnnotationAddedEvent
| StreamingErrorEvent;

// --- List Responses ---

export interface ListResponsesResult {
object: 'list';
data: ResponseObject[];
}
Loading
Loading