@meetopenbot/openbot 1.0.0 → 1.0.2

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.
@@ -1,4 +1,4 @@
1
- import { shouldUseCreditsAuth } from '@meetopenbot/plugin-sdk';
1
+ import { BYOK_ENV_KEYS, shouldUseCreditsAuth } from '@meetopenbot/plugin-sdk';
2
2
  /** Product alias shown in the picker. Never sent to a provider or the credits proxy. */
3
3
  export const AUTO_MODEL_ID = 'openbot/auto';
4
4
  export const AUTO_MODEL_OPTION = {
@@ -11,12 +11,9 @@ export const AUTO_MODEL_OPTION = {
11
11
  * order, failover, intent routing) without changing the `openbot/auto` alias.
12
12
  */
13
13
  export const AUTO_MODEL_POOL = ['deepseek/deepseek-v4-flash'];
14
- const PROVIDER_BYOK_ENV = {
15
- openai: 'OPENAI_API_KEY',
16
- anthropic: 'ANTHROPIC_API_KEY',
17
- google: 'GOOGLE_GENERATIVE_AI_API_KEY',
18
- deepseek: 'DEEPSEEK_API_KEY',
19
- };
14
+ function providerByokKeys(provider) {
15
+ return BYOK_ENV_KEYS[provider];
16
+ }
20
17
  export function isAutoModel(modelString) {
21
18
  if (!modelString)
22
19
  return true;
@@ -27,8 +24,8 @@ function providerOf(modelString) {
27
24
  return modelString.split('/')[0] ?? '';
28
25
  }
29
26
  function providerHasByokKey(provider, env) {
30
- const key = PROVIDER_BYOK_ENV[provider];
31
- return Boolean(key && env[key]?.trim());
27
+ const keys = providerByokKeys(provider);
28
+ return Boolean(keys?.some((key) => env[key]?.trim()));
32
29
  }
33
30
  /** Concrete models Auto may call, filtered by credits vs BYOK keys. */
34
31
  export function listAutoModelCandidates(ctx) {
@@ -57,8 +54,7 @@ export function missingByokApiKeyMessage(modelString, ctx) {
57
54
  const provider = providerOf(expandModelString(modelString, ctx));
58
55
  if (providerHasByokKey(provider, env))
59
56
  return undefined;
60
- const envVar = PROVIDER_BYOK_ENV[provider];
61
- if (!envVar)
57
+ if (!providerByokKeys(provider))
62
58
  return undefined;
63
59
  return `${provider} API key is missing`;
64
60
  }
package/dist/history.js CHANGED
@@ -164,12 +164,16 @@ export function eventsToModelMessages(events, options) {
164
164
  }
165
165
  case 'agent:invoke': {
166
166
  const invokeEvent = event;
167
- if (invokeEvent.data?.content && invokeEvent.data?.role) {
167
+ if (invokeEvent.data?.role) {
168
168
  const role = invokeEvent.data.role === 'agent' ? 'user' : invokeEvent.data.role;
169
- messages.push({
170
- role,
171
- content: invokeEvent.data.content,
172
- });
169
+ const content = invokeEvent.data.content ?? '';
170
+ const hasFiles = Boolean(invokeEvent.data.attachments?.length);
171
+ if (content || hasFiles) {
172
+ messages.push({
173
+ role,
174
+ content,
175
+ });
176
+ }
173
177
  }
174
178
  break;
175
179
  }
package/dist/images.js ADDED
@@ -0,0 +1,67 @@
1
+ import { BYOK_ENV_KEYS, shouldUseCreditsAuth } from '@meetopenbot/plugin-sdk';
2
+ export function isImageAttachment(file) {
3
+ if (file.mimeType?.startsWith('image/'))
4
+ return true;
5
+ return /\.(png|jpe?g|gif|webp)$/i.test(file.name);
6
+ }
7
+ export function eventsHaveImages(events) {
8
+ return events.some((event) => {
9
+ if (event.meta?.parentToolCallId)
10
+ return false;
11
+ if (event.type !== 'agent:invoke')
12
+ return false;
13
+ return Boolean(event.data?.attachments?.some(isImageAttachment));
14
+ });
15
+ }
16
+ function hasAnyKey(env, keys) {
17
+ return keys.some((key) => Boolean(env[key]?.trim()));
18
+ }
19
+ /** Auto uses DeepSeek, which cannot see pictures. Pick a cheap vision model. */
20
+ export function pickVisionModel(authMode, env = process.env) {
21
+ if (shouldUseCreditsAuth({ authMode }))
22
+ return 'google/gemini-3.5-flash';
23
+ if (hasAnyKey(env, BYOK_ENV_KEYS.openai))
24
+ return 'openai/gpt-4o-mini';
25
+ if (hasAnyKey(env, BYOK_ENV_KEYS.google))
26
+ return 'google/gemini-3.5-flash';
27
+ if (hasAnyKey(env, BYOK_ENV_KEYS.anthropic))
28
+ return 'anthropic/claude-haiku-4-5';
29
+ return undefined;
30
+ }
31
+ /** Put image files on matching user messages as AI SDK `file` parts. */
32
+ export async function addImageParts(messages, events, read) {
33
+ const result = [...messages];
34
+ let userIndex = 0;
35
+ for (const event of events) {
36
+ if (event.meta?.parentToolCallId)
37
+ continue;
38
+ if (event.type !== 'agent:invoke' || !event.data?.role)
39
+ continue;
40
+ const files = (event.data.attachments ?? []).filter(isImageAttachment);
41
+ const text = event.data.content ?? '';
42
+ if (!text && files.length === 0)
43
+ continue;
44
+ const current = result[userIndex];
45
+ userIndex += 1;
46
+ if (!current || files.length === 0)
47
+ continue;
48
+ const parts = [];
49
+ if (typeof current.content === 'string' && current.content) {
50
+ parts.push({ type: 'text', text: current.content });
51
+ }
52
+ for (const file of files) {
53
+ const loaded = await read(file.name);
54
+ if (!loaded?.data.byteLength)
55
+ continue;
56
+ parts.push({
57
+ type: 'file',
58
+ data: loaded.data,
59
+ mediaType: loaded.mediaType || 'image/png',
60
+ });
61
+ }
62
+ if (parts.some((part) => part.type === 'file')) {
63
+ result[userIndex - 1] = { role: current.role, content: parts };
64
+ }
65
+ }
66
+ return result;
67
+ }
package/dist/model.js CHANGED
@@ -1,26 +1,18 @@
1
1
  import { createOpenAI, openai as defaultOpenai } from '@ai-sdk/openai';
2
2
  import { createAnthropic, anthropic } from '@ai-sdk/anthropic';
3
3
  import { createDeepSeek, deepSeek } from '@ai-sdk/deepseek';
4
+ import { createGoogle, google as defaultGoogle } from '@ai-sdk/google';
4
5
  import { expandModelString } from './auto-model.js';
5
- import { CREDITS_API_KEY_PLACEHOLDER, INTEGRATIONS_TOKEN_HEADER, creditsProviderBaseUrl, resolveCreditsAuthConfig, shouldUseCreditsAuth, } from '@meetopenbot/plugin-sdk';
6
- function googleChat(modelId) {
7
- return createOpenAI({
8
- baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai',
9
- apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY,
10
- }).chat(modelId);
11
- }
12
- /**
13
- * OpenAI-compatible Chat Completions. Gemini's gateway only speaks this.
14
- * Do not use for OpenAI itself — gpt-5.6-luna rejects function tools on
15
- * `/v1/chat/completions` unless `reasoning_effort` is `none`.
16
- */
17
- function openAiChatModel(options, modelId) {
18
- return createOpenAI(options).chat(modelId);
19
- }
6
+ import { CREDITS_API_KEY_PLACEHOLDER, INTEGRATIONS_TOKEN_HEADER, creditsProviderBaseUrl, resolveByokApiKey, resolveCreditsAuthConfig, shouldUseCreditsAuth, } from '@meetopenbot/plugin-sdk';
20
7
  /** AI SDK default: OpenAI Responses API (`/v1/responses`), required for tools on Luna. */
21
8
  function openAiResponsesModel(options, modelId) {
22
9
  return createOpenAI(options)(modelId);
23
10
  }
11
+ /** `@ai-sdk/google` only reads `GOOGLE_GENERATIVE_AI_API_KEY`; accept `GEMINI_API_KEY` too. */
12
+ function googleByok(modelId) {
13
+ const apiKey = resolveByokApiKey('google');
14
+ return apiKey ? createGoogle({ apiKey })(modelId) : defaultGoogle(modelId);
15
+ }
24
16
  function resolveCreditsProvider(provider, modelId) {
25
17
  const config = resolveCreditsAuthConfig();
26
18
  if (!config) {
@@ -37,7 +29,7 @@ function resolveCreditsProvider(provider, modelId) {
37
29
  case 'anthropic':
38
30
  return createAnthropic(options)(modelId);
39
31
  case 'google':
40
- return openAiChatModel(options, modelId);
32
+ return createGoogle(options)(modelId);
41
33
  case 'deepseek':
42
34
  return createDeepSeek(options)(modelId);
43
35
  }
@@ -56,7 +48,7 @@ export function resolveModel(modelString, options) {
56
48
  case 'anthropic':
57
49
  return useCredits ? resolveCreditsProvider('anthropic', modelId) : anthropic(modelId);
58
50
  case 'google':
59
- return useCredits ? resolveCreditsProvider('google', modelId) : googleChat(modelId);
51
+ return useCredits ? resolveCreditsProvider('google', modelId) : googleByok(modelId);
60
52
  case 'deepseek':
61
53
  return useCredits ? resolveCreditsProvider('deepseek', modelId) : deepSeek(modelId);
62
54
  default:
@@ -66,16 +58,9 @@ export function resolveModel(modelString, options) {
66
58
  function providerOf(modelString) {
67
59
  return expandModelString(modelString).split('/')[0] ?? '';
68
60
  }
69
- /** Low thinking for DeepSeek (Auto). Google Chat Completions cannot round-trip thinking. */
61
+ /** Low thinking for DeepSeek (Auto). Other providers use their SDK defaults. */
70
62
  export function reasoningForModel(modelString) {
71
- const provider = providerOf(modelString);
72
- if (provider === 'deepseek')
63
+ if (providerOf(modelString) === 'deepseek')
73
64
  return 'low';
74
- if (provider === 'google')
75
- return 'none';
76
65
  return undefined;
77
66
  }
78
- /** Gemini's OpenAI Chat path drops thinking; omit parts so tool turns stay valid. */
79
- export function includeReasoningInHistory(modelString) {
80
- return providerOf(modelString) !== 'google';
81
- }
package/dist/runtime.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import { streamText, stepCountIs } from 'ai';
2
2
  import { eventsToModelMessages } from './history.js';
3
+ import { addImageParts, eventsHaveImages, pickVisionModel } from './images.js';
3
4
  import { buildContext } from './context.js';
4
5
  import { OPENBOT_SYSTEM_PROMPT } from './system-prompt.js';
5
6
  import { isAuthErrorMessage, isCreditsErrorMessage } from '@meetopenbot/plugin-sdk';
6
7
  import { AUTO_MODEL_ID, isAutoModel, isRetryableModelError, listAutoModelCandidates, missingByokApiKeyMessage, } from './auto-model.js';
7
- import { includeReasoningInHistory, reasoningForModel, resolveModel } from './model.js';
8
+ import { reasoningForModel, resolveModel } from './model.js';
8
9
  import { fetchModelRegistry, getProviderModelOptions, listApiKeyProviders, PROVIDER_API_KEY_LINKS, } from '@meetopenbot/plugin-sdk';
9
10
  import { errorFromStreamPart } from './stream-error.js';
10
11
  import { createAgentOutputBuffer, isOutputBoundaryPart } from './output-buffer.js';
@@ -97,11 +98,19 @@ export async function* runOpenbotTurn(args, options) {
97
98
  channelId: state.channelId ?? '',
98
99
  threadId,
99
100
  });
100
- const candidates = isAutoModel(configuredModelString)
101
+ const hasImages = eventsHaveImages(events);
102
+ let candidates = isAutoModel(configuredModelString)
101
103
  ? listAutoModelCandidates({ authMode })
102
104
  : [configuredModelString];
103
105
  try {
104
- const missingKey = missingByokApiKeyMessage(configuredModelString, { authMode });
106
+ if (hasImages && isAutoModel(configuredModelString)) {
107
+ const vision = pickVisionModel(authMode);
108
+ if (!vision) {
109
+ throw new Error('Looking at images needs an OpenAI, Google, or Anthropic API key. Auto\'s usual model cannot see pictures.');
110
+ }
111
+ candidates = [vision];
112
+ }
113
+ const missingKey = missingByokApiKeyMessage(candidates[0], { authMode });
105
114
  if (missingKey)
106
115
  throw new Error(missingKey);
107
116
  let lastError;
@@ -116,12 +125,28 @@ export async function* runOpenbotTurn(args, options) {
116
125
  };
117
126
  const tools = toAiTools(toolDefinitions, args, flushPendingOutput);
118
127
  try {
128
+ let messages = eventsToModelMessages(events);
129
+ if (hasImages) {
130
+ messages = await addImageParts(messages, events, async (name) => {
131
+ try {
132
+ const file = await storage.readThreadAttachment({
133
+ channelId: state.channelId,
134
+ threadId,
135
+ name,
136
+ });
137
+ return { data: new Uint8Array(file.body), mediaType: file.mimeType };
138
+ }
139
+ catch (error) {
140
+ console.warn('[openbot] could not read attachment', name, error);
141
+ return null;
142
+ }
143
+ });
144
+ console.info('[openbot] attached images for', candidate);
145
+ }
119
146
  const result = streamText({
120
147
  model: resolveModel(candidate, { authMode }),
121
148
  system: systemPrompt,
122
- messages: eventsToModelMessages(events, {
123
- includeReasoning: includeReasoningInHistory(candidate),
124
- }),
149
+ messages,
125
150
  tools,
126
151
  abortSignal,
127
152
  reasoning: reasoningForModel(candidate),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meetopenbot/openbot",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "OpenBot coordinator runtime: ask specialists, route work into Spaces, and track todos.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -21,10 +21,11 @@
21
21
  "dependencies": {
22
22
  "@ai-sdk/anthropic": "^4.0.42",
23
23
  "@ai-sdk/deepseek": "^3.0.32",
24
+ "@ai-sdk/google": "^4.0.58",
24
25
  "@ai-sdk/openai": "^4.0.46",
25
26
  "ai": "^7.0.77",
26
27
  "zod": "^4.3.5",
27
- "@meetopenbot/plugin-sdk": "^1.0.0"
28
+ "@meetopenbot/plugin-sdk": "^1.0.2"
28
29
  },
29
30
  "devDependencies": {
30
31
  "@types/node": "^20.10.1",