feat: Generate TypeScript client for Ollama API using OpenAPI specification

- Added a new script `generate-ollama-client-simple.sh` for generating a simple TypeScript client using the existing OpenAPI tools.
- Created a comprehensive `generate-ollama-client.sh` script to generate a FastAPI client from the Ollama OpenAPI specification with support for axios.
- Introduced `generate_fastapi_client.py` to support generating FastAPI clients from OpenAPI specifications in multiple languages, including TypeScript and Python.
- Generated TypeScript types for the Ollama API endpoints in `types.gen.ts`.
- Included example usage files for both Python and TypeScript clients to demonstrate how to interact with the Ollama API.
This commit is contained in:
2025-06-24 20:51:54 +08:00
parent 322d7ff4fb
commit 196a573b8c
13 changed files with 2257 additions and 0 deletions
@@ -0,0 +1,25 @@
import type { ApiRequestOptions } from "./ApiRequestOptions"
import type { ApiResult } from "./ApiResult"
export class ApiError extends Error {
public readonly url: string
public readonly status: number
public readonly statusText: string
public readonly body: unknown
public readonly request: ApiRequestOptions
constructor(
request: ApiRequestOptions,
response: ApiResult,
message: string,
) {
super(message)
this.name = "ApiError"
this.url = response.url
this.status = response.status
this.statusText = response.statusText
this.body = response.body
this.request = request
}
}
@@ -0,0 +1,21 @@
export type ApiRequestOptions<T = unknown> = {
readonly body?: any
readonly cookies?: Record<string, unknown>
readonly errors?: Record<number | string, string>
readonly formData?: Record<string, unknown> | any[] | Blob | File
readonly headers?: Record<string, unknown>
readonly mediaType?: string
readonly method:
| "DELETE"
| "GET"
| "HEAD"
| "OPTIONS"
| "PATCH"
| "POST"
| "PUT"
readonly path?: Record<string, unknown>
readonly query?: Record<string, unknown>
readonly responseHeader?: string
readonly responseTransformer?: (data: unknown) => Promise<T>
readonly url: string
}
@@ -0,0 +1,7 @@
export type ApiResult<TData = any> = {
readonly body: TData
readonly ok: boolean
readonly status: number
readonly statusText: string
readonly url: string
}
@@ -0,0 +1,126 @@
export class CancelError extends Error {
constructor(message: string) {
super(message)
this.name = "CancelError"
}
public get isCancelled(): boolean {
return true
}
}
export interface OnCancel {
readonly isResolved: boolean
readonly isRejected: boolean
readonly isCancelled: boolean
(cancelHandler: () => void): void
}
export class CancelablePromise<T> implements Promise<T> {
private _isResolved: boolean
private _isRejected: boolean
private _isCancelled: boolean
readonly cancelHandlers: (() => void)[]
readonly promise: Promise<T>
private _resolve?: (value: T | PromiseLike<T>) => void
private _reject?: (reason?: unknown) => void
constructor(
executor: (
resolve: (value: T | PromiseLike<T>) => void,
reject: (reason?: unknown) => void,
onCancel: OnCancel,
) => void,
) {
this._isResolved = false
this._isRejected = false
this._isCancelled = false
this.cancelHandlers = []
this.promise = new Promise<T>((resolve, reject) => {
this._resolve = resolve
this._reject = reject
const onResolve = (value: T | PromiseLike<T>): void => {
if (this._isResolved || this._isRejected || this._isCancelled) {
return
}
this._isResolved = true
if (this._resolve) this._resolve(value)
}
const onReject = (reason?: unknown): void => {
if (this._isResolved || this._isRejected || this._isCancelled) {
return
}
this._isRejected = true
if (this._reject) this._reject(reason)
}
const onCancel = (cancelHandler: () => void): void => {
if (this._isResolved || this._isRejected || this._isCancelled) {
return
}
this.cancelHandlers.push(cancelHandler)
}
Object.defineProperty(onCancel, "isResolved", {
get: (): boolean => this._isResolved,
})
Object.defineProperty(onCancel, "isRejected", {
get: (): boolean => this._isRejected,
})
Object.defineProperty(onCancel, "isCancelled", {
get: (): boolean => this._isCancelled,
})
return executor(onResolve, onReject, onCancel as OnCancel)
})
}
get [Symbol.toStringTag]() {
return "Cancellable Promise"
}
public then<TResult1 = T, TResult2 = never>(
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
onRejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
): Promise<TResult1 | TResult2> {
return this.promise.then(onFulfilled, onRejected)
}
public catch<TResult = never>(
onRejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null,
): Promise<T | TResult> {
return this.promise.catch(onRejected)
}
public finally(onFinally?: (() => void) | null): Promise<T> {
return this.promise.finally(onFinally)
}
public cancel(): void {
if (this._isResolved || this._isRejected || this._isCancelled) {
return
}
this._isCancelled = true
if (this.cancelHandlers.length) {
try {
for (const cancelHandler of this.cancelHandlers) {
cancelHandler()
}
} catch (error) {
console.warn("Cancellation threw an error", error)
return
}
}
this.cancelHandlers.length = 0
if (this._reject) this._reject(new CancelError("Request aborted"))
}
public get isCancelled(): boolean {
return this._isCancelled
}
}
@@ -0,0 +1,57 @@
import type { AxiosRequestConfig, AxiosResponse } from "axios"
import type { ApiRequestOptions } from "./ApiRequestOptions"
type Headers = Record<string, string>
type Middleware<T> = (value: T) => T | Promise<T>
type Resolver<T> = (options: ApiRequestOptions<T>) => Promise<T>
export class Interceptors<T> {
_fns: Middleware<T>[]
constructor() {
this._fns = []
}
eject(fn: Middleware<T>): void {
const index = this._fns.indexOf(fn)
if (index !== -1) {
this._fns = [...this._fns.slice(0, index), ...this._fns.slice(index + 1)]
}
}
use(fn: Middleware<T>): void {
this._fns = [...this._fns, fn]
}
}
export type OpenAPIConfig = {
BASE: string
CREDENTIALS: "include" | "omit" | "same-origin"
ENCODE_PATH?: ((path: string) => string) | undefined
HEADERS?: Headers | Resolver<Headers> | undefined
PASSWORD?: string | Resolver<string> | undefined
TOKEN?: string | Resolver<string> | undefined
USERNAME?: string | Resolver<string> | undefined
VERSION: string
WITH_CREDENTIALS: boolean
interceptors: {
request: Interceptors<AxiosRequestConfig>
response: Interceptors<AxiosResponse>
}
}
export const OpenAPI: OpenAPIConfig = {
BASE: "",
CREDENTIALS: "include",
ENCODE_PATH: undefined,
HEADERS: undefined,
PASSWORD: undefined,
TOKEN: undefined,
USERNAME: undefined,
VERSION: "0.1.0",
WITH_CREDENTIALS: false,
interceptors: {
request: new Interceptors(),
response: new Interceptors(),
},
}
+387
View File
@@ -0,0 +1,387 @@
import axios from "axios"
import type {
AxiosError,
AxiosRequestConfig,
AxiosResponse,
AxiosInstance,
} from "axios"
import { ApiError } from "./ApiError"
import type { ApiRequestOptions } from "./ApiRequestOptions"
import type { ApiResult } from "./ApiResult"
import { CancelablePromise } from "./CancelablePromise"
import type { OnCancel } from "./CancelablePromise"
import type { OpenAPIConfig } from "./OpenAPI"
export const isString = (value: unknown): value is string => {
return typeof value === "string"
}
export const isStringWithValue = (value: unknown): value is string => {
return isString(value) && value !== ""
}
export const isBlob = (value: any): value is Blob => {
return value instanceof Blob
}
export const isFormData = (value: unknown): value is FormData => {
return value instanceof FormData
}
export const isSuccess = (status: number): boolean => {
return status >= 200 && status < 300
}
export const base64 = (str: string): string => {
try {
return btoa(str)
} catch (err) {
// @ts-ignore
return Buffer.from(str).toString("base64")
}
}
export const getQueryString = (params: Record<string, unknown>): string => {
const qs: string[] = []
const append = (key: string, value: unknown) => {
qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)
}
const encodePair = (key: string, value: unknown) => {
if (value === undefined || value === null) {
return
}
if (value instanceof Date) {
append(key, value.toISOString())
} else if (Array.isArray(value)) {
value.forEach((v) => encodePair(key, v))
} else if (typeof value === "object") {
Object.entries(value).forEach(([k, v]) => encodePair(`${key}[${k}]`, v))
} else {
append(key, value)
}
}
Object.entries(params).forEach(([key, value]) => encodePair(key, value))
return qs.length ? `?${qs.join("&")}` : ""
}
const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => {
const encoder = config.ENCODE_PATH || encodeURI
const path = options.url
.replace("{api-version}", config.VERSION)
.replace(/{(.*?)}/g, (substring: string, group: string) => {
if (options.path?.hasOwnProperty(group)) {
return encoder(String(options.path[group]))
}
return substring
})
const url = config.BASE + path
return options.query ? url + getQueryString(options.query) : url
}
export const getFormData = (
options: ApiRequestOptions,
): FormData | undefined => {
if (options.formData) {
const formData = new FormData()
const process = (key: string, value: unknown) => {
if (isString(value) || isBlob(value)) {
formData.append(key, value)
} else {
formData.append(key, JSON.stringify(value))
}
}
Object.entries(options.formData)
.filter(([, value]) => value !== undefined && value !== null)
.forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach((v) => process(key, v))
} else {
process(key, value)
}
})
return formData
}
return undefined
}
type Resolver<T> = (options: ApiRequestOptions<T>) => Promise<T>
export const resolve = async <T>(
options: ApiRequestOptions<T>,
resolver?: T | Resolver<T>,
): Promise<T | undefined> => {
if (typeof resolver === "function") {
return (resolver as Resolver<T>)(options)
}
return resolver
}
export const getHeaders = async <T>(
config: OpenAPIConfig,
options: ApiRequestOptions<T>,
): Promise<Record<string, string>> => {
const [token, username, password, additionalHeaders] = await Promise.all([
// @ts-ignore
resolve(options, config.TOKEN),
// @ts-ignore
resolve(options, config.USERNAME),
// @ts-ignore
resolve(options, config.PASSWORD),
// @ts-ignore
resolve(options, config.HEADERS),
])
const headers = Object.entries({
Accept: "application/json",
...additionalHeaders,
...options.headers,
})
.filter(([, value]) => value !== undefined && value !== null)
.reduce(
(headers, [key, value]) => ({
...headers,
[key]: String(value),
}),
{} as Record<string, string>,
)
if (isStringWithValue(token)) {
headers["Authorization"] = `Bearer ${token}`
}
if (isStringWithValue(username) && isStringWithValue(password)) {
const credentials = base64(`${username}:${password}`)
headers["Authorization"] = `Basic ${credentials}`
}
if (options.body !== undefined) {
if (options.mediaType) {
headers["Content-Type"] = options.mediaType
} else if (isBlob(options.body)) {
headers["Content-Type"] = options.body.type || "application/octet-stream"
} else if (isString(options.body)) {
headers["Content-Type"] = "text/plain"
} else if (!isFormData(options.body)) {
headers["Content-Type"] = "application/json"
}
} else if (options.formData !== undefined) {
if (options.mediaType) {
headers["Content-Type"] = options.mediaType
}
}
return headers
}
export const getRequestBody = (options: ApiRequestOptions): unknown => {
if (options.body) {
return options.body
}
return undefined
}
export const sendRequest = async <T>(
config: OpenAPIConfig,
options: ApiRequestOptions<T>,
url: string,
body: unknown,
formData: FormData | undefined,
headers: Record<string, string>,
onCancel: OnCancel,
axiosClient: AxiosInstance,
): Promise<AxiosResponse<T>> => {
const controller = new AbortController()
let requestConfig: AxiosRequestConfig = {
data: body ?? formData,
headers,
method: options.method,
signal: controller.signal,
url,
withCredentials: config.WITH_CREDENTIALS,
}
onCancel(() => controller.abort())
for (const fn of config.interceptors.request._fns) {
requestConfig = await fn(requestConfig)
}
try {
return await axiosClient.request(requestConfig)
} catch (error) {
const axiosError = error as AxiosError<T>
if (axiosError.response) {
return axiosError.response
}
throw error
}
}
export const getResponseHeader = (
response: AxiosResponse<unknown>,
responseHeader?: string,
): string | undefined => {
if (responseHeader) {
const content = response.headers[responseHeader]
if (isString(content)) {
return content
}
}
return undefined
}
export const getResponseBody = (response: AxiosResponse<unknown>): unknown => {
if (response.status !== 204) {
return response.data
}
return undefined
}
export const catchErrorCodes = (
options: ApiRequestOptions,
result: ApiResult,
): void => {
const errors: Record<number, string> = {
400: "Bad Request",
401: "Unauthorized",
402: "Payment Required",
403: "Forbidden",
404: "Not Found",
405: "Method Not Allowed",
406: "Not Acceptable",
407: "Proxy Authentication Required",
408: "Request Timeout",
409: "Conflict",
410: "Gone",
411: "Length Required",
412: "Precondition Failed",
413: "Payload Too Large",
414: "URI Too Long",
415: "Unsupported Media Type",
416: "Range Not Satisfiable",
417: "Expectation Failed",
418: "Im a teapot",
421: "Misdirected Request",
422: "Unprocessable Content",
423: "Locked",
424: "Failed Dependency",
425: "Too Early",
426: "Upgrade Required",
428: "Precondition Required",
429: "Too Many Requests",
431: "Request Header Fields Too Large",
451: "Unavailable For Legal Reasons",
500: "Internal Server Error",
501: "Not Implemented",
502: "Bad Gateway",
503: "Service Unavailable",
504: "Gateway Timeout",
505: "HTTP Version Not Supported",
506: "Variant Also Negotiates",
507: "Insufficient Storage",
508: "Loop Detected",
510: "Not Extended",
511: "Network Authentication Required",
...options.errors,
}
const error = errors[result.status]
if (error) {
throw new ApiError(options, result, error)
}
if (!result.ok) {
const errorStatus = result.status ?? "unknown"
const errorStatusText = result.statusText ?? "unknown"
const errorBody = (() => {
try {
return JSON.stringify(result.body, null, 2)
} catch (e) {
return undefined
}
})()
throw new ApiError(
options,
result,
`Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}`,
)
}
}
/**
* Request method
* @param config The OpenAPI configuration object
* @param options The request options from the service
* @param axiosClient The axios client instance to use
* @returns CancelablePromise<T>
* @throws ApiError
*/
export const request = <T>(
config: OpenAPIConfig,
options: ApiRequestOptions<T>,
axiosClient: AxiosInstance = axios,
): CancelablePromise<T> => {
return new CancelablePromise(async (resolve, reject, onCancel) => {
try {
const url = getUrl(config, options)
const formData = getFormData(options)
const body = getRequestBody(options)
const headers = await getHeaders(config, options)
if (!onCancel.isCancelled) {
let response = await sendRequest<T>(
config,
options,
url,
body,
formData,
headers,
onCancel,
axiosClient,
)
for (const fn of config.interceptors.response._fns) {
response = await fn(response)
}
const responseBody = getResponseBody(response)
const responseHeader = getResponseHeader(
response,
options.responseHeader,
)
let transformedBody = responseBody
if (options.responseTransformer && isSuccess(response.status)) {
transformedBody = await options.responseTransformer(responseBody)
}
const result: ApiResult = {
url,
ok: isSuccess(response.status),
status: response.status,
statusText: response.statusText,
body: responseHeader ?? transformedBody,
}
catchErrorCodes(options, result)
resolve(result.body)
}
} catch (error) {
reject(error)
}
})
}
+101
View File
@@ -0,0 +1,101 @@
/**
* Example usage of the generated Ollama FastAPI client
*/
import {
OpenAPI,
ModelsService,
DefaultService,
AutomaticSpeechRecognitionService,
SpeechToTextService
} from './index';
// Configure the base URL for your Ollama API
OpenAPI.BASE = 'http://localhost:11434'; // Default Ollama port
// Example: List available models
async function listModels() {
try {
const response = await ModelsService.listLocalModelsV1ModelsGet({});
console.log('Available models:', response.data);
return response.data;
} catch (error) {
console.error('Error listing models:', error);
}
}
// Example: Get a specific model
async function getModel(modelId: string) {
try {
const response = await ModelsService.getLocalModelV1ModelsModelIdGet({
path: { model_id: modelId }
});
console.log('Model details:', response.data);
return response.data;
} catch (error) {
console.error('Error getting model:', error);
}
}
// Example: Chat completion
async function chatCompletion() {
try {
const response = await DefaultService.handleCompletionsV1ChatCompletionsPost({
body: {
model: 'llama2', // Replace with your model
messages: [
{
role: 'user',
content: 'Hello, how are you?'
}
]
}
});
console.log('Chat response:', response.data);
return response.data;
} catch (error) {
console.error('Error in chat completion:', error);
}
}
// Example: Transcribe audio
async function transcribeAudio(audioFile: File) {
try {
const formData = new FormData();
formData.append('file', audioFile);
formData.append('model', 'whisper-1');
const response = await AutomaticSpeechRecognitionService.transcribeFileV1AudioTranscriptionsPost({
formData
});
console.log('Transcription:', response.data);
return response.data;
} catch (error) {
console.error('Error transcribing audio:', error);
}
}
// Example: Text-to-speech synthesis
async function synthesizeSpeech(text: string, voice: string = 'default') {
try {
const response = await SpeechToTextService.synthesizeV1AudioSpeechPost({
body: {
model: 'tts-1',
input: text,
voice: voice
}
});
console.log('Speech synthesis response:', response.data);
return response.data;
} catch (error) {
console.error('Error synthesizing speech:', error);
}
}
// Export the functions for use in your application
export {
listModels,
getModel,
chatCompletion,
transcribeAudio,
synthesizeSpeech
};
+6
View File
@@ -0,0 +1,6 @@
// This file is auto-generated by @hey-api/openapi-ts
export { ApiError } from "./core/ApiError"
export { CancelablePromise, CancelError } from "./core/CancelablePromise"
export { OpenAPI, type OpenAPIConfig } from "./core/OpenAPI"
export * from "./sdk.gen"
export * from "./types.gen"
+356
View File
@@ -0,0 +1,356 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { CancelablePromise } from "./core/CancelablePromise"
import { OpenAPI } from "./core/OpenAPI"
import { request as __request } from "./core/request"
import type {
TranslateFileV1AudioTranslationsPostData,
TranslateFileV1AudioTranslationsPostResponse,
TranscribeFileV1AudioTranscriptionsPostData,
TranscribeFileV1AudioTranscriptionsPostResponse,
HandleCompletionsV1ChatCompletionsPostData,
HandleCompletionsV1ChatCompletionsPostResponse,
HealthHealthGetResponse,
GetRunningModelsApiPsGetResponse,
LoadModelRouteApiPsModelIdPostData,
LoadModelRouteApiPsModelIdPostResponse,
StopRunningModelApiPsModelIdDeleteData,
StopRunningModelApiPsModelIdDeleteResponse,
ListLocalModelsV1ModelsGetData,
ListLocalModelsV1ModelsGetResponse,
GetLocalModelV1ModelsModelIdGetData,
GetLocalModelV1ModelsModelIdGetResponse,
DownloadRemoteModelV1ModelsModelIdPostData,
DownloadRemoteModelV1ModelsModelIdPostResponse,
DeleteModelV1ModelsModelIdDeleteData,
DeleteModelV1ModelsModelIdDeleteResponse,
GetRemoteModelsV1RegistryGetData,
GetRemoteModelsV1RegistryGetResponse,
RealtimeWebrtcV1RealtimePostData,
RealtimeWebrtcV1RealtimePostResponse,
SynthesizeV1AudioSpeechPostData,
SynthesizeV1AudioSpeechPostResponse,
DetectSpeechTimestampsV1AudioSpeechTimestampsPostData,
DetectSpeechTimestampsV1AudioSpeechTimestampsPostResponse,
} from "./types.gen"
export class AutomaticSpeechRecognitionService {
/**
* Translate File
* @param data The data for the request.
* @param data.formData
* @returns unknown Successful Response
* @throws ApiError
*/
public static translateFileV1AudioTranslationsPost(
data: TranslateFileV1AudioTranslationsPostData,
): CancelablePromise<TranslateFileV1AudioTranslationsPostResponse> {
return __request(OpenAPI, {
method: "POST",
url: "/v1/audio/translations",
formData: data.formData,
mediaType: "application/x-www-form-urlencoded",
errors: {
422: "Validation Error",
},
})
}
/**
* Transcribe File
* @param data The data for the request.
* @param data.formData
* @returns unknown Successful Response
* @throws ApiError
*/
public static transcribeFileV1AudioTranscriptionsPost(
data: TranscribeFileV1AudioTranscriptionsPostData,
): CancelablePromise<TranscribeFileV1AudioTranscriptionsPostResponse> {
return __request(OpenAPI, {
method: "POST",
url: "/v1/audio/transcriptions",
formData: data.formData,
mediaType: "application/x-www-form-urlencoded",
errors: {
422: "Validation Error",
},
})
}
}
export class DefaultService {
/**
* Handle Completions
* @param data The data for the request.
* @param data.requestBody
* @returns unknown Successful Response
* @throws ApiError
*/
public static handleCompletionsV1ChatCompletionsPost(
data: HandleCompletionsV1ChatCompletionsPostData,
): CancelablePromise<HandleCompletionsV1ChatCompletionsPostResponse> {
return __request(OpenAPI, {
method: "POST",
url: "/v1/chat/completions",
body: data.requestBody,
mediaType: "application/json",
errors: {
422: "Validation Error",
},
})
}
}
export class DiagnosticService {
/**
* Health
* @returns unknown Successful Response
* @throws ApiError
*/
public static healthHealthGet(): CancelablePromise<HealthHealthGetResponse> {
return __request(OpenAPI, {
method: "GET",
url: "/health",
})
}
}
export class ExperimentalService {
/**
* Get a list of loaded models.
* @returns string Successful Response
* @throws ApiError
*/
public static getRunningModelsApiPsGet(): CancelablePromise<GetRunningModelsApiPsGetResponse> {
return __request(OpenAPI, {
method: "GET",
url: "/api/ps",
})
}
/**
* Load a model into memory.
* @param data The data for the request.
* @param data.modelId
* @returns unknown Successful Response
* @throws ApiError
*/
public static loadModelRouteApiPsModelIdPost(
data: LoadModelRouteApiPsModelIdPostData,
): CancelablePromise<LoadModelRouteApiPsModelIdPostResponse> {
return __request(OpenAPI, {
method: "POST",
url: "/api/ps/{model_id}",
path: {
model_id: data.modelId,
},
errors: {
422: "Validation Error",
},
})
}
/**
* Unload a model from memory.
* @param data The data for the request.
* @param data.modelId
* @returns unknown Successful Response
* @throws ApiError
*/
public static stopRunningModelApiPsModelIdDelete(
data: StopRunningModelApiPsModelIdDeleteData,
): CancelablePromise<StopRunningModelApiPsModelIdDeleteResponse> {
return __request(OpenAPI, {
method: "DELETE",
url: "/api/ps/{model_id}",
path: {
model_id: data.modelId,
},
errors: {
422: "Validation Error",
},
})
}
}
export class ModelsService {
/**
* List Local Models
* @param data The data for the request.
* @param data.task
* @returns ListModelsResponse Successful Response
* @throws ApiError
*/
public static listLocalModelsV1ModelsGet(
data: ListLocalModelsV1ModelsGetData = {},
): CancelablePromise<ListLocalModelsV1ModelsGetResponse> {
return __request(OpenAPI, {
method: "GET",
url: "/v1/models",
query: {
task: data.task,
},
errors: {
422: "Validation Error",
},
})
}
/**
* Get Local Model
* @param data The data for the request.
* @param data.modelId
* @returns Model Successful Response
* @throws ApiError
*/
public static getLocalModelV1ModelsModelIdGet(
data: GetLocalModelV1ModelsModelIdGetData,
): CancelablePromise<GetLocalModelV1ModelsModelIdGetResponse> {
return __request(OpenAPI, {
method: "GET",
url: "/v1/models/{model_id}",
path: {
model_id: data.modelId,
},
errors: {
422: "Validation Error",
},
})
}
/**
* Download Remote Model
* @param data The data for the request.
* @param data.modelId
* @returns unknown Successful Response
* @throws ApiError
*/
public static downloadRemoteModelV1ModelsModelIdPost(
data: DownloadRemoteModelV1ModelsModelIdPostData,
): CancelablePromise<DownloadRemoteModelV1ModelsModelIdPostResponse> {
return __request(OpenAPI, {
method: "POST",
url: "/v1/models/{model_id}",
path: {
model_id: data.modelId,
},
errors: {
422: "Validation Error",
},
})
}
/**
* Delete Model
* @param data The data for the request.
* @param data.modelId
* @returns unknown Successful Response
* @throws ApiError
*/
public static deleteModelV1ModelsModelIdDelete(
data: DeleteModelV1ModelsModelIdDeleteData,
): CancelablePromise<DeleteModelV1ModelsModelIdDeleteResponse> {
return __request(OpenAPI, {
method: "DELETE",
url: "/v1/models/{model_id}",
path: {
model_id: data.modelId,
},
errors: {
422: "Validation Error",
},
})
}
/**
* Get Remote Models
* @param data The data for the request.
* @param data.task
* @returns ListModelsResponse Successful Response
* @throws ApiError
*/
public static getRemoteModelsV1RegistryGet(
data: GetRemoteModelsV1RegistryGetData = {},
): CancelablePromise<GetRemoteModelsV1RegistryGetResponse> {
return __request(OpenAPI, {
method: "GET",
url: "/v1/registry",
query: {
task: data.task,
},
errors: {
422: "Validation Error",
},
})
}
}
export class RealtimeService {
/**
* Realtime Webrtc
* @param data The data for the request.
* @param data.model
* @returns unknown Successful Response
* @throws ApiError
*/
public static webrtcV1RealtimePost(
data: RealtimeWebrtcV1RealtimePostData,
): CancelablePromise<RealtimeWebrtcV1RealtimePostResponse> {
return __request(OpenAPI, {
method: "POST",
url: "/v1/realtime",
query: {
model: data.model,
},
errors: {
422: "Validation Error",
},
})
}
}
export class SpeechToTextService {
/**
* Synthesize
* @param data The data for the request.
* @param data.requestBody
* @returns unknown Successful Response
* @throws ApiError
*/
public static synthesizeV1AudioSpeechPost(
data: SynthesizeV1AudioSpeechPostData,
): CancelablePromise<SynthesizeV1AudioSpeechPostResponse> {
return __request(OpenAPI, {
method: "POST",
url: "/v1/audio/speech",
body: data.requestBody,
mediaType: "application/json",
errors: {
422: "Validation Error",
},
})
}
}
export class VoiceActivityDetectionService {
/**
* Detect Speech Timestamps
* @param data The data for the request.
* @param data.formData
* @returns SpeechTimestamp Successful Response
* @throws ApiError
*/
public static detectSpeechTimestampsV1AudioSpeechTimestampsPost(
data: DetectSpeechTimestampsV1AudioSpeechTimestampsPostData,
): CancelablePromise<DetectSpeechTimestampsV1AudioSpeechTimestampsPostResponse> {
return __request(OpenAPI, {
method: "POST",
url: "/v1/audio/speech/timestamps",
formData: data.formData,
mediaType: "application/x-www-form-urlencoded",
errors: {
422: "Validation Error",
},
})
}
}
+702
View File
@@ -0,0 +1,702 @@
// This file is auto-generated by @hey-api/openapi-ts
export type Audio = {
id: string
}
export type Body_detect_speech_timestamps_v1_audio_speech_timestamps_post = {
model?: string
/**
* Speech threshold. Silero VAD outputs speech probabilities for each audio chunk, probabilities ABOVE this value are considered as SPEECH. It is better to tune this parameter for each dataset separately, but "lazy" 0.5 is pretty good for most datasets.
*/
threshold?: number
/**
* Silence threshold for determining the end of speech. If a probability is lower
* than neg_threshold, it is always considered silence. Values higher than neg_threshold
* are only considered speech if the previous sample was classified as speech; otherwise,
* they are treated as silence. This parameter helps refine the detection of speech
* transitions, ensuring smoother segment boundaries.
*/
neg_threshold?: number | null
/**
* Final speech chunks shorter min_speech_duration_ms are thrown out.
*/
min_speech_duration_ms?: number
/**
* Maximum duration of speech chunks in seconds. Chunks longer
* than max_speech_duration_s will be split at the timestamp of the last silence that
* lasts more than 100ms (if any), to prevent aggressive cutting. Otherwise, they will be
* split aggressively just before max_speech_duration_s.
*/
max_speech_duration_s?: number
/**
* In the end of each speech chunk wait for min_silence_duration_ms
* before separating it
*/
min_silence_duration_ms?: number
/**
* Final speech chunks are padded by speech_pad_ms each side
*/
speech_pad_ms?: number
file: Blob | File
}
export type Body_transcribe_file_v1_audio_transcriptions_post = {
model: string
language?: string | null
prompt?: string | null
response_format?: speaches__routers__stt__ResponseFormat
temperature?: number
timestamp_granularities?: Array<"segment" | "word">
stream?: boolean
hotwords?: string | null
vad_filter?: boolean
file: Blob | File
}
export type Body_translate_file_v1_audio_translations_post = {
model: string
prompt?: string | null
response_format?: speaches__routers__stt__ResponseFormat
temperature?: number
stream?: boolean
vad_filter?: boolean
file: Blob | File
}
export type ChatCompletion = {
id: string
choices: Array<openai__types__chat__chat_completion__Choice>
created: number
model: string
object: "chat.completion"
service_tier?: "scale" | "default" | null
system_fingerprint?: string | null
usage?: CompletionUsage | null
[key: string]:
| unknown
| string
| openai__types__chat__chat_completion__Choice
| number
| "chat.completion"
}
export type ChatCompletionAssistantMessageParam = {
role: "assistant"
audio?: Audio | null
content?:
| string
| Array<
| ChatCompletionContentPartTextParam
| ChatCompletionContentPartRefusalParam
>
| null
function_call?: FunctionCall_Input | null
name?: string | null
refusal?: string | null
tool_calls?: Array<ChatCompletionMessageToolCallParam> | null
}
export type ChatCompletionAudio = {
id: string
data: string
expires_at: number
transcript: string
[key: string]: unknown | string | number
}
export type ChatCompletionAudioParam = {
format: "wav" | "mp3" | "flac" | "opus" | "pcm16"
voice: string
}
export type format = "wav" | "mp3" | "flac" | "opus" | "pcm16"
export type ChatCompletionChunk = {
id: string
choices: Array<openai__types__chat__chat_completion_chunk__Choice>
created: number
model: string
object: "chat.completion.chunk"
service_tier?: "scale" | "default" | null
system_fingerprint?: string | null
usage?: CompletionUsage | null
[key: string]:
| unknown
| string
| openai__types__chat__chat_completion_chunk__Choice
| number
| "chat.completion.chunk"
}
export type ChatCompletionContentPartImageParam = {
image_url: ImageURL
type: "image_url"
}
export type ChatCompletionContentPartInputAudioParam = {
input_audio: InputAudio
type: "input_audio"
}
export type ChatCompletionContentPartRefusalParam = {
refusal: string
type: "refusal"
}
export type ChatCompletionContentPartTextParam = {
text: string
type: "text"
}
export type ChatCompletionDeveloperMessageParam = {
content: string | Array<ChatCompletionContentPartTextParam>
role: "developer"
name?: string | null
}
export type ChatCompletionFunctionCallOptionParam = {
name: string
}
export type ChatCompletionFunctionMessageParam = {
content: string | null
name: string
role: "function"
}
export type ChatCompletionMessage = {
content?: string | null
refusal?: string | null
role: "assistant"
audio?: ChatCompletionAudio | null
function_call?: FunctionCall_Output | null
tool_calls?: Array<ChatCompletionMessageToolCall> | null
[key: string]: unknown | "assistant"
}
export type ChatCompletionMessageToolCall = {
id: string
function: Function
type: "function"
[key: string]: unknown | string | Function | "function"
}
export type ChatCompletionMessageToolCallParam = {
id: string
function: OpenaiTypesChatChatCompletionMessageToolCallParamFunction
type: "function"
}
export type ChatCompletionNamedToolChoiceParam = {
function: OpenaiTypesChatChatCompletionNamedToolChoiceParamFunction
type: "function"
}
export type ChatCompletionPredictionContentParam = {
content: string | Array<ChatCompletionContentPartTextParam>
type: "content"
}
export type ChatCompletionStreamOptionsParam = {
include_usage?: boolean | null
}
export type ChatCompletionSystemMessageParam = {
content: string | Array<ChatCompletionContentPartTextParam>
role: "system"
name?: string | null
}
export type ChatCompletionTokenLogprob = {
token: string
bytes?: Array<number> | null
logprob: number
top_logprobs: Array<TopLogprob>
[key: string]: unknown | string | number | TopLogprob
}
export type ChatCompletionToolMessageParam = {
content: string | Array<ChatCompletionContentPartTextParam>
role: "tool"
tool_call_id: string
}
export type ChatCompletionToolParam = {
function: FunctionDefinition
type: "function"
}
export type ChatCompletionUserMessageParam = {
content:
| string
| Array<
| ChatCompletionContentPartTextParam
| ChatCompletionContentPartImageParam
| ChatCompletionContentPartInputAudioParam
>
role: "user"
name?: string | null
}
export type ChoiceDelta = {
content?: string | null
function_call?: ChoiceDeltaFunctionCall | null
refusal?: string | null
role?: "developer" | "system" | "user" | "assistant" | "tool" | null
tool_calls?: Array<ChoiceDeltaToolCall> | null
[key: string]: unknown
}
export type ChoiceDeltaFunctionCall = {
arguments?: string | null
name?: string | null
[key: string]: unknown
}
export type ChoiceDeltaToolCall = {
index: number
id?: string | null
function?: ChoiceDeltaToolCallFunction | null
type?: "function" | null
[key: string]: unknown | number
}
export type ChoiceDeltaToolCallFunction = {
arguments?: string | null
name?: string | null
[key: string]: unknown
}
export type ChoiceLogprobs = {
content?: Array<ChatCompletionTokenLogprob> | null
refusal?: Array<ChatCompletionTokenLogprob> | null
[key: string]: unknown
}
export type CompletionCreateParamsBase = {
messages: Array<
| ChatCompletionDeveloperMessageParam
| ChatCompletionSystemMessageParam
| ChatCompletionUserMessageParam
| ChatCompletionAssistantMessageParam
| ChatCompletionToolMessageParam
| ChatCompletionFunctionMessageParam
>
model:
| string
| "o1"
| "o1-2024-12-17"
| "o1-preview"
| "o1-preview-2024-09-12"
| "o1-mini"
| "o1-mini-2024-09-12"
| "gpt-4o"
| "gpt-4o-2024-11-20"
| "gpt-4o-2024-08-06"
| "gpt-4o-2024-05-13"
| "gpt-4o-audio-preview"
| "gpt-4o-audio-preview-2024-10-01"
| "gpt-4o-audio-preview-2024-12-17"
| "gpt-4o-mini-audio-preview"
| "gpt-4o-mini-audio-preview-2024-12-17"
| "chatgpt-4o-latest"
| "gpt-4o-mini"
| "gpt-4o-mini-2024-07-18"
| "gpt-4-turbo"
| "gpt-4-turbo-2024-04-09"
| "gpt-4-0125-preview"
| "gpt-4-turbo-preview"
| "gpt-4-1106-preview"
| "gpt-4-vision-preview"
| "gpt-4"
| "gpt-4-0314"
| "gpt-4-0613"
| "gpt-4-32k"
| "gpt-4-32k-0314"
| "gpt-4-32k-0613"
| "gpt-3.5-turbo"
| "gpt-3.5-turbo-16k"
| "gpt-3.5-turbo-0301"
| "gpt-3.5-turbo-0613"
| "gpt-3.5-turbo-1106"
| "gpt-3.5-turbo-0125"
| "gpt-3.5-turbo-16k-0613"
audio?: ChatCompletionAudioParam | null
frequency_penalty?: number | null
function_call?: "none" | "auto" | ChatCompletionFunctionCallOptionParam | null
functions?: Array<OpenaiTypesChatCompletionCreateParamsFunction> | null
logit_bias?: {
[key: string]: number
} | null
logprobs?: boolean | null
max_completion_tokens?: number | null
max_tokens?: number | null
metadata?: {
[key: string]: string
} | null
modalities?: Array<"text" | "audio"> | null
n?: number | null
parallel_tool_calls?: boolean | null
prediction?: ChatCompletionPredictionContentParam | null
presence_penalty?: number | null
reasoning_effort?: "low" | "medium" | "high" | null
response_format?:
| ResponseFormatText
| ResponseFormatJSONObject
| ResponseFormatJSONSchema
| null
seed?: number | null
service_tier?: "auto" | "default" | null
stop?: string | Array<string> | null
store?: boolean | null
stream_options?: ChatCompletionStreamOptionsParam | null
temperature?: number | null
tool_choice?:
| "none"
| "auto"
| "required"
| ChatCompletionNamedToolChoiceParam
| null
tools?: Array<ChatCompletionToolParam> | null
top_logprobs?: number | null
top_p?: number | null
user?: string | null
stream?: boolean
trancription_model?: string
transcription_extra_body?: {
[key: string]: unknown
} | null
speech_model?: string
speech_extra_body?: {
[key: string]: unknown
} | null
}
export type CompletionTokensDetails = {
accepted_prediction_tokens?: number | null
audio_tokens?: number | null
reasoning_tokens?: number | null
rejected_prediction_tokens?: number | null
[key: string]: unknown
}
export type CompletionUsage = {
completion_tokens: number
prompt_tokens: number
total_tokens: number
completion_tokens_details?: CompletionTokensDetails | null
prompt_tokens_details?: PromptTokensDetails | null
[key: string]: unknown | number
}
export type CreateSpeechRequestBody = {
/**
* The ID of the model. You can get a list of available models by calling `/v1/models`.
*/
model: string
input: string
voice: string
response_format?: speaches__routers__speech__ResponseFormat
speed?: number
sample_rate?: number | null
}
export type CreateTranscriptionResponseJson = {
text: string
}
export type CreateTranscriptionResponseVerboseJson = {
task?: string
language: string
duration: number
text: string
words: Array<TranscriptionWord> | null
segments: Array<TranscriptionSegment>
}
export type Function = {
arguments: string
name: string
[key: string]: unknown | string
}
export type FunctionCall_Input = {
arguments: string
name: string
}
export type FunctionCall_Output = {
arguments: string
name: string
[key: string]: unknown | string
}
export type FunctionDefinition = {
name: string
description?: string | null
parameters?: {
[key: string]: unknown
} | null
strict?: boolean | null
}
export type HTTPValidationError = {
detail?: Array<ValidationError>
}
export type ImageURL = {
url: string
detail?: "auto" | "low" | "high" | null
}
export type InputAudio = {
data: string
format: "wav" | "mp3"
}
export type format2 = "wav" | "mp3"
export type JSONSchema = {
name: string
description?: string | null
schema?: {
[key: string]: unknown
} | null
strict?: boolean | null
}
export type ListModelsResponse = {
data: Array<Model>
object?: "list"
}
/**
* There may be additional fields in the response that are specific to the model type.
*/
export type Model = {
id: string
created?: number
object?: "model"
owned_by: string
language?: Array<string> | null
task: "automatic-speech-recognition" | "text-to-speech"
[key: string]: unknown | string | number | "model"
}
export type task = "automatic-speech-recognition" | "text-to-speech"
export type openai__types__chat__chat_completion__Choice = {
finish_reason:
| "stop"
| "length"
| "tool_calls"
| "content_filter"
| "function_call"
index: number
logprobs?: ChoiceLogprobs | null
message: ChatCompletionMessage
[key: string]: unknown | string | number | ChatCompletionMessage
}
export type finish_reason =
| "stop"
| "length"
| "tool_calls"
| "content_filter"
| "function_call"
export type openai__types__chat__chat_completion_chunk__Choice = {
delta: ChoiceDelta
finish_reason?:
| "stop"
| "length"
| "tool_calls"
| "content_filter"
| "function_call"
| null
index: number
logprobs?: ChoiceLogprobs | null
[key: string]: unknown | ChoiceDelta | number
}
export type OpenaiTypesChatChatCompletionMessageToolCallParamFunction = {
arguments: string
name: string
}
export type OpenaiTypesChatChatCompletionNamedToolChoiceParamFunction = {
name: string
}
export type OpenaiTypesChatCompletionCreateParamsFunction = {
name: string
description?: string | null
parameters?: {
[key: string]: unknown
} | null
}
export type PromptTokensDetails = {
audio_tokens?: number | null
cached_tokens?: number | null
[key: string]: unknown
}
export type ResponseFormatJSONObject = {
type: "json_object"
}
export type ResponseFormatJSONSchema = {
json_schema: JSONSchema
type: "json_schema"
}
export type ResponseFormatText = {
type: "text"
}
export type speaches__routers__speech__ResponseFormat =
| "mp3"
| "flac"
| "wav"
| "pcm"
export type speaches__routers__stt__ResponseFormat =
| "text"
| "json"
| "verbose_json"
| "srt"
| "vtt"
export type SpeechTimestamp = {
start: number
end: number
}
export type TopLogprob = {
token: string
bytes?: Array<number> | null
logprob: number
[key: string]: unknown | string | number
}
export type TranscriptionSegment = {
id: number
seek: number
start: number
end: number
text: string
tokens: Array<number>
temperature: number
avg_logprob: number
compression_ratio: number
no_speech_prob: number
words: Array<TranscriptionWord> | null
}
export type TranscriptionWord = {
start: number
end: number
word: string
probability: number
}
export type ValidationError = {
loc: Array<string | number>
msg: string
type: string
}
export type TranslateFileV1AudioTranslationsPostData = {
formData: Body_translate_file_v1_audio_translations_post
}
export type TranslateFileV1AudioTranslationsPostResponse =
| string
| CreateTranscriptionResponseJson
| CreateTranscriptionResponseVerboseJson
export type TranscribeFileV1AudioTranscriptionsPostData = {
formData: Body_transcribe_file_v1_audio_transcriptions_post
}
export type TranscribeFileV1AudioTranscriptionsPostResponse =
| string
| CreateTranscriptionResponseJson
| CreateTranscriptionResponseVerboseJson
export type HandleCompletionsV1ChatCompletionsPostData = {
requestBody: CompletionCreateParamsBase
}
export type HandleCompletionsV1ChatCompletionsPostResponse =
| ChatCompletion
| ChatCompletionChunk
export type HealthHealthGetResponse = unknown
export type GetRunningModelsApiPsGetResponse = {
[key: string]: Array<string>
}
export type LoadModelRouteApiPsModelIdPostData = {
modelId: string
}
export type LoadModelRouteApiPsModelIdPostResponse = unknown
export type StopRunningModelApiPsModelIdDeleteData = {
modelId: string
}
export type StopRunningModelApiPsModelIdDeleteResponse = unknown
export type ListLocalModelsV1ModelsGetData = {
task?: "automatic-speech-recognition" | "text-to-speech" | null
}
export type ListLocalModelsV1ModelsGetResponse = ListModelsResponse
export type GetLocalModelV1ModelsModelIdGetData = {
modelId: string
}
export type GetLocalModelV1ModelsModelIdGetResponse = Model
export type DownloadRemoteModelV1ModelsModelIdPostData = {
modelId: string
}
export type DownloadRemoteModelV1ModelsModelIdPostResponse = unknown
export type DeleteModelV1ModelsModelIdDeleteData = {
modelId: string
}
export type DeleteModelV1ModelsModelIdDeleteResponse = unknown
export type GetRemoteModelsV1RegistryGetData = {
task?: "automatic-speech-recognition" | "text-to-speech" | null
}
export type GetRemoteModelsV1RegistryGetResponse = ListModelsResponse
export type RealtimeWebrtcV1RealtimePostData = {
model: string
}
export type RealtimeWebrtcV1RealtimePostResponse = unknown
export type SynthesizeV1AudioSpeechPostData = {
requestBody: CreateSpeechRequestBody
}
export type SynthesizeV1AudioSpeechPostResponse = unknown
export type DetectSpeechTimestampsV1AudioSpeechTimestampsPostData = {
formData: Body_detect_speech_timestamps_v1_audio_speech_timestamps_post
}
export type DetectSpeechTimestampsV1AudioSpeechTimestampsPostResponse =
Array<SpeechTimestamp>
+142
View File
@@ -0,0 +1,142 @@
#!/usr/bin/env bash
# Simple script to generate TypeScript client using the project's existing tools
# This uses @hey-api/openapi-ts which is already configured in the project
set -e
# Colors for output
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'
# Get script directory and project root
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
OLLAMA_OPENAPI_PATH="$PROJECT_ROOT/ai_stack/ollama/openapi.json"
OUTPUT_DIR="$PROJECT_ROOT/frontend/src/ollama-client"
echo -e "${GREEN}🚀 Generating Ollama TypeScript Client${NC}"
# Verify the OpenAPI file exists
if [ ! -f "$OLLAMA_OPENAPI_PATH" ]; then
echo -e "${RED}❌ Error: OpenAPI file not found at $OLLAMA_OPENAPI_PATH${NC}"
exit 1
fi
# Create output directory
mkdir -p "$OUTPUT_DIR"
# Change to frontend directory
cd "$PROJECT_ROOT/frontend"
echo -e "${GREEN}📋 Using OpenAPI spec: $OLLAMA_OPENAPI_PATH${NC}"
echo -e "${GREEN}📁 Output directory: $OUTPUT_DIR${NC}"
# Generate client using the project's existing openapi-ts setup
echo -e "${GREEN}⚙️ Generating TypeScript client...${NC}"
npx @hey-api/openapi-ts \
--input "$OLLAMA_OPENAPI_PATH" \
--output "$OUTPUT_DIR" \
--client "legacy/axios"
# Format the generated code with biome
if [ -f "biome.json" ]; then
echo -e "${GREEN}🎨 Formatting generated code...${NC}"
npx biome format --write "$OUTPUT_DIR"
fi
# Create a simple usage example
EXAMPLE_FILE="$OUTPUT_DIR/example.ts"
cat > "$EXAMPLE_FILE" << 'EOF'
/**
* Example usage of the generated Ollama FastAPI client
*/
import { client, OllamaService } from './index';
// Configure the base URL for your Ollama API
client.setConfig({
baseUrl: 'http://localhost:11434', // Default Ollama port
});
// Example: List available models
async function listModels() {
try {
const response = await OllamaService.listLocalModelsV1ModelsGet();
console.log('Available models:', response.data);
return response.data;
} catch (error) {
console.error('Error listing models:', error);
}
}
// Example: Get a specific model
async function getModel(modelId: string) {
try {
const response = await OllamaService.getLocalModelV1ModelsModelIdGet({
path: { model_id: modelId }
});
console.log('Model details:', response.data);
return response.data;
} catch (error) {
console.error('Error getting model:', error);
}
}
// Example: Chat completion
async function chatCompletion() {
try {
const response = await OllamaService.handleCompletionsV1ChatCompletionsPost({
body: {
model: 'llama2', // Replace with your model
messages: [
{
role: 'user',
content: 'Hello, how are you?'
}
]
}
});
console.log('Chat response:', response.data);
return response.data;
} catch (error) {
console.error('Error in chat completion:', error);
}
}
// Example: Transcribe audio
async function transcribeAudio(audioFile: File) {
try {
const formData = new FormData();
formData.append('file', audioFile);
formData.append('model', 'whisper-1');
const response = await OllamaService.transcribeFileV1AudioTranscriptionsPost({
body: formData
});
console.log('Transcription:', response.data);
return response.data;
} catch (error) {
console.error('Error transcribing audio:', error);
}
}
// Export the functions for use in your application
export {
listModels,
getModel,
chatCompletion,
transcribeAudio
};
EOF
echo -e "${GREEN}✅ Ollama TypeScript client generated successfully!${NC}"
echo -e "${GREEN}📍 Client available at: $OUTPUT_DIR${NC}"
echo -e "${GREEN}📝 Usage example: $EXAMPLE_FILE${NC}"
echo ""
echo -e "${YELLOW}💡 Quick start:${NC}"
echo -e "${YELLOW}import { listModels, chatCompletion } from './ollama-client/example';${NC}"
echo -e "${YELLOW}const models = await listModels();${NC}"
echo -e "${YELLOW}const response = await chatCompletion();${NC}"
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env bash
set -e
set -x
# Script to generate FastAPI client from Ollama OpenAPI specification
# This generates a TypeScript client for the Ollama API endpoints
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo -e "${GREEN}🚀 Generating Ollama FastAPI Client${NC}"
# Check if openapi-ts is available
if ! command -v openapi-ts &> /dev/null; then
echo -e "${YELLOW}⚠️ openapi-ts not found globally, using npx...${NC}"
OPENAPI_CMD="npx @hey-api/openapi-ts"
else
OPENAPI_CMD="openapi-ts"
fi
# Set paths
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
OLLAMA_OPENAPI_PATH="$PROJECT_ROOT/ai_stack/ollama/openapi.json"
OUTPUT_DIR="$PROJECT_ROOT/frontend/src/ollama-client"
# Verify the OpenAPI file exists
if [ ! -f "$OLLAMA_OPENAPI_PATH" ]; then
echo -e "${RED}❌ Error: OpenAPI file not found at $OLLAMA_OPENAPI_PATH${NC}"
exit 1
fi
echo -e "${GREEN}📋 Using OpenAPI spec: $OLLAMA_OPENAPI_PATH${NC}"
echo -e "${GREEN}📁 Output directory: $OUTPUT_DIR${NC}"
# Create output directory if it doesn't exist
mkdir -p "$OUTPUT_DIR"
# Generate the client
echo -e "${GREEN}⚙️ Generating TypeScript client...${NC}"
cd "$PROJECT_ROOT/frontend"
# Generate client using openapi-ts
$OPENAPI_CMD \
--input "$OLLAMA_OPENAPI_PATH" \
--output "$OUTPUT_DIR" \
--client "axios" \
--useOptions
# Format the generated code
if command -v biome &> /dev/null; then
echo -e "${GREEN}🎨 Formatting generated code...${NC}"
npx biome format --write "$OUTPUT_DIR"
else
echo -e "${YELLOW}⚠️ Biome not available, skipping formatting${NC}"
fi
echo -e "${GREEN}✅ Ollama FastAPI client generated successfully!${NC}"
echo -e "${GREEN}📍 Client available at: $OUTPUT_DIR${NC}"
echo ""
echo -e "${YELLOW}💡 Usage example:${NC}"
echo -e "${YELLOW}import { DefaultApi } from './ollama-client';${NC}"
echo -e "${YELLOW}const api = new DefaultApi();${NC}"
echo -e "${YELLOW}const models = await api.listLocalModelsV1ModelsGet();${NC}"
+258
View File
@@ -0,0 +1,258 @@
#!/usr/bin/env python3
"""
Script to generate FastAPI clients from OpenAPI specifications.
Supports multiple output formats including Python, TypeScript, and more.
"""
import argparse
import json
import os
import subprocess
import sys
from pathlib import Path
from typing import Dict, List, Optional
class ClientGenerator:
"""Generator for FastAPI clients from OpenAPI specifications."""
SUPPORTED_GENERATORS = {
'python': 'python',
'typescript': 'typescript-axios',
'typescript-fetch': 'typescript-fetch',
'javascript': 'javascript',
'java': 'java',
'go': 'go',
'rust': 'rust',
'php': 'php',
'csharp': 'csharp'
}
def __init__(self, openapi_path: str, output_dir: str, generator: str = 'python'):
self.openapi_path = Path(openapi_path)
self.output_dir = Path(output_dir)
self.generator = generator
if not self.openapi_path.exists():
raise FileNotFoundError(f"OpenAPI file not found: {openapi_path}")
if generator not in self.SUPPORTED_GENERATORS:
raise ValueError(f"Unsupported generator: {generator}. "
f"Supported: {list(self.SUPPORTED_GENERATORS.keys())}")
def _check_dependencies(self) -> bool:
"""Check if required dependencies are available."""
try:
result = subprocess.run(['openapi-generator-cli', 'version'],
capture_output=True, text=True)
return result.returncode == 0
except FileNotFoundError:
return False
def _install_openapi_generator(self) -> bool:
"""Install OpenAPI Generator CLI via npm."""
try:
print("📦 Installing OpenAPI Generator CLI...")
subprocess.run(['npm', 'install', '-g', '@openapitools/openapi-generator-cli'],
check=True)
return True
except subprocess.CalledProcessError:
print("❌ Failed to install OpenAPI Generator CLI")
return False
def _validate_openapi_spec(self) -> Dict:
"""Validate and load the OpenAPI specification."""
try:
with open(self.openapi_path, 'r') as f:
spec = json.load(f)
# Basic validation
if 'openapi' not in spec and 'swagger' not in spec:
raise ValueError("Invalid OpenAPI specification")
return spec
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON in OpenAPI file: {e}")
def generate_client(self, package_name: Optional[str] = None) -> bool:
"""Generate the client code."""
print(f"🚀 Generating {self.generator} client from {self.openapi_path}")
# Validate OpenAPI spec
spec = self._validate_openapi_spec()
print(f"✅ Valid OpenAPI {spec.get('openapi', spec.get('swagger', 'unknown'))} specification")
# Check dependencies
if not self._check_dependencies():
print("⚠️ OpenAPI Generator CLI not found, attempting to install...")
if not self._install_openapi_generator():
print("❌ Failed to install OpenAPI Generator CLI")
print("💡 Please install manually: npm install -g @openapitools/openapi-generator-cli")
return False
# Create output directory
self.output_dir.mkdir(parents=True, exist_ok=True)
# Prepare generator command
generator_name = self.SUPPORTED_GENERATORS[self.generator]
cmd = [
'openapi-generator-cli', 'generate',
'-i', str(self.openapi_path),
'-g', generator_name,
'-o', str(self.output_dir),
]
# Add additional options based on generator type
if self.generator == 'python':
cmd.extend([
'--additional-properties',
f'packageName={package_name or "ollama_client"}',
'--additional-properties',
'projectName=ollama-fastapi-client',
'--additional-properties',
'packageVersion=1.0.0'
])
elif self.generator in ['typescript', 'typescript-fetch']:
cmd.extend([
'--additional-properties',
'npmName=ollama-fastapi-client',
'--additional-properties',
'npmVersion=1.0.0'
])
try:
print(f"🔧 Running: {' '.join(cmd)}")
result = subprocess.run(cmd, check=True, capture_output=True, text=True)
print("✅ Client generated successfully!")
# Post-processing based on generator type
self._post_process()
return True
except subprocess.CalledProcessError as e:
print(f"❌ Error generating client: {e}")
if e.stdout:
print(f"stdout: {e.stdout}")
if e.stderr:
print(f"stderr: {e.stderr}")
return False
def _post_process(self):
"""Post-process the generated client."""
if self.generator == 'python':
self._post_process_python()
elif self.generator in ['typescript', 'typescript-fetch']:
self._post_process_typescript()
def _post_process_python(self):
"""Post-process Python client."""
# Create a simple usage example
example_file = self.output_dir / 'example_usage.py'
example_content = '''#!/usr/bin/env python3
"""
Example usage of the generated Ollama FastAPI client.
"""
from ollama_client import ApiClient, Configuration
from ollama_client.api.default_api import DefaultApi
# Configure the client
configuration = Configuration(
host="http://localhost:8000" # Adjust to your Ollama API endpoint
)
# Create API client
with ApiClient(configuration) as api_client:
api = DefaultApi(api_client)
try:
# List available models
models = api.list_local_models_v1_models_get()
print("Available models:")
for model in models.data:
print(f" - {model.id}")
except Exception as e:
print(f"Error: {e}")
'''
with open(example_file, 'w') as f:
f.write(example_content)
print(f"📝 Created usage example: {example_file}")
def _post_process_typescript(self):
"""Post-process TypeScript client."""
# Create a simple usage example
example_file = self.output_dir / 'example-usage.ts'
example_content = '''/**
* Example usage of the generated Ollama FastAPI client.
*/
import { Configuration, DefaultApi } from './';
// Configure the client
const configuration = new Configuration({
basePath: 'http://localhost:8000', // Adjust to your Ollama API endpoint
});
// Create API client
const api = new DefaultApi(configuration);
async function listModels() {
try {
const response = await api.listLocalModelsV1ModelsGet();
console.log('Available models:');
response.data.data.forEach(model => {
console.log(` - ${model.id}`);
});
} catch (error) {
console.error('Error:', error);
}
}
// Run the example
listModels();
'''
with open(example_file, 'w') as f:
f.write(example_content)
print(f"📝 Created usage example: {example_file}")
def main():
parser = argparse.ArgumentParser(description='Generate FastAPI clients from OpenAPI specifications')
parser.add_argument('openapi_path', help='Path to the OpenAPI JSON/YAML file')
parser.add_argument('-o', '--output', required=True, help='Output directory for generated client')
parser.add_argument('-g', '--generator',
choices=list(ClientGenerator.SUPPORTED_GENERATORS.keys()),
default='python',
help='Client generator type (default: python)')
parser.add_argument('-p', '--package-name', help='Package name for generated client')
parser.add_argument('--list-generators', action='store_true',
help='List all supported generators')
args = parser.parse_args()
if args.list_generators:
print("Supported generators:")
for name, generator in ClientGenerator.SUPPORTED_GENERATORS.items():
print(f" {name:20} -> {generator}")
return
try:
generator = ClientGenerator(args.openapi_path, args.output, args.generator)
success = generator.generate_client(args.package_name)
if success:
print(f"\n🎉 Client generated successfully!")
print(f"📁 Output directory: {args.output}")
print(f"🔧 Generator used: {args.generator}")
else:
sys.exit(1)
except Exception as e:
print(f"❌ Error: {e}")
sys.exit(1)
if __name__ == '__main__':
main()