From 3e4f1fb003db766b285ca2fb170d090eed50bf84 Mon Sep 17 00:00:00 2001 From: bijan Date: Mon, 14 Apr 2025 14:21:22 -0400 Subject: [PATCH 1/9] openAI --- packages/api/src/client/base.ts | 9 + packages/api/src/client/client.ts | 11 +- packages/api/src/types/index.ts | 15 ++ packages/api/src/types/schema.ts | 26 +++ packages/api/src/types/types.ts | 169 ++++++++++++++ packages/examples/src/ai.ts | 28 +++ typegen/openapi/index.yaml | 2 + typegen/openapi/services/ai/openapi.yaml | 272 +++++++++++++++++++++++ 8 files changed, 531 insertions(+), 1 deletion(-) diff --git a/packages/api/src/client/base.ts b/packages/api/src/client/base.ts index 135c6dd..9cb8c95 100644 --- a/packages/api/src/client/base.ts +++ b/packages/api/src/client/base.ts @@ -97,6 +97,7 @@ import type { modifyWatchlistAssetsResponse, getTeamAllowanceResponse, getPermissionsResponse, + createChatCompletionOpenAIResponse, } from "../types"; import { LogLevel, type Logger, makeConsoleLogger, createFilteredLogger, noOpLogger } from "../logging"; import type { PaginatedResult, RequestOptions, ClientEventMap, ClientEventType, ClientEventHandler } from "./types"; @@ -113,6 +114,14 @@ export interface AIInterface { */ createChatCompletion(params: createChatCompletionParameters, options?: RequestOptions): Promise; + /** + * Creates a chat completion using OpenAI's API + * @param params Parameters for the chat completion request + * @param options Optional request configuration + * @returns A promise resolving to the chat completion response + */ + createChatCompletionOpenAI(params: createChatCompletionParameters, options?: RequestOptions): Promise; + /** * Extracts entities from text content * @param params Parameters for entity extraction diff --git a/packages/api/src/client/client.ts b/packages/api/src/client/client.ts index 6d433da..3fd217f 100644 --- a/packages/api/src/client/client.ts +++ b/packages/api/src/client/client.ts @@ -1,6 +1,7 @@ import { createChatCompletion, - extractEntities, + extractEntities, + createChatCompletionOpenAI, getNewsFeed, getNewsSources, getNewsFeedAssets, @@ -146,6 +147,7 @@ import type { getWatchlistResponse, updateWatchlistParameters, updateWatchlistResponse, + createChatCompletionOpenAIResponse, } from "../types"; import type { Agent } from "node:http"; import { pick } from "../utils"; @@ -683,6 +685,13 @@ export class MessariClient extends MessariClientBase { body: pick(params, createChatCompletion.bodyParams), options, }), + createChatCompletionOpenAI: (params: createChatCompletionParameters, options?: RequestOptions) => + this.request({ + method: createChatCompletionOpenAI.method, + path: createChatCompletionOpenAI.path(), + body: pick(params, createChatCompletionOpenAI.bodyParams), + options, + }), extractEntities: (params: extractEntitiesParameters, options?: RequestOptions) => this.request({ method: extractEntities.method, diff --git a/packages/api/src/types/index.ts b/packages/api/src/types/index.ts index 304a652..5a10cb4 100644 --- a/packages/api/src/types/index.ts +++ b/packages/api/src/types/index.ts @@ -47,6 +47,21 @@ export const extractEntities = { } as const; +export type createChatCompletionOpenAIResponse = components['schemas']['ChatCompletionResponseOpenAI']; +export type createChatCompletionOpenAIError = components['schemas']['APIError']; + +export type createChatCompletionOpenAIParameters = components['schemas']['ChatCompletionRequest']; + + +export const createChatCompletionOpenAI = { + method: 'POST' as const, + pathParams: [] as const, + queryParams: [] as const, + bodyParams: ['messages', 'verbosity', 'response_format', 'inline_citations', 'stream'] as const, + path: () => '/ai/v1/chat/completions/openai' +} as const; + + export type getAssetsV2Response = components['schemas']['V2AssetListItem'][]; export type getAssetsV2Error = components['schemas']['APIError']; diff --git a/packages/api/src/types/schema.ts b/packages/api/src/types/schema.ts index 6e42df0..e42a477 100644 --- a/packages/api/src/types/schema.ts +++ b/packages/api/src/types/schema.ts @@ -31,22 +31,40 @@ export type AssetV2Link = components['schemas']['AssetV2Link']; export type AssetV2MarketData = components['schemas']['AssetV2MarketData']; +export type Attribution = components['schemas']['Attribution']; + export type Author = components['schemas']['Author']; +export type ChartSource = components['schemas']['ChartSource']; + +export type ChartWidgetEntity = components['schemas']['ChartWidgetEntity']; + +export type ChartWidgetSpecification = components['schemas']['ChartWidgetSpecification']; + export type ChatCompletionMessage = components['schemas']['ChatCompletionMessage']; export type ChatCompletionRequest = components['schemas']['ChatCompletionRequest']; export type ChatCompletionResponse = components['schemas']['ChatCompletionResponse']; +export type ChatCompletionResponseChoiceOpenAI = components['schemas']['ChatCompletionResponseChoiceOpenAI']; + +export type ChatCompletionResponseMessageOpenAI = components['schemas']['ChatCompletionResponseMessageOpenAI']; + export type ChatCompletionResponseMetadata = components['schemas']['ChatCompletionResponseMetadata']; +export type ChatCompletionResponseMetadataV2 = components['schemas']['ChatCompletionResponseMetadataV2']; + +export type ChatCompletionResponseOpenAI = components['schemas']['ChatCompletionResponseOpenAI']; + export type CreateWatchlistRequest = components['schemas']['CreateWatchlistRequest']; export type Document = components['schemas']['Document']; export type DocumentList = components['schemas']['DocumentList']; +export type Domain = components['schemas']['Domain']; + export type Entity = components['schemas']['Entity']; export type EntityType = components['schemas']['EntityType']; @@ -123,6 +141,8 @@ export type PermissionsResponse = components['schemas']['PermissionsResponse']; export type Person = components['schemas']['Person']; +export type PointSchema = components['schemas']['PointSchema']; + export type Project = components['schemas']['Project']; export type ProjectRecapResponse = components['schemas']['ProjectRecapResponse']; @@ -143,12 +163,16 @@ export type Resource = components['schemas']['Resource']; export type SelectedEntity = components['schemas']['SelectedEntity']; +export type Series = components['schemas']['Series']; + export type Source = components['schemas']['Source']; export type SourceList = components['schemas']['SourceList']; export type SourceType = components['schemas']['SourceType']; +export type StandardSource = components['schemas']['StandardSource']; + export type SummaryResponse = components['schemas']['SummaryResponse']; export type Tag = components['schemas']['Tag']; @@ -165,6 +189,8 @@ export type TimeseriesMetadata = components['schemas']['TimeseriesMetadata']; export type TimeseriesPointSchema = components['schemas']['TimeseriesPointSchema']; +export type TimeseriesResult = components['schemas']['TimeseriesResult']; + export type TokenUnlockAllocation = components['schemas']['TokenUnlockAllocation']; export type TokenUnlockData = components['schemas']['TokenUnlockData']; diff --git a/packages/api/src/types/types.ts b/packages/api/src/types/types.ts index 43dd05a..4ec3935 100644 --- a/packages/api/src/types/types.ts +++ b/packages/api/src/types/types.ts @@ -34,6 +34,16 @@ export type paths = { */ post: operations["createChatCompletion"]; }; + "/ai/v1/chat/completions/openai": { + /** + * OpenAI-Compatible Chat Completion + * @description Creates a completion for the chat message in OpenAI-compatible format. + * Supports both streaming and non-streaming responses. + * The last message must be from the user role. + * Response is returned directly without the standard {data: } wrapper. + */ + post: operations["createChatCompletionOpenAI"]; + }; "/ai/v1/classification/extraction": { /** * Entity Extraction @@ -555,6 +565,8 @@ export type components = { */ volume24Hour?: number; }; + /** @description Attribution information (placeholder - add specific properties as needed) */ + Attribution: Record; Author: { /** @description Unique identifier for the author */ id: string; @@ -565,6 +577,41 @@ export type components = { /** @description Name of the author */ name: string; }; + ChartSource: { + /** @description Unique identifier for the citation */ + citationId?: number; + } & components["schemas"]["ChartWidgetSpecification"]; + ChartWidgetEntity: { + /** @description Identifier of the entity */ + entityId: string; + /** @description Type of the entity */ + entityType: string; + }; + ChartWidgetSpecification: { + /** @description Dataset identifier */ + dataset?: string; + /** + * Format: date-time + * @description End time for the chart data + */ + end?: string; + /** @description Array of entities for the chart */ + entities?: components["schemas"]["ChartWidgetEntity"][]; + /** @description Data granularity */ + granularity?: string; + /** @description The ID for the widget */ + id?: number; + /** @description Metric identifier */ + metric?: string; + metricTimeseries?: components["schemas"]["TimeseriesResult"]; + /** + * Format: date-time + * @description Start time for the chart data + */ + start?: string; + /** @description Tier information */ + tier?: string; + }; ChatCompletionMessage: { /** @description The message content */ content: string; @@ -602,10 +649,55 @@ export type components = { /** @description Array of response messages */ messages: components["schemas"]["ChatCompletionMessage"][]; }; + ChatCompletionResponseChoiceOpenAI: { + /** @description Reason the completion finished */ + finish_reason: string; + /** @description Index of the choice in the array */ + index: number; + message: components["schemas"]["ChatCompletionResponseMessageOpenAI"]; + }; + ChatCompletionResponseMessageOpenAI: { + /** @description The message content */ + content: string; + /** + * @description The role of the message sender + * @enum {string} + */ + role: "system" | "user" | "assistant"; + }; ChatCompletionResponseMetadata: { /** @description Current status of the chat completion */ status: string; }; + ChatCompletionResponseMetadataV2: { + /** @description Array of charts referenced in the response */ + charts?: components["schemas"]["ChartSource"][]; + /** @description Array of sources cited in the response */ + cited_sources?: components["schemas"]["StandardSource"][]; + /** @description Current status of the chat completion */ + status: string; + /** + * Format: uuid + * @description Unique trace ID for the request + */ + trace_id: string; + }; + ChatCompletionResponseOpenAI: { + /** @description Array of completion choices */ + choices: components["schemas"]["ChatCompletionResponseChoiceOpenAI"][]; + /** + * Format: int64 + * @description Unix timestamp of when the completion was created + */ + created: number; + /** @description Unique identifier for the completion */ + id: string; + metadata?: components["schemas"]["ChatCompletionResponseMetadataV2"]; + /** @description The model used for completion */ + model: string; + /** @description Object type, always "chat.completion" */ + object: string; + }; CreateWatchlistRequest: { assetIds: string[]; title: string; @@ -637,6 +729,8 @@ export type components = { }; /** @description List of news documents */ DocumentList: components["schemas"]["Document"][]; + /** @description Domain information (placeholder - add specific properties as needed) */ + Domain: Record; Entity: { /** * Format: float @@ -1177,6 +1271,21 @@ export type components = { }; /** @description Person details (to be defined) */ Person: Record; + PointSchema: { + attribution?: components["schemas"]["Attribution"][]; + description?: string; + format?: string; + /** @description Aggregate operation performed for the group */ + group_aggregate_operation?: string; + /** @description Deprecated - Use slug instead */ + id?: string; + is_timestamp?: boolean; + name?: string; + slug?: string; + subcategory?: string; + /** @description Aggregate operation performed for the time bucket */ + time_bucket_aggregate_operation?: string; + }; Project: { /** @description Category of the project */ category?: string; @@ -1363,6 +1472,13 @@ export type components = { name?: string; relevanceScore?: string; }; + Series: { + entity?: { + [key: string]: unknown; + }; + key: string; + points: Record[][]; + }; Source: { /** * Format: uuid @@ -1381,6 +1497,15 @@ export type components = { * @enum {string} */ SourceType: "News" | "Forum" | "Blog"; + StandardSource: { + /** @description Unique identifier for the citation */ + citationId?: number; + domain?: components["schemas"]["Domain"]; + /** @description Title of the source */ + title?: string; + /** @description URL of the source */ + url?: string; + }; /** @description Summary information */ SummaryResponse: { summary?: string; @@ -1428,6 +1553,10 @@ export type components = { /** @description Slug of the metric */ slug: string; }; + TimeseriesResult: { + point_schema: components["schemas"]["PointSchema"][]; + series: components["schemas"]["Series"][]; + }; TokenUnlockAllocation: { allocationRecipientCount?: number; allocations?: { @@ -2013,6 +2142,46 @@ export type operations = { }; }; }; + /** + * OpenAI-Compatible Chat Completion + * @description Creates a completion for the chat message in OpenAI-compatible format. + * Supports both streaming and non-streaming responses. + * The last message must be from the user role. + * Response is returned directly without the standard {data: } wrapper. + */ + createChatCompletionOpenAI: { + parameters: { + header: { + "x-messari-api-key": components["parameters"]["apiKey"]; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ChatCompletionRequest"]; + }; + }; + responses: { + /** @description Client error response */ + "4XX": { + content: { + "application/json": components["schemas"]["APIError"]; + }; + }; + /** @description Successful response */ + 200: { + content: { + "application/json": components["schemas"]["ChatCompletionResponseOpenAI"]; + "text/event-stream": string; + }; + }; + /** @description Server error response */ + 500: { + content: { + "application/json": components["schemas"]["APIError"]; + }; + }; + }; + }; /** * Entity Extraction * @description Extracts entities from the provided text content using AI models and database lookups. diff --git a/packages/examples/src/ai.ts b/packages/examples/src/ai.ts index ad0c047..406e790 100644 --- a/packages/examples/src/ai.ts +++ b/packages/examples/src/ai.ts @@ -114,6 +114,34 @@ async function main() { } catch (error) { console.error("Error calling extractEntities:", error); } + + // OpenAI Chat Completion + try { + console.log("\n--------------------------------"); + console.log("OpenAI Chat Completion"); + console.log("--------------------------------"); + console.log("Sending request..."); + console.log(`"What are the key differences between Bitcoin and Ethereum?"`); + + // Call the createChatCompletionOpenAI endpoint + const response = await client.ai.createChatCompletionOpenAI({ + messages: [ + { + role: "user", + content: "What are the key differences between Bitcoin and Ethereum?", + }, + ], + verbosity: "succinct", + response_format: "plaintext", + inline_citations: false, + stream: false, + }); + + console.log("Response received:"); + console.log(response.choices[0].message.content); + } catch (error) { + console.error("Error calling createChatCompletionOpenAI:", error); + } } main().catch(console.error); diff --git a/typegen/openapi/index.yaml b/typegen/openapi/index.yaml index d133ea4..91be202 100644 --- a/typegen/openapi/index.yaml +++ b/typegen/openapi/index.yaml @@ -55,6 +55,8 @@ paths: $ref: "./services/ai/openapi.yaml#/paths/~1ai~1v1~1chat~1completions" /ai/v1/classification/extraction: $ref: "./services/ai/openapi.yaml#/paths/~1ai~1v1~1classification~1extraction" + /ai/v1/chat/completions/openai: + $ref: "./services/ai/openapi.yaml#/paths/~1ai~1openai~1chat~1completions" # Asset Service Paths /metrics/v2/assets: diff --git a/typegen/openapi/services/ai/openapi.yaml b/typegen/openapi/services/ai/openapi.yaml index f36ed9b..d00dbf6 100644 --- a/typegen/openapi/services/ai/openapi.yaml +++ b/typegen/openapi/services/ai/openapi.yaml @@ -123,6 +123,51 @@ paths: schema: $ref: '../../common/components.yaml#/components/schemas/APIError' + /ai/openai/chat/completions: + post: + operationId: createChatCompletionOpenAI + summary: OpenAI-Compatible Chat Completion + description: | + Creates a completion for the chat message in OpenAI-compatible format. + Supports both streaming and non-streaming responses. + The last message must be from the user role. + Response is returned directly without the standard {data: } wrapper. + tags: + - Chat + security: + - apiKey: [] + parameters: + - $ref: '../../common/parameters.yaml#/components/parameters/apiKey' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ChatCompletionRequest' + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/ChatCompletionResponseOpenAI' + text/event-stream: + schema: + type: string + description: Server-sent events stream for chat completion + '4XX': + description: Client error response + content: + application/json: + schema: + $ref: '../../common/components.yaml#/components/schemas/APIError' + '500': + description: Server error response + content: + application/json: + schema: + $ref: '../../common/components.yaml#/components/schemas/APIError' + components: schemas: ChatCompletionMessage: @@ -295,6 +340,233 @@ components: type: string description: Current status of the extraction request + ChatCompletionResponseOpenAI: + type: object + required: + - id + - object + - created + - model + - choices + properties: + id: + type: string + description: Unique identifier for the completion + object: + type: string + description: Object type, always "chat.completion" + created: + type: integer + format: int64 + description: Unix timestamp of when the completion was created + model: + type: string + description: The model used for completion + choices: + type: array + items: + $ref: '#/components/schemas/ChatCompletionResponseChoiceOpenAI' + description: Array of completion choices + metadata: + $ref: '#/components/schemas/ChatCompletionResponseMetadataV2' + + ChatCompletionResponseChoiceOpenAI: + type: object + required: + - index + - message + - finish_reason + properties: + index: + type: integer + description: Index of the choice in the array + message: + $ref: '#/components/schemas/ChatCompletionResponseMessageOpenAI' + finish_reason: + type: string + description: Reason the completion finished + + ChatCompletionResponseMessageOpenAI: + type: object + required: + - role + - content + properties: + role: + type: string + enum: [system, user, assistant] + description: The role of the message sender + content: + type: string + description: The message content + + ChatCompletionResponseMetadataV2: + type: object + required: + - status + - trace_id + properties: + status: + type: string + description: Current status of the chat completion + trace_id: + type: string + format: uuid + description: Unique trace ID for the request + cited_sources: + type: array + items: + $ref: '#/components/schemas/StandardSource' + description: Array of sources cited in the response + charts: + type: array + items: + $ref: '#/components/schemas/ChartSource' + description: Array of charts referenced in the response + + StandardSource: + type: object + properties: + citationId: + type: integer + description: Unique identifier for the citation + domain: + $ref: '#/components/schemas/Domain' + title: + type: string + description: Title of the source + url: + type: string + description: URL of the source + + Domain: + type: object + description: Domain information (placeholder - add specific properties as needed) + + ChartSource: + type: object + allOf: + - type: object + properties: + citationId: + type: integer + description: Unique identifier for the citation + - $ref: '#/components/schemas/ChartWidgetSpecification' + + ChartWidgetSpecification: + type: object + properties: + id: + type: integer + description: The ID for the widget + entities: + type: array + items: + $ref: '#/components/schemas/ChartWidgetEntity' + description: Array of entities for the chart + dataset: + type: string + description: Dataset identifier + metric: + type: string + description: Metric identifier + start: + type: string + format: date-time + description: Start time for the chart data + end: + type: string + format: date-time + description: End time for the chart data + tier: + type: string + description: Tier information + metricTimeseries: + $ref: '#/components/schemas/TimeseriesResult' + granularity: + type: string + description: Data granularity + + ChartWidgetEntity: + type: object + required: + - entityType + - entityId + properties: + entityType: + type: string + description: Type of the entity + entityId: + type: string + description: Identifier of the entity + + TimeseriesResult: + type: object + required: + - point_schema + - series + properties: + point_schema: + type: array + items: + $ref: '#/components/schemas/PointSchema' + series: + type: array + items: + $ref: '#/components/schemas/Series' + + PointSchema: + type: object + properties: + id: + type: string + description: Deprecated - Use slug instead + name: + type: string + slug: + type: string + description: + type: string + is_timestamp: + type: boolean + format: + type: string + time_bucket_aggregate_operation: + type: string + description: Aggregate operation performed for the time bucket + group_aggregate_operation: + type: string + description: Aggregate operation performed for the group + subcategory: + type: string + attribution: + type: array + items: + $ref: '#/components/schemas/Attribution' + + Attribution: + type: object + description: Attribution information (placeholder - add specific properties as needed) + + Series: + type: object + required: + - key + - points + properties: + key: + type: string + entity: + type: object + additionalProperties: true + points: + type: array + items: + type: array + items: + type: object + description: Can be any type + securitySchemes: ApiKeyAuth: type: apiKey From 75e4ff018be95cef8975df143d9e8d227bd76e4c Mon Sep 17 00:00:00 2001 From: bijan Date: Mon, 14 Apr 2025 14:27:38 -0400 Subject: [PATCH 2/9] change path --- packages/api/src/types/index.ts | 2 +- packages/api/src/types/types.ts | 44 ++++++++++++++++----------------- typegen/openapi/index.yaml | 2 +- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/packages/api/src/types/index.ts b/packages/api/src/types/index.ts index 5a10cb4..475f683 100644 --- a/packages/api/src/types/index.ts +++ b/packages/api/src/types/index.ts @@ -58,7 +58,7 @@ export const createChatCompletionOpenAI = { pathParams: [] as const, queryParams: [] as const, bodyParams: ['messages', 'verbosity', 'response_format', 'inline_citations', 'stream'] as const, - path: () => '/ai/v1/chat/completions/openai' + path: () => '/ai/openai/chat/completions' } as const; diff --git a/packages/api/src/types/types.ts b/packages/api/src/types/types.ts index 4ec3935..5539a36 100644 --- a/packages/api/src/types/types.ts +++ b/packages/api/src/types/types.ts @@ -26,15 +26,7 @@ export type paths = { */ get: operations["getProjectRecap"]; }; - "/ai/v1/chat/completions": { - /** - * Chat Completion - * @description Creates a completion for the chat message. Supports both streaming and non-streaming responses. - * The last message must be from the user role. - */ - post: operations["createChatCompletion"]; - }; - "/ai/v1/chat/completions/openai": { + "/ai/openai/chat/completions": { /** * OpenAI-Compatible Chat Completion * @description Creates a completion for the chat message in OpenAI-compatible format. @@ -44,6 +36,14 @@ export type paths = { */ post: operations["createChatCompletionOpenAI"]; }; + "/ai/v1/chat/completions": { + /** + * Chat Completion + * @description Creates a completion for the chat message. Supports both streaming and non-streaming responses. + * The last message must be from the user role. + */ + post: operations["createChatCompletion"]; + }; "/ai/v1/classification/extraction": { /** * Entity Extraction @@ -2102,11 +2102,13 @@ export type operations = { }; }; /** - * Chat Completion - * @description Creates a completion for the chat message. Supports both streaming and non-streaming responses. + * OpenAI-Compatible Chat Completion + * @description Creates a completion for the chat message in OpenAI-compatible format. + * Supports both streaming and non-streaming responses. * The last message must be from the user role. + * Response is returned directly without the standard {data: } wrapper. */ - createChatCompletion: { + createChatCompletionOpenAI: { parameters: { header: { "x-messari-api-key": components["parameters"]["apiKey"]; @@ -2127,10 +2129,7 @@ export type operations = { /** @description Successful response */ 200: { content: { - "application/json": components["schemas"]["APIResponseWithMetadata"] & { - data?: components["schemas"]["ChatCompletionResponse"]; - metadata?: components["schemas"]["ChatCompletionResponseMetadata"]; - }; + "application/json": components["schemas"]["ChatCompletionResponseOpenAI"]; "text/event-stream": string; }; }; @@ -2143,13 +2142,11 @@ export type operations = { }; }; /** - * OpenAI-Compatible Chat Completion - * @description Creates a completion for the chat message in OpenAI-compatible format. - * Supports both streaming and non-streaming responses. + * Chat Completion + * @description Creates a completion for the chat message. Supports both streaming and non-streaming responses. * The last message must be from the user role. - * Response is returned directly without the standard {data: } wrapper. */ - createChatCompletionOpenAI: { + createChatCompletion: { parameters: { header: { "x-messari-api-key": components["parameters"]["apiKey"]; @@ -2170,7 +2167,10 @@ export type operations = { /** @description Successful response */ 200: { content: { - "application/json": components["schemas"]["ChatCompletionResponseOpenAI"]; + "application/json": components["schemas"]["APIResponseWithMetadata"] & { + data?: components["schemas"]["ChatCompletionResponse"]; + metadata?: components["schemas"]["ChatCompletionResponseMetadata"]; + }; "text/event-stream": string; }; }; diff --git a/typegen/openapi/index.yaml b/typegen/openapi/index.yaml index 91be202..a0003ad 100644 --- a/typegen/openapi/index.yaml +++ b/typegen/openapi/index.yaml @@ -55,7 +55,7 @@ paths: $ref: "./services/ai/openapi.yaml#/paths/~1ai~1v1~1chat~1completions" /ai/v1/classification/extraction: $ref: "./services/ai/openapi.yaml#/paths/~1ai~1v1~1classification~1extraction" - /ai/v1/chat/completions/openai: + /ai/openai/chat/completions: $ref: "./services/ai/openapi.yaml#/paths/~1ai~1openai~1chat~1completions" # Asset Service Paths From 621d74f1b652f61c962453bad8561d56add13b8b Mon Sep 17 00:00:00 2001 From: bijan Date: Mon, 14 Apr 2025 16:15:26 -0400 Subject: [PATCH 3/9] add response cleaning --- packages/api/src/client/client.ts | 24 ++++++++---- packages/examples/src/ai.ts | 64 +++++++++++++++++-------------- 2 files changed, 52 insertions(+), 36 deletions(-) diff --git a/packages/api/src/client/client.ts b/packages/api/src/client/client.ts index 3fd217f..0a8372e 100644 --- a/packages/api/src/client/client.ts +++ b/packages/api/src/client/client.ts @@ -326,12 +326,14 @@ export class MessariClient extends MessariClientBase { // Check if the response is JSON or text based on Content-Type header const contentType = response.headers.get("Content-Type"); - let responseData: { data: T }; + let responseData: T; if (contentType?.toLowerCase().includes("application/json")) { - responseData = await response.json(); + const jsonResponse = await response.json(); + // If response has data field and no error, unwrap it, otherwise use the whole response + responseData = jsonResponse.data && !jsonResponse.error ? jsonResponse.data : jsonResponse; } else { - responseData = { data: await response.text() } as { data: T }; + responseData = await response.text() as T; } this.logger(LogLevel.DEBUG, "request success", { responseData }); @@ -344,7 +346,7 @@ export class MessariClient extends MessariClientBase { data: responseData, }); - return responseData.data; + return responseData; } catch (error) { this.logger(LogLevel.ERROR, "request failed", { error }); @@ -455,10 +457,16 @@ export class MessariClient extends MessariClientBase { data: responseData, }); - return { - data: responseData.data, - metadata: responseData.metadata, - }; + // If response has data field, return wrapped format, otherwise treat whole response as data + return responseData.data !== undefined + ? { + data: responseData.data, + metadata: responseData.metadata, + } + : { + data: responseData, + metadata: {} as M, + }; } catch (error) { this.logger(LogLevel.ERROR, "request with metadata failed", { error }); diff --git a/packages/examples/src/ai.ts b/packages/examples/src/ai.ts index 406e790..f671274 100644 --- a/packages/examples/src/ai.ts +++ b/packages/examples/src/ai.ts @@ -19,7 +19,43 @@ const client = new MessariClient({ apiKey: API_KEY, }); +// Get command line arguments +const args = process.argv.slice(2); +const useOpenAI = args.includes("openai"); + async function main() { + if (useOpenAI) { + // OpenAI Chat Completion + try { + console.log("\n--------------------------------"); + console.log("OpenAI Chat Completion"); + console.log("--------------------------------"); + console.log("Sending request..."); + console.log(`"What are the key differences between Bitcoin and Ethereum?"`); + + // Call the createChatCompletionOpenAI endpoint + const response = await client.ai.createChatCompletionOpenAI({ + messages: [ + { + role: "user", + content: "What are the key differences between Bitcoin and Ethereum?", + }, + ], + verbosity: "succinct", + response_format: "plaintext", + inline_citations: false, + stream: false, + }); + console.log(response); + + console.log("Response received:"); + console.log(response.choices[0].message.content); + } catch (error) { + console.error("Error calling createChatCompletionOpenAI:", error); + } + return; + } + try { console.log("--------------------------------"); console.log("AI Chat Completion"); @@ -114,34 +150,6 @@ async function main() { } catch (error) { console.error("Error calling extractEntities:", error); } - - // OpenAI Chat Completion - try { - console.log("\n--------------------------------"); - console.log("OpenAI Chat Completion"); - console.log("--------------------------------"); - console.log("Sending request..."); - console.log(`"What are the key differences between Bitcoin and Ethereum?"`); - - // Call the createChatCompletionOpenAI endpoint - const response = await client.ai.createChatCompletionOpenAI({ - messages: [ - { - role: "user", - content: "What are the key differences between Bitcoin and Ethereum?", - }, - ], - verbosity: "succinct", - response_format: "plaintext", - inline_citations: false, - stream: false, - }); - - console.log("Response received:"); - console.log(response.choices[0].message.content); - } catch (error) { - console.error("Error calling createChatCompletionOpenAI:", error); - } } main().catch(console.error); From f9af3d2fb3b5398e6d741166a2beda16520e2b31 Mon Sep 17 00:00:00 2001 From: bijan Date: Mon, 14 Apr 2025 22:52:52 -0400 Subject: [PATCH 4/9] add streaming support --- packages/api/src/client/base.ts | 10 +- packages/api/src/client/client.ts | 225 +++++++++++++++++++++++++++--- packages/examples/src/ai.ts | 55 +++++--- 3 files changed, 253 insertions(+), 37 deletions(-) diff --git a/packages/api/src/client/base.ts b/packages/api/src/client/base.ts index 9cb8c95..564fcb7 100644 --- a/packages/api/src/client/base.ts +++ b/packages/api/src/client/base.ts @@ -120,7 +120,15 @@ export interface AIInterface { * @param options Optional request configuration * @returns A promise resolving to the chat completion response */ - createChatCompletionOpenAI(params: createChatCompletionParameters, options?: RequestOptions): Promise; + createChatCompletionOpenAI(params: Omit, options?: RequestOptions): Promise; + + /** + * Creates a streaming chat completion using OpenAI's API + * @param params Parameters for the chat completion request + * @param options Optional request configuration + * @returns A promise resolving to a readable stream of chat completion chunks + */ + createChatCompletionOpenAIStream(params: Omit, options?: RequestOptions): Promise>; /** * Extracts entities from text content diff --git a/packages/api/src/client/client.ts b/packages/api/src/client/client.ts index 0a8372e..87c8d48 100644 --- a/packages/api/src/client/client.ts +++ b/packages/api/src/client/client.ts @@ -148,6 +148,7 @@ import type { updateWatchlistParameters, updateWatchlistResponse, createChatCompletionOpenAIResponse, + createChatCompletionOpenAIParameters, } from "../types"; import type { Agent } from "node:http"; import { pick } from "../utils"; @@ -181,6 +182,7 @@ import type { } from "./base"; import { MessariClientBase } from "./base"; + /** * MessariClient is the main client class for interacting with the Messari API. * It provides a comprehensive interface for accessing market data, news, intelligence, @@ -247,7 +249,13 @@ export class MessariClient extends MessariClientBase { } } - private async request({ method, path, body, queryParams = {}, options = {} }: RequestParameters): Promise { + private async request({ + method, + path, + body, + queryParams = {}, + options = {} + }: RequestParameters): Promise { this.logger(LogLevel.DEBUG, "request start", { method, url: `${this.baseUrl}${path}`, @@ -326,29 +334,203 @@ export class MessariClient extends MessariClientBase { // Check if the response is JSON or text based on Content-Type header const contentType = response.headers.get("Content-Type"); - let responseData: T; - + if (contentType?.toLowerCase().includes("application/json")) { const jsonResponse = await response.json(); // If response has data field and no error, unwrap it, otherwise use the whole response - responseData = jsonResponse.data && !jsonResponse.error ? jsonResponse.data : jsonResponse; - } else { - responseData = await response.text() as T; + const data = jsonResponse.data && !jsonResponse.error ? jsonResponse.data : jsonResponse; + return data as T; } + + const text = await response.text(); + return text as T; + } catch (error) { + this.logger(LogLevel.ERROR, "request failed", { error }); - this.logger(LogLevel.DEBUG, "request success", { responseData }); - - // Emit response event - this.emit("response", { - method, - path, - status: response.status, - data: responseData, + // Emit error event + this.emit("error", { + error: error as Error, + request: { + method, + path, + queryParams, + }, }); - return responseData; + throw error; + } + } + + private async requestStream({ + method, + path, + body, + queryParams = {}, + options = {} + }: RequestParameters): Promise> { + this.logger(LogLevel.DEBUG, "stream request start", { + method, + url: `${this.baseUrl}${path}`, + queryParams, + }); + + this.emit("request", { + method, + path, + queryParams, + }); + + const queryString = Object.entries(queryParams) + .filter(([_, value]) => value !== undefined) + .map(([key, value]) => { + if (Array.isArray(value)) { + return value.map((item) => `${encodeURIComponent(key)}=${encodeURIComponent(String(item))}`).join("&"); + } + return `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`; + }) + .join("&"); + + const url = `${this.baseUrl}${path}${queryString ? `?${queryString}` : ""}`; + + const headers = { + ...this.defaultHeaders, + ...options.headers, + 'Accept': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + }; + + const timeoutMs = options.timeoutMs || this.timeoutMs; + + try { + const response = await RequestTimeoutError.rejectAfterTimeout( + this.fetchFn(url, { + method, + headers, + body: body ? JSON.stringify(body) : undefined, + signal: options.signal, + cache: options.cache, + credentials: options.credentials, + integrity: options.integrity, + keepalive: options.keepalive, + mode: options.mode, + redirect: options.redirect, + referrer: options.referrer, + referrerPolicy: options.referrerPolicy, + // @ts-ignore - Next.js specific options + next: options.next, + // Node.js specific option + agent: this.agent, + }), + timeoutMs, + ); + + if (!response.ok) { + const errorData = await response.json(); + this.logger(LogLevel.ERROR, "request error", { + status: response.status, + statusText: response.statusText, + error: errorData, + }); + + const error = new Error(errorData.error || "An error occurred"); + + this.emit("error", { + error, + request: { + method, + path, + queryParams, + }, + }); + + throw error; + } + + // For streaming responses, return a transformed stream that parses the chunks + if (!response.body) { + throw new Error("No reader available for streaming response"); + } + + let buffer = ''; + const decoder = new TextDecoder(); + + // Create a TransformStream that will parse the raw bytes into the expected type T + const transformer = new TransformStream({ + transform: async (chunk, controller) => { + try { + // Decode the chunk and add to buffer + const text = decoder.decode(chunk, { stream: true }); + buffer += text; + + // Process any complete lines in the buffer + const lines = buffer.split('\n'); + // Keep the last potentially incomplete line in the buffer + buffer = lines.pop() || ''; + + for (const line of lines) { + if (line.startsWith('data: ')) { + const jsonData = line.slice(6).trim(); // Remove 'data: ' prefix + + // Skip [DONE] marker + if (jsonData === '[DONE]') { + continue; + } + + if (jsonData) { + try { + const parsed = JSON.parse(jsonData); + controller.enqueue(parsed as T); + } catch (e) { + this.logger(LogLevel.ERROR, "Error parsing JSON from stream", { + error: e, + data: jsonData + }); + } + } + } else if (line.trim() && !line.startsWith(':')) { + // Try to parse non-empty lines that aren't comments + try { + const parsed = JSON.parse(line); + controller.enqueue(parsed as T); + } catch (e) { + // Not JSON, might be part of a multi-line chunk + if (line.trim()) { + this.logger(LogLevel.DEBUG, "Non-JSON line in stream", { line }); + } + } + } + } + } catch (error) { + this.logger(LogLevel.ERROR, "Error processing stream chunk", { error }); + controller.error(error); + } + }, + flush: (controller) => { + // Process any remaining data in the buffer + if (buffer.trim()) { + if (buffer.startsWith('data: ')) { + const jsonData = buffer.slice(6).trim(); + if (jsonData && jsonData !== '[DONE]') { + try { + const parsed = JSON.parse(jsonData); + controller.enqueue(parsed as T); + } catch (e) { + this.logger(LogLevel.ERROR, "Error parsing final JSON from stream", { + error: e, + data: jsonData + }); + } + } + } + } + } + }); + + // Pipe the response body through our transformer + return response.body.pipeThrough(transformer); } catch (error) { - this.logger(LogLevel.ERROR, "request failed", { error }); + this.logger(LogLevel.ERROR, "stream request failed", { error }); // Emit error event this.emit("error", { @@ -693,11 +875,18 @@ export class MessariClient extends MessariClientBase { body: pick(params, createChatCompletion.bodyParams), options, }), - createChatCompletionOpenAI: (params: createChatCompletionParameters, options?: RequestOptions) => + createChatCompletionOpenAI: (params: createChatCompletionParameters, options?: RequestOptions) => this.request({ method: createChatCompletionOpenAI.method, path: createChatCompletionOpenAI.path(), - body: pick(params, createChatCompletionOpenAI.bodyParams), + body: pick(params, createChatCompletionOpenAI.bodyParams) as createChatCompletionOpenAIParameters & { stream: false }, + options, + }), + createChatCompletionOpenAIStream: (params: createChatCompletionParameters, options?: RequestOptions) => + this.requestStream({ + method: createChatCompletionOpenAI.method, + path: createChatCompletionOpenAI.path(), + body: { ...pick(params, createChatCompletionOpenAI.bodyParams), stream: true }, options, }), extractEntities: (params: extractEntitiesParameters, options?: RequestOptions) => diff --git a/packages/examples/src/ai.ts b/packages/examples/src/ai.ts index f671274..2827728 100644 --- a/packages/examples/src/ai.ts +++ b/packages/examples/src/ai.ts @@ -22,6 +22,7 @@ const client = new MessariClient({ // Get command line arguments const args = process.argv.slice(2); const useOpenAI = args.includes("openai"); +const useStreaming = args.includes("stream"); async function main() { if (useOpenAI) { @@ -33,23 +34,41 @@ async function main() { console.log("Sending request..."); console.log(`"What are the key differences between Bitcoin and Ethereum?"`); - // Call the createChatCompletionOpenAI endpoint - const response = await client.ai.createChatCompletionOpenAI({ - messages: [ - { - role: "user", - content: "What are the key differences between Bitcoin and Ethereum?", - }, - ], - verbosity: "succinct", - response_format: "plaintext", - inline_citations: false, - stream: false, - }); - console.log(response); - - console.log("Response received:"); - console.log(response.choices[0].message.content); + if (useStreaming) { + // Call the createChatCompletionOpenAI endpoint + const response = await client.ai.createChatCompletionOpenAIStream({ + messages: [ + { + role: "user", + content: "What are the key differences between Bitcoin and Ethereum?", + }, + ], + verbosity: "succinct", + response_format: "plaintext", + inline_citations: false + }); + + for await (const chunk of response) { + console.log({chunk}); + } + + console.log('\n'); + } else { + const response = await client.ai.createChatCompletionOpenAI({ + messages: [ + { + role: "user", + content: "What are the key differences between Bitcoin and Ethereum?", + }, + ], + verbosity: "succinct", + response_format: "plaintext", + inline_citations: false, + + }); + console.log("Response received:"); + console.log(response); + } } catch (error) { console.error("Error calling createChatCompletionOpenAI:", error); } @@ -74,7 +93,7 @@ async function main() { verbosity: "succinct", response_format: "plaintext", inline_citations: false, - stream: false, + stream: useStreaming, }); const assistantMessage = response.messages[0].content; From 38f732daf81ac8805a7f44ed7f7a444a640e44bf Mon Sep 17 00:00:00 2001 From: bijan Date: Mon, 14 Apr 2025 22:53:20 -0400 Subject: [PATCH 5/9] add whitespace --- packages/api/src/client/client.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/api/src/client/client.ts b/packages/api/src/client/client.ts index 87c8d48..c3ea063 100644 --- a/packages/api/src/client/client.ts +++ b/packages/api/src/client/client.ts @@ -182,7 +182,6 @@ import type { } from "./base"; import { MessariClientBase } from "./base"; - /** * MessariClient is the main client class for interacting with the Messari API. * It provides a comprehensive interface for accessing market data, news, intelligence, From 3aaeb73740a34233f1e2a23335e62061a9089646 Mon Sep 17 00:00:00 2001 From: bijan Date: Mon, 14 Apr 2025 22:54:01 -0400 Subject: [PATCH 6/9] pretier --- packages/api/src/client/client.ts | 154 +++++++++++++++--------------- 1 file changed, 77 insertions(+), 77 deletions(-) diff --git a/packages/api/src/client/client.ts b/packages/api/src/client/client.ts index c3ea063..8513992 100644 --- a/packages/api/src/client/client.ts +++ b/packages/api/src/client/client.ts @@ -1,6 +1,6 @@ import { createChatCompletion, - extractEntities, + extractEntities, createChatCompletionOpenAI, getNewsFeed, getNewsSources, @@ -248,11 +248,11 @@ export class MessariClient extends MessariClientBase { } } - private async request({ - method, - path, - body, - queryParams = {}, + private async request({ + method, + path, + body, + queryParams = {}, options = {} }: RequestParameters): Promise { this.logger(LogLevel.DEBUG, "request start", { @@ -333,14 +333,14 @@ export class MessariClient extends MessariClientBase { // Check if the response is JSON or text based on Content-Type header const contentType = response.headers.get("Content-Type"); - + if (contentType?.toLowerCase().includes("application/json")) { const jsonResponse = await response.json(); // If response has data field and no error, unwrap it, otherwise use the whole response const data = jsonResponse.data && !jsonResponse.error ? jsonResponse.data : jsonResponse; return data as T; } - + const text = await response.text(); return text as T; } catch (error) { @@ -360,11 +360,11 @@ export class MessariClient extends MessariClientBase { } } - private async requestStream({ - method, - path, - body, - queryParams = {}, + private async requestStream({ + method, + path, + body, + queryParams = {}, options = {} }: RequestParameters): Promise> { this.logger(LogLevel.DEBUG, "stream request start", { @@ -450,10 +450,10 @@ export class MessariClient extends MessariClientBase { if (!response.body) { throw new Error("No reader available for streaming response"); } - + let buffer = ''; const decoder = new TextDecoder(); - + // Create a TransformStream that will parse the raw bytes into the expected type T const transformer = new TransformStream({ transform: async (chunk, controller) => { @@ -461,29 +461,29 @@ export class MessariClient extends MessariClientBase { // Decode the chunk and add to buffer const text = decoder.decode(chunk, { stream: true }); buffer += text; - + // Process any complete lines in the buffer const lines = buffer.split('\n'); // Keep the last potentially incomplete line in the buffer buffer = lines.pop() || ''; - + for (const line of lines) { if (line.startsWith('data: ')) { const jsonData = line.slice(6).trim(); // Remove 'data: ' prefix - + // Skip [DONE] marker if (jsonData === '[DONE]') { continue; } - + if (jsonData) { try { const parsed = JSON.parse(jsonData); controller.enqueue(parsed as T); } catch (e) { - this.logger(LogLevel.ERROR, "Error parsing JSON from stream", { - error: e, - data: jsonData + this.logger(LogLevel.ERROR, "Error parsing JSON from stream", { + error: e, + data: jsonData }); } } @@ -515,9 +515,9 @@ export class MessariClient extends MessariClientBase { const parsed = JSON.parse(jsonData); controller.enqueue(parsed as T); } catch (e) { - this.logger(LogLevel.ERROR, "Error parsing final JSON from stream", { - error: e, - data: jsonData + this.logger(LogLevel.ERROR, "Error parsing final JSON from stream", { + error: e, + data: jsonData }); } } @@ -525,7 +525,7 @@ export class MessariClient extends MessariClientBase { } } }); - + // Pipe the response body through our transformer return response.body.pipeThrough(transformer); } catch (error) { @@ -641,13 +641,13 @@ export class MessariClient extends MessariClientBase { // If response has data field, return wrapped format, otherwise treat whole response as data return responseData.data !== undefined ? { - data: responseData.data, - metadata: responseData.metadata, - } + data: responseData.data, + metadata: responseData.metadata, + } : { - data: responseData, - metadata: {} as M, - }; + data: responseData, + metadata: {} as M, + }; } catch (error) { this.logger(LogLevel.ERROR, "request with metadata failed", { error }); @@ -674,21 +674,21 @@ export class MessariClient extends MessariClientBase { // Convert PaginationResult to PaginationMetadata const metadata: PaginationMetadata = response.metadata ? { - page: response.metadata.page || 1, - limit: response.metadata.limit || 10, - total: response.metadata.total || 0, - totalRows: response.metadata.total || 0, - totalPages: Math.ceil((response.metadata.total || 0) / (response.metadata.limit || 10)), - hasMore: response.metadata.hasMore || false, - } + page: response.metadata.page || 1, + limit: response.metadata.limit || 10, + total: response.metadata.total || 0, + totalRows: response.metadata.total || 0, + totalPages: Math.ceil((response.metadata.total || 0) / (response.metadata.limit || 10)), + hasMore: response.metadata.hasMore || false, + } : { - page: 1, - limit: 10, - total: 0, - totalRows: 0, - totalPages: 0, - hasMore: false, - }; + page: 1, + limit: 10, + total: 0, + totalRows: 0, + totalPages: 0, + hasMore: false, + }; const currentPage = metadata.page; const hasNextPage = metadata.hasMore || false || currentPage < (metadata.totalPages || 0); @@ -718,17 +718,17 @@ export class MessariClient extends MessariClientBase { const nextPageResponse = await fetchPage(nextPageParams, options); const nextPageMetadata: PaginationMetadata = nextPageResponse.metadata ? { - page: nextPageResponse.metadata.page || nextPage, - limit: nextPageResponse.metadata.limit || metadata.limit, - totalRows: nextPageResponse.metadata.total || metadata.totalRows || 0, - totalPages: Math.ceil((nextPageResponse.metadata.total || metadata.totalRows || 0) / (nextPageResponse.metadata.limit || metadata.limit)), - } + page: nextPageResponse.metadata.page || nextPage, + limit: nextPageResponse.metadata.limit || metadata.limit, + totalRows: nextPageResponse.metadata.total || metadata.totalRows || 0, + totalPages: Math.ceil((nextPageResponse.metadata.total || metadata.totalRows || 0) / (nextPageResponse.metadata.limit || metadata.limit)), + } : { - page: nextPage, - limit: metadata.limit, - totalRows: metadata.totalRows || 0, - totalPages: metadata.totalPages || 0, - }; + page: nextPage, + limit: metadata.limit, + totalRows: metadata.totalRows || 0, + totalPages: metadata.totalPages || 0, + }; return { data: nextPageResponse.data, @@ -758,17 +758,17 @@ export class MessariClient extends MessariClientBase { const prevPageResponse = await fetchPage(prevPageParams, options); const prevPageMetadata: PaginationMetadata = prevPageResponse.metadata ? { - page: prevPageResponse.metadata.page || prevPage, - limit: prevPageResponse.metadata.limit || metadata.limit, - totalRows: prevPageResponse.metadata.total || metadata.totalRows || 0, - totalPages: Math.ceil((prevPageResponse.metadata.total || metadata.totalRows || 0) / (prevPageResponse.metadata.limit || metadata.limit)), - } + page: prevPageResponse.metadata.page || prevPage, + limit: prevPageResponse.metadata.limit || metadata.limit, + totalRows: prevPageResponse.metadata.total || metadata.totalRows || 0, + totalPages: Math.ceil((prevPageResponse.metadata.total || metadata.totalRows || 0) / (prevPageResponse.metadata.limit || metadata.limit)), + } : { - page: prevPage, - limit: metadata.limit, - totalRows: metadata.totalRows || 0, - totalPages: metadata.totalPages || 0, - }; + page: prevPage, + limit: metadata.limit, + totalRows: metadata.totalRows || 0, + totalPages: metadata.totalPages || 0, + }; return { data: prevPageResponse.data, @@ -793,17 +793,17 @@ export class MessariClient extends MessariClientBase { const pageResponse = await fetchPage(pageParams, options); const pageMetadata: PaginationMetadata = pageResponse.metadata ? { - page: pageResponse.metadata.page || page, - limit: pageResponse.metadata.limit || metadata.limit, - totalRows: pageResponse.metadata.total || metadata.totalRows || 0, - totalPages: Math.ceil((pageResponse.metadata.total || metadata.totalRows || 0) / (pageResponse.metadata.limit || metadata.limit)), - } + page: pageResponse.metadata.page || page, + limit: pageResponse.metadata.limit || metadata.limit, + totalRows: pageResponse.metadata.total || metadata.totalRows || 0, + totalPages: Math.ceil((pageResponse.metadata.total || metadata.totalRows || 0) / (pageResponse.metadata.limit || metadata.limit)), + } : { - page, - limit: metadata.limit, - totalRows: metadata.totalRows || 0, - totalPages: metadata.totalPages || 0, - }; + page, + limit: metadata.limit, + totalRows: metadata.totalRows || 0, + totalPages: metadata.totalPages || 0, + }; return { data: pageResponse.data, @@ -874,7 +874,7 @@ export class MessariClient extends MessariClientBase { body: pick(params, createChatCompletion.bodyParams), options, }), - createChatCompletionOpenAI: (params: createChatCompletionParameters, options?: RequestOptions) => + createChatCompletionOpenAI: (params: createChatCompletionParameters, options?: RequestOptions) => this.request({ method: createChatCompletionOpenAI.method, path: createChatCompletionOpenAI.path(), From 19d39afe5b053bd2587cb92e2c9f38e1b67ea982 Mon Sep 17 00:00:00 2001 From: bijan Date: Mon, 14 Apr 2025 22:55:34 -0400 Subject: [PATCH 7/9] biome formatting --- packages/api/src/client/base.ts | 7 +- packages/api/src/client/client.ts | 144 ++++++++++++++---------------- packages/examples/src/ai.ts | 9 +- 3 files changed, 75 insertions(+), 85 deletions(-) diff --git a/packages/api/src/client/base.ts b/packages/api/src/client/base.ts index 564fcb7..d355ed2 100644 --- a/packages/api/src/client/base.ts +++ b/packages/api/src/client/base.ts @@ -120,7 +120,7 @@ export interface AIInterface { * @param options Optional request configuration * @returns A promise resolving to the chat completion response */ - createChatCompletionOpenAI(params: Omit, options?: RequestOptions): Promise; + createChatCompletionOpenAI(params: Omit, options?: RequestOptions): Promise; /** * Creates a streaming chat completion using OpenAI's API @@ -128,7 +128,10 @@ export interface AIInterface { * @param options Optional request configuration * @returns A promise resolving to a readable stream of chat completion chunks */ - createChatCompletionOpenAIStream(params: Omit, options?: RequestOptions): Promise>; + createChatCompletionOpenAIStream( + params: Omit, + options?: RequestOptions, + ): Promise>; /** * Extracts entities from text content diff --git a/packages/api/src/client/client.ts b/packages/api/src/client/client.ts index 8513992..5ac520d 100644 --- a/packages/api/src/client/client.ts +++ b/packages/api/src/client/client.ts @@ -248,13 +248,7 @@ export class MessariClient extends MessariClientBase { } } - private async request({ - method, - path, - body, - queryParams = {}, - options = {} - }: RequestParameters): Promise { + private async request({ method, path, body, queryParams = {}, options = {} }: RequestParameters): Promise { this.logger(LogLevel.DEBUG, "request start", { method, url: `${this.baseUrl}${path}`, @@ -360,13 +354,7 @@ export class MessariClient extends MessariClientBase { } } - private async requestStream({ - method, - path, - body, - queryParams = {}, - options = {} - }: RequestParameters): Promise> { + private async requestStream({ method, path, body, queryParams = {}, options = {} }: RequestParameters): Promise> { this.logger(LogLevel.DEBUG, "stream request start", { method, url: `${this.baseUrl}${path}`, @@ -394,9 +382,9 @@ export class MessariClient extends MessariClientBase { const headers = { ...this.defaultHeaders, ...options.headers, - 'Accept': 'text/event-stream', - 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive', + "Accept": "text/event-stream", + "Cache-Control": "no-cache", + "Connection": "keep-alive", }; const timeoutMs = options.timeoutMs || this.timeoutMs; @@ -451,7 +439,7 @@ export class MessariClient extends MessariClientBase { throw new Error("No reader available for streaming response"); } - let buffer = ''; + let buffer = ""; const decoder = new TextDecoder(); // Create a TransformStream that will parse the raw bytes into the expected type T @@ -463,16 +451,16 @@ export class MessariClient extends MessariClientBase { buffer += text; // Process any complete lines in the buffer - const lines = buffer.split('\n'); + const lines = buffer.split("\n"); // Keep the last potentially incomplete line in the buffer - buffer = lines.pop() || ''; + buffer = lines.pop() || ""; for (const line of lines) { - if (line.startsWith('data: ')) { + if (line.startsWith("data: ")) { const jsonData = line.slice(6).trim(); // Remove 'data: ' prefix // Skip [DONE] marker - if (jsonData === '[DONE]') { + if (jsonData === "[DONE]") { continue; } @@ -483,11 +471,11 @@ export class MessariClient extends MessariClientBase { } catch (e) { this.logger(LogLevel.ERROR, "Error parsing JSON from stream", { error: e, - data: jsonData + data: jsonData, }); } } - } else if (line.trim() && !line.startsWith(':')) { + } else if (line.trim() && !line.startsWith(":")) { // Try to parse non-empty lines that aren't comments try { const parsed = JSON.parse(line); @@ -508,22 +496,22 @@ export class MessariClient extends MessariClientBase { flush: (controller) => { // Process any remaining data in the buffer if (buffer.trim()) { - if (buffer.startsWith('data: ')) { + if (buffer.startsWith("data: ")) { const jsonData = buffer.slice(6).trim(); - if (jsonData && jsonData !== '[DONE]') { + if (jsonData && jsonData !== "[DONE]") { try { const parsed = JSON.parse(jsonData); controller.enqueue(parsed as T); } catch (e) { this.logger(LogLevel.ERROR, "Error parsing final JSON from stream", { error: e, - data: jsonData + data: jsonData, }); } } } } - } + }, }); // Pipe the response body through our transformer @@ -641,13 +629,13 @@ export class MessariClient extends MessariClientBase { // If response has data field, return wrapped format, otherwise treat whole response as data return responseData.data !== undefined ? { - data: responseData.data, - metadata: responseData.metadata, - } + data: responseData.data, + metadata: responseData.metadata, + } : { - data: responseData, - metadata: {} as M, - }; + data: responseData, + metadata: {} as M, + }; } catch (error) { this.logger(LogLevel.ERROR, "request with metadata failed", { error }); @@ -674,21 +662,21 @@ export class MessariClient extends MessariClientBase { // Convert PaginationResult to PaginationMetadata const metadata: PaginationMetadata = response.metadata ? { - page: response.metadata.page || 1, - limit: response.metadata.limit || 10, - total: response.metadata.total || 0, - totalRows: response.metadata.total || 0, - totalPages: Math.ceil((response.metadata.total || 0) / (response.metadata.limit || 10)), - hasMore: response.metadata.hasMore || false, - } + page: response.metadata.page || 1, + limit: response.metadata.limit || 10, + total: response.metadata.total || 0, + totalRows: response.metadata.total || 0, + totalPages: Math.ceil((response.metadata.total || 0) / (response.metadata.limit || 10)), + hasMore: response.metadata.hasMore || false, + } : { - page: 1, - limit: 10, - total: 0, - totalRows: 0, - totalPages: 0, - hasMore: false, - }; + page: 1, + limit: 10, + total: 0, + totalRows: 0, + totalPages: 0, + hasMore: false, + }; const currentPage = metadata.page; const hasNextPage = metadata.hasMore || false || currentPage < (metadata.totalPages || 0); @@ -718,17 +706,17 @@ export class MessariClient extends MessariClientBase { const nextPageResponse = await fetchPage(nextPageParams, options); const nextPageMetadata: PaginationMetadata = nextPageResponse.metadata ? { - page: nextPageResponse.metadata.page || nextPage, - limit: nextPageResponse.metadata.limit || metadata.limit, - totalRows: nextPageResponse.metadata.total || metadata.totalRows || 0, - totalPages: Math.ceil((nextPageResponse.metadata.total || metadata.totalRows || 0) / (nextPageResponse.metadata.limit || metadata.limit)), - } + page: nextPageResponse.metadata.page || nextPage, + limit: nextPageResponse.metadata.limit || metadata.limit, + totalRows: nextPageResponse.metadata.total || metadata.totalRows || 0, + totalPages: Math.ceil((nextPageResponse.metadata.total || metadata.totalRows || 0) / (nextPageResponse.metadata.limit || metadata.limit)), + } : { - page: nextPage, - limit: metadata.limit, - totalRows: metadata.totalRows || 0, - totalPages: metadata.totalPages || 0, - }; + page: nextPage, + limit: metadata.limit, + totalRows: metadata.totalRows || 0, + totalPages: metadata.totalPages || 0, + }; return { data: nextPageResponse.data, @@ -758,17 +746,17 @@ export class MessariClient extends MessariClientBase { const prevPageResponse = await fetchPage(prevPageParams, options); const prevPageMetadata: PaginationMetadata = prevPageResponse.metadata ? { - page: prevPageResponse.metadata.page || prevPage, - limit: prevPageResponse.metadata.limit || metadata.limit, - totalRows: prevPageResponse.metadata.total || metadata.totalRows || 0, - totalPages: Math.ceil((prevPageResponse.metadata.total || metadata.totalRows || 0) / (prevPageResponse.metadata.limit || metadata.limit)), - } + page: prevPageResponse.metadata.page || prevPage, + limit: prevPageResponse.metadata.limit || metadata.limit, + totalRows: prevPageResponse.metadata.total || metadata.totalRows || 0, + totalPages: Math.ceil((prevPageResponse.metadata.total || metadata.totalRows || 0) / (prevPageResponse.metadata.limit || metadata.limit)), + } : { - page: prevPage, - limit: metadata.limit, - totalRows: metadata.totalRows || 0, - totalPages: metadata.totalPages || 0, - }; + page: prevPage, + limit: metadata.limit, + totalRows: metadata.totalRows || 0, + totalPages: metadata.totalPages || 0, + }; return { data: prevPageResponse.data, @@ -793,17 +781,17 @@ export class MessariClient extends MessariClientBase { const pageResponse = await fetchPage(pageParams, options); const pageMetadata: PaginationMetadata = pageResponse.metadata ? { - page: pageResponse.metadata.page || page, - limit: pageResponse.metadata.limit || metadata.limit, - totalRows: pageResponse.metadata.total || metadata.totalRows || 0, - totalPages: Math.ceil((pageResponse.metadata.total || metadata.totalRows || 0) / (pageResponse.metadata.limit || metadata.limit)), - } + page: pageResponse.metadata.page || page, + limit: pageResponse.metadata.limit || metadata.limit, + totalRows: pageResponse.metadata.total || metadata.totalRows || 0, + totalPages: Math.ceil((pageResponse.metadata.total || metadata.totalRows || 0) / (pageResponse.metadata.limit || metadata.limit)), + } : { - page, - limit: metadata.limit, - totalRows: metadata.totalRows || 0, - totalPages: metadata.totalPages || 0, - }; + page, + limit: metadata.limit, + totalRows: metadata.totalRows || 0, + totalPages: metadata.totalPages || 0, + }; return { data: pageResponse.data, diff --git a/packages/examples/src/ai.ts b/packages/examples/src/ai.ts index 2827728..8ca4e20 100644 --- a/packages/examples/src/ai.ts +++ b/packages/examples/src/ai.ts @@ -45,14 +45,14 @@ async function main() { ], verbosity: "succinct", response_format: "plaintext", - inline_citations: false + inline_citations: false, }); for await (const chunk of response) { - console.log({chunk}); + console.log({ chunk }); } - - console.log('\n'); + + console.log("\n"); } else { const response = await client.ai.createChatCompletionOpenAI({ messages: [ @@ -64,7 +64,6 @@ async function main() { verbosity: "succinct", response_format: "plaintext", inline_citations: false, - }); console.log("Response received:"); console.log(response); From 4190a170c4a608d323779f5a355dbfbfc2aeb04d Mon Sep 17 00:00:00 2001 From: Bijan Massoumi Date: Tue, 15 Apr 2025 09:24:47 -0400 Subject: [PATCH 8/9] Fix type and improve example (#35) Co-authored-by: Daniel Khoo --- packages/api/src/types/types.ts | 5 ++++- packages/examples/src/ai.ts | 6 +++++- typegen/openapi/services/ai/openapi.yaml | 6 ++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/api/src/types/types.ts b/packages/api/src/types/types.ts index 5539a36..154bd96 100644 --- a/packages/api/src/types/types.ts +++ b/packages/api/src/types/types.ts @@ -650,6 +650,10 @@ export type components = { messages: components["schemas"]["ChatCompletionMessage"][]; }; ChatCompletionResponseChoiceOpenAI: { + delta?: { + /** @description The content of the message */ + content?: string; + }; /** @description Reason the completion finished */ finish_reason: string; /** @description Index of the choice in the array */ @@ -2130,7 +2134,6 @@ export type operations = { 200: { content: { "application/json": components["schemas"]["ChatCompletionResponseOpenAI"]; - "text/event-stream": string; }; }; /** @description Server error response */ diff --git a/packages/examples/src/ai.ts b/packages/examples/src/ai.ts index 8ca4e20..b1cf2d6 100644 --- a/packages/examples/src/ai.ts +++ b/packages/examples/src/ai.ts @@ -48,9 +48,13 @@ async function main() { inline_citations: false, }); + let content = ""; for await (const chunk of response) { - console.log({ chunk }); + if (chunk.choices.length > 0 && chunk.choices[0].delta?.content) { + content += chunk.choices[0].delta.content; + } } + console.log(content); console.log("\n"); } else { diff --git a/typegen/openapi/services/ai/openapi.yaml b/typegen/openapi/services/ai/openapi.yaml index d00dbf6..f5cd9d2 100644 --- a/typegen/openapi/services/ai/openapi.yaml +++ b/typegen/openapi/services/ai/openapi.yaml @@ -382,6 +382,12 @@ components: description: Index of the choice in the array message: $ref: '#/components/schemas/ChatCompletionResponseMessageOpenAI' + delta: + type: object + properties: + content: + type: string + description: The content of the message finish_reason: type: string description: Reason the completion finished From d4ffe1a49c6e2ec4b9d22509dbe328ce846b0068 Mon Sep 17 00:00:00 2001 From: bijan Date: Tue, 15 Apr 2025 09:36:55 -0400 Subject: [PATCH 9/9] replace messari format with openAI --- packages/api/src/client/base.ts | 12 +-- packages/api/src/client/client.ts | 9 +- packages/examples/src/ai.ts | 158 ++++++++---------------------- 3 files changed, 45 insertions(+), 134 deletions(-) diff --git a/packages/api/src/client/base.ts b/packages/api/src/client/base.ts index d355ed2..7142f9f 100644 --- a/packages/api/src/client/base.ts +++ b/packages/api/src/client/base.ts @@ -106,21 +106,13 @@ import type { PaginatedResult, RequestOptions, ClientEventMap, ClientEventType, * Interface for the AI API methods */ export interface AIInterface { - /** - * Creates a chat completion using Messari's AI - * @param params Parameters for the chat completion request - * @param options Optional request configuration - * @returns A promise resolving to the chat completion response - */ - createChatCompletion(params: createChatCompletionParameters, options?: RequestOptions): Promise; - /** * Creates a chat completion using OpenAI's API * @param params Parameters for the chat completion request * @param options Optional request configuration * @returns A promise resolving to the chat completion response */ - createChatCompletionOpenAI(params: Omit, options?: RequestOptions): Promise; + createChatCompletion(params: Omit, options?: RequestOptions): Promise; /** * Creates a streaming chat completion using OpenAI's API @@ -128,7 +120,7 @@ export interface AIInterface { * @param options Optional request configuration * @returns A promise resolving to a readable stream of chat completion chunks */ - createChatCompletionOpenAIStream( + createChatCompletionStream( params: Omit, options?: RequestOptions, ): Promise>; diff --git a/packages/api/src/client/client.ts b/packages/api/src/client/client.ts index 5ac520d..bddb958 100644 --- a/packages/api/src/client/client.ts +++ b/packages/api/src/client/client.ts @@ -856,20 +856,13 @@ export class MessariClient extends MessariClientBase { public readonly ai: AIInterface = { createChatCompletion: (params: createChatCompletionParameters, options?: RequestOptions) => - this.request({ - method: createChatCompletion.method, - path: createChatCompletion.path(), - body: pick(params, createChatCompletion.bodyParams), - options, - }), - createChatCompletionOpenAI: (params: createChatCompletionParameters, options?: RequestOptions) => this.request({ method: createChatCompletionOpenAI.method, path: createChatCompletionOpenAI.path(), body: pick(params, createChatCompletionOpenAI.bodyParams) as createChatCompletionOpenAIParameters & { stream: false }, options, }), - createChatCompletionOpenAIStream: (params: createChatCompletionParameters, options?: RequestOptions) => + createChatCompletionStream: (params: createChatCompletionParameters, options?: RequestOptions) => this.requestStream({ method: createChatCompletionOpenAI.method, path: createChatCompletionOpenAI.path(), diff --git a/packages/examples/src/ai.ts b/packages/examples/src/ai.ts index b1cf2d6..59b9c29 100644 --- a/packages/examples/src/ai.ts +++ b/packages/examples/src/ai.ts @@ -21,133 +21,59 @@ const client = new MessariClient({ // Get command line arguments const args = process.argv.slice(2); -const useOpenAI = args.includes("openai"); const useStreaming = args.includes("stream"); async function main() { - if (useOpenAI) { - // OpenAI Chat Completion - try { - console.log("\n--------------------------------"); - console.log("OpenAI Chat Completion"); - console.log("--------------------------------"); - console.log("Sending request..."); - console.log(`"What are the key differences between Bitcoin and Ethereum?"`); - - if (useStreaming) { - // Call the createChatCompletionOpenAI endpoint - const response = await client.ai.createChatCompletionOpenAIStream({ - messages: [ - { - role: "user", - content: "What are the key differences between Bitcoin and Ethereum?", - }, - ], - verbosity: "succinct", - response_format: "plaintext", - inline_citations: false, - }); - - let content = ""; - for await (const chunk of response) { - if (chunk.choices.length > 0 && chunk.choices[0].delta?.content) { - content += chunk.choices[0].delta.content; - } - } - console.log(content); - - console.log("\n"); - } else { - const response = await client.ai.createChatCompletionOpenAI({ - messages: [ - { - role: "user", - content: "What are the key differences between Bitcoin and Ethereum?", - }, - ], - verbosity: "succinct", - response_format: "plaintext", - inline_citations: false, - }); - console.log("Response received:"); - console.log(response); - } - } catch (error) { - console.error("Error calling createChatCompletionOpenAI:", error); - } - return; - } - - try { - console.log("--------------------------------"); - console.log("AI Chat Completion"); - console.log("--------------------------------"); - console.log("Sending request..."); - console.log(`"What companies have both paradigm and a16z on their cap table?"`); - - // Call the createChatCompletion endpoint - const response = await client.ai.createChatCompletion({ - messages: [ - { - role: "user", - content: "What companies have both paradigm and a16z on their cap table?", - }, - ], - verbosity: "succinct", - response_format: "plaintext", - inline_citations: false, - stream: useStreaming, - }); - - const assistantMessage = response.messages[0].content; - console.log(assistantMessage); - } catch (error) { - console.error("Error calling createChatCompletion:", error); - } - - // Call the createChatCompletion endpoint with streaming + // OpenAI Chat Completion try { console.log("\n--------------------------------"); - console.log("AI Chat Completion Streaming"); + console.log("OpenAI Chat Completion (Streaming)"); console.log("--------------------------------"); console.log("Sending request..."); - console.log(`"What is the all time high price of Bitcoin?"`); - - // Call the createChatCompletion endpoint - const response = await client.ai.createChatCompletion({ - messages: [ - { - role: "user", - content: "What is the all time high price of Bitcoin?", - }, - ], - verbosity: "succinct", - response_format: "plaintext", - inline_citations: false, - stream: true, - }); - - // Treat the combined streamed Server-Sent Events (SSE) chunks as a single string - const rawResponse = response as unknown as string; - const chunks = rawResponse.split("\n\n").filter((line) => line.trim() !== ""); - - let content = ""; - for (const chunk of chunks) { - const dataMatch = chunk.match(/data: ({.*})/); - if (dataMatch) { - try { - const data = JSON.parse(dataMatch[1]); - if (data.data?.messages?.[0]?.delta?.content) { - content += data.data.messages[0].delta.content; - } - } catch (e) { - console.error("Error parsing SSE message:", e); + console.log(`"What are the key differences between Bitcoin and Ethereum?"`); + + if (useStreaming) { + // Call the createChatCompletionOpenAI endpoint + const response = await client.ai.createChatCompletionStream({ + messages: [ + { + role: "user", + content: "What are the key differences between Bitcoin and Ethereum?", + }, + ], + verbosity: "succinct", + response_format: "plaintext", + inline_citations: false, + }); + + // Process the stream and progressively print out the text + process.stdout.write("Response: "); + for await (const chunk of response) { + if (chunk.choices.length > 0 && chunk.choices[0].delta?.content) { + const content = chunk.choices[0].delta.content; + process.stdout.write(content); } } + process.stdout.write("\n"); + + console.log("\n"); + } else { + const response = await client.ai.createChatCompletion({ + messages: [ + { + role: "user", + content: "What are the key differences between Bitcoin and Ethereum?", + }, + ], + verbosity: "succinct", + response_format: "plaintext", + inline_citations: false, + }); + console.log("Response received:"); + console.log(response); } - console.log(content); } catch (error) { - console.error("Error calling createChatCompletion:", error); + console.error("Error calling createChatCompletionOpenAI:", error); } // Entity Extraction