@dsh-ai-providers/systemone 0.5.3 → 0.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { AiError, DecisionService, httpFailure, readJson, validateDecisionRequest } from "@dsh-ai-providers/core";
2
2
  import { credentialRef } from "@deepseek-ai/dsh-credentials";
3
- //#region src/connection.ts
3
+ // src/connection.ts
4
4
  /** Checks and normalizes a configured connection: no trailing slashes, HTTP or HTTPS only. */
5
5
  function validateConnection(connection) {
6
6
  const trimmed = connection.baseUrl.trim();
@@ -30,8 +30,7 @@ async function connectionHeaders(connection, credentials) {
30
30
  }
31
31
  return key === void 0 ? {} : { authorization: `Bearer ${key.value}` };
32
32
  }
33
- //#endregion
34
- //#region src/decision.ts
33
+ // src/decision.ts
35
34
  const number = (value) => typeof value === "number" && Number.isFinite(value);
36
35
  const isRaw = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
37
36
  /** The server's own words for what was wrong: a message, or the first problem of a validation error. */
@@ -159,7 +158,6 @@ var SystemOneApiService = class extends DecisionService {
159
158
  };
160
159
  }
161
160
  };
162
- //#endregion
163
161
  export { SystemOneApiService, connectionHeaders, validateConnection };
164
162
 
165
163
  //# sourceMappingURL=index.js.map
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/connection.ts","../src/decision.ts"],"sourcesContent":["import { credentialRef, type CredentialProvider } from '@deepseek-ai/dsh-credentials'\nimport { AiError } from '@dsh-ai-providers/core'\n\n/** A validated way to reach a decision API: its root, and the name of the credential for it, if any. */\nexport interface ApiConnection { baseUrl: string; apiKeyRef: string }\n\n/** Checks and normalizes a configured connection: no trailing slashes, HTTP or HTTPS only. */\nexport function validateConnection(connection: { name: string; baseUrl: string; apiKeyRef: string }): ApiConnection {\n const trimmed = connection.baseUrl.trim()\n let end = trimmed.length\n while (trimmed[end - 1] === '/') end -= 1\n const baseUrl = trimmed.slice(0, end)\n if (baseUrl === '') throw new AiError('not-configured', 'no base URL is configured')\n let url: URL\n try { url = new URL(baseUrl) }\n catch (error) { throw new AiError('not-configured', `the base URL for connection \"${connection.name}\" is invalid`, { cause: error }) }\n if (url.protocol !== 'http:' && url.protocol !== 'https:') throw new AiError('not-configured', `the base URL for connection \"${connection.name}\" must use HTTP or HTTPS`)\n return { baseUrl, apiKeyRef: connection.apiKeyRef.trim() }\n}\n\nexport async function connectionHeaders(connection: ApiConnection, credentials: () => Pick<CredentialProvider, 'resolve'> | undefined): Promise<Record<string, string>> {\n if (connection.apiKeyRef === '') return {}\n let key: Awaited<ReturnType<CredentialProvider['resolve']>>\n try { key = await credentials()?.resolve(credentialRef(connection.apiKeyRef)) }\n catch (error) { throw new AiError('not-configured', `the credential name \"${connection.apiKeyRef}\" is not valid: ${error instanceof Error ? error.message : String(error)}`, { cause: error }) }\n return key === undefined ? {} : { authorization: `Bearer ${key.value}` }\n}\n","import type { CredentialProvider } from '@deepseek-ai/dsh-credentials'\nimport { AiError, DecisionService, httpFailure, readJson, validateDecisionRequest, type ChoiceQuestion, type DecisionAnswers, type DecisionOptions, type DecisionQuestion, type DecisionQuestions, type DecisionResult, type DecisionState, type ScoreQuestion } from '@dsh-ai-providers/core'\nimport { connectionHeaders, type ApiConnection } from './connection.ts'\n\n/** The settings a decision call reads, live, so a vendor passes the fields of its own configuration. */\nexport interface DecisionSettings {\n /** The decision model, as the API names it. */\n decisionModel: { get(): string }\n /** How long a request may take before it is given up, in milliseconds. */\n timeoutMs: { get(): number }\n}\n\nexport interface SystemOneApiDeps<Options extends DecisionOptions = DecisionOptions> {\n config: DecisionSettings\n /** The provider id the service reports. */\n provider: string\n /** Fields added to the request body, from the call's options and the vendor's configuration: a vendor's own controls. */\n body?: (options: Options) => Readonly<Record<string, unknown>>\n /** Headers added to the request, such as one that switches on a longer context. */\n headers?: () => Readonly<Record<string, string>>\n /** The credentials service, looked up on each call: the key is never held. */\n credentials: () => Pick<CredentialProvider, 'resolve'> | undefined\n connection: () => ApiConnection\n /** Replaceable for tests. */\n fetch?: typeof fetch\n}\n\ntype Raw = Record<string, unknown>\n\ninterface ResponseBody {\n model?: unknown\n answers?: Record<string, unknown>\n usage?: { input_tokens?: unknown }\n error?: { message?: unknown }\n message?: unknown\n detail?: unknown\n}\n\nconst number = (value: unknown): value is number => typeof value === 'number' && Number.isFinite(value)\nconst isRaw = (value: unknown): value is Raw => typeof value === 'object' && value !== null && !Array.isArray(value)\n\n/** The server's own words for what was wrong: a message, or the first problem of a validation error. */\nfunction said(body: ResponseBody | undefined): string | undefined {\n const detail = body?.detail\n const first = Array.isArray(detail) ? (detail[0] as { msg?: unknown; loc?: unknown } | undefined) : undefined\n const where = Array.isArray(first?.loc) ? ` (${first.loc.join('.')})` : ''\n const fromDetail = typeof first?.msg === 'string' ? `${first.msg}${where}` : typeof detail === 'string' ? detail : undefined\n return [body?.error?.message, body?.message, fromDetail].find((candidate): candidate is string => typeof candidate === 'string')\n}\n\n/** The probabilities of a choice, as the server gave them; a server that leaves its confidence out has it taken as the highest probability. */\nfunction choiceOf(raw: Raw, question: ChoiceQuestion): DecisionAnswers<{ q: ChoiceQuestion }>['q'] | undefined {\n const probabilities = raw['probabilities']\n if (typeof raw['choice'] !== 'string' || !isRaw(probabilities) || !Object.values(probabilities).every(number)) return undefined\n const given = probabilities as Record<string, number>\n const confidence = number(raw['confidence']) ? raw['confidence'] : Math.max(0, ...Object.values(given))\n return { type: 'choice', choice: raw['choice'], confidence, probabilities: Object.fromEntries(Object.keys(question.criteria).flatMap(key => (key in given ? [[key, given[key]!]] : []))) }\n}\n\n/** The probabilities of a score arrive by level number (`\"0\"`, `\"1\"`, ...) or as a list, and become a list in the order of the question's levels. */\nfunction scoreOf(raw: Raw, question: ScoreQuestion): DecisionAnswers<{ q: ScoreQuestion }>['q'] | undefined {\n const probabilities = raw['probabilities']\n if (!number(raw['score']) || !(Array.isArray(probabilities) || isRaw(probabilities))) return undefined\n const level = (index: number): unknown => (Array.isArray(probabilities) ? probabilities[index] : (probabilities as Raw)[String(index)])\n const levels = question.criteria.map((_description, index) => level(index))\n if (!levels.every(number)) return undefined\n return { type: 'score', score: raw['score'], confidence: number(raw['confidence']) ? raw['confidence'] : Math.max(0, ...levels), probabilities: levels }\n}\n\n/**\n * A noul is a probability. A server that bands it into a boolean for its own use may add the number it banded as\n * `noul_raw`, and that is the one a caller can gate on.\n */\nfunction noulOf(raw: Raw): DecisionAnswers<{ q: { type: 'noul'; instructions: string } }>['q'] | undefined {\n const value = number(raw['noul_raw']) ? raw['noul_raw'] : raw['noul']\n return number(value) ? { type: 'noul', probability: value } : undefined\n}\n\nfunction answerOf(question: DecisionQuestion, raw: unknown): unknown {\n if (!isRaw(raw)) return undefined\n if (question.type === 'noul') return noulOf(raw)\n return question.type === 'choice' ? choiceOf(raw, question) : scoreOf(raw, question)\n}\n\n/**\n * Decisions from `POST {baseUrl}/systemone`, the API TypeSafe's Jev introduced and that LocalAI, llama.cpp's server,\n * LM Studio and others copy: a model, a state and named typed questions go in, and an answer for each (`noul`,\n * `choice` and `score`, with the probabilities behind it) and the tokens counted come out. Everything it needs is\n * read from the configuration and the credentials on each call.\n */\nexport class SystemOneApiService<Options extends DecisionOptions = DecisionOptions> extends DecisionService<Options> {\n readonly provider: string\n private readonly send: typeof fetch\n\n constructor(private readonly deps: SystemOneApiDeps<Options>) {\n super()\n this.provider = deps.provider\n this.send = deps.fetch ?? fetch\n }\n\n get model(): string {\n return this.deps.config.decisionModel.get()\n }\n\n async decide<Questions extends DecisionQuestions>(state: DecisionState, questions: Questions, options: Options = {} as Options): Promise<DecisionResult<Questions>> {\n const { config, credentials } = this.deps\n validateDecisionRequest(state, questions)\n const cancelled = (): boolean => options.signal?.aborted === true\n if (cancelled()) throw new AiError('cancelled', 'cancelled')\n const model = config.decisionModel.get().trim()\n if (model === '') throw new AiError('not-configured', 'no decision model is configured')\n const connection = this.deps.connection()\n const root = connection.baseUrl\n\n const headers: Record<string, string> = { 'content-type': 'application/json', ...this.deps.headers?.(), ...await connectionHeaders(connection, credentials) }\n const timeout = AbortSignal.timeout(Math.max(1, config.timeoutMs.get()))\n const signal = options.signal === undefined ? timeout : AbortSignal.any([options.signal, timeout])\n\n let response: Response\n try {\n response = await this.send(`${root}/systemone`, { method: 'POST', headers, signal, redirect: 'error', body: JSON.stringify({ ...this.deps.body?.(options), model, state, questions }) })\n } catch (error) {\n if (cancelled()) throw new AiError('cancelled', 'cancelled', { cause: error })\n if (timeout.aborted) throw new AiError('unavailable', `${root} did not answer within ${config.timeoutMs.get()} ms`, { cause: error })\n throw new AiError('unavailable', `${root} could not be reached: ${error instanceof Error ? error.message : String(error)}`, { cause: error })\n }\n\n const body = await readJson<ResponseBody>(response)\n if (!response.ok) throw httpFailure(response, said(body))\n if (!isRaw(body?.answers)) throw new AiError('unavailable', `${root} answered without answers`)\n const answers: Record<string, unknown> = {}\n for (const [name, question] of Object.entries(questions)) {\n const answer = answerOf(question, body.answers[name])\n if (answer === undefined) throw new AiError('unavailable', `${root} gave no readable ${question.type} answer to question \"${name}\"`)\n answers[name] = answer\n }\n return {\n answers: answers as DecisionAnswers<Questions>,\n model: typeof body.model === 'string' ? body.model : model,\n ...(number(body.usage?.input_tokens) ? { tokens: body.usage.input_tokens } : {}),\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/connection.ts","../src/decision.ts"],"sourcesContent":["import { credentialRef, type CredentialProvider } from '@deepseek-ai/dsh-credentials'\nimport { AiError } from '@dsh-ai-providers/core'\n\n/** A validated way to reach a decision API: its root, and the name of the credential for it, if any. */\nexport interface ApiConnection { baseUrl: string; apiKeyRef: string }\n\n/** Checks and normalizes a configured connection: no trailing slashes, HTTP or HTTPS only. */\nexport function validateConnection(connection: { name: string; baseUrl: string; apiKeyRef: string }): ApiConnection {\n const trimmed = connection.baseUrl.trim()\n let end = trimmed.length\n while (trimmed[end - 1] === '/') end -= 1\n const baseUrl = trimmed.slice(0, end)\n if (baseUrl === '') throw new AiError('not-configured', 'no base URL is configured')\n let url: URL\n try { url = new URL(baseUrl) }\n catch (error) { throw new AiError('not-configured', `the base URL for connection \"${connection.name}\" is invalid`, { cause: error }) }\n if (url.protocol !== 'http:' && url.protocol !== 'https:') throw new AiError('not-configured', `the base URL for connection \"${connection.name}\" must use HTTP or HTTPS`)\n return { baseUrl, apiKeyRef: connection.apiKeyRef.trim() }\n}\n\nexport async function connectionHeaders(connection: ApiConnection, credentials: () => Pick<CredentialProvider, 'resolve'> | undefined): Promise<Record<string, string>> {\n if (connection.apiKeyRef === '') return {}\n let key: Awaited<ReturnType<CredentialProvider['resolve']>>\n try { key = await credentials()?.resolve(credentialRef(connection.apiKeyRef)) }\n catch (error) { throw new AiError('not-configured', `the credential name \"${connection.apiKeyRef}\" is not valid: ${error instanceof Error ? error.message : String(error)}`, { cause: error }) }\n return key === undefined ? {} : { authorization: `Bearer ${key.value}` }\n}\n","import type { CredentialProvider } from '@deepseek-ai/dsh-credentials'\nimport { AiError, DecisionService, httpFailure, readJson, validateDecisionRequest, type ChoiceQuestion, type DecisionAnswers, type DecisionOptions, type DecisionQuestion, type DecisionQuestions, type DecisionResult, type DecisionState, type ScoreQuestion } from '@dsh-ai-providers/core'\nimport { connectionHeaders, type ApiConnection } from './connection.ts'\n\n/** The settings a decision call reads, live, so a vendor passes the fields of its own configuration. */\nexport interface DecisionSettings {\n /** The decision model, as the API names it. */\n decisionModel: { get(): string }\n /** How long a request may take before it is given up, in milliseconds. */\n timeoutMs: { get(): number }\n}\n\nexport interface SystemOneApiDeps<Options extends DecisionOptions = DecisionOptions> {\n config: DecisionSettings\n /** The provider id the service reports. */\n provider: string\n /** Fields added to the request body, from the call's options and the vendor's configuration: a vendor's own controls. */\n body?: (options: Options) => Readonly<Record<string, unknown>>\n /** Headers added to the request, such as one that switches on a longer context. */\n headers?: () => Readonly<Record<string, string>>\n /** The credentials service, looked up on each call: the key is never held. */\n credentials: () => Pick<CredentialProvider, 'resolve'> | undefined\n connection: () => ApiConnection\n /** Replaceable for tests. */\n fetch?: typeof fetch\n}\n\ntype Raw = Record<string, unknown>\n\ninterface ResponseBody {\n model?: unknown\n answers?: Record<string, unknown>\n usage?: { input_tokens?: unknown }\n error?: { message?: unknown }\n message?: unknown\n detail?: unknown\n}\n\nconst number = (value: unknown): value is number => typeof value === 'number' && Number.isFinite(value)\nconst isRaw = (value: unknown): value is Raw => typeof value === 'object' && value !== null && !Array.isArray(value)\n\n/** The server's own words for what was wrong: a message, or the first problem of a validation error. */\nfunction said(body: ResponseBody | undefined): string | undefined {\n const detail = body?.detail\n const first = Array.isArray(detail) ? (detail[0] as { msg?: unknown; loc?: unknown } | undefined) : undefined\n const where = Array.isArray(first?.loc) ? ` (${first.loc.join('.')})` : ''\n const fromDetail = typeof first?.msg === 'string' ? `${first.msg}${where}` : typeof detail === 'string' ? detail : undefined\n return [body?.error?.message, body?.message, fromDetail].find((candidate): candidate is string => typeof candidate === 'string')\n}\n\n/** The probabilities of a choice, as the server gave them; a server that leaves its confidence out has it taken as the highest probability. */\nfunction choiceOf(raw: Raw, question: ChoiceQuestion): DecisionAnswers<{ q: ChoiceQuestion }>['q'] | undefined {\n const probabilities = raw['probabilities']\n if (typeof raw['choice'] !== 'string' || !isRaw(probabilities) || !Object.values(probabilities).every(number)) return undefined\n const given = probabilities as Record<string, number>\n const confidence = number(raw['confidence']) ? raw['confidence'] : Math.max(0, ...Object.values(given))\n return { type: 'choice', choice: raw['choice'], confidence, probabilities: Object.fromEntries(Object.keys(question.criteria).flatMap(key => (key in given ? [[key, given[key]!]] : []))) }\n}\n\n/** The probabilities of a score arrive by level number (`\"0\"`, `\"1\"`, ...) or as a list, and become a list in the order of the question's levels. */\nfunction scoreOf(raw: Raw, question: ScoreQuestion): DecisionAnswers<{ q: ScoreQuestion }>['q'] | undefined {\n const probabilities = raw['probabilities']\n if (!number(raw['score']) || !(Array.isArray(probabilities) || isRaw(probabilities))) return undefined\n const level = (index: number): unknown => (Array.isArray(probabilities) ? probabilities[index] : (probabilities as Raw)[String(index)])\n const levels = question.criteria.map((_description, index) => level(index))\n if (!levels.every(number)) return undefined\n return { type: 'score', score: raw['score'], confidence: number(raw['confidence']) ? raw['confidence'] : Math.max(0, ...levels), probabilities: levels }\n}\n\n/**\n * A noul is a probability. A server that bands it into a boolean for its own use may add the number it banded as\n * `noul_raw`, and that is the one a caller can gate on.\n */\nfunction noulOf(raw: Raw): DecisionAnswers<{ q: { type: 'noul'; instructions: string } }>['q'] | undefined {\n const value = number(raw['noul_raw']) ? raw['noul_raw'] : raw['noul']\n return number(value) ? { type: 'noul', probability: value } : undefined\n}\n\nfunction answerOf(question: DecisionQuestion, raw: unknown): unknown {\n if (!isRaw(raw)) return undefined\n if (question.type === 'noul') return noulOf(raw)\n return question.type === 'choice' ? choiceOf(raw, question) : scoreOf(raw, question)\n}\n\n/**\n * Decisions from `POST {baseUrl}/systemone`, the API TypeSafe's Jev introduced and that LocalAI, llama.cpp's server,\n * LM Studio and others copy: a model, a state and named typed questions go in, and an answer for each (`noul`,\n * `choice` and `score`, with the probabilities behind it) and the tokens counted come out. Everything it needs is\n * read from the configuration and the credentials on each call.\n */\nexport class SystemOneApiService<Options extends DecisionOptions = DecisionOptions> extends DecisionService<Options> {\n readonly provider: string\n private readonly send: typeof fetch\n\n constructor(private readonly deps: SystemOneApiDeps<Options>) {\n super()\n this.provider = deps.provider\n this.send = deps.fetch ?? fetch\n }\n\n get model(): string {\n return this.deps.config.decisionModel.get()\n }\n\n async decide<Questions extends DecisionQuestions>(state: DecisionState, questions: Questions, options: Options = {} as Options): Promise<DecisionResult<Questions>> {\n const { config, credentials } = this.deps\n validateDecisionRequest(state, questions)\n const cancelled = (): boolean => options.signal?.aborted === true\n if (cancelled()) throw new AiError('cancelled', 'cancelled')\n const model = config.decisionModel.get().trim()\n if (model === '') throw new AiError('not-configured', 'no decision model is configured')\n const connection = this.deps.connection()\n const root = connection.baseUrl\n\n const headers: Record<string, string> = { 'content-type': 'application/json', ...this.deps.headers?.(), ...await connectionHeaders(connection, credentials) }\n const timeout = AbortSignal.timeout(Math.max(1, config.timeoutMs.get()))\n const signal = options.signal === undefined ? timeout : AbortSignal.any([options.signal, timeout])\n\n let response: Response\n try {\n response = await this.send(`${root}/systemone`, { method: 'POST', headers, signal, redirect: 'error', body: JSON.stringify({ ...this.deps.body?.(options), model, state, questions }) })\n } catch (error) {\n if (cancelled()) throw new AiError('cancelled', 'cancelled', { cause: error })\n if (timeout.aborted) throw new AiError('unavailable', `${root} did not answer within ${config.timeoutMs.get()} ms`, { cause: error })\n throw new AiError('unavailable', `${root} could not be reached: ${error instanceof Error ? error.message : String(error)}`, { cause: error })\n }\n\n const body = await readJson<ResponseBody>(response)\n if (!response.ok) throw httpFailure(response, said(body))\n if (!isRaw(body?.answers)) throw new AiError('unavailable', `${root} answered without answers`)\n const answers: Record<string, unknown> = {}\n for (const [name, question] of Object.entries(questions)) {\n const answer = answerOf(question, body.answers[name])\n if (answer === undefined) throw new AiError('unavailable', `${root} gave no readable ${question.type} answer to question \"${name}\"`)\n answers[name] = answer\n }\n return {\n answers: answers as DecisionAnswers<Questions>,\n model: typeof body.model === 'string' ? body.model : model,\n ...(number(body.usage?.input_tokens) ? { tokens: body.usage.input_tokens } : {}),\n }\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-ai-providers/systemone",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "description": "Reusable System One decision transport for DSH AI providers.",
5
5
  "author": {
6
6
  "name": "Firefox2100",
@@ -48,11 +48,11 @@
48
48
  "license": "GPL-3.0-only",
49
49
  "peerDependencies": {
50
50
  "@deepseek-ai/dsh-credentials": "0.2.0-rc.2 || >=0.2.1-0 <0.2.2",
51
- "@dsh-ai-providers/core": "0.5.3"
51
+ "@dsh-ai-providers/core": "0.5.5"
52
52
  },
53
53
  "devDependencies": {
54
- "@deepseek-ai/dsh-credentials": "0.2.1-rc.3",
55
- "@dsh-ai-providers/core": "0.5.3",
54
+ "@deepseek-ai/dsh-credentials": "0.2.1-rc.5",
55
+ "@dsh-ai-providers/core": "0.5.5",
56
56
  "@types/node": "^22.20.0",
57
57
  "tsdown": "^0.22.2",
58
58
  "typescript": "^6.0.3",