From f8bbf4735e009d40f9ac4bbff7d137ab376dabdd Mon Sep 17 00:00:00 2001 From: Olafur Geirsson Date: Sun, 27 Oct 2024 21:40:00 +0100 Subject: [PATCH] API Docs: implement custom OpenAPI component Previously, there was no easy way to reference docs for our REST API endpoints. This PR adds a new `` React component that we can use in MDX files to embed docs for a single API operation. The component renders the code examples, request schema, and response schema using existing components like code blocks and HTML tables avoiding the need for custom CSS styling. --- docs/api/cody.mdx | 13 + docs/api/index.mdx | 1 + package.json | 4 +- pnpm-lock.yaml | 29 + scripts/openapi-compile.sh | 21 + scripts/openapi-watch.sh | 16 + src/components/MdxComponents.tsx | 4 +- src/components/mdx/Tabs.tsx | 1 + src/components/openapi/ApiOperation.tsx | 326 ++++++++ src/components/openapi/ApiSpec.ts | 87 ++ .../openapi/openapi.Sourcegraph.Latest.json | 790 ++++++++++++++++++ src/components/openapi/types.ts | 105 +++ src/data/navigation.ts | 4 + 13 files changed, 1399 insertions(+), 2 deletions(-) create mode 100644 docs/api/cody.mdx create mode 100755 scripts/openapi-compile.sh create mode 100755 scripts/openapi-watch.sh create mode 100644 src/components/openapi/ApiOperation.tsx create mode 100644 src/components/openapi/ApiSpec.ts create mode 100644 src/components/openapi/openapi.Sourcegraph.Latest.json create mode 100644 src/components/openapi/types.ts diff --git a/docs/api/cody.mdx b/docs/api/cody.mdx new file mode 100644 index 000000000..5e41fd7df --- /dev/null +++ b/docs/api/cody.mdx @@ -0,0 +1,13 @@ +# Sourcegraph Cody API + +## `POST /.api/cody/context` + + +## `POST /.api/llm/chat/completions` + + +## `GET /.api/llm/models` + + +## `GET /.api/llm/models/{modelId}` + diff --git a/docs/api/index.mdx b/docs/api/index.mdx index 92f195b56..d567c14b1 100644 --- a/docs/api/index.mdx +++ b/docs/api/index.mdx @@ -3,4 +3,5 @@ Sourcegraph exposes the following APIs: - [Sourcegraph GraphQL API](/api/graphql/), for accessing data stored or computed by Sourcegraph +- [Sourcegraph Cody API](/api/cody/), for interacting with Cody. - [Sourcegraph Stream API](/api/stream_api/), for consuming search results as a stream of events diff --git a/package.json b/package.json index f7dd6b1f8..7c3de2f14 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "next lint" + "lint": "next lint", + "openapi:watch": "./scripts/openapi-watch.sh" }, "browserslist": "defaults, not ie <= 11", "dependencies": { @@ -46,6 +47,7 @@ "react": "18.3.1", "react-dom": "18.3.1", "react-highlight-words": "^0.20.0", + "react-markdown": "^9.0.1", "react-syntax-highlighter": "^15.5.0", "rehype-autolink-headings": "^7.1.0", "rehype-pretty-code": "^0.10.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8cffacec7..37ad8f84b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -113,6 +113,9 @@ dependencies: react-highlight-words: specifier: ^0.20.0 version: 0.20.0(react@18.3.1) + react-markdown: + specifier: ^9.0.1 + version: 9.0.1(@types/react@18.2.20)(react@18.3.1) react-syntax-highlighter: specifier: ^15.5.0 version: 15.5.0(react@18.3.1) @@ -4192,6 +4195,10 @@ packages: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} dev: false + /html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + dev: false + /html-void-elements@2.0.1: resolution: {integrity: sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==} dev: false @@ -6505,6 +6512,28 @@ packages: /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + /react-markdown@9.0.1(@types/react@18.2.20)(react@18.3.1): + resolution: {integrity: sha512-186Gw/vF1uRkydbsOIkcGXw7aHq0sZOCRFFjGrr7b9+nVZg4UfA4enXCaxm4fUzecU38sWfrNDitGhshuU7rdg==} + peerDependencies: + '@types/react': '>=18' + react: '>=18' + dependencies: + '@types/hast': 3.0.3 + '@types/react': 18.2.20 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.1 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.0 + react: 18.3.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.1 + unified: 11.0.4 + unist-util-visit: 5.0.0 + vfile: 6.0.1 + transitivePeerDependencies: + - supports-color + dev: false + /react-remove-scroll-bar@2.3.6(@types/react@18.2.20)(react@18.3.1): resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==} engines: {node: '>=10'} diff --git a/scripts/openapi-compile.sh b/scripts/openapi-compile.sh new file mode 100755 index 000000000..1623abf8c --- /dev/null +++ b/scripts/openapi-compile.sh @@ -0,0 +1,21 @@ +#!/bin/bash +set -eux + +# Check if yq is installed, if not, install it using Homebrew +if ! command -v yq &> /dev/null +then + echo "yq could not be found, installing via Homebrew..." + if ! command -v brew &> /dev/null + then + echo "Homebrew is not installed. Please install Homebrew first." + exit 1 + fi + brew install yq +fi + + +SOURCEGRAPH_DIR=$1 +DOCS_DIR=$2 +cd "$SOURCEGRAPH_DIR" +pnpm -C internal/openapi compile +yq eval -o=json internal/openapi/tsp-output/@typespec/openapi3/openapi.Sourcegraph.Latest.yaml > "$DOCS_DIR"/src/components/openapi/openapi.Sourcegraph.Latest.json diff --git a/scripts/openapi-watch.sh b/scripts/openapi-watch.sh new file mode 100755 index 000000000..646dbb0f5 --- /dev/null +++ b/scripts/openapi-watch.sh @@ -0,0 +1,16 @@ +#!/bin/bash +set -eux + + +# Check if entr is installed +if ! command -v entr &> /dev/null +then + echo "entr could not be found, installing with Homebrew..." + brew install entr +fi + +COMPILE_SCRIPT=$(dirname "${BASH_SOURCE[0]}")/openapi-compile.sh +DOCS_DIR=$(realpath $(dirname "${BASH_SOURCE[0]}")/../) +SOURCEGRAPH_DIR=${SOURCEGRAPH_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../sourcegraph} +SOURCEGRAPH_DIR=$(realpath $SOURCEGRAPH_DIR) +echo $SOURCEGRAPH_DIR/internal/openapi/public.tsp | entr $COMPILE_SCRIPT $SOURCEGRAPH_DIR $DOCS_DIR diff --git a/src/components/MdxComponents.tsx b/src/components/MdxComponents.tsx index 09f5b146d..4a5175d1a 100644 --- a/src/components/MdxComponents.tsx +++ b/src/components/MdxComponents.tsx @@ -1,6 +1,7 @@ import AWSOneClickLaunchForm from './AWSOneClickLaunchForm'; import { ContentTab, ContentTabs } from './ContentTabs'; import FeatureParity from './FeatureParity'; +import { PreCode, PreCodeBlock } from './PreCodeBlock'; import Accordion from './mdx/Accordion'; import { Callout } from './mdx/Callout'; import { CustomLink } from './mdx/CustomLink'; @@ -9,12 +10,13 @@ import { LinkCard, LinkCards } from './mdx/LinkCards'; import { ProductCard, ProductCards } from './mdx/ProductCards'; import { QuickLink, QuickLinks } from './mdx/QuickLinks'; import { Tab, Tabs } from './mdx/Tabs'; -import { PreCodeBlock, PreCode } from './PreCodeBlock'; +import { ApiOperation } from './openapi/ApiOperation'; import ResourceEstimator from './resource-estimator/ResourceEstimator'; import { Badge } from './ui/badge'; const MdxComponents = (version?: string) => { return { + ApiOperation, FeatureParity, ResourceEstimator, AWSOneClickLaunchForm, diff --git a/src/components/mdx/Tabs.tsx b/src/components/mdx/Tabs.tsx index 5e990363c..862af6e14 100644 --- a/src/components/mdx/Tabs.tsx +++ b/src/components/mdx/Tabs.tsx @@ -68,6 +68,7 @@ export function Tabs({children}: TabsProps) { } interface TabProps { + title?: string; children: ReactNode; } diff --git a/src/components/openapi/ApiOperation.tsx b/src/components/openapi/ApiOperation.tsx new file mode 100644 index 000000000..8f8e6d6c0 --- /dev/null +++ b/src/components/openapi/ApiOperation.tsx @@ -0,0 +1,326 @@ +import openapi from './openapi.Sourcegraph.Latest.json'; + +import {OAISchema, OAISpec, Operation, SchemaProperty} from './types'; +import {ApiSpec, newApiSpec} from './ApiSpec'; +import {Tab, Tabs} from '../mdx/Tabs'; +import {PreCode, PreCodeBlock} from '../PreCodeBlock'; +import Markdown from 'react-markdown' + +let spec: ApiSpec | undefined; +function loadSpec() { + if (!spec) { + spec = newApiSpec(openapi); + } + return spec; +} + +export function ApiOperation(props: {operation: string}) { + const operation = loadSpec().findOperation(props.operation); + if (!operation) { + return
Operation {props.operation} not found
; + } + + return ( +
+

{operation.description}

+ {example(operation)} + {request(operation)} + {response(operation)} +
+ ); +} + +function request(operation: Operation) { + if ( + !operation.schema.requestBody && + operation.schema.parameters.length === 0 + ) { + return null; + } + return ( +
+

Request: application/json

+ {operation.schema.requestBody && + schema(operation.schema.requestBody)} + {/* {operation.schema.requestBody && ( +
+					Request body:{' '}
+					{JSON.stringify(operation.schema.requestBody, null, 2)}
+				
+ )} */} + {/* {operation.schema.parameters && ( +
+					Parameters:{' '}
+					{JSON.stringify(operation.schema.parameters, null, 2)}
+				
+ )} */} +
+ ); +} + +function response(operation: Operation) { + if (!operation.schema.response) { + return null; + } + return ( +
+

Response: application/json

+ {schema(operation.schema.response)} +
+ ); +} + +function Code(props: {lang: string; children: React.ReactNode}) { + return ( + + {props.children} + + ); +} + +function example(operation: Operation) { + if (!operation.example) { + return null; + } + return ( +
+

Example

+ + + {curlCommand(operation)} + + + + {typescriptExample(operation)} + + + + {pythonExample(operation)} + + +

+ Example response: +

+ + {JSON.stringify(operation.example.response, null, 2)} + +
+ ); +} + +function formatJson(json: any, indent: string) { + return JSON.stringify(json, null, 2).replaceAll('\n', '\n' + indent); +} + +function typescriptExample(operation: Operation): string { + const out: string[] = []; + out.push(`import fetch from 'node-fetch'`); + out.push(`const endpoint = process.env.SRC_ENDPOINT`); + out.push(`const accessToken = process.env.SRC_ACCESS_TOKEN`); + out.push( + `const response = await fetch(\`\${endpoint}${operation.path}\`, {` + ); + if (operation.method !== 'get') { + out.push(` method: '${operation.method}',`); + } + out.push(` headers: {`); + out.push(` 'Authorization': \`token \${accessToken}\`,`); + out.push(` },`); + if (operation.example?.request) { + out.push( + ` body: JSON.stringify(${formatJson( + operation.example.request, + ' ' + )})` + ); + } + out.push(`})`); + out.push(`console.log(await response.json())`); + return out.join('\n'); +} + +function pythonExample(operation: Operation): string { + const out: string[] = []; + out.push(`import os`); + out.push(`import requests`); + out.push(`endpoint = os.getenv('SRC_ENDPOINT')`); + out.push(`access_token = os.getenv('SRC_ACCESS_TOKEN')`); + out.push(`response = requests.${operation.method.toLowerCase()}(`); + out.push(` url=f'{endpoint}${operation.path}',`); + out.push(` headers={"Authorization": f'token {access_token}'},`); + if (operation.example?.request) { + out.push(` json=${formatJson(operation.example.request, ' ')}`); + } + out.push(`)`); + out.push(`print(response.json())`); + return out.join('\n'); +} + +function curlCommand(operation: Operation): string { + const out: string[] = []; + out.push(`curl "$SRC_ENDPOINT${operation.path}"`); + out.push('--header "Authorization: token $SRC_ACCESS_TOKEN"'); + if (operation.method !== 'get') { + out.push(`--request ${operation.method.toUpperCase()}`); + } + if (operation.example?.request) { + out.push(`--data '${formatJson(operation.example.request, ' ')}'`); + } + + return out.join(' \\\n '); +} + +function schema(schema: OAISchema): React.ReactNode { + const spec = loadSpec(); + schema = spec.canonical(schema); + const rows: React.ReactNode[] = []; + const refs = new Set(); + const isRendered = new Set(); + schemaProperties(spec, '', schema, rows, refs); + while (refs.size > 0) { + const ref = refs.values().next().value; + refs.delete(ref); + if (isRendered.has(ref)) { + // Important: avoid infinite loop + continue; + } + isRendered.add(ref); + schemaProperties(spec, componentName(ref), spec.ref(ref), rows, refs); + } + + return ( + + + + + + + + + + {rows} +
FieldTypeRequiredDescription
+ ); +} + +function schemaProperties( + spec: ApiSpec, + prefix: string, + schema: OAISchema, + rows: React.ReactNode[], + refs: Set +): void { + const properties = spec.properties(schema); + for (const property of properties) { + rows.push( + + + + {prefix ? `${prefix}.${property.name}` : property.name} + + + {schemaType(property.schema, refs)} + {schemaRequired(schema, property)} + {schemaDescription(property.schema)} + + ); + } +} + +function componentName(component: string): string { + return component.split('/').pop() ?? component; +} +function refType(ref: string, refs: Set): string { + refs.add(ref); + return componentName(ref); +} +function schemaType(schema: OAISchema, refs: Set): React.ReactNode { + if (schema.$ref) { + return {refType(schema.$ref, refs)}; + } + if (schema.type === 'array') { + const name = schema.items?.$ref + ? refType(schema.items.$ref, refs) + : schema.items?.type ?? ''; + return {name}[]; + } + if (schema.anyOf && schema.anyOf.length > 0) { + const parts = schema.anyOf.map(tpe => ( + {schemaType(tpe, refs)} + )); + return joinReactNodes(parts, ' | '); + } + return schema.type; +} + +function schemaRequired( + schema: OAISchema, + property: SchemaProperty +): React.ReactNode { + console.log({schema: schema, property: property.name}); + if (schema.required?.includes(property.name)) { + return Yes; + } + return No; +} + +function schemaDescription(schema: OAISchema): React.ReactNode { + const parts: React.ReactNode[] = []; + if (schema.description) { + parts.push({schema.description}); + } + if (schema.enum && schema.type === 'string') { + const values = schema.enum.map(value => "{value}"); + if (values.length === 1) { + parts.push(Value: {values[0]}); + } else { + parts.push(One of: {joinReactNodes(values, ",")}); + } + } + + + if ( + typeof schema.minimum === 'number' && + typeof schema.maximum === 'number' + ) { + parts.push( + + Range:{' '} + + {schema.minimum} <= x <= {schema.maximum} + + + ); + } else if (typeof schema.minimum === 'number') { + parts.push( + + Minimum: {schema.minimum} + + ); + } else if (typeof schema.maximum === 'number') { + parts.push( + + Maximum: {schema.maximum} + + ); + } + + return joinReactNodes(parts, <>); +} + +function joinReactNodes(values: React.ReactNode[], separator: React.ReactNode) { + if (values.length === 0) { + return null; + } + return values.reduce((prev, curr, i) => + i === 0 ? ( + curr + ) : ( + <> + {prev} + {separator} + {curr} + + ) + ); +} diff --git a/src/components/openapi/ApiSpec.ts b/src/components/openapi/ApiSpec.ts new file mode 100644 index 000000000..58bf476e2 --- /dev/null +++ b/src/components/openapi/ApiSpec.ts @@ -0,0 +1,87 @@ +import {OAISchema, OAISpec, Operation, SchemaProperty} from './types'; + +export function newApiSpec(spec: OAISpec): ApiSpec { + const components = new Map(); + const operations: Operation[] = []; + for (const path of Object.keys(spec.paths)) { + const pathItem = spec.paths[path as keyof typeof spec.paths]; + for (const method of Object.keys(pathItem)) { + const operation = pathItem[method as keyof typeof pathItem]; + if (operation && 'operationId' in operation) { + const requestExample = + operation.requestBody?.content?.['application/json'] + ?.example; + const responseExample = + operation.responses['200']?.content?.['application/json'] + ?.example; + operations.push({ + id: `${method.toUpperCase()} ${path}`, + method, + path, + description: operation.description, + example: + requestExample || responseExample + ? { + request: requestExample, + response: responseExample + } + : undefined, + schema: { + requestBody: + operation.requestBody?.content?.['application/json'] + ?.schema, + response: + operation.responses['200']?.content?.[ + 'application/json' + ]?.schema, + parameters: operation.parameters ?? [] + } + }); + } + } + } + for (const component of Object.keys(spec.components?.schemas ?? {})) { + const schema = spec.components?.schemas?.[component]; + if (!schema) { + continue; + } + components.set(`#/components/schemas/${component}`, schema); + } + return new ApiSpec(components, operations); +} + +export class ApiSpec { + constructor( + public readonly components: Map, + public readonly operations: Operation[] + ) {} + public findOperation(operation: string): Operation | undefined { + for (const op of this.operations) { + if (op.id === operation) { + return op; + } + } + return undefined; + } + public ref(ref: string): OAISchema { + const component = this.components.get(ref); + if (!component) { + throw new Error(`$ref not found: ${ref}`); + } + return component; + } + public canonical(schema: OAISchema): OAISchema { + if (schema.$ref) { + return this.canonical(this.ref(schema.$ref)); + } + return schema; + } + public properties(schema: OAISchema): SchemaProperty[] { + if (schema.$ref) { + return this.properties(this.ref(schema.$ref)); + } + return Object.entries(schema.properties ?? {}).map( + ([name, schema]) => ({name, schema}) + ); + } +} diff --git a/src/components/openapi/openapi.Sourcegraph.Latest.json b/src/components/openapi/openapi.Sourcegraph.Latest.json new file mode 100644 index 000000000..4e67dbe10 --- /dev/null +++ b/src/components/openapi/openapi.Sourcegraph.Latest.json @@ -0,0 +1,790 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Sourcegraph", + "version": "Latest" + }, + "tags": [], + "paths": { + "/.api/cody/context": { + "post": { + "operationId": "CodyService_context", + "description": "Send a natural language query with a list of repositories, and Cody locates related code examples from those repos.", + "parameters": [], + "responses": { + "200": { + "description": "The request has succeeded.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CodyContextResponse" + }, + "example": { + "results": [ + { + "blob": { + "path": "vscode/src/chat/chat-view/ChatController.ts", + "repository": { + "id": "UmVwb3NpdG9yeToyNzU5OQ==", + "name": "github.com/sourcegraph/cody" + }, + "commit": { + "oid": "fdcc8a185b21c81d1987bc1daf2c29cec3d19b06" + }, + "url": "/github.com/sourcegraph/cody@fdcc8a185b21c81d1987bc1daf2c29cec3d19b06/-/blob/vscode/src/chat/chat-view/ChatController.ts" + }, + "startLine": 156, + "endLine": 181, + "chunkContent": "\n/**\n * ChatController is the view controller class for the chat panel.\n * It handles all events sent from the view, keeps track of the underlying chat model,\n * and interacts with the rest of the extension.\n *\n * Its methods are grouped into the following sections, each of which is demarcated\n * by a comment block (search for \"// #region \"):\n *\n * 1. top-level view action handlers\n * 2. view updaters\n * 3. chat request lifecycle methods\n * 4. session management\n * 5. webview container management\n * 6. other public accessors and mutators\n *\n * The following invariants should be maintained:\n * 1. top-level view action handlers\n * a. should all follow the handle$ACTION naming convention\n * b. should be private (with the existing exceptions)\n * 2. view updaters\n * a. should all follow the post$ACTION naming convention\n * b. should NOT mutate model state\n * 3. Keep the public interface of this class small in order to\n * avoid tight coupling with other classes. If communication\n" + } + ] + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CodyContextRequest" + }, + "example": { + "query": "What does ChatController do?", + "repos": [ + { + "name": "github.com/sourcegraph/cody" + } + ] + } + } + } + } + } + }, + "/.api/llm/chat/completions": { + "post": { + "operationId": "LLMService_chatCompletions", + "description": "Send a structured list of input messages with text and/or image content, and the model will generate the next message in the conversation.", + "parameters": [], + "responses": { + "200": { + "description": "The request has succeeded.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateChatCompletionResponse" + }, + "example": { + "id": "chat-UUID", + "created": 1727692163829, + "model": "anthropic::2023-06-01::claude-3.5-sonnet", + "object": "object", + "choices": [ + { + "index": 0, + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": "URIs identify, URLs locate" + } + } + ] + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateChatCompletionRequest" + }, + "example": { + "model": "anthropic::2023-06-01::claude-3.5-sonnet", + "max_tokens": 2000, + "messages": [ + { + "role": "user", + "content": "what is the difference between URI and URL?" + } + ] + } + } + } + } + } + }, + "/.api/llm/models": { + "get": { + "operationId": "LLMService_list", + "description": "Lists the currently available models, and provides basic information about each one such as the owner and availability.", + "parameters": [], + "responses": { + "200": { + "description": "The request has succeeded.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAIListModelsResponse" + }, + "example": { + "object": "list", + "data": [ + { + "id": "anthropic::2023-06-01::claude-3.5-sonnet", + "object": "model", + "created": 0, + "owned_by": "anthropic" + } + ] + } + } + } + } + } + } + }, + "/.api/llm/models/{modelId}": { + "get": { + "operationId": "LLMService_retrieveModel", + "description": "Retrieves a model instance, providing basic information about the model such as the owner and permissioning.", + "parameters": [ + { + "name": "modelId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The request has succeeded.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAIModel" + }, + "example": { + "id": "anthropic::2023-06-01::claude-3.5-sonnet", + "object": "model", + "created": 0, + "owned_by": "anthropic" + } + } + } + } + } + } + } + }, + "security": [ + { + "SourcegraphTokenAuth": [] + } + ], + "components": { + "schemas": { + "BlobInfo": { + "type": "object", + "required": [ + "path", + "repository", + "commit", + "url" + ], + "properties": { + "path": { + "type": "string" + }, + "repository": { + "$ref": "#/components/schemas/RepositoryInfo" + }, + "commit": { + "$ref": "#/components/schemas/CommitInfo" + }, + "url": { + "type": "string" + } + } + }, + "ChatCompletionChoice": { + "type": "object", + "required": [ + "index", + "message" + ], + "properties": { + "finish_reason": { + "type": "string" + }, + "index": { + "type": "integer", + "format": "int32" + }, + "message": { + "$ref": "#/components/schemas/ChatCompletionResponseMessage" + }, + "logprobs": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/ChatCompletionLogprobs" + } + ], + "nullable": true + } + } + }, + "ChatCompletionLogprobs": { + "type": "object", + "properties": { + "content": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChatCompletionTokenLogprob" + } + } + } + }, + "ChatCompletionRequestMessage": { + "type": "object", + "required": [ + "role", + "content" + ], + "properties": { + "role": { + "type": "string", + "enum": [ + "user", + "assistant", + "system" + ] + }, + "content": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/MessageContentPart" + } + } + ] + } + } + }, + "ChatCompletionResponseMessage": { + "type": "object", + "required": [ + "role", + "content" + ], + "properties": { + "role": { + "type": "string", + "enum": [ + "user", + "assistant" + ] + }, + "content": { + "type": "string" + } + } + }, + "ChatCompletionStreamOptions": { + "type": "object", + "properties": { + "include_usage": { + "type": "boolean", + "nullable": true + } + } + }, + "ChatCompletionTokenLogprob": { + "type": "object", + "required": [ + "token", + "logprob", + "bytes", + "top_logprobs" + ], + "properties": { + "token": { + "type": "string" + }, + "logprob": { + "type": "number", + "format": "double" + }, + "bytes": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + }, + "top_logprobs": { + "type": "object", + "required": [ + "token", + "logprob", + "bytes" + ], + "properties": { + "token": { + "type": "string" + }, + "logprob": { + "type": "number", + "format": "double" + }, + "bytes": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + } + }, + "description": "The template for omitting properties." + } + } + }, + "CodyContextRequest": { + "type": "object", + "required": [ + "query" + ], + "properties": { + "repos": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RepoSpec" + }, + "description": "The list of repos to search through." + }, + "query": { + "type": "string", + "description": "The natural language query to find relevant context from the provided list of repos." + }, + "codeResultsCount": { + "type": "integer", + "format": "int32", + "minimum": 0, + "maximum": 100, + "description": "The number of results to return from source code (example: Python or TypeScript).", + "default": 15 + }, + "textResultsCount": { + "type": "integer", + "format": "int32", + "minimum": 0, + "maximum": 100, + "description": "The number of results to return from text sources like Markdown.", + "default": 5 + }, + "filePatterns": { + "type": "array", + "items": { + "type": "string" + }, + "description": "An optional list of file patterns used to filter the results. The\npatterns are regex strings. For a file chunk to be returned a context\nresult, the path must match at least one of these patterns." + }, + "version": { + "type": "string", + "enum": [ + "1.0", + "2.0" + ], + "description": "The version number of the context API\n\nValid versions:\n- \"1.0\": The old context API (default).\n- \"2.0\": The new context API.", + "default": "1.0" + } + } + }, + "CodyContextResponse": { + "type": "object", + "required": [ + "results" + ], + "properties": { + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileChunkContext" + } + } + } + }, + "CommitInfo": { + "type": "object", + "required": [ + "oid" + ], + "properties": { + "oid": { + "type": "string" + } + } + }, + "CompletionUsage": { + "type": "object", + "required": [ + "completion_tokens", + "prompt_tokens", + "total_tokens" + ], + "properties": { + "completion_tokens": { + "type": "integer", + "format": "int32", + "description": "Number of tokens in the generated completion." + }, + "prompt_tokens": { + "type": "integer", + "format": "int32", + "description": "Number of tokens in the prompt." + }, + "total_tokens": { + "type": "integer", + "format": "int32", + "description": "Total number of tokens used in the request (prompt + completion)." + } + }, + "description": "Usage statistics for the completion request." + }, + "CreateChatCompletionRequest": { + "type": "object", + "required": [ + "model" + ], + "properties": { + "messages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChatCompletionRequestMessage" + }, + "description": "A list of messages to start the thread with." + }, + "model": { + "type": "string", + "description": "A model name using the syntax `${ProviderID}::${APIVersionID}::${ModelID}`:\n- ProviderID: lowercase name of the LLM provider. Example: `\"anthropic\"` in\n`\"anthropic::2023-06-01::claude-3.5-sonnet\"`.\n- APIVersionID: the upstream LLM provider API version. Typically formatted as\na date. Example, `\"2024-02-01\"` in `\"openai::2024-02-01::gpt-4o\"`.\n- ModelID: the name of the model. Example, `\"mixtral-8x7b-instruct\"` in\n`\"mistral::v1::mixtral-8x7b-instruct\"`.\n\nUse `GET /.api/llm/models` to list available models." + }, + "max_tokens": { + "type": "integer", + "format": "int32", + "nullable": true, + "maximum": 8000, + "description": "The maximum number of tokens that can be generated in the completion." + }, + "logit_bias": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int32" + }, + "nullable": true + }, + "logprobs": { + "type": "boolean", + "nullable": true + }, + "top_logprobs": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "n": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "frequency_penalty": { + "type": "number", + "format": "double", + "nullable": true + }, + "presence_penalty": { + "type": "number", + "format": "double", + "nullable": true + }, + "response_format": { + "type": "string", + "enum": [ + "text", + "json_object" + ], + "nullable": true + }, + "seed": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "service_tier": { + "type": "string", + "nullable": true + }, + "stop": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ], + "nullable": true + }, + "stream": { + "type": "boolean", + "nullable": true + }, + "stream_options": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/ChatCompletionStreamOptions" + } + ], + "nullable": true + }, + "temperature": { + "type": "number", + "format": "float", + "nullable": true + }, + "top_p": { + "type": "number", + "format": "float", + "nullable": true + }, + "user": { + "type": "string", + "nullable": true + } + } + }, + "CreateChatCompletionResponse": { + "type": "object", + "required": [ + "id", + "choices", + "created", + "model", + "object" + ], + "properties": { + "id": { + "type": "string" + }, + "choices": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChatCompletionChoice" + } + }, + "created": { + "type": "integer", + "format": "int64" + }, + "model": { + "type": "string" + }, + "service_tier": { + "type": "string", + "nullable": true + }, + "system_fingerprint": { + "type": "string", + "nullable": true + }, + "object": { + "type": "string", + "enum": [ + "object" + ] + }, + "usage": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/CompletionUsage" + } + ], + "nullable": true + } + } + }, + "Error": { + "type": "object", + "required": [ + "type", + "message" + ], + "properties": { + "type": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "FileChunkContext": { + "type": "object", + "required": [ + "blob", + "startLine", + "endLine", + "chunkContent" + ], + "properties": { + "blob": { + "$ref": "#/components/schemas/BlobInfo" + }, + "startLine": { + "type": "integer", + "format": "int32" + }, + "endLine": { + "type": "integer", + "format": "int32" + }, + "chunkContent": { + "type": "string" + } + } + }, + "MessageContentPart": { + "type": "object", + "required": [ + "type", + "text" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "text" + ] + }, + "text": { + "type": "string" + } + } + }, + "OAIListModelsResponse": { + "type": "object", + "required": [ + "object", + "data" + ], + "properties": { + "object": { + "type": "string", + "enum": [ + "list" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OAIModel" + } + } + } + }, + "OAIModel": { + "type": "object", + "required": [ + "id", + "object", + "created", + "owned_by" + ], + "properties": { + "id": { + "type": "string", + "description": "The model identifier, which can be referenced in the API endpoints." + }, + "object": { + "type": "string", + "enum": [ + "model" + ], + "description": "The object type, which is always \"model\"." + }, + "created": { + "type": "integer", + "format": "int64", + "description": "The Unix timestamp (in seconds) when the model was created." + }, + "owned_by": { + "type": "string", + "description": "The organization that owns the model." + } + }, + "description": "Describes an OpenAI model offering that can be used with the API." + }, + "RepoSpec": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the repository." + }, + "id": { + "type": "string", + "description": "The ID of the repository." + } + }, + "description": "RepoSpec matches a repository either by name or ID.\n\nExactly one of the properties must be defined. For example, the message\n`{id:\"id\", name:\"name\"}` is invalid because it declares both id and name." + }, + "RepositoryInfo": { + "type": "object", + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "Versions": { + "type": "string", + "enum": [ + "V5_7", + "V5_8", + "Latest" + ] + } + }, + "securitySchemes": { + "SourcegraphTokenAuth": { + "type": "apiKey", + "in": "header", + "name": "Authorization", + "description": "Authenticate to Sourcegraph APIs with the HTTP header \"Authorization\" using\nthe following formatting:\n\n```\nAuthorization: token TOKEN_VALUE\n```\nIn most cases, a Sourcegraph access token looks like this `sgp_asdadakjaaaaaaabbbbbbssswwwwaaal2131kasdaakkkkkq21asdasaa`.\n\nIn rare cases, you may encounter other kinds of token formats, which are documented in the table below.\n\n| Token Name | Description | Type | Regular Expression | |\n| -------------------------------------------- | -------------------------------------------------------------------------------- | -------------------------- | ------------------------- | ----------------------- |\n| Sourcegraph Access Token (v3) | Token used to access the Sourcegraph GraphQL API | User-generated | `sgp_(?:[a-fA-F0-9]{16}\\|local)_[a-fA-F0-9]{40}` |\n| Sourcegraph Access Token (v2, deprecated) | Token used to access the Sourcegraph GraphQL API | User-generated | `sgp_[a-fA-F0-9]{40}` | |\n| Sourcegraph Access Token (v1, deprecated) | Token used to access the Sourcegraph GraphQL API | User-generated | `[a-fA-F0-9]{40}` | |\n| Sourcegraph Dotcom User Gateway Access Token | Token used to grant sourcegraph.com users access to Cody | Backend (not user-visible) | `sgd_[a-fA-F0-9]{64}` | |\n| Sourcegraph License Key Token | Token used for product subscriptions, derived from a Sourcegraph license key | Backend (not user-visible) | `slk_[a-fA-F0-9]{64}` | |\n| Sourcegraph Enterprise subscription (aka \"product subscription\") Token | Token used for Enterprise subscriptions, derived from a Sourcegraph license key | Backend (not user-visible) | `sgs_[a-fA-F0-9]{64}` | |" + } + } + } +} diff --git a/src/components/openapi/types.ts b/src/components/openapi/types.ts new file mode 100644 index 000000000..eb427ad16 --- /dev/null +++ b/src/components/openapi/types.ts @@ -0,0 +1,105 @@ +export interface Operation { + id: string + path: string + description: string + method: string + example?: { + request?: any + response: any + } + schema: { + parameters: OAIParameter[] + requestBody?: any + response?: any + } +} +export interface OAISpec { + openapi: string + info: { + title: string + version: string + }, + tags: { + name: string + description?: string + }[], + paths: { + [key: string]: OAIPathItem + } + components?: { + schemas?: { + [key: string]: OAISchema + } + } + securitySchemes?: { + [key: string]: OAISecurityScheme + } +} +export interface OAISecurityScheme { + type: string + in: string + name: string + description: string +} +export interface OAIPathItem { + [key: string]: OAIOperation +} +export interface OAISchema { + type?: string + $ref?: string + nullable?: boolean + format?: string + minimum?: number + maximum?: number + default?: any + anyOf?: OAISchema[] + items?: OAISchema + required?: string[] + enum?: string[] + properties?: { + [key: string]: OAISchema + } + description?: string +} +export interface OAIParameter { + name: string + in: string + required: boolean + schema: OAISchema +} +export interface OAIOperation { + operationId: string + description: string + parameters?: OAIParameter[] + responses: { + '200': { + description: string + content: { + 'application/json': { + schema: OAISchema + example?: any + examples?: { + [key: string]: any + } + } + } + } + } + requestBody?: { + required: boolean + content: { + 'application/json': { + schema: OAISchema + example?: any + examples?: { + [key: string]: any + } + } + } + } +} + +export interface SchemaProperty { + name: string; + schema: OAISchema; +} diff --git a/src/data/navigation.ts b/src/data/navigation.ts index 0a70c1909..7b9a9cb5b 100644 --- a/src/data/navigation.ts +++ b/src/data/navigation.ts @@ -294,6 +294,10 @@ export const navigation: NavigationItem[] = [ title: "Sourcegraph GraphQL API", href: "/api/graphql", }, + { + title: "Sourcegraph Cody API", + href: "/api/cody", + }, { title: "Sourcegraph Stream API", href: "/api/stream_api",