-
Notifications
You must be signed in to change notification settings - Fork 519
[codex] show local freebuff model availability #542
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| import { describe, expect, test } from 'bun:test' | ||
|
|
||
| import { | ||
| getFreebuffDeploymentAvailabilityLabel, | ||
| isFreebuffDeploymentHours, | ||
| } from '../constants/freebuff-models' | ||
|
|
||
| describe('freebuff model availability', () => { | ||
| test('formats the close time in the user local timezone while deployment is open', () => { | ||
| expect( | ||
| getFreebuffDeploymentAvailabilityLabel(new Date('2026-01-05T18:00:00Z'), { | ||
| locale: 'en-US', | ||
| timeZone: 'America/Los_Angeles', | ||
| }), | ||
| ).toBe('until 5:00 PM local') | ||
| }) | ||
|
|
||
| test('formats the next open time in the user local timezone while deployment is closed', () => { | ||
| expect( | ||
| getFreebuffDeploymentAvailabilityLabel(new Date('2026-01-05T12:00:00Z'), { | ||
| locale: 'en-US', | ||
| timeZone: 'America/Los_Angeles', | ||
| }), | ||
| ).toBe('opens 6:00 AM local') | ||
| }) | ||
|
|
||
| test('includes the weekday when the next opening is on a later local day', () => { | ||
| expect( | ||
| getFreebuffDeploymentAvailabilityLabel(new Date('2026-01-10T20:00:00Z'), { | ||
| locale: 'en-US', | ||
| timeZone: 'America/Los_Angeles', | ||
| }), | ||
| ).toBe('opens Mon 6:00 AM local') | ||
| }) | ||
|
|
||
| test('tracks deployment hours correctly across the open and close boundaries', () => { | ||
| expect(isFreebuffDeploymentHours(new Date('2026-01-05T13:59:00Z'))).toBe( | ||
| false, | ||
| ) | ||
| expect(isFreebuffDeploymentHours(new Date('2026-01-05T14:00:00Z'))).toBe( | ||
| true, | ||
| ) | ||
| expect(isFreebuffDeploymentHours(new Date('2026-01-06T00:59:00Z'))).toBe( | ||
| true, | ||
| ) | ||
| expect(isFreebuffDeploymentHours(new Date('2026-01-06T01:00:00Z'))).toBe( | ||
| false, | ||
| ) | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -17,9 +17,28 @@ export interface FreebuffModelOption { | |||||||||||||||||||||||||||||||||||
| availability: 'always' | 'deployment_hours' | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| /** Server-facing fallback copy for APIs and provider errors that can't know | ||||||||||||||||||||||||||||||||||||
| * the caller's local timezone. The CLI should render | ||||||||||||||||||||||||||||||||||||
| * `getFreebuffDeploymentAvailabilityLabel()` instead. */ | ||||||||||||||||||||||||||||||||||||
| export const FREEBUFF_DEPLOYMENT_HOURS_LABEL = '9am ET-5pm PT' | ||||||||||||||||||||||||||||||||||||
| export const FREEBUFF_GLM_MODEL_ID = 'z-ai/glm-5.1' | ||||||||||||||||||||||||||||||||||||
| export const FREEBUFF_MINIMAX_MODEL_ID = 'minimax/minimax-m2.7' | ||||||||||||||||||||||||||||||||||||
| const FREEBUFF_EASTERN_TIMEZONE = 'America/New_York' | ||||||||||||||||||||||||||||||||||||
| const FREEBUFF_PACIFIC_TIMEZONE = 'America/Los_Angeles' | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| interface ZonedDateParts { | ||||||||||||||||||||||||||||||||||||
| year: number | ||||||||||||||||||||||||||||||||||||
| month: number | ||||||||||||||||||||||||||||||||||||
| day: number | ||||||||||||||||||||||||||||||||||||
| weekday: string | ||||||||||||||||||||||||||||||||||||
| hour: number | ||||||||||||||||||||||||||||||||||||
| minute: number | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+29
to
+36
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The interface stores both
Suggested change
Then in return eastern.hour * 60 + eastern.minute >= 9 * 60 && pacific.hour * 60 + pacific.minute < 17 * 60Prompt To Fix With AIThis is a comment left during a code review.
Path: common/src/constants/freebuff-models.ts
Line: 26-34
Comment:
**Redundant `minutes` field alongside `minute` in `ZonedDateParts`**
The interface stores both `minute` (raw 0–59) and `minutes` (`hour * 60 + minute`) as similarly-named fields. `minutes` is only consumed in `isFreebuffDeploymentHours`; everywhere else the code uses `hour` and `minute` separately. Dropping `minutes` from the interface and inlining the arithmetic in `isFreebuffDeploymentHours` removes the ambiguity and one confusingly similar name:
```suggestion
interface ZonedDateParts {
year: number
month: number
day: number
weekday: string
hour: number
minute: number
}
```
Then in `isFreebuffDeploymentHours`:
```ts
return eastern.hour * 60 + eastern.minute >= 9 * 60 && pacific.hour * 60 + pacific.minute < 17 * 60
```
How can I resolve this? If you propose a fix, please make it concise.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed in |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| interface LocalTimeFormatOptions { | ||||||||||||||||||||||||||||||||||||
| locale?: string | ||||||||||||||||||||||||||||||||||||
| timeZone?: string | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| export const FREEBUFF_MODELS = [ | ||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||
|
|
@@ -71,31 +90,172 @@ export function getFreebuffModel(id: string): FreebuffModelOption { | |||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| function getZonedParts( | ||||||||||||||||||||||||||||||||||||
| date: Date, | ||||||||||||||||||||||||||||||||||||
| timeZone: string, | ||||||||||||||||||||||||||||||||||||
| ): { weekday: string; minutes: number } { | ||||||||||||||||||||||||||||||||||||
| function getZonedParts(date: Date, timeZone: string): ZonedDateParts { | ||||||||||||||||||||||||||||||||||||
| const parts = new Intl.DateTimeFormat('en-US', { | ||||||||||||||||||||||||||||||||||||
| timeZone, | ||||||||||||||||||||||||||||||||||||
| year: 'numeric', | ||||||||||||||||||||||||||||||||||||
| month: '2-digit', | ||||||||||||||||||||||||||||||||||||
| day: '2-digit', | ||||||||||||||||||||||||||||||||||||
| weekday: 'short', | ||||||||||||||||||||||||||||||||||||
| hour: '2-digit', | ||||||||||||||||||||||||||||||||||||
| minute: '2-digit', | ||||||||||||||||||||||||||||||||||||
| hourCycle: 'h23', | ||||||||||||||||||||||||||||||||||||
| }).formatToParts(date) | ||||||||||||||||||||||||||||||||||||
| const value = (type: string) => parts.find((part) => part.type === type)?.value | ||||||||||||||||||||||||||||||||||||
| const value = (type: string) => | ||||||||||||||||||||||||||||||||||||
| parts.find((part) => part.type === type)?.value | ||||||||||||||||||||||||||||||||||||
| const year = Number(value('year') ?? 0) | ||||||||||||||||||||||||||||||||||||
| const month = Number(value('month') ?? 1) | ||||||||||||||||||||||||||||||||||||
| const day = Number(value('day') ?? 1) | ||||||||||||||||||||||||||||||||||||
| const hour = Number(value('hour') ?? 0) | ||||||||||||||||||||||||||||||||||||
| const minute = Number(value('minute') ?? 0) | ||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||
| year, | ||||||||||||||||||||||||||||||||||||
| month, | ||||||||||||||||||||||||||||||||||||
| day, | ||||||||||||||||||||||||||||||||||||
| weekday: value('weekday') ?? '', | ||||||||||||||||||||||||||||||||||||
| minutes: hour * 60 + minute, | ||||||||||||||||||||||||||||||||||||
| hour, | ||||||||||||||||||||||||||||||||||||
| minute, | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| function addDaysToYmd( | ||||||||||||||||||||||||||||||||||||
| year: number, | ||||||||||||||||||||||||||||||||||||
| month: number, | ||||||||||||||||||||||||||||||||||||
| day: number, | ||||||||||||||||||||||||||||||||||||
| days: number, | ||||||||||||||||||||||||||||||||||||
| ): Pick<ZonedDateParts, 'year' | 'month' | 'day'> { | ||||||||||||||||||||||||||||||||||||
| const next = new Date(Date.UTC(year, month - 1, day)) | ||||||||||||||||||||||||||||||||||||
| next.setUTCDate(next.getUTCDate() + days) | ||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||
| year: next.getUTCFullYear(), | ||||||||||||||||||||||||||||||||||||
| month: next.getUTCMonth() + 1, | ||||||||||||||||||||||||||||||||||||
| day: next.getUTCDate(), | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| function getUtcForZonedTime( | ||||||||||||||||||||||||||||||||||||
| parts: Pick<ZonedDateParts, 'year' | 'month' | 'day'>, | ||||||||||||||||||||||||||||||||||||
| timeZone: string, | ||||||||||||||||||||||||||||||||||||
| hour: number, | ||||||||||||||||||||||||||||||||||||
| minute: number, | ||||||||||||||||||||||||||||||||||||
| ): Date { | ||||||||||||||||||||||||||||||||||||
| let guess = new Date( | ||||||||||||||||||||||||||||||||||||
| Date.UTC(parts.year, parts.month - 1, parts.day, hour, minute), | ||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| for (let i = 0; i < 3; i++) { | ||||||||||||||||||||||||||||||||||||
| const actual = getZonedParts(guess, timeZone) | ||||||||||||||||||||||||||||||||||||
| const desiredUtc = Date.UTC( | ||||||||||||||||||||||||||||||||||||
| parts.year, | ||||||||||||||||||||||||||||||||||||
| parts.month - 1, | ||||||||||||||||||||||||||||||||||||
| parts.day, | ||||||||||||||||||||||||||||||||||||
| hour, | ||||||||||||||||||||||||||||||||||||
| minute, | ||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||
| const actualUtc = Date.UTC( | ||||||||||||||||||||||||||||||||||||
| actual.year, | ||||||||||||||||||||||||||||||||||||
| actual.month - 1, | ||||||||||||||||||||||||||||||||||||
| actual.day, | ||||||||||||||||||||||||||||||||||||
| actual.hour, | ||||||||||||||||||||||||||||||||||||
| actual.minute, | ||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||
| guess = new Date(guess.getTime() + (desiredUtc - actualUtc)) | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| return guess | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| function isWeekend( | ||||||||||||||||||||||||||||||||||||
| parts: Pick<ZonedDateParts, 'year' | 'month' | 'day'>, | ||||||||||||||||||||||||||||||||||||
| ): boolean { | ||||||||||||||||||||||||||||||||||||
| const weekday = getWeekdayIndex(parts) | ||||||||||||||||||||||||||||||||||||
| return weekday === 0 || weekday === 6 | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| function getWeekdayIndex( | ||||||||||||||||||||||||||||||||||||
| parts: Pick<ZonedDateParts, 'year' | 'month' | 'day'>, | ||||||||||||||||||||||||||||||||||||
| ): number { | ||||||||||||||||||||||||||||||||||||
| return new Date(Date.UTC(parts.year, parts.month - 1, parts.day)).getUTCDay() | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| function getNextFreebuffDeploymentStart(now: Date): Date { | ||||||||||||||||||||||||||||||||||||
| const easternNow = getZonedParts(now, FREEBUFF_EASTERN_TIMEZONE) | ||||||||||||||||||||||||||||||||||||
| const weekday = getWeekdayIndex(easternNow) | ||||||||||||||||||||||||||||||||||||
| const isBeforeTodayOpen = easternNow.hour < 9 | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const offset = | ||||||||||||||||||||||||||||||||||||
| weekday === 6 | ||||||||||||||||||||||||||||||||||||
| ? 2 | ||||||||||||||||||||||||||||||||||||
| : weekday === 0 | ||||||||||||||||||||||||||||||||||||
| ? 1 | ||||||||||||||||||||||||||||||||||||
| : isBeforeTodayOpen | ||||||||||||||||||||||||||||||||||||
| ? 0 | ||||||||||||||||||||||||||||||||||||
| : weekday === 5 | ||||||||||||||||||||||||||||||||||||
| ? 3 | ||||||||||||||||||||||||||||||||||||
| : 1 | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| return getUtcForZonedTime( | ||||||||||||||||||||||||||||||||||||
| addDaysToYmd(easternNow.year, easternNow.month, easternNow.day, offset), | ||||||||||||||||||||||||||||||||||||
| FREEBUFF_EASTERN_TIMEZONE, | ||||||||||||||||||||||||||||||||||||
| 9, | ||||||||||||||||||||||||||||||||||||
| 0, | ||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+181
to
+203
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The 8-iteration loop skipping weekends and checking whether the 9 am ET candidate is still in the future can be replaced with direct arithmetic: if the current ET day is already past 9 am (or is a weekend), advance by the number of days to the next weekday, then construct the UTC instant directly. The current fallback after the loop ( Prompt To Fix With AIThis is a comment left during a code review.
Path: common/src/constants/freebuff-models.ts
Line: 176-197
Comment:
**`getNextFreebuffDeploymentStart` loop is harder to follow than necessary**
The 8-iteration loop skipping weekends and checking whether the 9 am ET candidate is still in the future can be replaced with direct arithmetic: if the current ET day is already past 9 am (or is a weekend), advance by the number of days to the next weekday, then construct the UTC instant directly. The current fallback after the loop (`offset = 8`) is also unreachable — in any 8-day window there will always be at least one weekday whose 9 am ET instant is in the future. Simplifying to at-most-a-few lines of offset arithmetic would make the intent clearer.
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed in |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| function getCurrentFreebuffDeploymentEnd(now: Date): Date { | ||||||||||||||||||||||||||||||||||||
| const pacificNow = getZonedParts(now, FREEBUFF_PACIFIC_TIMEZONE) | ||||||||||||||||||||||||||||||||||||
| return getUtcForZonedTime(pacificNow, FREEBUFF_PACIFIC_TIMEZONE, 17, 0) | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| function isSameLocalDay(left: Date, right: Date, timeZone?: string): boolean { | ||||||||||||||||||||||||||||||||||||
| const formatter = new Intl.DateTimeFormat('en-CA', { | ||||||||||||||||||||||||||||||||||||
| timeZone, | ||||||||||||||||||||||||||||||||||||
| year: 'numeric', | ||||||||||||||||||||||||||||||||||||
| month: '2-digit', | ||||||||||||||||||||||||||||||||||||
| day: '2-digit', | ||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||
| return formatter.format(left) === formatter.format(right) | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| function formatLocalTime( | ||||||||||||||||||||||||||||||||||||
| date: Date, | ||||||||||||||||||||||||||||||||||||
| referenceNow: Date, | ||||||||||||||||||||||||||||||||||||
| options: LocalTimeFormatOptions = {}, | ||||||||||||||||||||||||||||||||||||
| ): string { | ||||||||||||||||||||||||||||||||||||
| const shouldShowWeekday = !isSameLocalDay( | ||||||||||||||||||||||||||||||||||||
| date, | ||||||||||||||||||||||||||||||||||||
| referenceNow, | ||||||||||||||||||||||||||||||||||||
| options.timeZone, | ||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||
| return new Intl.DateTimeFormat(options.locale, { | ||||||||||||||||||||||||||||||||||||
| timeZone: options.timeZone, | ||||||||||||||||||||||||||||||||||||
| weekday: shouldShowWeekday ? 'short' : undefined, | ||||||||||||||||||||||||||||||||||||
| hour: 'numeric', | ||||||||||||||||||||||||||||||||||||
| minute: '2-digit', | ||||||||||||||||||||||||||||||||||||
| }).format(date) | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| export function getFreebuffDeploymentAvailabilityLabel( | ||||||||||||||||||||||||||||||||||||
| now: Date = new Date(), | ||||||||||||||||||||||||||||||||||||
| options: LocalTimeFormatOptions = {}, | ||||||||||||||||||||||||||||||||||||
| ): string { | ||||||||||||||||||||||||||||||||||||
| if (isFreebuffDeploymentHours(now)) { | ||||||||||||||||||||||||||||||||||||
| const closesAt = getCurrentFreebuffDeploymentEnd(now) | ||||||||||||||||||||||||||||||||||||
| return `until ${formatLocalTime(closesAt, now, options)} local` | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const opensAt = getNextFreebuffDeploymentStart(now) | ||||||||||||||||||||||||||||||||||||
| return `opens ${formatLocalTime(opensAt, now, options)} local` | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| export function isFreebuffDeploymentHours(now: Date = new Date()): boolean { | ||||||||||||||||||||||||||||||||||||
| const eastern = getZonedParts(now, 'America/New_York') | ||||||||||||||||||||||||||||||||||||
| const pacific = getZonedParts(now, 'America/Los_Angeles') | ||||||||||||||||||||||||||||||||||||
| const eastern = getZonedParts(now, FREEBUFF_EASTERN_TIMEZONE) | ||||||||||||||||||||||||||||||||||||
| const pacific = getZonedParts(now, FREEBUFF_PACIFIC_TIMEZONE) | ||||||||||||||||||||||||||||||||||||
| if (eastern.weekday === 'Sat' || eastern.weekday === 'Sun') return false | ||||||||||||||||||||||||||||||||||||
| return eastern.minutes >= 9 * 60 && pacific.minutes < 17 * 60 | ||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||
| eastern.hour * 60 + eastern.minute >= 9 * 60 && | ||||||||||||||||||||||||||||||||||||
| pacific.hour * 60 + pacific.minute < 17 * 60 | ||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| export function isFreebuffModelAvailable( | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FREEBUFF_DEPLOYMENT_HOURS_LABEL('9am ET-5pm PT') is still exported but the component now callsgetFreebuffDeploymentAvailabilityLabelinstead. Anyone importing the constant from another call site would still see the static string while the UI shows a dynamic local-time label. If this constant is no longer intended for external use, removing or deprecating it avoids silent drift; if it still needs to remain for other callers, a doc comment clarifying that it is not the UI-facing label would help.Prompt To Fix With AI
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in
c703c2ac8. I kept the constant becauseweb/src/server/free-session/public-api.tsandweb/src/llm-api/fireworks.tsstill need a timezone-agnostic server-side message, and added a doc comment clarifying that the CLI should usegetFreebuffDeploymentAvailabilityLabel()instead.